kotlin.ts 9.6 KB
Newer Older
fxy060608's avatar
fxy060608 已提交
1 2 3
import path from 'path'
import fs from 'fs-extra'
import { relative } from '../utils'
fxy060608's avatar
fxy060608 已提交
4
import { originalPositionFor, originalPositionForSync } from '../sourceMap'
5 6 7 8 9 10 11 12 13
import {
  COLORS,
  type GenerateRuntimeCodeFrameOptions,
  generateCodeFrame,
  lineColumnToStartEnd,
  resolveSourceMapDirByCacheDir,
  resolveSourceMapFileBySourceFile,
  splitRE,
} from './utils'
fxy060608's avatar
fxy060608 已提交
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

export interface MessageSourceLocation {
  type: 'exception' | 'error' | 'warning' | 'info' | 'logging' | 'output'
  message: string
  file?: string
  line?: number
  column?: number
  code?: string
}

interface GenerateCodeFrameOptions {
  inputDir: string
  sourceMapDir: string
  replaceTabsWithSpace?: boolean
  format: (msg: MessageSourceLocation) => string
}

export function hbuilderFormatter(m: MessageSourceLocation) {
  const msgs: string[] = []
33 34 35
  if (m.type === 'error' || m.type === 'exception') {
    m.message = formatKotlinError(m.message, [], compileFormatters)
  }
fxy060608's avatar
fxy060608 已提交
36 37 38
  let msg = m.type + ': ' + m.message
  if (m.type === 'warning') {
    // 忽略部分警告
39 40 41 42
    if (
      msg.includes(`Classpath entry points to a non-existent location:`) &&
      !msg.includes('.gradle') // gradle 的警告需要输出
    ) {
fxy060608's avatar
fxy060608 已提交
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
      return ''
    }
    msg
      .replace(/\r\n/g, '\n')
      .split('\n')
      .forEach((m) => {
        msgs.push('\u200B' + m + '\u200B')
      })
  } else if (m.type === 'error' || m.type === 'exception') {
    msg
      .replace(/\r\n/g, '\n')
      .split('\n')
      .forEach((m) => {
        msgs.push('\u200C' + m + '\u200C')
      })
  } else {
    msgs.push(msg)
  }
  if (m.file) {
    if (m.file.includes('?')) {
      ;[m.file] = m.file.split('?')
    }
    msgs.push(`at ${m.file}:${m.line}:${m.column}`)
  }
fxy060608's avatar
fxy060608 已提交
67 68 69
  if (m.code) {
    msgs.push(m.code)
  }
fxy060608's avatar
fxy060608 已提交
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
  return msgs.join('\n')
}

export async function parseUTSKotlinStacktrace(
  messages: MessageSourceLocation[],
  options: GenerateCodeFrameOptions
) {
  if (typeof messages === 'string') {
    try {
      messages = JSON.parse(messages)
    } catch (e) {}
  }
  const msgs: string[] = []
  if (Array.isArray(messages) && messages.length) {
    for (const m of messages) {
      if (m.file) {
fxy060608's avatar
fxy060608 已提交
86 87 88 89 90
        const sourceMapFile = resolveSourceMapFile(
          m.file,
          options.sourceMapDir,
          options.inputDir
        )
fxy060608's avatar
fxy060608 已提交
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
        if (sourceMapFile) {
          const originalPosition = await originalPositionFor({
            sourceMapFile,
            line: m.line!,
            column: m.column!,
            withSourceContent: true,
          })

          if (originalPosition.source && originalPosition.sourceContent) {
            m.file = originalPosition.source.split('?')[0]
            if (originalPosition.line !== null) {
              m.line = originalPosition.line
            }
            if (originalPosition.column !== null) {
              m.column = originalPosition.column
            }
            if (
              originalPosition.line !== null &&
              originalPosition.column !== null
            ) {
              m.code = generateCodeFrame(originalPosition.sourceContent, {
                line: originalPosition.line,
                column: originalPosition.column,
              }).replace(/\t/g, ' ')
            }
          }
        }
      }
      const msg = options.format(m)
      if (msg) {
        msgs.push(msg)
      }
    }
  }
  return msgs.join('\n')
}
fxy060608's avatar
fxy060608 已提交
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141

function resolveSourceMapFile(
  file: string,
  sourceMapDir: string,
  inputDir: string
) {
  const sourceMapFile = path.resolve(
    sourceMapDir,
    relative(file, inputDir) + '.map'
  )
  if (fs.existsSync(sourceMapFile)) {
    return sourceMapFile
  }
}

