diff --git a/.changeset/sour-balloons-smoke.md b/.changeset/sour-balloons-smoke.md new file mode 100644 index 000000000..9a835b091 --- /dev/null +++ b/.changeset/sour-balloons-smoke.md @@ -0,0 +1,12 @@ +--- +'providence-analytics': minor +--- + +- use oxc for all analyzers (oxc is way smaller and more performant than swc, let alone babel) +- make swcTraverse compatible with oxc +- expand scope functionality of swcTraverse + +BREAKING: + +- make parsers peerDependencies (babel or swc should be loaded by external analyzers) +- rename `swcTraverse` to `oxcTraverse` diff --git a/package-lock.json b/package-lock.json index e8881d768..ca35254d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4198,15 +4198,6 @@ "node": ">=18" } }, - "node_modules/@putout/babel": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@putout/babel/-/babel-2.8.0.tgz", - "integrity": "sha512-Vq4DgAR6Zfc0VXyspQndmgT4T7sTgJBm8kwigN2zPxtyTtz8R199qjxSrypY1P2d+iAGatG2imksrzlPOlombg==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/@rocket/blog": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@rocket/blog/-/blog-0.4.0.tgz", @@ -5525,6 +5516,7 @@ "version": "1.7.36", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.36.tgz", "integrity": "sha512-bu7ymMX+LCJOSSrKank25Jaq66ymLVA9fOUuy4ck3/6rbXdLw+pIJPnIDKQ9uNcxww8KDxOuJk9Ui9pqR+aGFw==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -5566,6 +5558,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5582,6 +5575,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5598,6 +5592,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5614,6 +5609,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5630,6 +5626,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5646,6 +5643,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5662,6 +5660,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5678,6 +5677,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5694,6 +5694,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5710,6 +5711,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5723,12 +5725,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", "integrity": "sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -28032,17 +28036,13 @@ "license": "MIT" }, "packages-node/providence-analytics": { - "version": "0.16.5", + "version": "0.16.8", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.8", - "@babel/plugin-syntax-import-assertions": "^7.25.7", "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.8", - "@putout/babel": "^2.8.0", "@rollup/plugin-node-resolve": "^15.3.0", - "@swc/core": "^1.7.36", "commander": "^2.20.3", + "oxc-parser": "^0.34.0", "parse5": "^7.2.0", "semver": "^7.6.3" }, @@ -28050,6 +28050,9 @@ "providence": "src/cli/index.js" }, "devDependencies": { + "@babel/parser": "^7.25.8", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@swc/core": "^1.7.36", "@types/inquirer": "^9.0.7", "@types/mocha": "^10.0.9", "@web/dev-server": "^0.4.6", @@ -28060,6 +28063,134 @@ }, "engines": { "node": ">=18.0.0" + }, + "peerDependencies": { + "@babel/parser": "^7.25.8", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@swc/core": "^1.7.36" + } + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.34.0.tgz", + "integrity": "sha512-1G99sWa40ylI1bX8VxUnvl3eEnT7C045t6HJ5UkYg+B6XoJouUgo/wrhFn53+kRBm9gT65YHBGs4gCLpk8b8dw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.34.0.tgz", + "integrity": "sha512-7+EcaPjG7PlnM/Oj2Cpf1tYFCXmABlO7P9uYNa/aiNVHv4oZklL2q1HT3fSsU/7nNa09IDKsl8yvQHhwfkNbWw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.34.0.tgz", + "integrity": "sha512-uiQfQESB5WLxVUwWe+/wiaujoT0we5tDm7fz3EwpgqKDsqA3Y/IobLsymIPp5/dfOmOUElB9dMUZUaDTrQeWtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.34.0.tgz", + "integrity": "sha512-08ChBq0X4U60B6ervmNDdSIjxleeCLrztbhul/cFFRcqUNj10F7ZLgz7/rucfxD80hE8Cf2PsbVK5e89qY7Y/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.34.0.tgz", + "integrity": "sha512-OVXEcQu9/FxUeSK1RGgvBzHNKQYjiYL536GahOzuptCyYjYUhrz+eopu7P0wIB/1irrMv33gCNt211inVnWaZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.34.0.tgz", + "integrity": "sha512-/9WFKdTDKVRs2JJh4oxF1gEbnlJcZtoIAH6yToTqftghal7NFLoHBGdp1Jo8b2m0Vn4L3yiDE/sAbrB0XgIAsw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.34.0.tgz", + "integrity": "sha512-eKO8BmgDSWl47SoKBtxj+XQjvn8SqXbpZ/NuZbcYkZVzpwWui4Ycqo76GRLrEhp+ykN3Nj9gl29nka3oenrk7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "packages-node/providence-analytics/node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.34.0.tgz", + "integrity": "sha512-7KWqCm7DmkFVd8MRMp14/HjxpYgWaj9k2E2pAQGOnExPCuzzF2Wji0qHcNzuMQMjTDKswjRkAOg6gk5b45JQRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "packages-node/providence-analytics/node_modules/oxc-parser": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.34.0.tgz", + "integrity": "sha512-k9PJKDD4+U3VzLic2Pup+N4YNIlBhzUkEWZP6yVkXtwEVZn1gIp1ixAt1e9+9EagzzAiY/Kx6EPEsZxNb3d1fg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-darwin-arm64": "0.34.0", + "@oxc-parser/binding-darwin-x64": "0.34.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.34.0", + "@oxc-parser/binding-linux-arm64-musl": "0.34.0", + "@oxc-parser/binding-linux-x64-gnu": "0.34.0", + "@oxc-parser/binding-linux-x64-musl": "0.34.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.34.0", + "@oxc-parser/binding-win32-x64-msvc": "0.34.0" } }, "packages-node/publish-docs": { @@ -28164,7 +28295,7 @@ }, "packages/ui": { "name": "@lion/ui", - "version": "0.7.9", + "version": "0.8.0", "license": "MIT", "dependencies": { "@bundled-es-modules/message-format": "^6.2.4", diff --git a/packages-node/providence-analytics/CHANGELOG.md b/packages-node/providence-analytics/CHANGELOG.md index e7e58506d..e8dd8339c 100644 --- a/packages-node/providence-analytics/CHANGELOG.md +++ b/packages-node/providence-analytics/CHANGELOG.md @@ -89,7 +89,7 @@ - bdb038e1: Many improvements: - rewritten from babel to swc - - swcTraverse tool, compatible with babel traverse api + - oxcTraverse tool, compatible with babel traverse api - increased performance - better windows compatibility diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/ChangeLog b/packages-node/providence-analytics/inlined-swc-to-babel/ChangeLog deleted file mode 100644 index aa4eff104..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/ChangeLog +++ /dev/null @@ -1,266 +0,0 @@ -2023.10.18, v3.0.1 - -fix: -- d37e664 types - -2023.10.17, v3.0.0 - -feature: -- 7aefd72 types -- aa0ef7b babel v8 -- be3c7a6 ImportAttributes -- f927c37 package: putout v32.2.2 -- 4a8b9e3 package: eslint-plugin-putout v20.0.0 - -2023.07.18, v2.2.0 - -fix: -- ce8a51a incorrect line number calculation and fix bug with JSX elements not having loc keys (#20) - -2023.07.12, v2.1.0 - -feature: -- 68dfe2d package: eslint-plugin-putout v18.1.0 -- a303550 package: c8 v8.0.0 -- 8e70e4c package: putout v30.4.0 -- 8f5df0a package: eslint-plugin-n v16.0.1 -- 19700e5 package: nodemon v3.0.1 - -2023.04.26, v2.0.0 - -fix: -- 606fd45 handle null element in holey arrays (#18) - -feature: -- 6a1e3a3 swc-to-babel: drop support of node < 16 -- ab3263e swc-to-babel: use @putout/printer -- d21f30e package: eslint-plugin-putout v17.5.1 -- e14d18c package: check-dts v0.7.1 -- a3cabd8 package: typescript v5.0.4 -- 48a0b6c package: putout v29.3.0 - -2022.10.12, v1.26.0 - -fix: -- swc-to-babel: parenthesized const assertion error (#15) - -feature: -- package: supertape v8.1.0 - -2022.08.24, v1.25.1 - -fix: -- getters/setters: make getters work properly and fix tests (#12) - -2022.08.23, v1.25.0 - -feature: -- make source an optional parameter (#10) - -2022.08.23, v1.24.0 - -feature: -- add type definitions (#8) - -2022.08.23, v1.23.0 - -feature: -- add support for GetterProperty and SetterProperty (#7) - -2022.08.21, v1.22.0 - -feature: -- add support of KeyValueProperty nodes - -2022.08.21, v1.21.0 - -feature: -- package: eslint-plugin-putout v16.0.1 -- package: putout v27.1.0 -- add support of TSKeywordType (close #1) - -2022.06.12, v1.20.1 - -fix: -- swc-to-babel: add directives - - -2022.06.12, v1.20.0 - -feature: -- swc-to-babel: add support of ExportDefaultDeclaration - - -2022.06.11, v1.19.0 - -feature: -- swc-to-babel: ObjectProperty instead of AssignmentPatternProperty - - -2022.06.11, v1.18.0 - -feature: -- swc-to-babel: add support of ClassExpression and ExportSpecifier - - -2022.06.11, v1.17.1 - -fix: -- swc-to-babl: ImportSpecifier - - -2022.06.11, v1.17.0 - -feature: -- swc-to-babel: add support of ObjectProperty - - -2022.06.11, v1.16.1 - -fix: -- swc-to-babel: ArrowFunctionExpression - - -2022.06.11, v1.16.0 - -feature: -- swc-to-babel: NewExpression: arguments field should always present - - -2022.06.11, v1.15.0 - -feature: -- swc-to-babel: add support of ArrayExpression - - -2022.06.11, v1.14.0 - -feature: -- swc-to-babel: add support of FunctionDeclaration - - -2022.06.10, v1.13.0 - -feature: -- swc-to-babel: TSAliasDeclaration - - -2022.06.10, v1.12.0 - -feature: -- swc-to-babel: handle typeParameters - - -2022.06.10, v1.11.2 - -feature: -- swc-to-babel: improve SpreadElement support - - -2022.06.10, v1.11.1 - -feature: -- swc-to-babel: improve support of SpreadElement - - -2022.06.10, v1.11.0 - -feature: -- swc-to-babel: add support of NewExpression - - -2022.06.10, v1.10.0 - -feature: -- swc-to-babel: improve support of MemberExpression - - -2022.06.10, v1.9.0 - -feature: -- swc-to-babel: add support of ClassDeclaration - - -2022.06.10, v1.8.0 - -feature: -- swc-to-babel: add support of ParenthesisExpression - - -2022.06.10, v1.7.1 - -feature: -- swc-to-babel: improve support of typeAnnotation in Identifiers -- swc-to-babel: add support of ExportNamedDeclaration and ExportDefaultDeclaration - - -2022.06.10, v1.7.0 - -feature: -- swc-to-babel: add support of ExportNamedDeclaration and ExportDefaultDeclaration - - -2022.06.09, v1.6.0 - -feature: -- swc-to-babel: CallExpression has no typeArguments -- swc-to-babel: TemplateElement - - -2022.06.09, v1.5.0 - -feature: -- swc-to-babel: TemplateElement -- package: eslint-plugin-putout v15.6.0 - - -2022.06.09, v1.4.0 - -feature: -- swc-to-babel: add support of typescript - - -2022.06.09, v1.3.1 - -fix: -- swc-to-babel: position - - -2022.06.09, v1.3.0 - -feature: -- swc-to-babel: add support of BlockStatement - - -2022.06.09, v1.2.0 - -feature: -- swc-to-babel: CallExpression - - -2022.06.09, v1.1.1 - -fix: -- swc-to-babel: no type - - -2022.06.09, v1.1.0 - -feature: -- (package) supertape v7.3.0 -- (package) putout v26.13.0 -- (package) madrun v9.0.4 -- swc-to-babel: add support of Identifier - - -2022.02.06, v1.0.2 - -feature: -- swc-to-babel: rm unused - - -2022.02.05, v1.0.1 - -fix: -- lint - diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/LICENSE b/packages-node/providence-analytics/inlined-swc-to-babel/LICENSE deleted file mode 100644 index eaec9a10f..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) coderaiser - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/README.md b/packages-node/providence-analytics/inlined-swc-to-babel/README.md deleted file mode 100644 index 69bcc7b4d..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Temp inline version of swc-to-babel, as we need to change a few things for 100% compatibility with our analyzers. -"version": "3.0.1", [swc-to-babel](http://github.com/coderaiser/swc-to-babel) diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/lib/get-ast.cjs b/packages-node/providence-analytics/inlined-swc-to-babel/lib/get-ast.cjs deleted file mode 100644 index c3f4e02d7..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/lib/get-ast.cjs +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -module.exports = ({tokens, ...program}) => { - const ast = { - type: 'File', - - program: { - ...program, - directives: [], - }, - - comments: [], - tokens, - }; - - return ast; -}; diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc-to-babel.cjs b/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc-to-babel.cjs deleted file mode 100644 index 628a55e26..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc-to-babel.cjs +++ /dev/null @@ -1,134 +0,0 @@ -'use strict'; - -const { types, traverse } = require('@putout/babel'); - -const { - convertModuleToProgram, - convertSpanToPosition, - convertVariableDeclarator, - convertStringLiteral, - convertIdentifier, - convertCallExpression, - convertObjectProperty, - BlockStatement, - TemplateElement, - convertTSTypeParameter, - convertExportDeclaration, - convertExportDefaultExpression, - convertParenthesisExpression, - convertGetterSetter, - ClassMethod, - ClassDeclaration, - ArrayExpression, - MemberExpression, - NewExpression, - Function, - ImportDeclaration, - ImportSpecifier, - ExportNamedDeclaration, - ExportDefaultDeclaration, - ExportSpecifier, - TSTypeAliasDeclaration, - TSMappedType, - TSTypeReference, - TSTypeOperator, - TSTypeParameter, - TSIndexedAccessType, - TSAsExpression, - JSXElement, - JSXFragment, -} = require('./swc/index.cjs'); - -const getAST = require('./get-ast.cjs'); -const { isIdentifier } = types; - -/** - * Convert an SWC ast to a babel ast - * @param ast {Module} SWC ast - * @param {string} [src=""] Source code - * @returns {ParseResult} Babel ast - */ -function toBabel(node, source = '') { - const ast = getAST(node); - - traverse(ast, { - noScope: true, - - BlockStatement, - TemplateElement, - ClassMethod, - ClassDeclaration, - ClassExpression: ClassDeclaration, - ArrayExpression, - MemberExpression, - NewExpression, - Function, - ImportDeclaration, - ImportSpecifier, - ExportNamedDeclaration, - ExportSpecifier, - ExportDefaultDeclaration, - - TSTypeAliasDeclaration, - TSMappedType, - TSTypeReference, - TSTypeOperator, - TSTypeParameter, - TSIndexedAccessType, - TSAsExpression, - - JSXElement, - JSXFragment, - - enter(path) { - const { node } = path; - const { type } = node; - - if ('span' in path.node) convertSpanToPosition(path, source); - - delete node.start; - delete node.end; - - if (type?.startsWith('Ts')) node.type = type.replace('Ts', 'TS'); - - if (type?.endsWith('Literal')) setEsprimaRaw(node); - - if (isIdentifier(path)) return convertIdentifier(path); - - if (path.isStringLiteral()) return convertStringLiteral(path); - - if (type === 'Module') return convertModuleToProgram(path); - - if (path.isVariableDeclarator()) return convertVariableDeclarator(path); - - if (path.isCallExpression()) return convertCallExpression(path); - - if (path.isTSTypeParameter()) return convertTSTypeParameter(path); - - if (path.type === 'ExportDeclaration') return convertExportDeclaration(path); - - if (path.type === 'ExportDefaultExpression') return convertExportDefaultExpression(path); - - if (path.type === 'ParenthesisExpression') return convertParenthesisExpression(path); - - if (/^(KeyValue|KeyValuePattern|AssignmentPattern)Property$/.test(path.type)) - return convertObjectProperty(path); - - if (path.type === 'GetterProperty' || path.type === 'SetterProperty') - return convertGetterSetter(path); - }, - }); - - return ast; -} - -module.exports = toBabel; - -function setEsprimaRaw(node) { - const { raw } = node; - - node.raw = raw || node.extra?.raw; - node.extra = node.extra || { - raw, - }; -} diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc/get-position-by-offset.cjs b/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc/get-position-by-offset.cjs deleted file mode 100644 index cbe2a0978..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc/get-position-by-offset.cjs +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -module.exports.getPositionByOffset = (offset, source) => { - let line = 1; - let column = 0; - - if (offset > source.length) - throw Error('end cannot be more then length ' + offset + ', ' + source.length); - - for (let i = 0; i < offset; i++) { - if (source[i] === '\n' && i !== offset - 1) { - line++; - column = 0; - } else { - column++; - } - } - - return { - line, - column, - index: offset - 1, - }; -}; diff --git a/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc/index.cjs b/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc/index.cjs deleted file mode 100644 index 86381b26d..000000000 --- a/packages-node/providence-analytics/inlined-swc-to-babel/lib/swc/index.cjs +++ /dev/null @@ -1,390 +0,0 @@ -'use strict'; - -const { getPositionByOffset } = require('./get-position-by-offset.cjs'); - -const isNull = a => !a && typeof a === 'object'; -const { assign } = Object; - -module.exports.convertModuleToProgram = path => { - path.node.type = 'Program'; - path.node.sourceType = 'module'; -}; - -module.exports.convertSpanToPosition = (path, source) => { - const { start, end } = path.node.span; - - delete path.node.span; - - if (end > source.length) - return assign(path.node, { - start, - end, - }); - - const startPosition = getPositionByOffset(start, source); - const endPosition = getPositionByOffset(end, source); - - assign(path.node, { - start: startPosition.index, - end: endPosition.index, - loc: { - start: startPosition, - end: endPosition, - }, - }); -}; - -module.exports.convertVariableDeclarator = path => { - delete path.parentPath.node.declare; - delete path.node.optional; - delete path.node.definite; -}; - -module.exports.convertStringLiteral = path => { - delete path.node.hasEscape; - delete path.node.kind; -}; - -module.exports.convertIdentifier = ({ node }) => { - convertIdentifier(node); -}; - -function convertIdentifier(node) { - const { typeAnnotation } = node; - - node.name = node.value; - - if (isNull(typeAnnotation)) { - delete node.typeAnnotation; - } - - delete node.value; - delete node.optional; - delete node.span; -} - -module.exports.convertCallExpression = path => { - const newArgs = []; - - for (const arg of path.node.arguments) { - newArgs.push(arg.expression); - } - - delete path.node.typeArguments; - path.node.arguments = newArgs; -}; - -module.exports.BlockStatement = path => { - path.node.body = path.node.stmts; - delete path.node.stmts; - path.node.directives = []; -}; - -module.exports.TSMappedType = path => { - path.node.typeParameter = path.node.typeParam; - - if (!path.node.nameType) path.node.nameType = null; - - if (!path.node.readonly) delete path.node.readonly; - - if (!path.node.optional) delete path.node.optional; - - delete path.node.typeParam; -}; - -module.exports.convertTSTypeParameter = path => { - convertIdentifier(path.node.name); -}; - -module.exports.TemplateElement = path => { - const { cooked, raw } = path.node; - - path.node.value = { - cooked, - raw, - }; - - delete path.node.cooked; - delete path.node.raw; - delete path.node.tail; -}; - -module.exports.convertExportDeclaration = path => { - path.node.type = 'ExportNamedDeclaration'; -}; - -module.exports.convertExportDefaultExpression = path => { - path.node.type = 'ExportDefaultDeclaration'; - path.node.declaration = path.node.expression; - - delete path.node.expression; - delete path.node.declare; -}; - -module.exports.convertParenthesisExpression = path => { - const expressionPath = path.get('expression'); - - if (expressionPath.type === 'TsAsExpression') convertTSAsExpression(expressionPath); - else if (expressionPath.type === 'TsConstAssertion') convertTSConstAssertion(expressionPath); - - path.replaceWith(expressionPath.node); -}; - -module.exports.ClassMethod = path => { - const { node } = path; - const { key } = path.node; - - Object.assign(node, { - ...path.node.function, - key, - }); - - if (node.kind === 'getter') { - node.kind = 'get'; - } - if (node.kind === 'setter') { - node.kind = 'set'; - } - - node.static = node.isStatic; - - delete path.node.isStatic; - delete path.node.accessibility; - delete path.node.isAbstract; - delete path.node.isOptional; - delete path.node.isOverride; - delete path.node.optional; - delete path.node.function; - delete path.node.decorators; - delete path.node.typeParameters; - delete path.node.returnType; - delete path.node.span; -}; - -module.exports.ClassDeclaration = path => { - path.node.id = path.node.identifier; - path.node.body = { - type: 'ClassBody', - body: path.node.body, - }; - - delete path.node.identifier; - delete path.node.declare; - delete path.node.decorators; - delete path.node.isAbstract; - delete path.node.typeParams; - delete path.node.superTypeParams; - delete path.node.implements; -}; - -module.exports.MemberExpression = ({ node }) => { - node.computed = node.property.type === 'Computed'; - - if (node.computed) node.property = node.property.expression; -}; - -function convertSpreadElement(node) { - const { expression } = node; - - assign(node, { - type: 'SpreadElement', - argument: expression, - }); - - delete node.spread; - delete node.expression; -} - -function maybeConvertSpread(arg) { - if (arg === null) return; - - const { spread } = arg; - - if (spread) { - convertSpreadElement(arg); - return; - } - - assign(arg, arg.expression); - - delete arg.spread; - delete arg.expression; -} - -module.exports.NewExpression = path => { - path.node.arguments = path.node.arguments || []; - path.node.arguments.forEach(maybeConvertSpread); - - delete path.node.typeArguments; -}; - -module.exports.ArrayExpression = path => { - path.node.elements.forEach(maybeConvertSpread); -}; - -module.exports.Function = path => { - const { node } = path; - - if (path.parentPath.isExportDefaultDeclaration()) path.node.type = 'FunctionDeclaration'; - - const { params, typeParameters } = node; - - node.id = node.identifier || null; - - delete node.identifier; - delete node.decorators; - - if (!node.returnType) delete node.returnType; - - for (const [index, param] of params.entries()) { - if (param.type === 'Parameter') params[index] = param.pat; - } - - if (isNull(typeParameters)) delete node.typeParameters; - - delete node.declare; -}; - -module.exports.TSTypeAliasDeclaration = path => { - delete path.node.declare; - delete path.node.typeParams; -}; - -module.exports.TSAsExpression = convertTSAsExpression; -function convertTSAsExpression({ node }) { - node.type = 'TSAsExpression'; - - if (node.typeAnnotation.kind === 'any') - assign(node.typeAnnotation, { - type: 'TSAnyKeyword', - }); -} - -module.exports.TSConstAssertion = convertTSConstAssertion; -function convertTSConstAssertion({ node }) { - assign(node, { - type: 'TSAsExpression', - extra: { - parenthesized: true, - parenStart: 0, - }, - typeAnnotation: { - type: 'TSTypeReference', - typeName: { - type: 'Identifier', - name: 'const', - }, - }, - }); -} - -module.exports.TSTypeReference = path => { - delete path.node.typeParams; -}; - -module.exports.TSTypeOperator = path => { - path.node.operator = path.node.op; - - delete path.node.op; -}; - -module.exports.TSTypeParameter = path => { - path.node.name = path.node.name.name; - - delete path.node.in; - delete path.node.out; - delete path.node.default; -}; - -module.exports.TSIndexedAccessType = path => { - delete path.node.readonly; -}; - -module.exports.ImportDeclaration = ({ node }) => { - const { typeOnly } = node; - - node.assertions = node.asserts?.properties || []; - node.importKind = typeOnly ? 'type' : 'value'; - - delete node.asserts; - delete node.typeOnly; -}; - -module.exports.ImportSpecifier = ({ node }) => { - if (!node.imported) - node.imported = { - ...node.local, - }; - - delete node.isTypeOnly; -}; - -module.exports.convertObjectProperty = path => { - const { node } = path; - - node.type = 'ObjectProperty'; - node.shorthand = !node.value; - - if (!node.value) - node.value = { - ...node.key, - }; - - delete path.parentPath.node.optional; -}; - -module.exports.convertGetterSetter = ({ node }) => { - node.kind = node.type === 'GetterProperty' ? 'get' : 'set'; - node.type = 'ObjectMethod'; - node.params = node.param ? [node.param] : []; - - delete node.param; -}; - -module.exports.ExportDefaultDeclaration = ({ node }) => { - // node.declaration may have been already provided by convertExportDefaultExpression - node.declaration = node.declaration || node.decl; - node.exportKind = 'value'; - node.assertions = node.asserts?.properties || []; - - delete node.decl; -}; - -module.exports.ExportNamedDeclaration = ({ node }) => { - const { typeOnly } = node; - - node.assertions = node.asserts?.properties || []; - // node.source = null; - node.specifiers = node.specifiers || []; - - node.exportKind = typeOnly ? 'type' : 'value'; - - delete node.asserts; - delete node.typeOnly; -}; - -module.exports.ExportSpecifier = ({ node }) => { - const { orig, exported } = node; - - node.local = orig; - node.exported = exported || { - ...orig, - }; - - delete node.isTypeOnly; - delete node.orig; -}; - -module.exports.JSXElement = path => { - path.node.openingElement = path.node.opening; - delete path.node.opening; - path.node.closingElement = path.node.closing; - delete path.node.closing; -}; - -module.exports.JSXFragment = path => { - path.node.openingFragment = path.node.opening; - delete path.node.opening; - path.node.closingFragment = path.node.closing; - delete path.node.closing; -}; diff --git a/packages-node/providence-analytics/package.json b/packages-node/providence-analytics/package.json index 9effde363..cc7a532a7 100644 --- a/packages-node/providence-analytics/package.json +++ b/packages-node/providence-analytics/package.json @@ -23,7 +23,6 @@ "providence": "./src/cli/index.js" }, "files": [ - "inlined-swc-to-babel", "src", "types" ], @@ -38,18 +37,22 @@ "test:node:unit": "mocha './{test-node,src}/**/*.test.js'" }, "dependencies": { - "@babel/parser": "^7.25.8", - "@babel/plugin-syntax-import-assertions": "^7.25.7", "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.8", - "@putout/babel": "^2.8.0", "@rollup/plugin-node-resolve": "^15.3.0", - "@swc/core": "^1.7.36", "commander": "^2.20.3", + "oxc-parser": "^0.34.0", "parse5": "^7.2.0", "semver": "^7.6.3" }, + "peerDependencies": { + "@babel/parser": "^7.25.8", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@swc/core": "^1.7.36" + }, "devDependencies": { + "@babel/parser": "^7.25.8", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@swc/core": "^1.7.36", "@types/inquirer": "^9.0.7", "@types/mocha": "^10.0.9", "@web/dev-server": "^0.4.6", diff --git a/packages-node/providence-analytics/src/program/analyzers/find-classes.js b/packages-node/providence-analytics/src/program/analyzers/find-classes.js index 9d5d6be23..3afe22fe1 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-classes.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-classes.js @@ -1,10 +1,9 @@ /* eslint-disable no-shadow, no-param-reassign */ import path from 'path'; -import babelTraverse from '@babel/traverse'; -import t from '@babel/types'; +import { oxcTraverse, isProperty } from '../utils/oxc-traverse.js'; -import { trackDownIdentifierFromScope } from '../utils/track-down-identifier--legacy.js'; +import { trackDownIdentifierFromScope } from '../utils/track-down-identifier.js'; import { Analyzer } from '../core/Analyzer.js'; /** @@ -16,6 +15,7 @@ import { Analyzer } from '../core/Analyzer.js'; * @typedef {import('../../../types/index.js').FindClassesAnalyzerOutputFile} FindClassesAnalyzerOutputFile * @typedef {import('../../../types/index.js').FindClassesAnalyzerEntry} FindClassesAnalyzerEntry * @typedef {import('../../../types/index.js').FindClassesConfig} FindClassesConfig + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst */ /** @@ -132,55 +132,58 @@ async function findMembersPerAstEntry(babelAst, fullCurrentFilePath, projectPath methods: [], }; - astPath.traverse({ - ClassMethod(astPath) { - // if (isBlacklisted(astPath)) { - // return; - // } - if (isStaticProperties(astPath)) { - let hasFoundTopLvlObjExpr = false; - astPath.traverse({ - ObjectExpression(astPath) { - if (hasFoundTopLvlObjExpr) return; - hasFoundTopLvlObjExpr = true; - astPath.node.properties.forEach(objectProperty => { - if (!t.isProperty(objectProperty)) { - // we can also have a SpreadElement - return; - } - const propRes = {}; - const { name } = objectProperty.key; - propRes.name = name; - propRes.accessType = computeAccessType(name); - propRes.kind = [...(propRes.kind || []), objectProperty.kind]; - classRes.members.props.push(propRes); - }); - }, - }); - return; + const handleMethodDefinitionOrClassMethod = astPath => { + // if (isBlacklisted(astPath)) { + // return; + // } + if (isStaticProperties(astPath)) { + let hasFoundTopLvlObjExpr = false; + astPath.traverse({ + ObjectExpression(astPath) { + if (hasFoundTopLvlObjExpr) return; + hasFoundTopLvlObjExpr = true; + astPath.node.properties.forEach(objectProperty => { + if (!isProperty(objectProperty)) { + // we can also have a SpreadElement + return; + } + const propRes = {}; + const { name } = objectProperty.key; + propRes.name = name; + propRes.accessType = computeAccessType(name); + propRes.kind = [...(propRes.kind || []), objectProperty.kind]; + classRes.members.props.push(propRes); + }); + }, + }); + return; + } + + const methodRes = {}; + const { name } = astPath.node.key; + methodRes.name = name; + methodRes.accessType = computeAccessType(name); + + if (astPath.node.kind === 'set' || astPath.node.kind === 'get') { + if (astPath.node.static) { + methodRes.static = true; } - - const methodRes = {}; - const { name } = astPath.node.key; - methodRes.name = name; - methodRes.accessType = computeAccessType(name); - - if (astPath.node.kind === 'set' || astPath.node.kind === 'get') { - if (astPath.node.static) { - methodRes.static = true; - } - methodRes.kind = [...(methodRes.kind || []), astPath.node.kind]; - // Merge getter/setters into one - const found = classRes.members.props.find(p => p.name === name); - if (found) { - found.kind = [...(found.kind || []), astPath.node.kind]; - } else { - classRes.members.props.push(methodRes); - } + methodRes.kind = [...(methodRes.kind || []), astPath.node.kind]; + // Merge getter/setters into one + const found = classRes.members.props.find(p => p.name === name); + if (found) { + found.kind = [...(found.kind || []), astPath.node.kind]; } else { - classRes.members.methods.push(methodRes); + classRes.members.props.push(methodRes); } - }, + } else { + classRes.members.methods.push(methodRes); + } + }; + + astPath.traverse({ + ClassMethod: handleMethodDefinitionOrClassMethod, + MethodDefinition: handleMethodDefinitionOrClassMethod, }); classesFound.push(classRes); @@ -188,7 +191,7 @@ async function findMembersPerAstEntry(babelAst, fullCurrentFilePath, projectPath const classesToTraverse = []; - babelTraverse.default(babelAst, { + oxcTraverse(babelAst, { ClassDeclaration(astPath) { classesToTraverse.push({ astPath, isMixin: false }); }, @@ -228,8 +231,8 @@ export default class FindClassesAnalyzer extends Analyzer { /** @type {AnalyzerName} */ static analyzerName = 'find-classes'; - /** @type {'babel'|'swc-to-babel'} */ - static requiredAst = 'babel'; + /** @type {AnalyzerAst} */ + static requiredAst = 'oxc'; /** * Will find all public members (properties (incl. getter/setters)/functions) of a class and diff --git a/packages-node/providence-analytics/src/program/analyzers/find-customelements.js b/packages-node/providence-analytics/src/program/analyzers/find-customelements.js index 3a19654b5..9f3e9a2a0 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-customelements.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-customelements.js @@ -1,9 +1,9 @@ import path from 'path'; -import babelTraverse from '@babel/traverse'; -import t from '@babel/types'; +// import babelTraverse from '@babel/traverse'; +import { oxcTraverse } from '../utils/oxc-traverse.js'; -import { trackDownIdentifierFromScope } from '../utils/track-down-identifier--legacy.js'; +import { trackDownIdentifierFromScope } from '../utils/track-down-identifier.js'; import { Analyzer } from '../core/Analyzer.js'; /** @@ -40,27 +40,29 @@ async function trackdownRoot(transformedEntry, relativePath, projectPath) { /** * Finds import specifiers and sources - * @param {File} babelAst + * @param {File} oxcAst */ -function findCustomElementsPerAstFile(babelAst) { +function findCustomElementsPerAstFile(oxcAst) { const definitions = []; - babelTraverse.default(babelAst, { + oxcTraverse(oxcAst, { CallExpression(astPath) { let found = false; // Doing it like this we detect 'customElements.define()', // but also 'window.customElements.define()' astPath.traverse({ - MemberExpression(memberPath) { - if (memberPath.parentPath !== astPath) { + // MemberExpression in babel + StaticMemberExpression(memberPath) { + if (memberPath.node !== astPath.node.callee) { return; } + const { node } = memberPath; + if (node.object.name === 'customElements' && node.property.name === 'define') { found = true; } if ( - node.object.object && - node.object.object.name === 'window' && + node.object.object?.name === 'window' && node.object.property.name === 'customElements' && node.property.name === 'define' ) { @@ -72,7 +74,7 @@ function findCustomElementsPerAstFile(babelAst) { let tagName; let constructorIdentifier; - if (t.isLiteral(astPath.node.arguments[0])) { + if (astPath.node.arguments[0].type === 'StringLiteral') { tagName = astPath.node.arguments[0].value; } else { // No Literal found. For now, we only mark them as '[variable]' @@ -95,8 +97,8 @@ export default class FindCustomelementsAnalyzer extends Analyzer { /** @type {AnalyzerName} */ static analyzerName = 'find-customelements'; - /** @type {'babel'|'swc-to-babel'} */ - static requiredAst = 'swc-to-babel'; + /** @type {AnalyzerAst} */ + static requiredAst = 'oxc'; /** * Finds export specifiers and sources diff --git a/packages-node/providence-analytics/src/program/analyzers/find-exports.js b/packages-node/providence-analytics/src/program/analyzers/find-exports.js index 19ab5968d..c4cae7f31 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-exports.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-exports.js @@ -5,7 +5,7 @@ import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-d import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; import { trackDownIdentifier } from '../utils/track-down-identifier.js'; import { getAssertionType } from '../utils/get-assertion-type.js'; -import { swcTraverse } from '../utils/swc-traverse.js'; +import { oxcTraverse } from '../utils/oxc-traverse.js'; import { LogService } from '../core/LogService.js'; import { Analyzer } from '../core/Analyzer.js'; @@ -110,20 +110,25 @@ function cleanup(transformedFile) { function getExportSpecifiers(node) { // handles default [export const g = 4]; if (node.declaration?.declarations) { - return [node.declaration.declarations[0].id.value]; + return [node.declaration.declarations[0].id.value || node.declaration.declarations[0].id.name]; } if (node.declaration?.identifier) { - return [node.declaration.identifier.value]; + return [node.declaration.identifier.value || node.declaration.identifier.name]; + } + if (node.declaration?.id) { + return [node.declaration.id.value || node.declaration.id.name]; } // handles (re)named specifiers [export { x (as y)} from 'y']; return (node.specifiers || []).map(s => { if (s.exported) { // { x as y } - return s.exported.value === 'default' ? '[default]' : s.exported.value; + return (s.exported.value || s.exported.name) === 'default' + ? '[default]' + : s.exported.value || s.exported.name; } // { x } - return s.orig.value; + return s.orig.value || s.local.name; }); } @@ -133,11 +138,18 @@ function getExportSpecifiers(node) { function getLocalNameSpecifiers(node) { return (node.declaration?.declarations || node.specifiers || []) .map(s => { - if (s.exported && s.orig && s.exported.value !== s.orig.value) { + if ( + s.exported && + (s.orig || s.local) && + (s.exported.value || s.exported.name) !== (s.orig?.value || s.local?.name) + ) { return { // if reserved keyword 'default' is used, translate it into 'providence keyword' - local: s.orig.value === 'default' ? '[default]' : s.orig.value, - exported: s.exported.value, + local: + (s.orig?.value || s.local?.name) === 'default' + ? '[default]' + : s.orig?.value || s.local?.name, + exported: s.exported.value || s.exported.name, }; } return undefined; @@ -150,10 +162,10 @@ const isImportingSpecifier = pathOrNode => /** * Finds import specifiers and sources for a given ast result - * @param {SwcAstModule} swcAst + * @param {SwcAstModule} oxcAst * @param {FindExportsConfig} config */ -function findExportsPerAstFile(swcAst, { skipFileImports }) { +function findExportsPerAstFile(oxcAst, { skipFileImports }) { LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`); // Visit AST... @@ -169,7 +181,7 @@ function findExportsPerAstFile(swcAst, { skipFileImports }) { const exportHandler = (/** @type {SwcPath} */ astPath) => { const exportSpecifiers = getExportSpecifiers(astPath.node); const localMap = getLocalNameSpecifiers(astPath.node); - const source = astPath.node.source?.value; + const source = astPath.node.source?.value || astPath.node.source?.name; const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } }; const assertionType = getAssertionType(astPath.node); if (assertionType) { @@ -180,15 +192,18 @@ function findExportsPerAstFile(swcAst, { skipFileImports }) { const exportDefaultHandler = (/** @type {SwcPath} */ astPath) => { const exportSpecifiers = ['[default]']; + const { node } = astPath; let source; // Is it an inline declaration like "export default class X {};" ? if ( - astPath.node.decl?.type === 'Identifier' || - astPath.node.expression?.type === 'Identifier' + node.decl?.type === 'Identifier' || + node.expression?.type === 'Identifier' || + node.declaration?.type === 'Identifier' ) { // It is a reference to an identifier like "export { x } from 'y';" const importOrDeclPath = getReferencedDeclaration({ - referencedIdentifierName: astPath.node.decl?.value || astPath.node.expression.value, + referencedIdentifierName: + node.decl?.value || node.expression?.value || node.declaration?.name, globalScopeBindings, }); if (isImportingSpecifier(importOrDeclPath)) { @@ -198,18 +213,23 @@ function findExportsPerAstFile(swcAst, { skipFileImports }) { transformedFile.push({ exportSpecifiers, source, __tmp: { astPath } }); }; + const globalScopeHandler = ({ scope }) => { + globalScopeBindings = scope.bindings; + }; + /** @type {SwcVisitor} */ const visitor = { - Module({ scope }) { - globalScopeBindings = scope.bindings; - }, + // for swc + Module: globalScopeHandler, + // for oxc and babel + Program: globalScopeHandler, ExportDeclaration: exportHandler, ExportNamedDeclaration: exportHandler, ExportDefaultDeclaration: exportDefaultHandler, ExportDefaultExpression: exportDefaultHandler, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); if (!skipFileImports) { // Always add an entry for just the file 'relativePath' @@ -226,7 +246,7 @@ function findExportsPerAstFile(swcAst, { skipFileImports }) { export default class FindExportsAnalyzer extends Analyzer { static analyzerName = /** @type {AnalyzerName} */ ('find-exports'); - static requiredAst = /** @type {AnalyzerAst} */ ('swc'); + static requiredAst = /** @type {AnalyzerAst} */ ('oxc'); /** * @typedef FindExportsConfig diff --git a/packages-node/providence-analytics/src/program/analyzers/find-imports.js b/packages-node/providence-analytics/src/program/analyzers/find-imports.js index 226f8ce09..c4a2af9cd 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-imports.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-imports.js @@ -2,7 +2,7 @@ import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; import { isRelativeSourcePath } from '../utils/relative-source-path.js'; import { getAssertionType } from '../utils/get-assertion-type.js'; -import { swcTraverse } from '../utils/swc-traverse.js'; +import { oxcTraverse } from '../utils/oxc-traverse.js'; import { LogService } from '../core/LogService.js'; import { Analyzer } from '../core/Analyzer.js'; @@ -13,49 +13,80 @@ import { Analyzer } from '../core/Analyzer.js'; * @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst - * @typedef {import("@swc/core").Module} SwcAstModule + * @typedef {import("@swc/core").Module} oxcAstModule * @typedef {import("@swc/core").Node} SwcNode */ +// const exportedNames = ['exported']; +// const importedNames = ['orig', 'imported', 'local']; +// const valueNames = ['name', 'value']; + +/** + * Intends to work for oxc, swc, and babel asts + */ +function getSpecifierValue(s) { + // for (const exportedorImportedName of [...exportedNames, ...importedNames]) { + // for (const valueName of valueNames) { + // const result = s[exportedorImportedName][valueName]; + // if (result) return result; + // } + // } + // return undefined; + + return ( + // These are regular import values and must be checked first + s.imported?.value || + s.imported?.name || + s.orig?.value || + s.orig?.name || + s.local?.value || + s.local?.name || + // Re-export + s.exported?.value || + s.exported?.name + ); +} + /** * @param {SwcNode} node */ function getImportOrReexportsSpecifiers(node) { // @ts-expect-error - return node.specifiers.map(s => { + return (node.specifiers || []).map(s => { if ( s.type === 'ImportDefaultSpecifier' || s.type === 'ExportDefaultSpecifier' || - (s.type === 'ExportSpecifier' && s.exported?.value === 'default') + (s.type === 'ExportSpecifier' && + (s.exported?.value === 'default' || s.exported?.name === 'default')) ) { return '[default]'; } if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') { return '[*]'; } - const importedValue = s.imported?.value || s.orig?.value || s.exported?.value || s.local?.value; + const importedValue = getSpecifierValue(s); return importedValue; }); } /** * Finds import specifiers and sources - * @param {SwcAstModule} swcAst + * @param {oxcAstModule} oxcAst */ -function findImportsPerAstFile(swcAst) { +function findImportsPerAstFile(oxcAst) { LogService.debug(`Analyzer "find-imports": started findImportsPerAstFile method`); // https://github.com/babel/babel/blob/672a58660f0b15691c44582f1f3fdcdac0fa0d2f/packages/babel-core/src/transformation/index.ts#L110 // Visit AST... /** @type {Partial[]} */ const transformedFile = []; - - swcTraverse(swcAst, { + oxcTraverse(oxcAst, { ImportDeclaration({ node }) { const importSpecifiers = getImportOrReexportsSpecifiers(node); if (!importSpecifiers.length) { importSpecifiers.push('[file]'); // apparently, there was just a file import } + const source = node.source.value; const entry = /** @type {Partial} */ ({ importSpecifiers, source }); const assertionType = getAssertionType(node); @@ -65,9 +96,9 @@ function findImportsPerAstFile(swcAst) { transformedFile.push(entry); }, ExportNamedDeclaration({ node }) { - if (!node.source) { - return; // we are dealing with a regular export, not a reexport - } + // Are we dealing with a regular export, not a re-export? + if (!node.source) return; + const importSpecifiers = getImportOrReexportsSpecifiers(node); const source = node.source.value; const entry = /** @type {Partial} */ ({ importSpecifiers, source }); @@ -77,7 +108,22 @@ function findImportsPerAstFile(swcAst) { } transformedFile.push(entry); }, - // Dynamic imports + ExportAllDeclaration({ node }) { + // Are we dealing with a regular export, not a re-export? + if (!node.source) return; + + const importSpecifiers = ['[*]']; + + const source = node.source.value; + const entry = /** @type {Partial} */ ({ importSpecifiers, source }); + const assertionType = getAssertionType(node); + if (assertionType) { + entry.assertionType = assertionType; + } + transformedFile.push(entry); + }, + // Dynamic imports for swc + // TODO: remove if swc is completely phased out CallExpression({ node }) { if (node.callee?.type !== 'Import') { return; @@ -92,6 +138,21 @@ function findImportsPerAstFile(swcAst) { : '[variable]'; transformedFile.push({ importSpecifiers, source }); }, + // Dynamic imports for oxc + + ExpressionStatement({ node }) { + if (node.expression.type !== 'ImportExpression') return; + + // TODO: check for specifiers catched via obj destructuring? + // TODO: also check for ['file'] + const importSpecifiers = ['[default]']; + const dynamicImportExpression = node.expression; + const source = + dynamicImportExpression.source?.type === 'StringLiteral' + ? dynamicImportExpression.source.value + : '[variable]'; + transformedFile.push({ importSpecifiers, source }); + }, }); return transformedFile; @@ -100,7 +161,7 @@ function findImportsPerAstFile(swcAst) { export default class FindImportsSwcAnalyzer extends Analyzer { static analyzerName = /** @type {AnalyzerName} */ ('find-imports'); - static requiredAst = /** @type {AnalyzerAst} */ ('swc'); + static requiredAst = /** @type {AnalyzerAst} */ ('oxc'); /** * Finds import specifiers and sources @@ -132,9 +193,9 @@ export default class FindImportsSwcAnalyzer extends Analyzer { /** * Traverse */ - const queryOutput = await this._traverse(async (swcAst, context) => { + const queryOutput = await this._traverse(async (oxcAst, context) => { // @ts-expect-error - let transformedFile = findImportsPerAstFile(swcAst); + let transformedFile = findImportsPerAstFile(oxcAst); // Post processing based on configuration... transformedFile = await normalizeSourcePaths( transformedFile, diff --git a/packages-node/providence-analytics/src/program/analyzers/match-imports.js b/packages-node/providence-analytics/src/program/analyzers/match-imports.js index 747c4e192..50dab503a 100644 --- a/packages-node/providence-analytics/src/program/analyzers/match-imports.js +++ b/packages-node/providence-analytics/src/program/analyzers/match-imports.js @@ -159,7 +159,8 @@ async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerRes export default class MatchImportsAnalyzer extends Analyzer { static analyzerName = /** @type {AnalyzerName} */ ('match-imports'); - static requiredAst = /** @type {AnalyzerAst} */ ('swc'); + // N.B. implicit + static requiredAst = /** @type {AnalyzerAst} */ ('oxc'); static requiresReference = true; diff --git a/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js b/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js index 83f336f65..bc3d612f3 100644 --- a/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js +++ b/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js @@ -74,7 +74,7 @@ function storeResult(resultsObj, exportId, filteredList, meta) { * @param {FindClassesAnalyzerResult} targetClassesAnalyzerResult * @param {FindClassesAnalyzerResult} refClassesAResult * @param {MatchSubclassesConfig} customConfig - * @returns {AnalyzerQueryResult} + * @returns {Promise} */ async function matchSubclassesPostprocess( refExportsAnalyzerResult, @@ -281,7 +281,7 @@ export default class MatchSubclassesAnalyzer extends Analyzer { /** @type {AnalyzerName} */ static analyzerName = 'match-subclasses'; - static requiredAst = /** @type {AnalyzerAst} */ ('babel'); + static requiredAst = /** @type {AnalyzerAst} */ ('oxc'); static requiresReference = true; diff --git a/packages-node/providence-analytics/src/program/core/AstService.js b/packages-node/providence-analytics/src/program/core/AstService.js index 40337867e..2322fcef6 100644 --- a/packages-node/providence-analytics/src/program/core/AstService.js +++ b/packages-node/providence-analytics/src/program/core/AstService.js @@ -1,15 +1,21 @@ -import babelParser from '@babel/parser'; import * as parse5 from 'parse5'; -import swc from '@swc/core'; import { traverseHtml } from '../utils/traverse-html.js'; import { LogService } from './LogService.js'; -import { guardedSwcToBabel } from '../utils/guarded-swc-to-babel.js'; + +/** @type {import('@babel/parser')} */ +let babelParser; +/** @type {import('@swc/core')} */ +let swcParser; +/** @type {import('oxc-parser')} */ +let oxcParser; /** - * @typedef {import("@babel/types").File} File - * @typedef {import("@swc/core").Module} SwcAstModule - * @typedef {import("@babel/parser").ParserOptions} ParserOptions * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst + * @typedef {import("oxc-parser").ParseResult} OxcParseResult + * @typedef {import("@babel/parser").ParserOptions} ParserOptions + * @typedef {import("@swc/core").Module} SwcAstModule + * @typedef {import("@babel/types").File} File */ export class AstService { @@ -17,9 +23,13 @@ export class AstService { * Compiles an array of file paths using Babel. * @param {string} code * @param {ParserOptions} parserOptions - * @returns {File} + * @returns {Promise} */ - static _getBabelAst(code, parserOptions = {}) { + static async _getBabelAst(code, parserOptions = {}) { + if (!babelParser) { + babelParser = (await import('@babel/parser')).default; + } + const ast = babelParser.parse(code, { sourceType: 'module', plugins: [ @@ -34,32 +44,18 @@ export class AstService { return ast; } - /** - * Compiles an array of file paths using Babel. - * @param {string} code - * @param {ParserOptions} parserOptions - * @returns {File} - */ - static _getSwcToBabelAst(code, parserOptions = {}) { - if (this.fallbackToBabel) { - return this._getBabelAst(code, parserOptions); - } - const ast = swc.parseSync(code, { - syntax: 'typescript', - // importAssertions: true, - ...parserOptions, - }); - return guardedSwcToBabel(ast, code); - } - /** * Compiles an array of file paths using swc. * @param {string} code * @param {ParserOptions} parserOptions - * @returns {SwcAstModule} + * @returns {Promise} */ - static _getSwcAst(code, parserOptions = {}) { - const ast = swc.parseSync(code, { + static async _getSwcAst(code, parserOptions = {}) { + if (!swcParser) { + swcParser = (await import('@swc/core')).default; + } + + const ast = swcParser.parseSync(code, { syntax: 'typescript', target: 'es2022', ...parserOptions, @@ -72,7 +68,22 @@ export class AstService { * @returns {number} */ static _getSwcOffset() { - return swc.parseSync('').span.end; + return swcParser.parseSync('').span.end; + } + + /** + * Compiles an array of file paths using swc. + * @param {string} code + * @param {ParserOptions} parserOptions + * @returns {Promise} + */ + static async _getOxcAst(code, parserOptions = {}) { + if (!oxcParser) { + // eslint-disable-next-line import/no-extraneous-dependencies + oxcParser = (await import('oxc-parser')).default; + } + + return oxcParser.parseSync(code, parserOptions).program; } /** @@ -100,22 +111,22 @@ export class AstService { /** * Returns the Babel AST * @param { string } code - * @param { 'babel'|'swc-to-babel'|'swc'} astType + * @param {AnalyzerAst} astType * @param { {filePath?: PathFromSystemRoot} } options - * @returns {File|undefined|SwcAstModule} + * @returns {Promise} */ // eslint-disable-next-line consistent-return - static getAst(code, astType, { filePath } = {}) { + static async getAst(code, astType, { filePath } = {}) { // eslint-disable-next-line default-case try { if (astType === 'babel') { - return this._getBabelAst(code); - } - if (astType === 'swc-to-babel') { - return this._getSwcToBabelAst(code); + return await this._getBabelAst(code); } if (astType === 'swc') { - return this._getSwcAst(code); + return await this._getSwcAst(code); + } + if (astType === 'oxc') { + return await this._getOxcAst(code); } throw new Error(`astType "${astType}" not supported.`); } catch (e) { diff --git a/packages-node/providence-analytics/src/program/core/QueryService.js b/packages-node/providence-analytics/src/program/core/QueryService.js index 24a4e1d82..b9f92adaa 100644 --- a/packages-node/providence-analytics/src/program/core/QueryService.js +++ b/packages-node/providence-analytics/src/program/core/QueryService.js @@ -107,20 +107,27 @@ export class QueryService { * @param {AnalyzerAst} requiredAst */ static async addAstToProjectsData(projectsData, requiredAst) { - return projectsData.map(projectData => { + const resultWithAsts = []; + + for (const projectData of projectsData) { const cachedData = astProjectsDataCache.get(projectData.project.path); if (cachedData) { return cachedData; } - const resultEntries = projectData.entries.map(entry => { - const ast = AstService.getAst(entry.context.code, requiredAst, { filePath: entry.file }); - return { ...entry, ast }; - }); + const resultEntries = []; + for (const entry of projectData.entries) { + const ast = await AstService.getAst(entry.context.code, requiredAst, { + filePath: entry.file, + }); + resultEntries.push({ ...entry, ast }); + } const astData = { ...projectData, entries: resultEntries }; this._addToProjectsDataCache(`${projectData.project.path}#${requiredAst}`, astData); - return astData; - }); + resultWithAsts.push(astData); + } + + return resultWithAsts; } /** diff --git a/packages-node/providence-analytics/src/program/utils/get-assertion-type.js b/packages-node/providence-analytics/src/program/utils/get-assertion-type.js index 811798e97..214042d5e 100644 --- a/packages-node/providence-analytics/src/program/utils/get-assertion-type.js +++ b/packages-node/providence-analytics/src/program/utils/get-assertion-type.js @@ -10,5 +10,9 @@ export function getAssertionType(node) { if (node.assertions) { return node.assertions.properties[0].value?.value; } + // oxc + if (node.withClause) { + return node.withClause.withEntries[0].value?.name; + } return undefined; } diff --git a/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration--legacy.js b/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration--legacy.js deleted file mode 100644 index 0f58e62e5..000000000 --- a/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration--legacy.js +++ /dev/null @@ -1,190 +0,0 @@ -import path from 'path'; - -import babelTraversePkg from '@babel/traverse'; - -import { trackDownIdentifier } from './track-down-identifier.js'; -import { AstService } from '../core/AstService.js'; -import { toPosixPath } from './to-posix-path.js'; -import { fsAdapter } from './fs-adapter.js'; - -/** - * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot - * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot - * @typedef {import('@babel/traverse').NodePath} NodePath - * @typedef {import('@babel/types').Node} Node - */ - -/** - * @param {{rootPath:PathFromSystemRoot; localPath:PathRelativeFromProjectRoot}} opts - * @returns - */ -export function getFilePathOrExternalSource({ rootPath, localPath }) { - if (!localPath.startsWith('.')) { - // We are not resolving external files like '@lion/input-amount/x.js', - // but we give a 100% score if from and to are same here.. - return localPath; - } - return toPosixPath(path.resolve(rootPath, localPath)); -} - -/** - * Assume we had: - * ```js - * const x = 88; - * const y = x; - * export const myIdentifier = y; - * ``` - * - We started in getSourceCodeFragmentOfDeclaration (looking for 'myIdentifier'), which found VariableDeclarator of export myIdentifier - * - getReferencedDeclaration is called with { referencedIdentifierName: 'y', ... } - * - now we will look in globalScopeBindings, till we find declaration of 'y' - * - Is it a ref? Call ourselves with referencedIdentifierName ('x' in example above) - * - is it a non ref declaration? Return the path of the node - * @param {{ referencedIdentifierName:string, globalScopeBindings:BabelBinding; }} opts - * @returns {NodePath} - */ -export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) { - const [, refDeclaratorBinding] = Object.entries(globalScopeBindings).find( - ([key]) => key === referencedIdentifierName, - ); - - if ( - refDeclaratorBinding.path.type === 'ImportSpecifier' || - refDeclaratorBinding.path.type === 'ImportDefaultSpecifier' - ) { - return refDeclaratorBinding.path; - } - - if (refDeclaratorBinding.path.node.init.type === 'Identifier') { - return getReferencedDeclaration({ - referencedIdentifierName: refDeclaratorBinding.path.node.init.name, - globalScopeBindings, - }); - } - - return refDeclaratorBinding.path.get('init'); -} - -/** - * @example - * ```js - * // ------ input file -------- - * const x = 88; - * const y = x; - * export const myIdentifier = y; - * // -------------------------- - * - * await getSourceCodeFragmentOfDeclaration(code) // finds "88" - * ``` - * - * @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts - * @returns {Promise<{ sourceNodePath: string; sourceFragment: string|null; externalImportSource: string; }>} - */ -export async function getSourceCodeFragmentOfDeclaration({ - filePath, - exportedIdentifier, - projectRootPath, -}) { - const code = fsAdapter.fs.readFileSync(filePath, 'utf8'); - // TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst - const babelAst = AstService.getAst(code, 'babel', { filePath }); - - /** @type {NodePath} */ - let finalNodePath; - - babelTraversePkg.default(babelAst, { - Program(astPath) { - astPath.stop(); - - // Situations - // - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' ) - // - declared right away (for instance a class) - // - referenced (possibly recursively) by other declaration - // - Identifier is part of a named export - // - declared right away - // - referenced (possibly recursively) by other declaration - - const globalScopeBindings = astPath.get('body')[0].scope.bindings; - - if (exportedIdentifier === '[default]') { - const defaultExportPath = astPath - .get('body') - .find(child => child.node.type === 'ExportDefaultDeclaration'); - // @ts-expect-error - const isReferenced = defaultExportPath?.node.declaration?.type === 'Identifier'; - - if (!isReferenced) { - finalNodePath = defaultExportPath.get('declaration'); - } else { - finalNodePath = getReferencedDeclaration({ - referencedIdentifierName: defaultExportPath.node.declaration.name, - globalScopeBindings, - }); - } - } else { - const variableDeclaratorPath = astPath.scope.getBinding(exportedIdentifier).path; - const varDeclNode = variableDeclaratorPath.node; - const isReferenced = varDeclNode.init?.type === 'Identifier'; - const contentPath = varDeclNode.init - ? variableDeclaratorPath.get('init') - : variableDeclaratorPath; - - const name = varDeclNode.init - ? varDeclNode.init.name - : varDeclNode.id?.name || varDeclNode.imported.name; - - if (!isReferenced) { - // it must be an exported declaration - finalNodePath = contentPath; - } else { - finalNodePath = getReferencedDeclaration({ - referencedIdentifierName: name, - globalScopeBindings, - }); - } - } - }, - }); - - if (finalNodePath.type === 'ImportSpecifier') { - const importDeclNode = finalNodePath.parentPath.node; - const source = importDeclNode.source.value; - const identifierName = finalNodePath.node.imported.name; - const currentFilePath = filePath; - - const rootFile = await trackDownIdentifier( - source, - identifierName, - currentFilePath, - projectRootPath, - ); - const filePathOrSrc = getFilePathOrExternalSource({ - rootPath: projectRootPath, - localPath: rootFile.file, - }); - - // TODO: allow resolving external project file paths - if (!filePathOrSrc.startsWith('/')) { - // So we have external project; smth like '@lion/input/x.js' - return { - sourceNodePath: finalNodePath, - sourceFragment: null, - externalImportSource: filePathOrSrc, - }; - } - - return getSourceCodeFragmentOfDeclaration({ - filePath: filePathOrSrc, - exportedIdentifier: rootFile.specifier, - projectRootPath, - }); - } - - return { - sourceNodePath: finalNodePath, - sourceFragment: code.slice( - finalNodePath.node?.loc?.start.index, - finalNodePath.node?.loc?.end.index, - ), - externalImportSource: null, - }; -} diff --git a/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration.js b/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration.js index 354f32dce..11e88f2b0 100644 --- a/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration.js +++ b/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration.js @@ -1,7 +1,7 @@ import path from 'path'; +import { oxcTraverse, getPathFromNode, nameOf } from './oxc-traverse.js'; import { trackDownIdentifier } from './track-down-identifier.js'; -import { swcTraverse, getPathFromNode } from './swc-traverse.js'; import { AstService } from '../core/AstService.js'; import { toPosixPath } from './to-posix-path.js'; import { fsAdapter } from './fs-adapter.js'; @@ -9,6 +9,7 @@ import { fsAdapter } from './fs-adapter.js'; /** * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst * @typedef {import('../../../types/index.js').SwcBinding} SwcBinding * @typedef {import('../../../types/index.js').SwcPath} SwcPath * @typedef {import('@swc/core').Node} SwcNode @@ -57,9 +58,9 @@ export function getReferencedDeclaration({ referencedIdentifierName, globalScope return refDeclaratorBinding.path; } - if (refDeclaratorBinding.identifier.init.type === 'Identifier') { + if (refDeclaratorBinding.path.node.init.type === 'Identifier') { return getReferencedDeclaration({ - referencedIdentifierName: refDeclaratorBinding.identifier.init.value, + referencedIdentifierName: nameOf(refDeclaratorBinding.path.node.init), globalScopeBindings, }); } @@ -79,87 +80,98 @@ export function getReferencedDeclaration({ referencedIdentifierName, globalScope * await getSourceCodeFragmentOfDeclaration(code) // finds "88" * ``` * - * @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts + * @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot; parser: AnalyzerAst }} opts * @returns {Promise<{ sourceNodePath: SwcPath; sourceFragment: string|null; externalImportSource: string|null; }>} */ export async function getSourceCodeFragmentOfDeclaration({ exportedIdentifier, projectRootPath, + parser = 'oxc', filePath, }) { - const code = fsAdapter.fs.readFileSync(filePath, 'utf8'); + const code = await fsAdapter.fs.promises.readFile(filePath, 'utf8'); // compensate for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812 - const offset = AstService._getSwcOffset(); - // TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst - const swcAst = AstService._getSwcAst(code); + const offset = parser === 'swc' ? await AstService._getSwcOffset() : -1; + const ast = await AstService.getAst(code, parser); /** @type {SwcPath} */ let finalNodePath; - swcTraverse( - swcAst, + const moduleOrProgramHandler = astPath => { + astPath.stop(); + + // Situations + // - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' ) + // - declared right away (for instance a class) + // - referenced (possibly recursively) by other declaration + // - Identifier is part of a named export + // - declared right away + // - referenced (possibly recursively) by other declaration + + const globalScopeBindings = getPathFromNode(astPath.node.body?.[0])?.scope.bindings; + + if (exportedIdentifier === '[default]') { + const defaultExportPath = /** @type {SwcPath} */ ( + getPathFromNode( + astPath.node.body.find((/** @type {{ type: string; }} */ child) => + ['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type), + ), + ) + ); + + const isReferenced = + (defaultExportPath?.node.declaration?.type || defaultExportPath?.node.expression?.type) === + 'Identifier'; + + if (!isReferenced) { + finalNodePath = + defaultExportPath.get('declaration') || + defaultExportPath.get('decl') || + defaultExportPath.get('expression'); + } else { + finalNodePath = /** @type {SwcPath} */ ( + getReferencedDeclaration({ + referencedIdentifierName: nameOf( + defaultExportPath.node.declaration || defaultExportPath.node.expression, + ), + // @ts-expect-error + globalScopeBindings, + }) + ); + } + } else { + const variableDeclaratorPath = astPath.scope.bindings[exportedIdentifier].path; + const varDeclNode = variableDeclaratorPath.node; + const isReferenced = varDeclNode.init?.type === 'Identifier'; + const contentPath = varDeclNode.init + ? variableDeclaratorPath.get('init') + : variableDeclaratorPath; + + const name = varDeclNode.init + ? nameOf(varDeclNode.init) + : nameOf(varDeclNode.id) || nameOf(varDeclNode.imported) || nameOf(varDeclNode.orig); + + if (!isReferenced) { + // it must be an exported declaration + finalNodePath = contentPath; + } else { + finalNodePath = /** @type {SwcPath} */ ( + getReferencedDeclaration({ + referencedIdentifierName: name, + // @ts-expect-error + globalScopeBindings, + }) + ); + } + } + }; + + oxcTraverse( + ast, { - Module(astPath) { - astPath.stop(); - - // Situations - // - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' ) - // - declared right away (for instance a class) - // - referenced (possibly recursively) by other declaration - // - Identifier is part of a named export - // - declared right away - // - referenced (possibly recursively) by other declaration - - const globalScopeBindings = getPathFromNode(astPath.node.body?.[0])?.scope.bindings; - - if (exportedIdentifier === '[default]') { - const defaultExportPath = /** @type {SwcPath} */ ( - getPathFromNode( - astPath.node.body.find((/** @type {{ type: string; }} */ child) => - ['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type), - ), - ) - ); - const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier'; - - if (!isReferenced) { - finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression'); - } else { - finalNodePath = /** @type {SwcPath} */ ( - getReferencedDeclaration({ - referencedIdentifierName: defaultExportPath.node.expression.value, - // @ts-expect-error - globalScopeBindings, - }) - ); - } - } else { - const variableDeclaratorPath = astPath.scope.bindings[exportedIdentifier].path; - const varDeclNode = variableDeclaratorPath.node; - const isReferenced = varDeclNode.init?.type === 'Identifier'; - const contentPath = varDeclNode.init - ? variableDeclaratorPath.get('init') - : variableDeclaratorPath; - - const name = varDeclNode.init - ? varDeclNode.init.value - : varDeclNode.id?.value || varDeclNode.imported?.value || varDeclNode.orig?.value; - - if (!isReferenced) { - // it must be an exported declaration - finalNodePath = contentPath; - } else { - finalNodePath = /** @type {SwcPath} */ ( - getReferencedDeclaration({ - referencedIdentifierName: name, - // @ts-expect-error - globalScopeBindings, - }) - ); - } - } - }, + Module: moduleOrProgramHandler, + Program: moduleOrProgramHandler, }, { needsAdvancedPaths: true }, ); @@ -168,9 +180,9 @@ export async function getSourceCodeFragmentOfDeclaration({ if (finalNodePath.type === 'ImportSpecifier') { // @ts-expect-error const importDeclNode = finalNodePath.parentPath.node; - const source = importDeclNode.source.value; + const source = nameOf(importDeclNode.source); // @ts-expect-error - const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value; + const identifierName = nameOf(finalNodePath.node.imported) || nameOf(finalNodePath.node.local); const currentFilePath = filePath; const rootFile = await trackDownIdentifier( @@ -199,17 +211,20 @@ export async function getSourceCodeFragmentOfDeclaration({ filePath: /** @type {PathFromSystemRoot} */ (filePathOrSrc), exportedIdentifier: rootFile.specifier, projectRootPath, + parser, }); } + const startOf = node => node.start || node.span.start; + const endOf = node => node.end || node.span.end; return { // @ts-expect-error sourceNodePath: finalNodePath, sourceFragment: code.slice( // @ts-expect-error - finalNodePath.node.span.start - 1 - offset, + startOf(finalNodePath.node) - 1 - offset, // @ts-expect-error - finalNodePath.node.span.end - 1 - offset, + endOf(finalNodePath.node) - 1 - offset, ), // sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value, externalImportSource: null, diff --git a/packages-node/providence-analytics/src/program/utils/guarded-swc-to-babel.js b/packages-node/providence-analytics/src/program/utils/guarded-swc-to-babel.js deleted file mode 100644 index 827a91074..000000000 --- a/packages-node/providence-analytics/src/program/utils/guarded-swc-to-babel.js +++ /dev/null @@ -1,23 +0,0 @@ -import toBabel from '../../../inlined-swc-to-babel/lib/swc-to-babel.cjs'; - -/** - * @typedef {import('@babel/types').File} File - */ - -/** - * Internal wrapper around swc-to-babel... - * Allows to easily switch all swc based analyzers to Babel in case - * they turn out to be not stable yet (for instance printing a transformed ast with @babel/generator) - * Checks first whether it gets a Babel ast provided or not... - * @param {*} swcOrBabelAst - * @param {string} source - * @returns {File} - */ -export function guardedSwcToBabel(swcOrBabelAst, source) { - const isSwcAst = swcOrBabelAst.type === 'Module'; - if (isSwcAst) { - // @ts-ignore - return toBabel(swcOrBabelAst, source); - } - return swcOrBabelAst; -} diff --git a/packages-node/providence-analytics/src/program/utils/index.js b/packages-node/providence-analytics/src/program/utils/index.js index c4cf4127e..12b017af1 100644 --- a/packages-node/providence-analytics/src/program/utils/index.js +++ b/packages-node/providence-analytics/src/program/utils/index.js @@ -5,7 +5,7 @@ export { getFilePathOrExternalSource, } from './get-source-code-fragment-of-declaration.js'; export { optimisedGlob } from './optimised-glob.js'; -export { swcTraverse } from './swc-traverse.js'; +export { oxcTraverse } from './oxc-traverse.js'; export { fsAdapter } from './fs-adapter.js'; export { memoize } from './memoize.js'; export { hash } from './hash.js'; diff --git a/packages-node/providence-analytics/src/program/utils/swc-traverse.js b/packages-node/providence-analytics/src/program/utils/oxc-traverse.js similarity index 82% rename from packages-node/providence-analytics/src/program/utils/swc-traverse.js rename to packages-node/providence-analytics/src/program/utils/oxc-traverse.js index 88d228360..10216082b 100644 --- a/packages-node/providence-analytics/src/program/utils/swc-traverse.js +++ b/packages-node/providence-analytics/src/program/utils/oxc-traverse.js @@ -1,13 +1,14 @@ /** - * @typedef {import('@swc/core').Module} SwcAstModule - * @typedef {import('@swc/core').Node} SwcNode + * @typedef {import('../../../types/index.js').SwcTraversalContext} SwcTraversalContext * @typedef {import('@swc/core').VariableDeclarator} SwcVariableDeclarator * @typedef {import('@swc/core').Identifier} SwcIdentifierNode + * @typedef {import('@swc/core').Module} SwcAstModule + * @typedef {import('@swc/core').Node} SwcNode * @typedef {import('../../../types/index.js').SwcPath} SwcPath * @typedef {import('../../../types/index.js').SwcScope} SwcScope * @typedef {import('../../../types/index.js').SwcVisitor} SwcVisitor * @typedef {import('../../../types/index.js').SwcBinding} SwcBinding - * @typedef {import('../../../types/index.js').SwcTraversalContext} SwcTraversalContext + * @typedef {import('oxc-parser').ParseResult} OxcNode */ /** @@ -26,6 +27,42 @@ const fnTypes = [ const nonBlockParentTypes = [...fnTypes, 'SwitchStatement', 'ClassDeclaration']; +/** + * @param {SwcNode|OxcNode} node + */ +export function nameOf(node) { + // @ts-expect-error + return node.value || node.name; +} + +/** + * @param {SwcNode|OxcNode} node + */ +export function importedOf(node) { + // @ts-expect-error + // babel/oxc vs swc + return node?.imported || node?.orig || node?.local; +} + +/** + * @param {SwcNode|OxcNode} node + */ +export function isProperty(node) { + if (!node) return false; + + switch (node.type) { + case 'ObjectProperty': + case 'ClassProperty': + case 'ClassAccessorProperty': + case 'ClassPrivateProperty': + break; + default: + return false; + } + + return false; +} + /** * @param {SwcPath} swcPath * @param {SwcScope} currentScope @@ -48,6 +85,15 @@ function getNewScope(swcPath, currentScope, traversalContext) { parentScope: currentScope, path: swcPath, bindings: {}, + getBinding(identifierName) { + let parentScope = currentScope; + let foundBinding; + while (!foundBinding && parentScope) { + foundBinding = parentScope.bindings[identifierName]; + parentScope = parentScope.parentScope; + } + return foundBinding; + }, _pendingRefsWithoutBinding: [], _isIsolatedBlockStatement: isIsolatedBlockStatement, }; @@ -83,7 +129,7 @@ function createSwcPath(node, parent, stop, scope) { const swcPathForNode = getPathFromNode(node[id]); if (node[id] && !swcPathForNode) { // throw new Error( - // `[swcTraverse]: Use {needsAdvancedPaths: true} to find path for node: ${node[name]}`, + // `[oxcTraverse]: Use {needsAdvancedPaths: true} to find path for node: ${node[name]}`, // ); // TODO: "pre-traverse" the missing path parts instead } @@ -92,6 +138,10 @@ function createSwcPath(node, parent, stop, scope) { get type() { return node.type; }, + traverse(visitor) { + // eslint-disable-next-line no-use-before-define + return oxcTraverse(node, visitor); + }, }; swcPathCache.set(node, swcPath); return swcPath; @@ -103,19 +153,18 @@ function createSwcPath(node, parent, stop, scope) { * - an import specifier (like "import { a } from 'b'")? * Handy to know if the parents of Identifiers mark a binding * @param {SwcNode} parent - * @param {string} identifierValue + * @param {string} identifierName */ -function isBindingNode(parent, identifierValue) { - if (parent.type === 'VariableDeclarator') { +function isBindingNode(parent, identifierName) { + if (['VariableDeclarator', 'ClassDeclaration'].includes(parent.type)) { // @ts-expect-error - return parent.id.value === identifierValue; + return nameOf(parent.id) === identifierName; } return [ - 'ClassDeclaration', - 'FunctionDeclaration', 'ArrowFunctionExpression', - 'ImportSpecifier', 'ImportDefaultSpecifier', + 'FunctionDeclaration', + 'ImportSpecifier', ].includes(parent.type); } @@ -141,15 +190,13 @@ function isBindingRefNode(parent) { function addPotentialBindingOrRefToScope(swcPathForIdentifier) { const { node, parent, scope, parentPath } = swcPathForIdentifier; - if (node.type !== 'Identifier') { - return; - } + if (node.type !== 'Identifier') return; // const parentPath = getPathFromNode(parent); - if (isBindingNode(parent, node.value)) { + if (isBindingNode(parent, nameOf(node))) { /** @type {SwcBinding} */ const binding = { - identifier: parent, + identifier: parent?.id || parent?.identifier, // kind: 'var', refs: [], path: swcPathForIdentifier.parentPath, @@ -170,19 +217,20 @@ function addPotentialBindingOrRefToScope(swcPathForIdentifier) { 1, ); } - const idName = node.value || node.local?.value || node.orig?.value; + const idName = nameOf(node) || nameOf(node.local) || nameOf(node.orig || node.imported); + // eslint-disable-next-line no-param-reassign scopeBindingBelongsTo.bindings[idName] = binding; // Align with Babel... => in example `class Q {}`, Q has binding to root scope and ClassDeclaration scope - if (parent.type === 'ClassDeclaration') { + if (parent.type === 'ClassDeclaration' && (parent.id || parent.identifier) === node) { scope.bindings[idName] = binding; } } // In other cases, we are dealing with a reference that must be bound to a binding else if (isBindingRefNode(parent)) { // eslint-disable-next-line no-prototype-builtins - const binding = scope.bindings.hasOwnProperty(node.value) && scope.bindings[node.value]; + const binding = scope.bindings.hasOwnProperty(nameOf(node)) && scope.bindings[nameOf(node)]; if (binding) { binding.refs.push(parentPath); } else { @@ -200,7 +248,7 @@ function addPotentialBindingOrRefToScope(swcPathForIdentifier) { * @returns {boolean} */ function isRootNode(node) { - return node.type === 'Module' || node.type === 'Script'; + return node.type === 'Program' || node.type === 'Module' || node.type === 'Script'; } /** @@ -260,12 +308,12 @@ function visit(swcPath, visitor, traversalContext) { /** * Simple traversal for swc ast. - * @param {SwcAstModule} swcAst + * @param {SwcAstModule|SwcNode} oxcAst * @param {SwcVisitor} visitor * @param {object} config * @param {boolean} [config.needsAdvancedPaths] needs a full traversal before starting the visitor, which is less performant. Only enable when path.get() is used */ -export function swcTraverse(swcAst, visitor, { needsAdvancedPaths = false } = {}) { +export function oxcTraverse(oxcAst, visitor, { needsAdvancedPaths = false } = {}) { /** * For performance, the author of a visitor can call this to stop further traversal */ @@ -344,15 +392,18 @@ export function swcTraverse(swcAst, visitor, { needsAdvancedPaths = false } = {} id: traversalContext.scopeId, bindings: {}, path: null, + getBinding(/** @type {string} */ identifierName) { + return initialScope.bindings[identifierName]; + }, _pendingRefsWithoutBinding: [], _isIsolatedBlockStatement: false, }; if (needsAdvancedPaths) { // Do one full traversal to prepare advanced path functionality like path.get() and path.scope.bindings // TODO: improve with on the fly, partial tree traversal for best performance - prepareTree(swcAst, null, initialScope, traversalContext); + prepareTree(oxcAst, null, initialScope, traversalContext); } - visitTree(swcAst, null, initialScope, { hasPreparedTree: needsAdvancedPaths }, traversalContext); + visitTree(oxcAst, null, initialScope, { hasPreparedTree: needsAdvancedPaths }, traversalContext); // @ts-expect-error traversalContext.visitOnExitFns.reverse().forEach(fn => fn()); } diff --git a/packages-node/providence-analytics/src/program/utils/track-down-identifier--legacy.js b/packages-node/providence-analytics/src/program/utils/track-down-identifier--legacy.js deleted file mode 100644 index 489ab8021..000000000 --- a/packages-node/providence-analytics/src/program/utils/track-down-identifier--legacy.js +++ /dev/null @@ -1,334 +0,0 @@ -/* eslint-disable no-shadow */ -// @ts-nocheck -import path from 'path'; - -import babelTraverse from '@babel/traverse'; - -import { isRelativeSourcePath, toRelativeSourcePath } from './relative-source-path.js'; -import { InputDataService } from '../core/InputDataService.js'; -import { resolveImportPath } from './resolve-import-path.js'; -import { AstService } from '../core/AstService.js'; -import { LogService } from '../core/LogService.js'; -import { fsAdapter } from './fs-adapter.js'; -import { memoize } from './memoize.js'; - -/** - * @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot - * @typedef {import('../../../../types/index.js').SpecifierSource} SpecifierSource - * @typedef {import('../../../../types/index.js').IdentifierName} IdentifierName - * @typedef {import('../../../../types/index.js').RootFile} RootFile - * @typedef {import('@babel/traverse').NodePath} NodePath - */ - -/** - * @param {string} source - * @param {string} projectName - */ -function isSelfReferencingProject(source, projectName) { - return source.split('/')[0] === projectName; -} - -/** - * @param {string} source - * @param {string} projectName - */ -function isExternalProject(source, projectName) { - return ( - !source.startsWith('#') && - !isRelativeSourcePath(source) && - !isSelfReferencingProject(source, projectName) - ); -} - -/** - * Other than with import, no binding is created for MyClass by Babel(?) - * This means 'path.scope.getBinding('MyClass')' returns undefined - * and we have to find a different way to retrieve this value. - * @param {NodePath} astPath Babel ast traversal path - * @param {IdentifierName} identifierName the name that should be tracked (and that exists inside scope of astPath) - */ -function getBindingAndSourceReexports(astPath, identifierName) { - // Get to root node of file and look for exports like `export { identifierName } from 'src';` - let source; - let bindingType; - let bindingPath; - - let curPath = astPath; - while (curPath.parentPath) { - curPath = curPath.parentPath; - } - const rootPath = curPath; - rootPath.traverse({ - ExportSpecifier(astPath) { - // eslint-disable-next-line arrow-body-style - const found = - astPath.node.exported.name === identifierName || astPath.node.local.name === identifierName; - if (found) { - bindingPath = astPath; - bindingType = 'ExportSpecifier'; - source = astPath.parentPath.node.source - ? astPath.parentPath.node.source.value - : '[current]'; - astPath.stop(); - } - }, - }); - return [source, bindingType, bindingPath]; -} - -/** - * Retrieves source (like '@lion/core') and importedIdentifierName (like 'lit') from ast for - * current file. - * We might be an import that was locally renamed. - * Since we are traversing, we are interested in the imported name. Or in case of a re-export, - * the local name. - * @param {NodePath} astPath Babel ast traversal path - * @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath) - * @returns {{ source:string, importedIdentifierName:string }} - */ -export function getImportSourceFromAst(astPath, identifierName) { - let source; - let importedIdentifierName; - - const binding = astPath.scope.getBinding(identifierName); - let bindingType = binding?.path.type; - let bindingPath = binding?.path; - const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier']; - - if (bindingType && matchingTypes.includes(bindingType)) { - source = binding?.path?.parentPath?.node?.source?.value; - } else { - // no binding - [source, bindingType, bindingPath] = getBindingAndSourceReexports(astPath, identifierName); - } - - const shouldLookForDefaultExport = bindingType === 'ImportDefaultSpecifier'; - if (shouldLookForDefaultExport) { - importedIdentifierName = '[default]'; - } else if (source) { - const { node } = bindingPath; - importedIdentifierName = (node.imported && node.imported.name) || node.local.name; - } - - return { source, importedIdentifierName }; -} - -/** - * @typedef {(source:SpecifierSource,identifierName:IdentifierName,currentFilePath:PathFromSystemRoot,rootPath:PathFromSystemRoot,projectName?: string,depth?:number) => Promise} TrackDownIdentifierFn - */ - -/** - * Follows the full path of an Identifier until its declaration ('root file') is found. - * @example - *```js - * // 1. Starting point - * // target-proj/my-comp-import.js - * import { MyComp as TargetComp } from 'ref-proj'; - * - * // 2. Intermediate stop: a re-export - * // ref-proj/exportsIndex.js (package.json has main: './exportsIndex.js') - * export { RefComp as MyComp } from './src/RefComp.js'; - * - * // 3. End point: our declaration - * // ref-proj/src/RefComp.js - * export class RefComp extends LitElement {...} - *``` - * - * -param {SpecifierSource} source an importSpecifier source, like 'ref-proj' or '../file' - * -param {IdentifierName} identifierName imported reference/Identifier name, like 'MyComp' - * -param {PathFromSystemRoot} currentFilePath file path, like '/path/to/target-proj/my-comp-import.js' - * -param {PathFromSystemRoot} rootPath dir path, like '/path/to/target-proj' - * -param {string} [projectName] like 'target-proj' or '@lion/input' - * -returns {Promise} file: path of file containing the binding (exported declaration), - * like '/path/to/ref-proj/src/RefComp.js' - */ -/** @type {TrackDownIdentifierFn} */ -// eslint-disable-next-line import/no-mutable-exports -export let trackDownIdentifier; - -/** @type {TrackDownIdentifierFn} */ -async function trackDownIdentifierFn( - source, - identifierName, - currentFilePath, - rootPath, - projectName, - depth = 0, -) { - let rootFilePath; // our result path - let rootSpecifier; // the name under which it was imported - - if (!projectName) { - // eslint-disable-next-line no-param-reassign - projectName = InputDataService.getPackageJson(rootPath)?.name; - } - - if (projectName && isExternalProject(source, projectName)) { - // So, it is an external ref like '@lion/core' or '@open-wc/scoped-elements/index.js' - // At this moment in time, we don't know if we have file system access to this particular - // project. Therefore, we limit ourselves to tracking down local references. - // In case this helper is used inside an analyzer like 'match-subclasses', the external - // (search-target) project can be accessed and paths can be resolved to local ones, - // just like in 'match-imports' analyzer. - /** @type {RootFile} */ - const result = { file: source, specifier: identifierName }; - return result; - } - - const resolvedSourcePath = await resolveImportPath(source, currentFilePath); - - LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`); - const allowedJsModuleExtensions = ['.mjs', '.js']; - if (!allowedJsModuleExtensions.includes(path.extname(resolvedSourcePath))) { - // We have an import assertion - return /** @type { RootFile } */ { - file: toRelativeSourcePath(resolvedSourcePath, rootPath), - specifier: '[default]', - }; - } - const code = fsAdapter.fs.readFileSync(resolvedSourcePath, 'utf8'); - const babelAst = AstService.getAst(code, 'swc-to-babel', { filePath: resolvedSourcePath }); - - const shouldLookForDefaultExport = identifierName === '[default]'; - - let reexportMatch = false; // named specifier declaration - let exportMatch; - let pendingTrackDownPromise; - - babelTraverse.default(babelAst, { - ExportDefaultDeclaration(astPath) { - if (!shouldLookForDefaultExport) { - return; - } - - let newSource; - if (astPath.node.declaration.type === 'Identifier') { - newSource = getImportSourceFromAst(astPath, astPath.node.declaration.name).source; - } - - if (newSource) { - pendingTrackDownPromise = trackDownIdentifier( - newSource, - '[default]', - resolvedSourcePath, - rootPath, - projectName, - depth + 1, - ); - } else { - // We found our file! - rootSpecifier = identifierName; - rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath); - } - astPath.stop(); - }, - ExportNamedDeclaration: { - enter(astPath) { - if (reexportMatch || shouldLookForDefaultExport) { - return; - } - - // Are we dealing with a re-export ? - if (astPath.node.specifiers?.length) { - exportMatch = astPath.node.specifiers.find(s => s.exported.name === identifierName); - - if (exportMatch) { - const localName = exportMatch.local.name; - let newSource; - if (astPath.node.source) { - /** - * @example - * export { x } from 'y' - */ - newSource = astPath.node.source.value; - } else { - /** - * @example - * import { x } from 'y' - * export { x } - */ - newSource = getImportSourceFromAst(astPath, identifierName).source; - - if (!newSource || newSource === '[current]') { - /** - * @example - * const x = 12; - * export { x } - */ - return; - } - } - reexportMatch = true; - pendingTrackDownPromise = trackDownIdentifier( - newSource, - localName, - resolvedSourcePath, - rootPath, - projectName, - depth + 1, - ); - astPath.stop(); - } - } - }, - exit(astPath) { - if (!reexportMatch) { - // We didn't find a re-exported Identifier, that means the reference is declared - // in current file... - rootSpecifier = identifierName; - rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath); - - if (exportMatch) { - astPath.stop(); - } - } - }, - }, - }); - - if (pendingTrackDownPromise) { - // We can't handle promises inside Babel traverse, so we do it here... - const resObj = await pendingTrackDownPromise; - rootFilePath = resObj.file; - rootSpecifier = resObj.specifier; - } - - return /** @type { RootFile } */ { file: rootFilePath, specifier: rootSpecifier }; -} - -trackDownIdentifier = memoize(trackDownIdentifierFn); - -/** - * @param {NodePath} astPath - * @param {string} identifierNameInScope - * @param {PathFromSystemRoot} fullCurrentFilePath - * @param {PathFromSystemRoot} projectPath - * @param {string} [projectName] - */ -async function trackDownIdentifierFromScopeFn( - astPath, - identifierNameInScope, - fullCurrentFilePath, - projectPath, - projectName, -) { - const sourceObj = getImportSourceFromAst(astPath, identifierNameInScope); - - /** @type {RootFile} */ - let rootFile; - if (sourceObj.source) { - rootFile = await trackDownIdentifier( - sourceObj.source, - sourceObj.importedIdentifierName, - fullCurrentFilePath, - projectPath, - projectName, - ); - } else { - const specifier = sourceObj.importedIdentifierName || identifierNameInScope; - rootFile = { file: '[current]', specifier }; - } - return rootFile; -} - -export const trackDownIdentifierFromScope = memoize(trackDownIdentifierFromScopeFn); diff --git a/packages-node/providence-analytics/src/program/utils/track-down-identifier.js b/packages-node/providence-analytics/src/program/utils/track-down-identifier.js index d8a1b57c7..029deadb2 100644 --- a/packages-node/providence-analytics/src/program/utils/track-down-identifier.js +++ b/packages-node/providence-analytics/src/program/utils/track-down-identifier.js @@ -4,7 +4,7 @@ import { isRelativeSourcePath, toRelativeSourcePath } from './relative-source-pa import { InputDataService } from '../core/InputDataService.js'; import { resolveImportPath } from './resolve-import-path.js'; import { AstService } from '../core/AstService.js'; -import { swcTraverse } from './swc-traverse.js'; +import { oxcTraverse, nameOf, importedOf } from './oxc-traverse.js'; import { fsAdapter } from './fs-adapter.js'; import { memoize } from './memoize.js'; @@ -55,18 +55,19 @@ function getBindingAndSourceReexports(swcPath, identifierName) { } const rootPath = curPath; - swcTraverse(rootPath.node, { + oxcTraverse(rootPath.node, { ExportSpecifier(astPath) { + const { node } = astPath; // eslint-disable-next-line arrow-body-style const found = - astPath.node.orig?.value === identifierName || - astPath.node.exported?.value === identifierName || - astPath.node.local?.value === identifierName; + nameOf(importedOf(node)) === identifierName || + nameOf(node.exported) === identifierName || + nameOf(node.local) === identifierName; if (found) { bindingPath = astPath; bindingType = 'ExportSpecifier'; source = astPath.parentPath.node.source - ? astPath.parentPath.node.source.value + ? nameOf(astPath.parentPath.node.source) : '[current]'; astPath.stop(); } @@ -89,13 +90,15 @@ export function getImportSourceFromAst(astPath, identifierName) { let source; let importedIdentifierName; - const binding = astPath.scope.bindings[identifierName]; + // TODO: use (smth like) getReferencedDeclaration if we want to catch renamed variables + const binding = astPath.scope.getBinding(identifierName); + let bindingType = binding?.path.type; let bindingPath = binding?.path; const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier']; if (bindingType && matchingTypes.includes(bindingType)) { - source = binding?.path?.parentPath?.node?.source?.value; + source = nameOf(binding?.path?.parentPath?.node?.source); } else { // no binding [source, bindingType, bindingPath] = getBindingAndSourceReexports(astPath, identifierName); @@ -106,7 +109,7 @@ export function getImportSourceFromAst(astPath, identifierName) { importedIdentifierName = '[default]'; } else if (source) { const { node } = bindingPath; - importedIdentifierName = node.orig?.value || node.imported?.value || node.local?.value; + importedIdentifierName = nameOf(importedOf(node)) || nameOf(node.local); } return { source, importedIdentifierName }; @@ -194,8 +197,11 @@ async function trackDownIdentifierFn( specifier: '[default]', }; } - const code = fsAdapter.fs.readFileSync(/** @type {string} */ (resolvedSourcePath), 'utf8'); - const swcAst = AstService._getSwcAst(code); + const code = await fsAdapter.fs.promises.readFile( + /** @type {string} */ (resolvedSourcePath), + 'utf8', + ); + const oxcAst = await AstService._getOxcAst(code); const shouldLookForDefaultExport = identifierName === '[default]'; @@ -204,16 +210,16 @@ async function trackDownIdentifierFn( let pendingTrackDownPromise; const handleExportDefaultDeclOrExpr = astPath => { - if (!shouldLookForDefaultExport) { - return; - } + if (!shouldLookForDefaultExport) return; + + const { node } = astPath; let newSource; - if ( - astPath.node.expression?.type === 'Identifier' || - astPath.node.declaration?.type === 'Identifier' - ) { - newSource = getImportSourceFromAst(astPath, astPath.node.expression.value).source; + if (node.expression?.type === 'Identifier' || node.declaration?.type === 'Identifier') { + newSource = getImportSourceFromAst( + astPath, + nameOf(node.expression || node.declaration), + ).source; } if (newSource) { @@ -237,54 +243,54 @@ async function trackDownIdentifierFn( }; const handleExportDeclOrNamedDecl = { enter(astPath) { - if (reexportMatch || shouldLookForDefaultExport) { - return; - } + if (reexportMatch || shouldLookForDefaultExport) return; + + const { node } = astPath; // Are we dealing with a re-export ? - if (astPath.node.specifiers?.length) { - exportMatch = astPath.node.specifiers.find( - s => s.orig?.value === identifierName || s.exported?.value === identifierName, - ); + if (!node.specifiers?.length) return; - if (exportMatch) { - const localName = exportMatch.orig.value; - let newSource; - if (astPath.node.source) { - /** - * @example - * export { x } from 'y' - */ - newSource = astPath.node.source.value; - } else { - /** - * @example - * import { x } from 'y' - * export { x } - */ - newSource = getImportSourceFromAst(astPath, identifierName).source; + exportMatch = node.specifiers.find( + s => nameOf(importedOf(s)) === identifierName || nameOf(s.exported) === identifierName, + ); - if (!newSource || newSource === '[current]') { - /** - * @example - * const x = 12; - * export { x } - */ - return; - } - } - reexportMatch = true; - pendingTrackDownPromise = trackDownIdentifier( - newSource, - localName, - resolvedSourcePath, - rootPath, - projectName, - depth + 1, - ); - astPath.stop(); + if (!exportMatch) return; + + const localName = nameOf(importedOf(exportMatch)); + let newSource; + if (node.source) { + /** + * @example + * export { x } from 'y' + */ + newSource = nameOf(node.source); + } else { + /** + * @example + * import { x } from 'y' + * export { x } + */ + newSource = getImportSourceFromAst(astPath, identifierName).source; + + if (!newSource || newSource === '[current]') { + /** + * @example + * const x = 12; + * export { x } + */ + return; } } + reexportMatch = true; + pendingTrackDownPromise = trackDownIdentifier( + newSource, + localName, + resolvedSourcePath, + rootPath, + projectName, + depth + 1, + ); + astPath.stop(); }, exit(astPath) { if (!reexportMatch) { @@ -307,7 +313,7 @@ async function trackDownIdentifierFn( ExportDeclaration: handleExportDeclOrNamedDecl, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); if (pendingTrackDownPromise) { // We can't handle promises inside Babel traverse, so we do it here... @@ -351,6 +357,7 @@ async function trackDownIdentifierFromScopeFn( const specifier = sourceObj.importedIdentifierName || identifierNameInScope; rootFile = { file: '[current]', specifier }; } + return rootFile; } diff --git a/packages-node/providence-analytics/test-helpers/mock-project-helpers.js b/packages-node/providence-analytics/test-helpers/mock-project-helpers.js index aafed1671..89316796f 100644 --- a/packages-node/providence-analytics/test-helpers/mock-project-helpers.js +++ b/packages-node/providence-analytics/test-helpers/mock-project-helpers.js @@ -1,3 +1,4 @@ +import module from 'module'; import path from 'path'; // eslint-disable-next-line import/no-extraneous-dependencies import mockFs from 'mock-fs'; @@ -59,6 +60,46 @@ function getMockObjectForProject(files, cfg = {}, existingMock = {}) { return totalMock; } +/** + * @param {string} resolvedPath + * @param {string} dynamicImport + * @returns {string} + */ +function getPackageRootFromNodeModulesPath(resolvedPath, dynamicImport) { + const scope = dynamicImport.startsWith('@') ? dynamicImport.split('/')[0] : dynamicImport; + const tailOfRootPath = `${path.sep}node_modules${path.sep}${scope}`; + const lio = resolvedPath.lastIndexOf(tailOfRootPath); + return resolvedPath.slice(0, lio + tailOfRootPath.length); +} + +function incorporateDynamicImports( + dynamicImports = [ + { + name: 'oxc-parser', + siblings: [ + '@oxc-parser', + // '@oxc-resolver' + ], + }, + { name: '@babel/parser' }, + { name: '@swc/core' }, + ], +) { + const require = module.createRequire(import.meta.url); + const importablePaths = []; + for (const dynamicImport of dynamicImports) { + const resolvedPath = require.resolve(dynamicImport.name); + const rootPath = getPackageRootFromNodeModulesPath(resolvedPath, dynamicImport.name); + importablePaths.push(rootPath); + for (const sibling of dynamicImport.siblings || []) { + const siblingPath = `${rootPath.split(path.sep).slice(0, -1).join(path.sep)}${path.sep}${sibling}`; + importablePaths.push(siblingPath); + } + } + return importablePaths; +} +const importablePaths = incorporateDynamicImports(); + /** * Makes sure that, whenever the main program (providence) calls * "InputDataService.createDataObject", it gives back a mocked response. @@ -72,7 +113,7 @@ function getMockObjectForProject(files, cfg = {}, existingMock = {}) { */ export function mockProject(files, cfg = {}, existingMock = {}) { const obj = getMockObjectForProject(files, cfg, existingMock); - mockFs(obj); + mockFs({ ...obj, ...Object.fromEntries(importablePaths.map(p => [p, mockFs.load(p)])) }); return obj; } diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-classes_-_importing-target-project_0.0.2-target-mock__-905964591.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-classes_-_importing-target-project_0.0.2-target-mock__-905964591.json index 28c1b6ab3..51181c200 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-classes_-_importing-target-project_0.0.2-target-mock__-905964591.json +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-classes_-_importing-target-project_0.0.2-target-mock__-905964591.json @@ -3,7 +3,7 @@ "searchType": "ast-analyzer", "analyzerMeta": { "name": "find-classes", - "requiredAst": "babel", + "requiredAst": "oxc", "identifier": "importing-target-project_0.0.2-target-mock__-905964591", "targetProject": { "mainEntry": "./target-src/match-imports/root-level-imports.js", diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-customelements_-_importing-target-project_0.0.2-target-mock__61665553.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-customelements_-_importing-target-project_0.0.2-target-mock__61665553.json index 341216313..9dc5643eb 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-customelements_-_importing-target-project_0.0.2-target-mock__61665553.json +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-customelements_-_importing-target-project_0.0.2-target-mock__61665553.json @@ -3,7 +3,7 @@ "searchType": "ast-analyzer", "analyzerMeta": { "name": "find-customelements", - "requiredAst": "swc-to-babel", + "requiredAst": "oxc", "identifier": "importing-target-project_0.0.2-target-mock__61665553", "targetProject": { "mainEntry": "./target-src/match-imports/root-level-imports.js", diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-exports_-_exporting-ref-project_1.0.0__-42206859.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-exports_-_exporting-ref-project_1.0.0__-42206859.json index a89cc0b31..7291ada15 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-exports_-_exporting-ref-project_1.0.0__-42206859.json +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-exports_-_exporting-ref-project_1.0.0__-42206859.json @@ -3,7 +3,7 @@ "searchType": "ast-analyzer", "analyzerMeta": { "name": "find-exports", - "requiredAst": "swc", + "requiredAst": "oxc", "identifier": "exporting-ref-project_1.0.0__-42206859", "targetProject": { "mainEntry": "./index.js", diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-imports_-_importing-target-project_0.0.2-target-mock__349742630.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-imports_-_importing-target-project_0.0.2-target-mock__349742630.json index 286623df3..f018952ba 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-imports_-_importing-target-project_0.0.2-target-mock__349742630.json +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-imports_-_importing-target-project_0.0.2-target-mock__349742630.json @@ -3,7 +3,7 @@ "searchType": "ast-analyzer", "analyzerMeta": { "name": "find-imports", - "requiredAst": "swc", + "requiredAst": "oxc", "identifier": "importing-target-project_0.0.2-target-mock__349742630", "targetProject": { "mainEntry": "./target-src/match-imports/root-level-imports.js", diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-imports_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1789378150.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-imports_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1789378150.json index f84160770..9c18dde34 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-imports_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1789378150.json +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-imports_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1789378150.json @@ -3,7 +3,7 @@ "searchType": "ast-analyzer", "analyzerMeta": { "name": "match-imports", - "requiredAst": "swc", + "requiredAst": "oxc", "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1789378150", "targetProject": { "mainEntry": "./target-src/match-imports/root-level-imports.js", diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-subclasses_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1982316146.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-subclasses_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1982316146.json index f28cc6a6a..e85c53c6c 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-subclasses_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1982316146.json +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-subclasses_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1982316146.json @@ -3,7 +3,7 @@ "searchType": "ast-analyzer", "analyzerMeta": { "name": "match-subclasses", - "requiredAst": "babel", + "requiredAst": "oxc", "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1982316146", "targetProject": { "mainEntry": "./target-src/match-imports/root-level-imports.js", diff --git a/packages-node/providence-analytics/test-node/program/analyzers/find-imports.test.js b/packages-node/providence-analytics/test-node/program/analyzers/find-imports.test.js index 1da643c2f..33a073405 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/find-imports.test.js +++ b/packages-node/providence-analytics/test-node/program/analyzers/find-imports.test.js @@ -87,7 +87,7 @@ describe('Analyzer "find-imports"', async () => { expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[*]'); }); - describe('Reexports', () => { + describe('Re-exports', () => { it(`supports [export { x } from 'imported/source'] (re-exported named specifiers)`, async () => { mockProject([`export { x } from 'imported/source'`]); const queryResults = await providence(findImportsQueryConfig, _providenceCfg); diff --git a/packages-node/providence-analytics/test-node/program/utils/get-source-code-fragment-of-declaration.test.js b/packages-node/providence-analytics/test-node/program/utils/get-source-code-fragment-of-declaration.test.js index 01405bc29..57d555116 100644 --- a/packages-node/providence-analytics/test-node/program/utils/get-source-code-fragment-of-declaration.test.js +++ b/packages-node/providence-analytics/test-node/program/utils/get-source-code-fragment-of-declaration.test.js @@ -2,9 +2,13 @@ import { expect } from 'chai'; import { it } from 'mocha'; import { getSourceCodeFragmentOfDeclaration } from '../../../src/program/utils/index.js'; -import { mock } from '../../../test-helpers/mock-project-helpers.js'; +import { mockProject } from '../../../test-helpers/mock-project-helpers.js'; import { memoize } from '../../../src/program/utils/memoize.js'; +/** + * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot + */ + describe('getSourceCodeFragmentOfDeclaration', () => { const initialMemoizeCacheEnabled = memoize.isCacheEnabled; before(() => { @@ -19,10 +23,10 @@ describe('getSourceCodeFragmentOfDeclaration', () => { const fakeFs = { '/my/proj/exports/file.js': 'export const x = 0;', }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ - filePath: '/my/proj/exports/file.js', + filePath: /** @type {PathFromSystemRoot} */ ('/my/proj/exports/file.js'), exportedIdentifier: 'x', }); @@ -36,7 +40,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export const x = y; `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/file.js', @@ -54,7 +58,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export const myIdentifier = y; `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/file.js', @@ -74,7 +78,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export const black67 = black59; `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/file-2.js', @@ -93,7 +97,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export class AjaxClass extends LionAjaxClass {} `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/ajax.js', @@ -109,7 +113,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export function myFn() {} `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/myFn.js', @@ -126,7 +130,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { const fakeFs = { '/my/proj/exports/file.js': 'export default class {};', }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/file.js', @@ -143,7 +147,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export default myIdentifier; `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/file.js', @@ -161,7 +165,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export default myIdentifier; `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/file.js', @@ -180,7 +184,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export default class AjaxClass extends LionAjaxClass {} `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/ajax.js', @@ -196,7 +200,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { export default function myFn() {} `, }; - mock(fakeFs); + mockProject(fakeFs); const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ filePath: '/my/proj/exports/myFn.js', diff --git a/packages-node/providence-analytics/test-node/program/utils/swc-traverse.test.js b/packages-node/providence-analytics/test-node/program/utils/swc-traverse.test.js index 566a89421..fe89c452c 100644 --- a/packages-node/providence-analytics/test-node/program/utils/swc-traverse.test.js +++ b/packages-node/providence-analytics/test-node/program/utils/swc-traverse.test.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { it } from 'mocha'; // @ts-ignore import babelTraversePkg from '@babel/traverse'; -import { swcTraverse } from '../../../src/program/utils/swc-traverse.js'; +import { oxcTraverse } from '../../../src/program/utils/oxc-traverse.js'; import { AstService } from '../../../src/program/core/AstService.js'; /** @@ -12,12 +12,12 @@ import { AstService } from '../../../src/program/core/AstService.js'; */ /** - * @param {SwcAstModule} swcAst + * @param {SwcAstModule} oxcAst */ -function gatherAllScopes(swcAst) { +function gatherAllScopes(oxcAst) { /** @type {SwcScope[]} */ const swcScopes = []; - swcTraverse(swcAst, { + oxcTraverse(oxcAst, { enter({ scope }) { if (!swcScopes.includes(scope)) { swcScopes.push(scope); @@ -27,11 +27,18 @@ function gatherAllScopes(swcAst) { return swcScopes; } -describe('swcTraverse', () => { +/** + * @param {*} node + */ +function nameOf(node) { + return node.value || node.name; +} + +describe('oxcTraverse', () => { describe('Visitor', () => { it('traverses an swc AST based on visitor', async () => { const code = `import x from 'y';`; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); let foundImportDeclarationPath; const visitor = { @@ -39,14 +46,14 @@ describe('swcTraverse', () => { foundImportDeclarationPath = path; }, }; - swcTraverse(swcAst, visitor); + oxcTraverse(oxcAst, visitor); expect(foundImportDeclarationPath).to.not.be.undefined; }); it('supports "enter" as a generic arrival handler', async () => { const code = `import x from 'y';`; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {string[]} */ const foundTypes = []; @@ -58,10 +65,11 @@ describe('swcTraverse', () => { foundTypes.push(path.node.type); }, }; - swcTraverse(swcAst, visitor); + oxcTraverse(oxcAst, visitor); expect(foundTypes).to.deep.equal([ - 'Module', + // 'Module', + 'Program', 'ImportDeclaration', 'ImportDefaultSpecifier', 'Identifier', @@ -71,7 +79,7 @@ describe('swcTraverse', () => { it('supports "enter" and "exit" as generic handlers inside handlers', async () => { const code = `import x from 'y';`; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {string[]} */ const visitedPaths = []; @@ -88,7 +96,7 @@ describe('swcTraverse', () => { }, }, }; - swcTraverse(swcAst, visitor); + oxcTraverse(oxcAst, visitor); expect(visitedPaths[0].path).to.equal(visitedPaths[1].path); expect(visitedPaths[0].phase).to.equal('enter'); @@ -97,7 +105,7 @@ describe('swcTraverse', () => { it('supports "root" as alternative for Program', async () => { const code = `import x from 'y';`; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); let rootPath; const visitor = { @@ -108,17 +116,17 @@ describe('swcTraverse', () => { rootPath = path; }, }; - swcTraverse(swcAst, visitor); + oxcTraverse(oxcAst, visitor); // TODO: also add case for Script - expect(rootPath.node.type).to.equal('Module'); + expect(rootPath.node.type).to.equal('Program'); }); it('does not fail on object prototype built-ins (like "toString")', async () => { const code = `const { toString } = x;`; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); - expect(swcTraverse(swcAst, {})).to.not.throw; + expect(oxcTraverse(oxcAst, {})).to.not.throw; }); }); @@ -147,7 +155,7 @@ describe('swcTraverse', () => { } const alsoGlobalScope = 3; `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -159,13 +167,13 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); expect(declaratorPaths[0].scope.id).to.equal(0); expect(declaratorPaths[1].scope.id).to.equal(1); expect(declaratorPaths[2].scope.id).to.equal(2); - expect(declaratorPaths[0].node.id.value).to.equal('globalScope'); + expect(nameOf(declaratorPaths[0].node.id)).to.equal('globalScope'); expect(Object.keys(declaratorPaths[0].scope.bindings)).to.deep.equal([ 'globalScope', 'alsoGlobalScope', @@ -173,10 +181,10 @@ describe('swcTraverse', () => { // 0 and 3 are the same scope expect(declaratorPaths[0].scope).to.equal(declaratorPaths[3].scope); // Scope bindings refer to Declarator nodes - expect(declaratorPaths[0].scope.bindings.globalScope.identifier).to.equal( + expect(declaratorPaths[0].scope.bindings.globalScope.path.node).to.equal( declaratorPaths[0].node, ); - expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.identifier).to.equal( + expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.path.node).to.equal( declaratorPaths[3].node, ); @@ -194,7 +202,7 @@ describe('swcTraverse', () => { } } `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -203,8 +211,8 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); - const scopes = gatherAllScopes(swcAst); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); + const scopes = gatherAllScopes(oxcAst); expect(scopes[1].path?.node).to.equal(declaratorPaths[0].node); expect(scopes[2].path?.node).to.equal(declaratorPaths[1].node); @@ -223,7 +231,7 @@ describe('swcTraverse', () => { } } `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -232,7 +240,7 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); expect(declaratorPaths[0].scope.id).to.equal(2); }); @@ -246,7 +254,7 @@ describe('swcTraverse', () => { break; default: }`; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -255,10 +263,10 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); - expect(declaratorPaths[0].node.id.value).to.equal('myCases'); - expect(declaratorPaths[1].node.id.value).to.equal('x'); + expect(nameOf(declaratorPaths[0].node.id)).to.equal('myCases'); + expect(nameOf(declaratorPaths[1].node.id)).to.equal('x'); expect(declaratorPaths[0].scope.id).to.equal(0); expect(declaratorPaths[1].scope.id).to.equal(1); }); @@ -269,18 +277,19 @@ describe('swcTraverse', () => { toString(dateObj, opt = {}) {}, }; `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const results = []; const visitor = { - MethodProperty(/** @type {any} */ path) { + // MethodProperty for swc... + ObjectProperty(/** @type {any} */ path) { results.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); - expect(results[0].node.key.value).to.equal('toString'); + expect(nameOf(results[0].node.key)).to.equal('toString'); expect(results[0].scope.id).to.equal(0); }); @@ -292,7 +301,7 @@ describe('swcTraverse', () => { }, }; `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -301,10 +310,10 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); - expect(declaratorPaths[0].node.id.value).to.equal('x'); - expect(declaratorPaths[1].node.id.value).to.equal('z'); + expect(nameOf(declaratorPaths[0].node.id)).to.equal('x'); + expect(nameOf(declaratorPaths[1].node.id)).to.equal('z'); expect(declaratorPaths[0].scope.id).to.equal(0); expect(declaratorPaths[1].scope.id).to.equal(1); }); @@ -322,7 +331,7 @@ describe('swcTraverse', () => { } let alsoGlobalScope = 3; `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -334,17 +343,17 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); expect(Object.keys(declaratorPaths[0].scope.bindings)).to.deep.equal([ 'globalScope', 'alsoGlobalScope', ]); // Scope bindings refer to Declarator nodes - expect(declaratorPaths[0].scope.bindings.globalScope.identifier).to.equal( + expect(declaratorPaths[0].scope.bindings.globalScope.path.node).to.equal( declaratorPaths[0].node, ); - expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.identifier).to.equal( + expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.path.node).to.equal( declaratorPaths[3].node, ); }); @@ -359,7 +368,7 @@ describe('swcTraverse', () => { } } `; - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); /** @type {SwcPath[]} */ const declaratorPaths = []; @@ -368,7 +377,7 @@ describe('swcTraverse', () => { declaratorPaths.push(path); }, }; - swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + oxcTraverse(oxcAst, visitor, { needsAdvancedPaths: true }); expect(Object.keys(declaratorPaths[0].scope.bindings)).to.deep.equal([ 'globalScope', @@ -389,7 +398,7 @@ describe('swcTraverse', () => { * @param {string} code */ async function compareScopeResultsWithBabel(code) { - const swcAst = await AstService._getSwcAst(code); + const oxcAst = await AstService._getOxcAst(code); const babelAst = await AstService._getBabelAst(code); /** @@ -407,7 +416,7 @@ describe('swcTraverse', () => { /** @type {SwcScope[]} */ const swcScopes = []; - swcTraverse(swcAst, { + oxcTraverse(oxcAst, { enter({ scope }) { if (!swcScopes.includes(scope)) { swcScopes.push(scope); @@ -420,9 +429,17 @@ describe('swcTraverse', () => { expect(babelScopes.length).to.equal(swcScopes.length); for (let i = 0; i < babelScopes.length; i += 1) { expect(babelScopes[i].uid - babelRootScopeIdOffset).to.equal(swcScopes[i].id); - expect(Object.keys(babelScopes[i].bindings)).to.deep.equal( - Object.keys(swcScopes[i].bindings), - ); + + const babelEntries = Object.entries(babelScopes[i].bindings); + const swcEntries = Object.entries(swcScopes[i].bindings); + for (const [j, [bindingKey, binding]] of babelEntries.entries()) { + expect(bindingKey).to.equal(swcEntries[j][0]); + expect(binding.path.node.type).to.equal(swcEntries[j][1]?.path?.node.type); + } + + // expect(Object.keys(babelScopes[i].bindings)).to.deep.equal( + // Object.keys(swcScopes[i].bindings), + // ); // expect(babelScopes[i].references).to.deep.equal(swcResults[i].references); } } @@ -459,5 +476,15 @@ describe('swcTraverse', () => { await compareScopeResultsWithBabel(code); }); + + it('handles all kinds of lexical scopes and bindings in a similar way 2', async () => { + const code = ` + import { LionComp } from 'ref/LionComp.js'; + + export class WolfComp extends LionComp {} + `; + + await compareScopeResultsWithBabel(code); + }); }); }); diff --git a/packages-node/providence-analytics/test-node/program/analyzers/helpers/track-down-identifier.test.js b/packages-node/providence-analytics/test-node/program/utils/track-down-identifier.test.js similarity index 86% rename from packages-node/providence-analytics/test-node/program/analyzers/helpers/track-down-identifier.test.js rename to packages-node/providence-analytics/test-node/program/utils/track-down-identifier.test.js index 5f5ecb576..7b47f6987 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/helpers/track-down-identifier.test.js +++ b/packages-node/providence-analytics/test-node/program/utils/track-down-identifier.test.js @@ -1,14 +1,14 @@ import { expect } from 'chai'; import { it } from 'mocha'; -import { setupAnalyzerTest } from '../../../../test-helpers/setup-analyzer-test.js'; -import { mockProject } from '../../../../test-helpers/mock-project-helpers.js'; -import { swcTraverse } from '../../../../src/program/utils/swc-traverse.js'; -import { AstService } from '../../../../src/program/core/AstService.js'; +import { setupAnalyzerTest } from '../../../test-helpers/setup-analyzer-test.js'; +import { mockProject } from '../../../test-helpers/mock-project-helpers.js'; +import { oxcTraverse } from '../../../src/program/utils/oxc-traverse.js'; +import { AstService } from '../../../src/program/core/AstService.js'; import { trackDownIdentifier, trackDownIdentifierFromScope, -} from '../../../../src/program/utils/track-down-identifier.js'; +} from '../../../src/program/utils/track-down-identifier.js'; /** * @typedef {import('@babel/traverse').NodePath} NodePath @@ -295,7 +295,7 @@ describe('trackDownIdentifierFromScope', () => { mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); // const ast = AstService._getBabelAst(projectFiles['./src/declarationOfMyClass.js']); - const ast = AstService._getSwcAst(projectFiles['./src/declarationOfMyClass.js']); + const ast = await AstService._getOxcAst(projectFiles['./src/declarationOfMyClass.js']); // Let's say we want to track down 'MyClass' in the code above const identifierNameInScope = 'MyClass'; @@ -309,7 +309,7 @@ describe('trackDownIdentifierFromScope', () => { // astPath = path; // }, // }); - swcTraverse(ast, { + oxcTraverse(ast, { ClassDeclaration(path) { astPath = path; }, @@ -346,7 +346,7 @@ describe('trackDownIdentifierFromScope', () => { mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); // const ast = AstService._getBabelAst(projectFiles['./imported.js']); - const ast = AstService._getSwcAst(projectFiles['./imported.js']); + const ast = await AstService._getOxcAst(projectFiles['./imported.js']); // Let's say we want to track down 'MyClass' in the code above const identifierNameInScope = 'MyClass'; @@ -360,7 +360,7 @@ describe('trackDownIdentifierFromScope', () => { // astPath = path; // }, // }); - swcTraverse(ast, { + oxcTraverse(ast, { ImportDeclaration(path) { astPath = path; }, @@ -394,7 +394,7 @@ describe('trackDownIdentifierFromScope', () => { mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); // const ast = AstService._getBabelAst(projectFiles['./imported.js']); - const ast = AstService._getSwcAst(projectFiles['./imported.js']); + const ast = await AstService._getOxcAst(projectFiles['./imported.js']); // Let's say we want to track down 'MyClass' in the code above const identifierNameInScope = 'El1'; @@ -408,7 +408,7 @@ describe('trackDownIdentifierFromScope', () => { // astPath = path; // }, // }); - swcTraverse(ast, { + oxcTraverse(ast, { ClassDeclaration(path) { astPath = path; }, @@ -426,4 +426,54 @@ describe('trackDownIdentifierFromScope', () => { specifier: 'El1', }); }); + + it(`handles edge cases`, async () => { + const projectName = 'my-project'; + const projectPath = '/my/project'; + + const targetProject = { + path: projectPath, + name: projectName, + files: [ + { + file: './WolfComp.js', + code: ` + import { LionComp } from 'ref/LionComp.js'; + + export class WolfComp extends LionComp {} + `, + }, + { + file: './node_modules/ref/LionComp.js', + code: `export class LionComp extends HTMLElement {};`, + }, + ], + }; + + mockProject(targetProject, { projectName, projectPath }); + const ast = await AstService._getOxcAst(targetProject.files[0].code); + + // Let's say we want to track down 'LionComp' in the code above + const identifierNameInScope = 'LionComp'; + const fullCurrentFilePath = '/my/project/WolfComp.js'; + + /** @type {NodePath} */ + let astPath; + + oxcTraverse(ast, { + ClassDeclaration(path) { + astPath = path; + }, + }); + + const rootFile = await trackDownIdentifierFromScope( + // @ts-ignore + astPath, + identifierNameInScope, + fullCurrentFilePath, + projectPath, + ); + + expect(rootFile).to.deep.equal({ file: 'ref/LionComp.js', specifier: 'LionComp' }); + }); }); diff --git a/packages-node/providence-analytics/types/core/Analyzer.d.ts b/packages-node/providence-analytics/types/core/Analyzer.d.ts index f4bba1058..bb3563807 100644 --- a/packages-node/providence-analytics/types/core/Analyzer.d.ts +++ b/packages-node/providence-analytics/types/core/Analyzer.d.ts @@ -16,7 +16,7 @@ import { */ export type AnalyzerName = `${'find' | 'match'}-${string}` | ''; -export type AnalyzerAst = 'babel' | 'swc-to-babel' | 'swc'; +export type AnalyzerAst = 'babel' | 'swc' | 'oxc'; // TODO: make sure that data structures of JSON output (generated in ReportService) // and data structure generated in Analyzer.prototype._finalize match exactly (move logic from ReportSerivce to _finalize) diff --git a/packages-node/providence-analytics/types/utils/index.d.ts b/packages-node/providence-analytics/types/utils/index.d.ts index 0db8d6c98..211267ead 100644 --- a/packages-node/providence-analytics/types/utils/index.d.ts +++ b/packages-node/providence-analytics/types/utils/index.d.ts @@ -1,8 +1,11 @@ +import { IdentifierName } from "../index.js"; + export type SwcScope = { id: number; parentScope?: Scope; bindings: { [key: string]: SwcBinding }; path: SwcPath | null; + getBinding: (IdentifierName:string) => SwcBinding; _pendingRefsWithoutBinding: SwcNode[]; _isIsolatedBlockStatement: boolean; }; @@ -19,7 +22,7 @@ export type SwcPath = { node: SwcNode; parent: SwcNode; stop: function; - scope: SwcScope | undefined; + scope: SwcScope; parentPath: SwcPath | null | undefined; get: (id: string) => SwcPath | undefined; type: string; diff --git a/tsconfig.json b/tsconfig.json index 3fa8febcf..a42a71f42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "NodeNext", - "lib": ["es2017", "dom"], + "lib": ["es2022", "dom"], "allowJs": true, "checkJs": true, "strict": true,