diff --git a/.changeset/yellow-masks-bake.md b/.changeset/yellow-masks-bake.md new file mode 100644 index 0000000000..f06fc675b4 --- /dev/null +++ b/.changeset/yellow-masks-bake.md @@ -0,0 +1,6 @@ +--- +'@rsbuild/doctor-utils': patch +'@rsbuild/doctor-sdk': patch +--- + +feat: rsbuild doctor add rule-utils package diff --git a/packages/doctor-sdk/modern.config.esm.ts b/packages/doctor-sdk/modern.config.esm.ts index 068656fbae..05c0218170 100644 --- a/packages/doctor-sdk/modern.config.esm.ts +++ b/packages/doctor-sdk/modern.config.esm.ts @@ -8,5 +8,8 @@ export default defineConfig({ format: 'esm', target: 'esnext', outDir: './dist/esm', + dts: { + distPath: '../type', + }, }, }); diff --git a/packages/doctor-utils/modern.config.esm.ts b/packages/doctor-utils/modern.config.esm.ts index 068656fbae..05c0218170 100644 --- a/packages/doctor-utils/modern.config.esm.ts +++ b/packages/doctor-utils/modern.config.esm.ts @@ -8,5 +8,8 @@ export default defineConfig({ format: 'esm', target: 'esnext', outDir: './dist/esm', + dts: { + distPath: '../type', + }, }, }); diff --git a/packages/doctor-utils/package.json b/packages/doctor-utils/package.json index 6d69b571b5..d9f60067e3 100644 --- a/packages/doctor-utils/package.json +++ b/packages/doctor-utils/package.json @@ -26,6 +26,11 @@ "types": "./dist/type/build/index.d.ts", "require": "./dist/cjs/build/index.js", "import": "./dist/esm/build/index.js" + }, + "./ruleUtils": { + "types": "./dist/type/rule-utils/index.d.ts", + "require": "./dist/cjs/burule-utilsild/index.js", + "import": "./dist/esm/rule-utils/index.js" } }, "typesVersions": { @@ -35,6 +40,9 @@ ], "build": [ "./dist/type/build/index.d.ts" + ], + "ruleUtils": [ + "./dist/type/rule-utils/index.d.ts" ] } }, @@ -54,6 +62,11 @@ "fs-extra": "^11.1.1", "get-port": "5.1.1", "json-stream-stringify": "3.0.1", + "acorn": "^8.10.0", + "acorn-import-assertions": "1.8.0", + "acorn-walk": "8.2.0", + "lines-and-columns": "2.0.3", + "@types/estree": "1.0.0", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/doctor-utils/src/index.ts b/packages/doctor-utils/src/index.ts index 1ab32d61a1..b2d3d85ffc 100644 --- a/packages/doctor-utils/src/index.ts +++ b/packages/doctor-utils/src/index.ts @@ -1,2 +1,3 @@ export * from './build'; export * from './common'; +export * as RuleUtils from './rule-utils'; diff --git a/packages/doctor-utils/src/rule-utils/document/document.ts b/packages/doctor-utils/src/rule-utils/document/document.ts new file mode 100644 index 0000000000..2d312b2aec --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/document/document.ts @@ -0,0 +1,99 @@ +import { LinesAndColumns } from 'lines-and-columns'; +import { isUndefined, isNumber } from 'lodash'; +import { Range, OffsetRange, Position, DocumentEditData } from './types'; + +/** Document Catalogue */ +export class Document { + /** Actual document content. */ + private _text = ''; + + /** Get the displacement of the file position in the text. */ + positionAt!: (offset: number) => Position | undefined; + + /** Get the position of the displacement point in the file. */ + offsetAt!: (position: Position) => number | undefined; + + constructor(content: string) { + this._text; + this._text = content; + this.createFinder(); + } + + /** Generate location search */ + private createFinder() { + const find = new LinesAndColumns(this._text); + + this.positionAt = (offset) => { + if (offset >= this._text.length) { + offset = this._text.length - 1; + } + + if (offset < 0) { + offset = 0; + } + + const result = find.locationForIndex(offset); + + if (!result) { + return; + } + + return { + line: result.line + 1, + column: result.column, + }; + }; + + this.offsetAt = (position) => { + return ( + find.indexForLocation({ + line: position.line - 1, + column: position.column, + }) ?? undefined + ); + }; + } + + getText(range?: Range | OffsetRange) { + if (!range) { + return this._text; + } + + const start = + typeof range.start === 'number' + ? range.start + : this.offsetAt(range.start); + const end = + typeof range.end === 'number' ? range.end : this.offsetAt(range.end); + + if (isUndefined(start)) { + throw new Error(`Location ${JSON.stringify(start)} is illegal`); + } + + if (isUndefined(end)) { + throw new Error(`Location ${JSON.stringify(end)} is illegal`); + } + + return this._text.slice(start, end); + } + + /** Edit document data */ + edit(data: DocumentEditData) { + let { _text: content } = this; + const startOffset = isNumber(data.start) + ? data.start + : this.offsetAt(data.start); + const endOffset = isNumber(data.end) ? data.end : this.offsetAt(data.end); + + if (isUndefined(startOffset) || isUndefined(endOffset)) { + return; + } + + const startTxt = content.substring(0, startOffset); + const endTxt = content.substring(endOffset, content.length); + + content = startTxt + data.newText + endTxt; + + return content; + } +} diff --git a/packages/doctor-utils/src/rule-utils/document/index.ts b/packages/doctor-utils/src/rule-utils/document/index.ts new file mode 100644 index 0000000000..67be376f62 --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/document/index.ts @@ -0,0 +1,3 @@ +export * from './document'; +export * from './types'; +export * from './server'; diff --git a/packages/doctor-utils/src/rule-utils/document/server.ts b/packages/doctor-utils/src/rule-utils/document/server.ts new file mode 100644 index 0000000000..525b9e05e9 --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/document/server.ts @@ -0,0 +1,18 @@ +import { Document } from './document'; + +const store = new Map(); + +/** Create Document */ +export function getDocument(content: string) { + if (store.has(content)) { + return store.get(content)!; + } + + const doc = new Document(content); + store.set(content, doc); + return doc; +} + +export function clearDocument() { + store.clear(); +} diff --git a/packages/doctor-utils/src/rule-utils/document/types.ts b/packages/doctor-utils/src/rule-utils/document/types.ts new file mode 100644 index 0000000000..1f357261df --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/document/types.ts @@ -0,0 +1,34 @@ +/** + * Location + * - line starting point is 1 + * - column starting point is 0 + */ +export interface Position { + line: number; + column: number; +} + +/** Location range */ +export interface Range { + start: Position; + end: Position; +} + +/** Offset range */ +export interface OffsetRange { + start: number; + end: number; +} + +/** Text repair data */ +export interface DocumentEditData { + /** Modify the starting position of string in the original text */ + start: number | Position; + /** Modify string in the key position of the original text */ + end: number | Position; + /** + * Replaced new text + * - If empty, delete the original text + */ + newText?: string; +} diff --git a/packages/doctor-utils/src/rule-utils/index.ts b/packages/doctor-utils/src/rule-utils/index.ts new file mode 100644 index 0000000000..4b9b16b29f --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/index.ts @@ -0,0 +1,2 @@ +export * from './document'; +export * from './parser'; diff --git a/packages/doctor-utils/src/rule-utils/parser/asserts.ts b/packages/doctor-utils/src/rule-utils/parser/asserts.ts new file mode 100644 index 0000000000..7d27199d34 --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/parser/asserts.ts @@ -0,0 +1,154 @@ +import { isObject } from 'lodash'; +import { Node } from './types'; + +function isSyntaxNode(node: unknown): node is Node.SyntaxNode { + return isObject(node) && 'type' in node; +} + +function assertCreator(type: string) { + return (node: unknown): node is T => { + return isSyntaxNode(node) && node.type === type; + }; +} + +export const asserts = { + isProgram: assertCreator('Program'), + isEmptyStatement: assertCreator('EmptyStatement'), + isBlockStatement: assertCreator('BlockStatement'), + isStaticBlock: assertCreator('StaticBlock'), + isExpressionStatement: assertCreator( + 'ExpressionStatement', + ), + isIfStatement: assertCreator('IfStatement'), + isLabeledStatement: assertCreator('LabeledStatement'), + isBreakStatement: assertCreator('BreakStatement'), + isContinueStatement: + assertCreator('ContinueStatement'), + isWithStatement: assertCreator('WithStatement'), + isSwitchStatement: assertCreator('SwitchStatement'), + isReturnStatement: assertCreator('ReturnStatement'), + isThrowStatement: assertCreator('ThrowStatement'), + isTryStatement: assertCreator('TryStatement'), + isWhileStatement: assertCreator('WhileStatement'), + isDoWhileStatement: assertCreator('DoWhileStatement'), + isForStatement: assertCreator('ForStatement'), + isForInStatement: assertCreator('ForInStatement'), + isForOfStatement: assertCreator('ForOfStatement'), + isDebuggerStatement: + assertCreator('DebuggerStatement'), + isFunctionDeclaration: assertCreator( + 'FunctionDeclaration', + ), + isVariableDeclaration: assertCreator( + 'VariableDeclaration', + ), + isVariableDeclarator: + assertCreator('VariableDeclarator'), + isChainExpression: assertCreator('ChainExpression'), + isThisExpression: assertCreator('ThisExpression'), + isArrayExpression: assertCreator('ArrayExpression'), + isObjectExpression: assertCreator('ObjectExpression'), + isPrivateIdentifier: + assertCreator('PrivateIdentifier'), + isProperty: assertCreator('Property'), + isPropertyDefinition: + assertCreator('PropertyDefinition'), + isFunctionExpression: + assertCreator('FunctionExpression'), + isSequenceExpression: + assertCreator('SequenceExpression'), + isUnaryExpression: assertCreator('UnaryExpression'), + isBinaryExpression: assertCreator('BinaryExpression'), + isAssignmentExpression: assertCreator( + 'AssignmentExpression', + ), + isUpdateExpression: assertCreator('UpdateExpression'), + isLogicalExpression: + assertCreator('LogicalExpression'), + isConditionalExpression: assertCreator( + 'ConditionalExpression', + ), + isNewExpression: assertCreator('NewExpression'), + isSwitchCase: assertCreator('SwitchCase'), + isCatchClause: assertCreator('CatchClause'), + isIdentifier: assertCreator('Identifier'), + isLiteral: assertCreator('Literal'), + isSuper: assertCreator('Super'), + isSpreadElement: assertCreator('SpreadElement'), + isArrowFunctionExpression: assertCreator( + 'ArrowFunctionExpression', + ), + isYieldExpression: assertCreator('YieldExpression'), + isTemplateLiteral: assertCreator('TemplateLiteral'), + isTaggedTemplateExpression: assertCreator( + 'TaggedTemplateExpression', + ), + isTemplateElement: assertCreator('TemplateElement'), + isObjectPattern: assertCreator('ObjectPattern'), + isArrayPattern: assertCreator('ArrayPattern'), + isRestElement: assertCreator('RestElement'), + isAssignmentPattern: + assertCreator('AssignmentPattern'), + isClassBody: assertCreator('ClassBody'), + isClassDeclaration: assertCreator('ClassDeclaration'), + isClassExpression: assertCreator('ClassExpression'), + isMetaProperty: assertCreator('MetaProperty'), + isImportDeclaration: + assertCreator('ImportDeclaration'), + isImportSpecifier: assertCreator('ImportSpecifier'), + isImportExpression: assertCreator('ImportExpression'), + isImportDefaultSpecifier: assertCreator( + 'ImportDefaultSpecifier', + ), + isImportNamespaceSpecifier: assertCreator( + 'ImportNamespaceSpecifier', + ), + isExportNamedDeclaration: assertCreator( + 'ExportNamedDeclaration', + ), + isExportSpecifier: assertCreator('ExportSpecifier'), + isExportDefaultDeclaration: assertCreator( + 'ExportDefaultDeclaration', + ), + isExportAllDeclaration: assertCreator( + 'ExportAllDeclaration', + ), + isAwaitExpression: assertCreator('AwaitExpression'), + isMethodDefinition: assertCreator('MethodDefinition'), + isMemberExpression: assertCreator('MemberExpression'), + + isComment(node: unknown): node is Node.Comment { + return ( + isSyntaxNode(node) && (node.type === 'Line' || node.type === 'Block') + ); + }, + isDirective(node: unknown): node is Node.Directive { + return asserts.isExpressionStatement(node) && 'directive' in node; + }, + isSimpleCallExpression(node: unknown): node is Node.SimpleCallExpression { + return isSyntaxNode(node) && node.type === 'CallExpression'; + }, + isAssignmentProperty(node: unknown): node is Node.AssignmentProperty { + return asserts.isProperty(node) && node.kind === 'init'; + }, + isSimpleLiteral(node: unknown): node is Node.SimpleLiteral { + return ( + asserts.isLiteral(node) && + !asserts.isRegExpLiteral(node) && + !asserts.isBigIntLiteral(node) + ); + }, + isRegExpLiteral(node: unknown): node is Node.RegExpLiteral { + return asserts.isLiteral(node) && 'regex' in node; + }, + isBigIntLiteral(node: unknown): node is Node.BigIntLiteral { + return asserts.isLiteral(node) && 'bigint' in node; + }, + isExportStatement(node: unknown): node is Node.ExportStatement { + return ( + asserts.isExportAllDeclaration(node) || + asserts.isExportDefaultDeclaration(node) || + asserts.isExportNamedDeclaration(node) + ); + }, +} as const; diff --git a/packages/doctor-utils/src/rule-utils/parser/index.ts b/packages/doctor-utils/src/rule-utils/parser/index.ts new file mode 100644 index 0000000000..6f00837fed --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/parser/index.ts @@ -0,0 +1,4 @@ +export * from './asserts'; +export * from './parser'; +export * from './utils'; +export * from './types'; diff --git a/packages/doctor-utils/src/rule-utils/parser/parser.ts b/packages/doctor-utils/src/rule-utils/parser/parser.ts new file mode 100644 index 0000000000..ac50a7dd9f --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/parser/parser.ts @@ -0,0 +1,74 @@ +import { Parser as AcornParser, Options, Position } from 'acorn'; +// @ts-ignore +import { importAssertions } from 'acorn-import-assertions'; +import * as walk from 'acorn-walk'; +import { asserts } from './asserts'; +import * as utils from './utils'; +import type { Node } from './types'; + +export type { Options as ParseOptions } from 'acorn'; + +export interface ParseError extends Error { + loc?: Position; + pos: number; + raisedAt: number; +} + +/** + * parser for internal + */ +const acornParserInternal = AcornParser.extend(importAssertions); + +/** + * parser for developers + */ +let acornParserExport = AcornParser.extend(importAssertions); + +export const parser = { + /** AST iterator */ + walk, + /** + * Compile code + * - Output root node is `Node.Program` + */ + parse: (input: string, options: Options) => { + return acornParserExport.parse(input, options) as Node.Program; + }, + /** + * Compile the next first expression + * - The output root node is `Node.ExpressionStatement` + */ + parseExpressionAt: (input: string, pos: number, options: Options) => { + return acornParserExport.parseExpressionAt( + input, + pos, + options, + ) as Node.ExpressionStatement; + }, + /** + * add plugins for acorn + */ + extend(...args: Parameters) { + acornParserExport = acornParserExport.extend(...args); + return acornParserExport; + }, + /** Set of assertions */ + asserts, + utils, + /** + * @internal + * parser for internal packages + */ + internal: { + parse: (input: string, options: Options) => { + return acornParserInternal.parse(input, options) as Node.Program; + }, + parseExpressionAt: (input: string, pos: number, options: Options) => { + return acornParserInternal.parseExpressionAt( + input, + pos, + options, + ) as Node.ExpressionStatement; + }, + }, +}; diff --git a/packages/doctor-utils/src/rule-utils/parser/types.ts b/packages/doctor-utils/src/rule-utils/parser/types.ts new file mode 100644 index 0000000000..bf26b70992 --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/parser/types.ts @@ -0,0 +1,129 @@ +import type * as Node from 'estree'; + +export { Node }; + +export enum ECMAVersion { + ES5 = 'ES5', + ES6 = 'ES6', + ES7P = 'ES7+', +} + +/** + * estree supplement type + * Mainly to match the node type of acorn + * The main reason for not using the type of acorn directly is that the node type described by acorn itself is too simple + */ +declare module 'estree' { + interface BaseNode { + start: number; + end: number; + loc?: SourceLocation | undefined; + } + + interface Position { + offset: number; + } + + /** all syntax nodes */ + type SyntaxNode = + | Node.Comment + | Node.Program + | Node.Directive + | Node.EmptyStatement + | Node.BlockStatement + | Node.StaticBlock + | Node.ExpressionStatement + | Node.IfStatement + | Node.LabeledStatement + | Node.BreakStatement + | Node.ContinueStatement + | Node.WithStatement + | Node.SwitchStatement + | Node.ReturnStatement + | Node.ThrowStatement + | Node.TryStatement + | Node.WhileStatement + | Node.DoWhileStatement + | Node.ForStatement + | Node.ForInStatement + | Node.DebuggerStatement + | Node.FunctionDeclaration + | Node.VariableDeclaration + | Node.VariableDeclarator + | Node.ChainExpression + | Node.ThisExpression + | Node.ArrayExpression + | Node.ObjectExpression + | Node.PrivateIdentifier + | Node.Property + | Node.PropertyDefinition + | Node.FunctionExpression + | Node.SequenceExpression + | Node.UnaryExpression + | Node.BinaryExpression + | Node.AssignmentExpression + | Node.UpdateExpression + | Node.LogicalExpression + | Node.ConditionalExpression + | Node.SimpleCallExpression + | Node.NewExpression + | Node.MemberExpression + | Node.SwitchCase + | Node.CatchClause + | Node.Identifier + | Node.SimpleLiteral + | Node.RegExpLiteral + | Node.BigIntLiteral + | Node.ForOfStatement + | Node.Super + | Node.SpreadElement + | Node.ArrowFunctionExpression + | Node.YieldExpression + | Node.TemplateLiteral + | Node.TaggedTemplateExpression + | Node.TemplateElement + | Node.AssignmentProperty + | Node.ObjectPattern + | Node.ArrayPattern + | Node.RestElement + | Node.AssignmentPattern + | Node.ClassBody + | Node.MethodDefinition + | Node.ClassDeclaration + | Node.ClassExpression + | Node.MetaProperty + | Node.ImportDeclaration + | Node.ImportSpecifier + | Node.ImportExpression + | Node.ImportDefaultSpecifier + | Node.ImportNamespaceSpecifier + | Node.ExportNamedDeclaration + | Node.ExportSpecifier + | Node.ExportDefaultDeclaration + | Node.ExportAllDeclaration + | Node.AwaitExpression; + + /** all operators */ + type SyntaxOperator = + | Node.UnaryOperator + | Node.BinaryOperator + | Node.LogicalOperator + | Node.AssignmentOperator + | Node.UpdateOperator; + + /** Block-scoped statement */ + type BlockScopeStatement = + | Node.StaticBlock + | Node.BlockStatement + | Node.ForInStatement + | Node.ForOfStatement + | Node.ForStatement + | Node.CatchClause + | Node.SwitchStatement; + + /** Export statement */ + type ExportStatement = + | Node.ExportAllDeclaration + | Node.ExportDefaultDeclaration + | Node.ExportNamedDeclaration; +} diff --git a/packages/doctor-utils/src/rule-utils/parser/utils.ts b/packages/doctor-utils/src/rule-utils/parser/utils.ts new file mode 100644 index 0000000000..c29f35c2ba --- /dev/null +++ b/packages/doctor-utils/src/rule-utils/parser/utils.ts @@ -0,0 +1,233 @@ +import { parse, ecmaVersion } from 'acorn'; +import { Node, ECMAVersion } from './types'; +import { asserts } from './asserts'; + +// TODO: so much Ast node type,not complete yet. +/** + * Is the node semantics the same? + * @deprecated + * - Recursively compare whether the content of the node itself is the same + * - Ignore comments, positions, string symbols (single and double quotation marks) + * - String templates and string addition will be considered different + */ +export function isSameSemantics( + node1: Node.SyntaxNode, + node2: Node.SyntaxNode, +): boolean { + if (node1.type !== node2.type) { + return false; + } + + switch (node1.type) { + case 'CallExpression': { + const next = node2 as Node.SimpleCallExpression; + return ( + node1.arguments.length === next.arguments.length && + Boolean(node1.optional) === Boolean(next.optional) && + isSameSemantics(node1.callee, next.callee) && + node1.arguments.every((node, i) => + isSameSemantics(node, next.arguments[i]), + ) + ); + } + case 'MemberExpression': { + const next = node2 as Node.MemberExpression; + return ( + node1.computed === next.computed && + Boolean(node1.optional) === Boolean(next.optional) && + isSameSemantics(node1.object, next.object) && + isSameSemantics(node1.property, next.property) + ); + } + case 'Identifier': { + return node1.name === (node2 as Node.Identifier).name; + } + case 'Literal': { + if (asserts.isSimpleLiteral(node1) && asserts.isSimpleLiteral(node2)) { + return node1.value === (node2 as Node.Literal).value; + } + + return node1.raw === (node2 as Node.Literal).raw; + } + case 'ObjectExpression': { + const next = node2 as Node.ObjectExpression; + return ( + node1.properties.length === next.properties.length && + node1.properties.every((prop, i) => + isSameSemantics(prop, next.properties[i]), + ) + ); + } + case 'Property': { + const next = node2 as Node.Property; + return ( + node1.computed === next.computed && + node1.kind === next.kind && + node1.method === next.method && + isSameSemantics(node1.key, next.key) && + isSameSemantics(node1.value, next.value) + ); + } + + default: { + throw new Error(`Unknown node type: ${node1.type}`); + } + } +} + +/** + * Get all default reference statements + */ +export function getDefaultImports( + node: Node.Program, +): Node.ImportDeclaration[] { + return node.body.filter((statement): statement is Node.ImportDeclaration => { + if (statement.type !== 'ImportDeclaration') { + return false; + } + + const specifier = statement?.specifiers?.[0]; + + if (specifier?.type === 'ImportDefaultSpecifier') { + return true; + } + + return false; + }); +} + +/** Get the literal in the text. */ +export function getIdentifierInPattern( + name: string, + node: Node.Pattern, +): Node.Identifier | undefined { + if (asserts.isIdentifier(node) && node.name === name) { + return node; + } + + if (asserts.isObjectPattern(node)) { + for (const prop of node.properties) { + if (asserts.isAssignmentProperty(prop)) { + return getIdentifierInPattern(name, prop.value); + } + + if (asserts.isRestElement(prop)) { + return getIdentifierInPattern(name, prop); + } + } + } + + if (asserts.isArrayPattern(node)) { + for (const el of node.elements) { + if (el) { + const result = getIdentifierInPattern(name, el); + + if (result) { + return result; + } + } + } + } + + if (asserts.isRestElement(node)) { + return getIdentifierInPattern(name, node.argument); + } + + if (asserts.isAssignmentPattern(node)) { + return getIdentifierInPattern(name, node.left); + } +} + +/** Get the variable declaration statement identifier. */ +export function getIdentifierInDeclaration( + name: string, + node: Node.SyntaxNode, +) { + function getId(node: { id: unknown }) { + return asserts.isIdentifier(node.id) && node.id.name === name + ? node.id + : undefined; + } + + if (asserts.isFunctionDeclaration(node)) { + return getId(node); + } + + if (asserts.isClassDeclaration(node)) { + return getId(node); + } + + if (asserts.isVariableDeclaration(node)) { + return node.declarations.find((item) => + getIdentifierInPattern(name, item.id), + )?.id as Node.Identifier; + } +} + +/** Get the reference declaration statement identifier. */ +export function getIdentifierInImport(name: string, node: Node.SyntaxNode) { + if (asserts.isImportDeclaration(node)) { + for (const specifier of node.specifiers ?? []) { + if (specifier.local.name === name) { + return specifier.local; + } + } + } +} + +/** Get the export statement identifier. */ +export function getIdentifierInExport(name: string, node: Node.SyntaxNode) { + if (asserts.isExportNamedDeclaration(node)) { + if (node.declaration) { + return getIdentifierInDeclaration(name, node.declaration); + } + + for (const specifier of node.specifiers ?? []) { + if (specifier.exported.name === name) { + return specifier.exported; + } + } + } + + if (asserts.isExportAllDeclaration(node) && node.exported) { + if (node.exported.name === name) { + return node.exported; + } + } +} + +/** Determine that it can be resolved using the specified ECMA version */ +export function canParse(code: string, ecmaVersion: ecmaVersion) { + try { + parse(code, { + ecmaVersion, + sourceType: + typeof ecmaVersion === 'number' && ecmaVersion <= 5 + ? 'script' + : 'module', + }); + return true; + } catch (err) { + return false; + } +} + +/** Determine whether it is all ES5 version code. */ +export function isES5(code: string) { + return canParse(code, 5); +} + +/** Determine whether it is all ES6 version code. */ +export function isES6(code: string) { + return canParse(code, 6); +} + +/** Detect ECMA version. */ +export function detectECMAVersion(code: string) { + if (isES6(code)) { + if (isES5(code)) return ECMAVersion.ES5; + + return ECMAVersion.ES6; + } + return ECMAVersion.ES7P; +} diff --git a/packages/doctor-utils/tests/parser/parser.test.ts b/packages/doctor-utils/tests/parser/parser.test.ts new file mode 100644 index 0000000000..c7ad681d81 --- /dev/null +++ b/packages/doctor-utils/tests/parser/parser.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { parser } from '../../src/rule-utils/parser'; + +describe('test src/rule-utils/parser/parser.ts', () => { + describe('extend', () => { + it('extend nothing', () => { + const parser1 = parser.extend(); + const parser2 = parser.extend(); + + expect(parser1 === parser2).toBeTruthy(); + }); + + it('extend acorn plugin', () => { + const parser1 = parser.extend(); + const parser2 = parser.extend((P) => class A extends P {}); + + expect(parser1 === parser2).toBeFalsy(); + expect(parser.extend() === parser1).toBeFalsy(); + expect(parser.extend() === parser2).toBeTruthy(); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6de164383..5bfd6ac031 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -565,6 +565,18 @@ importers: packages/doctor-utils: dependencies: + '@types/estree': + specifier: 1.0.0 + version: 1.0.0 + acorn: + specifier: ^8.10.0 + version: 8.10.0 + acorn-import-assertions: + specifier: 1.8.0 + version: 1.8.0(acorn@8.10.0) + acorn-walk: + specifier: 8.2.0 + version: 8.2.0 bytes: specifier: 3.1.2 version: 3.1.2 @@ -583,6 +595,9 @@ importers: json-stream-stringify: specifier: 3.0.1 version: 3.0.1 + lines-and-columns: + specifier: 2.0.3 + version: 2.0.3 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -6328,6 +6343,14 @@ packages: acorn-walk: 8.2.0 dev: true + /acorn-import-assertions@1.8.0(acorn@8.10.0): + resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.10.0 + dev: false + /acorn-import-assertions@1.9.0(acorn@8.10.0): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: @@ -6359,7 +6382,6 @@ packages: /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} - dev: true /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} @@ -10087,7 +10109,6 @@ packages: /lines-and-columns@2.0.3: resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true /load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}