142
const DEFAULT_APPID = '__UNI__uniappx'
fxy060608's avatar
fxy060608 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155

function normalizeAppid(appid: string) {
  return appid.replace(/_/g, '')
}
function createRegExp(appid: string) {
  return new RegExp('uni\\.' + appid + '\\.(.*)\\..*\\(*\\.kt:([0-9]+)\\)')
}

let kotlinManifest = {
  mtimeMs: 0,
  manifest: {} as Record<string, string>,
}

fxy060608's avatar
fxy060608 已提交
156 157 158 159 160
export interface KotlinManifestCache {
  version: string
  env: Record<string, string>
  files: Record<string, Record<string, string>>
}
fxy060608's avatar
fxy060608 已提交
161 162 163 164 165
function updateUTSKotlinSourceMapManifestCache(cacheDir: string) {
  const manifestFile = path.resolve(cacheDir, 'src/.manifest.json')
  const stats = fs.statSync(manifestFile)
  if (stats.isFile()) {
    if (kotlinManifest.mtimeMs !== stats.mtimeMs) {
fxy060608's avatar
fxy060608 已提交
166 167 168 169 170 171 172 173 174 175 176 177
      const { files } = fs.readJSONSync(manifestFile) as KotlinManifestCache
      if (files) {
        const classManifest: Record<string, string> = {}
        Object.keys(files).forEach((name) => {
          const kotlinClass = files[name].class
          if (kotlinClass) {
            classManifest[kotlinClass] = name
          }
        })
        kotlinManifest.mtimeMs = stats.mtimeMs
        kotlinManifest.manifest = classManifest
      }
fxy060608's avatar
fxy060608 已提交
178 179 180 181 182 183 184 185
    }
  }
}

function parseFilenameByClassName(className: string) {
  return kotlinManifest.manifest[className.split('$')[0]] || 'index.kt'
}

186
export interface GenerateKotlinRuntimeCodeFrameOptions
187
  extends GenerateRuntimeCodeFrameOptions {
fxy060608's avatar
fxy060608 已提交
188
  appid: string
189
  language: 'kotlin'
fxy060608's avatar
fxy060608 已提交
190 191
}

fxy060608's avatar
fxy060608 已提交
192
export function parseUTSKotlinRuntimeStacktrace(
fxy060608's avatar
fxy060608 已提交
193
  stacktrace: string,
194
  options: GenerateKotlinRuntimeCodeFrameOptions
fxy060608's avatar
fxy060608 已提交
195 196 197
) {
  const appid = normalizeAppid(options.appid || DEFAULT_APPID)
  if (!stacktrace.includes('uni.' + appid + '.')) {
fxy060608's avatar
fxy060608 已提交
198
    return ''
fxy060608's avatar
fxy060608 已提交
199 200 201 202 203
  }
  updateUTSKotlinSourceMapManifestCache(options.cacheDir)
  const re = createRegExp(appid)
  const res: string[] = []
  const lines = stacktrace.split(splitRE)
204
  const sourceMapDir = resolveSourceMapDirByCacheDir(options.cacheDir)
fxy060608's avatar
fxy060608 已提交
205 206
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
207
    const codes = parseUTSKotlinRuntimeStacktraceLine(line, re, sourceMapDir)
fxy060608's avatar
fxy060608 已提交
208 209 210 211
    if (codes.length && res.length) {
      const color = options.logType
        ? COLORS[options.logType as string] || ''
        : ''
212 213
      let error =
        'error: ' + formatKotlinError(res[0], codes, runtimeFormatters)
fxy060608's avatar
fxy060608 已提交
214 215 216
      if (color) {
        error = color + error + color
      }
fxy060608's avatar
fxy060608 已提交
217
      return [error, ...codes].join('\n')
fxy060608's avatar
fxy060608 已提交
218 219 220 221
    } else {
      res.push(line)
    }
  }
fxy060608's avatar
fxy060608 已提交
222
  return ''
fxy060608's avatar
fxy060608 已提交
223 224
}

