import type { ScanConfigSchema } from '@/scripts/op-config' import type { ApiScanResultItem } from '@/types/scan' import { parse, type ParserOptions } from '@babel/parser' import traverser from '@babel/traverse' import t from '@babel/types' import fse from 'fs-extra' // @ts-expect-error https://github.com/babel/babel/issues/13855 const traverse: typeof traverser = traverser.default const isPathLikeStringNode = (node: t.Node, checkIsPathLikeString: (value: string) => boolean) => { if (t.isStringLiteral(node)) { return checkIsPathLikeString(node.value) } else if (t.isTemplateLiteral(node)) { const { quasis = [] } = node const templateStrings = quasis.map((quasi) => quasi.value.raw).join('') return checkIsPathLikeString(templateStrings) } return false } const isPathLikeBinaryExpressionNode = ( node: t.Node, checkIsPathLikeString: (value: string) => boolean, ) => { if ( t.isBinaryExpression(node, { operator: '+', }) ) { const { left, right } = node for (const subNode of [left, right]) { if (isPathLikeStringNode(subNode, checkIsPathLikeString)) { return true } } } return false } export type ApiCollectedResultItem = Pick export const scanCodingAPIs = async (filePath: string, scanConfig: ScanConfigSchema) => { const code = await fse.readFile(filePath, 'utf-8') const { checkIsPathLikeString } = scanConfig const babelPlugins = ['typescript', 'decorators'] as Exclude if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) { babelPlugins.push('jsx') } const ast = parse(code, { plugins: babelPlugins, sourceType: 'unambiguous', errorRecovery: true, }) const collected: Array = [] const ignoredLines: number[] = [] const isIgnored = (node: t.Node) => ignoredLines.includes(node!.loc!.start.line) const getLocation = (node: t.Node) => { const { line, column } = node!.loc!.start return { file: filePath, line, column, } } traverse(ast, { 'ImportDeclaration': (path) => { path.skip() }, 'ExportNamedDeclaration|ExportAllDeclaration': (path) => { if ((path.node as t.ExportNamedDeclaration | t.ExportAllDeclaration).source) { path.skip() } }, 'Program': (path) => { const commentsNodes = (path.parent as t.File).comments || [] commentsNodes.forEach((comment) => { const content = comment.value .split('') .filter((char) => !['\n', '*'].includes(char)) .join('') .trim() if (content === '@ones-api-ignore-file') { path.stop() } const { line, column } = comment!.loc!.start if (content === '@ones-api-ignore') { ignoredLines.push(line + 1) } const [, rawApi] = content.split('@ones-api:') const api = rawApi?.trim() if (api && checkIsPathLikeString(api)) { collected.push({ value: api, location: { file: filePath, line, column, }, }) } }) }, 'StringLiteral|TemplateLiteral': (path) => { // @ts-expect-error todo if (!isIgnored(path.node) && isPathLikeStringNode(path.node, checkIsPathLikeString)) { collected.push({ value: path.toString(), // @ts-expect-error todo location: getLocation(path.node), // @ts-expect-error todo ast: path.node, }) // @ts-expect-error todo if (t.isTemplateLiteral(path.node)) { path.skip() } } }, 'BinaryExpression': (path) => { if ( // @ts-expect-error todo !isIgnored(path.node) && // @ts-expect-error todo isPathLikeBinaryExpressionNode(path.node, checkIsPathLikeString) ) { const targetPath = path.find( (p) => // @ts-expect-error todo t.isBinaryExpression(p.node, { operator: '+' }) && // @ts-expect-error todo !t.isBinaryExpression(p.parent, { operator: '+' }), ) if (targetPath) { collected.push({ value: targetPath.toString(), // @ts-expect-error todo location: getLocation(targetPath.node), // @ts-expect-error todo ast: targetPath.node, }) } path.skip() } }, }) return collected }