fxy060608's avatar
fxy060608 已提交
225
function parseUTSKotlinRuntimeStacktraceLine(
fxy060608's avatar
fxy060608 已提交
226 227 228 229
  lineStr: string,
  re: RegExp,
  sourceMapDir: string
) {
fxy060608's avatar
fxy060608 已提交
230
  const lines: string[] = []
fxy060608's avatar
fxy060608 已提交
231 232
  const matches = lineStr.match(re)
  if (!matches) {
fxy060608's avatar
fxy060608 已提交
233
    return lines
fxy060608's avatar
fxy060608 已提交
234
  }
fxy060608's avatar
fxy060608 已提交
235

fxy060608's avatar
fxy060608 已提交
236
  const [, className, line] = matches
237
  const sourceMapFile = resolveSourceMapFileBySourceFile(
fxy060608's avatar
fxy060608 已提交
238 239 240 241
    parseFilenameByClassName(className),
    sourceMapDir
  )
  if (!sourceMapFile) {
fxy060608's avatar
fxy060608 已提交
242
    return lines
fxy060608's avatar
fxy060608 已提交
243
  }
fxy060608's avatar
fxy060608 已提交
244
  const originalPosition = originalPositionForSync({
fxy060608's avatar
fxy060608 已提交
245 246 247 248 249 250
    sourceMapFile,
    line: parseInt(line),
    column: 0,
    withSourceContent: true,
  })
  if (originalPosition.source && originalPosition.sourceContent) {
fxy060608's avatar
fxy060608 已提交
251 252 253 254 255
    lines.push(
      `at ${originalPosition.source.split('?')[0]}:${originalPosition.line}:${
        originalPosition.column
      }`
    )
fxy060608's avatar
fxy060608 已提交
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    if (originalPosition.line !== null && originalPosition.column !== null) {
      const { start, end } = lineColumnToStartEnd(
        originalPosition.sourceContent,
        originalPosition.line,
        originalPosition.column
      )
      lines.push(
        generateCodeFrame(originalPosition.sourceContent, start, end).replace(
          /\t/g,
          ' '
        )
      )
    }
  }
  return lines
}
272 273 274 275 276

interface Formatter {
  format(error: string, codes: string[]): string | undefined
}

277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
const TYPE_MISMATCH_RE =
  /Type mismatch: inferred type is (.*) but (.*) was expected/

function normalizeType(type: string) {
  if (type.endsWith('?')) {
    let nonOptional = type.slice(0, -1)
    if (nonOptional.startsWith('(') && nonOptional.endsWith(')')) {
      nonOptional = nonOptional.slice(1, -1)
    }
    return `${type}(可为空的${nonOptional})`
  }
  return type
}

const extApiErrorFormatter: Formatter = {
  format(error, codes) {
    if (error.includes('Failed resolution of: L')) {
      let isUniExtApi =
        error.includes('uts/sdk/modules/DCloudUni') ||
        error.includes('io/dcloud/uniapp/extapi/')
      let isUniCloudApi =
        !isUniExtApi && error.includes('io/dcloud/unicloud/UniCloud')
      if (isUniExtApi || isUniCloudApi) {
        let api = ''
        // 第一步先遍历查找^^^^^的索引
        const codeFrames = codes[codes.length - 1].split(splitRE)
        const index = codeFrames.findIndex((frame) => frame.includes('^^^^^'))
        if (index > 0) {
          // 第二步,取前一条记录,查找uni.开头的api
          api = findApi(
            codeFrames[index - 1],
            isUniCloudApi ? UNI_CLOUD_API_RE : UNI_API_RE
          )
310
        }
311 312 313 314 315 316
        if (api) {
          api = `api ${api}`
        } else {
          api = `您使用到的api`
        }
        return `[EXCEPTION] 当前运行的基座未包含${api},请重新打包自定义基座再运行。`
317
      }
318 319 320 321 322 323 324 325 326 327 328 329
    }
  },
}
const typeMismatchErrorFormatter: Formatter = {
  format(error, _) {
    const matches = error.match(TYPE_MISMATCH_RE)
    if (matches) {
      const [, inferredType, expectedType] = matches
      return `类型不匹配: 推断类型是${normalizeType(
        inferredType
      )},但预期的是${normalizeType(expectedType)}。`
    }
330
  },
331 332 333 334 335
}

const compileFormatters: Formatter[] = [typeMismatchErrorFormatter]

const runtimeFormatters: Formatter[] = [extApiErrorFormatter]
336

fxy060608's avatar
fxy060608 已提交
337
const UNI_API_RE = /(uni\.\w+)/
338 339 340
const UNI_CLOUD_API_RE = /(uniCloud\.\w+)/
function findApi(msg: string, re: RegExp) {
  const matches = msg.match(re)
341 342 343 344 345 346
  if (matches) {
    return matches[1]
  }
  return ''
}

347 348 349 350 351
function formatKotlinError(
  error: string,
  codes: string[],
  formatters: Formatter[]
): string {
352 353 354 355 356 357 358 359
  for (const formatter of formatters) {
    const err = formatter.format(error, codes)
    if (err) {
      return err
    }
  }
  return error
}