From ea803894ae3d050258b0d7f873dfe4c2bcf3d5ff Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 9 May 2023 16:15:21 +0200 Subject: [PATCH] feat(providence): full swc support, including performant traverse tool --- .../providence-analytics/package.json | 3 +- .../providence-analytics/src/cli/cli.js | 2 +- .../app/components/p-table/PTable.js | 0 .../{ => src}/dashboard/app/p-board.js | 0 .../dashboard/app/styles/global.css.js | 0 .../app/styles/tableDecoration.css.js | 0 .../dashboard/app/styles/tooltip.css.js | 0 .../dashboard/app/styles/utils.css.js | 0 .../dashboard/app/tooltipComponentStyles.js | 0 .../dashboard/app/utils/DecorateMixin.js | 0 .../dashboard/app/utils/GlobalDecorator.js | 0 .../dashboard/app/utils/downloadFile.js | 0 .../{ => src}/dashboard/index.html | 0 .../{ => src}/dashboard/server.js | 12 +- .../src/program/analyzers/find-classes.js | 5 +- .../program/analyzers/find-customelements.js | 4 +- .../program/analyzers/find-exports--legacy.js | 272 +++++++++++ .../src/program/analyzers/find-exports.js | 190 ++++---- .../program/analyzers/find-imports--legacy.js | 189 ++++++++ .../src/program/analyzers/find-imports.js | 121 ++--- .../helpers/track-down-identifier--legacy.js | 330 +++++++++++++ .../helpers/track-down-identifier.js | 225 +++++---- .../src/program/analyzers/match-imports.js | 6 +- .../src/program/core/Analyzer.js | 101 ++-- .../src/program/core/AstService.js | 30 +- .../src/program/core/InputDataService.js | 20 +- .../src/program/core/QueryService.js | 7 +- .../src/program/core/ReportService.js | 3 +- ...ce-code-fragment-of-declaration--legacy.js | 188 ++++++++ ...get-source-code-fragment-of-declaration.js | 134 +++--- .../src/program/utils/resolve-import-path.js | 6 +- .../src/program/utils/swc-traverse.js | 359 ++++++++++++++ .../test-node/cli/cli.test.js | 2 +- .../dashboard/dashboard-server.test.js | 7 +- ...project_0.0.2-target-mock__-905964591.json | 220 +++++++++ ...t-project_0.0.2-target-mock__61665553.json | 52 ++ ...xporting-ref-project_1.0.0__-42206859.json | 194 ++++++++ ...-project_0.0.2-target-mock__349742630.json | 204 ++++++++ ...xporting-ref-project_1.0.0__142861209.json | 94 ++++ ...xporting-ref-project_1.0.0__142861209.json | 94 ++++ ...porting-ref-project_1.0.0__1982316146.json | 67 +++ .../analyzers/find-exports.test--legacy.js | 330 +++++++++++++ .../program/analyzers/find-exports.test.js | 19 +- .../helpers/track-down-identifier.test.js | 42 +- .../test-node/program/core/Analyzer.test.js | 4 +- ...ource-code-fragment-of-declaration.test.js | 3 +- .../program/utils/swc-traverse.test.js | 454 ++++++++++++++++++ .../providence-analytics/tsconfig.json | 2 +- .../types/core/Analyzer.d.ts | 6 +- .../providence-analytics/types/core/core.d.ts | 10 +- .../providence-analytics/types/index.ts | 1 + .../types/utils/index.d.ts | 31 ++ 52 files changed, 3584 insertions(+), 459 deletions(-) rename packages-node/providence-analytics/{ => src}/dashboard/app/components/p-table/PTable.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/p-board.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/styles/global.css.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/styles/tableDecoration.css.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/styles/tooltip.css.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/styles/utils.css.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/tooltipComponentStyles.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/utils/DecorateMixin.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/utils/GlobalDecorator.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/app/utils/downloadFile.js (100%) rename packages-node/providence-analytics/{ => src}/dashboard/index.html (100%) rename packages-node/providence-analytics/{ => src}/dashboard/server.js (92%) create mode 100644 packages-node/providence-analytics/src/program/analyzers/find-exports--legacy.js create mode 100644 packages-node/providence-analytics/src/program/analyzers/find-imports--legacy.js create mode 100644 packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier--legacy.js create mode 100644 packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration--legacy.js create mode 100644 packages-node/providence-analytics/src/program/utils/swc-traverse.js create mode 100644 packages-node/providence-analytics/test-node/program/analyzers/e2e/find-classes_-_importing-target-project_0.0.2-target-mock__-905964591.json create mode 100644 packages-node/providence-analytics/test-node/program/analyzers/e2e/find-customelements_-_importing-target-project_0.0.2-target-mock__61665553.json create mode 100644 packages-node/providence-analytics/test-node/program/analyzers/e2e/find-exports_-_exporting-ref-project_1.0.0__-42206859.json create mode 100644 packages-node/providence-analytics/test-node/program/analyzers/e2e/find-imports_-_importing-target-project_0.0.2-target-mock__349742630.json create mode 100644 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__142861209.json create mode 100644 packages-node/providence-analytics/test-node/program/analyzers/e2e/match-paths_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209.json create mode 100644 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 create mode 100644 packages-node/providence-analytics/test-node/program/analyzers/find-exports.test--legacy.js create mode 100644 packages-node/providence-analytics/test-node/program/utils/swc-traverse.test.js create mode 100644 packages-node/providence-analytics/types/utils/index.d.ts diff --git a/packages-node/providence-analytics/package.json b/packages-node/providence-analytics/package.json index 9ba0db0ca..097c7b319 100644 --- a/packages-node/providence-analytics/package.json +++ b/packages-node/providence-analytics/package.json @@ -23,11 +23,10 @@ "providence": "./src/cli/index.js" }, "files": [ - "dashboard/src", "src" ], "scripts": { - "dashboard": "node ./dashboard/server.js --run-server --serve-from-package-root", + "dashboard": "node ./src/dashboard/server.js --run-server --serve-from-package-root", "postinstall": "npx patch-package", "match-lion-imports": "npm run providence -- analyze match-imports --search-target-collection @lion-targets --reference-collection @lion-references --measure-perf --skip-check-match-compatibility", "providence": "node --max-old-space-size=8192 ./src/cli/index.js", diff --git a/packages-node/providence-analytics/src/cli/cli.js b/packages-node/providence-analytics/src/cli/cli.js index 22a74614b..641d98262 100755 --- a/packages-node/providence-analytics/src/cli/cli.js +++ b/packages-node/providence-analytics/src/cli/cli.js @@ -7,7 +7,7 @@ import { QueryService } from '../program/core/QueryService.js'; import { InputDataService } from '../program/core/InputDataService.js'; import { toPosixPath } from '../program/utils/to-posix-path.js'; import { getCurrentDir } from '../program/utils/get-current-dir.js'; -import { dashboardServer } from '../../dashboard/server.js'; +import { dashboardServer } from '../dashboard/server.js'; import { _providenceModule } from '../program/providence.js'; import { _cliHelpersModule } from './cli-helpers.js'; import { _extendDocsModule } from './launch-providence-with-extend-docs.js'; diff --git a/packages-node/providence-analytics/dashboard/app/components/p-table/PTable.js b/packages-node/providence-analytics/src/dashboard/app/components/p-table/PTable.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/components/p-table/PTable.js rename to packages-node/providence-analytics/src/dashboard/app/components/p-table/PTable.js diff --git a/packages-node/providence-analytics/dashboard/app/p-board.js b/packages-node/providence-analytics/src/dashboard/app/p-board.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/p-board.js rename to packages-node/providence-analytics/src/dashboard/app/p-board.js diff --git a/packages-node/providence-analytics/dashboard/app/styles/global.css.js b/packages-node/providence-analytics/src/dashboard/app/styles/global.css.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/styles/global.css.js rename to packages-node/providence-analytics/src/dashboard/app/styles/global.css.js diff --git a/packages-node/providence-analytics/dashboard/app/styles/tableDecoration.css.js b/packages-node/providence-analytics/src/dashboard/app/styles/tableDecoration.css.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/styles/tableDecoration.css.js rename to packages-node/providence-analytics/src/dashboard/app/styles/tableDecoration.css.js diff --git a/packages-node/providence-analytics/dashboard/app/styles/tooltip.css.js b/packages-node/providence-analytics/src/dashboard/app/styles/tooltip.css.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/styles/tooltip.css.js rename to packages-node/providence-analytics/src/dashboard/app/styles/tooltip.css.js diff --git a/packages-node/providence-analytics/dashboard/app/styles/utils.css.js b/packages-node/providence-analytics/src/dashboard/app/styles/utils.css.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/styles/utils.css.js rename to packages-node/providence-analytics/src/dashboard/app/styles/utils.css.js diff --git a/packages-node/providence-analytics/dashboard/app/tooltipComponentStyles.js b/packages-node/providence-analytics/src/dashboard/app/tooltipComponentStyles.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/tooltipComponentStyles.js rename to packages-node/providence-analytics/src/dashboard/app/tooltipComponentStyles.js diff --git a/packages-node/providence-analytics/dashboard/app/utils/DecorateMixin.js b/packages-node/providence-analytics/src/dashboard/app/utils/DecorateMixin.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/utils/DecorateMixin.js rename to packages-node/providence-analytics/src/dashboard/app/utils/DecorateMixin.js diff --git a/packages-node/providence-analytics/dashboard/app/utils/GlobalDecorator.js b/packages-node/providence-analytics/src/dashboard/app/utils/GlobalDecorator.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/utils/GlobalDecorator.js rename to packages-node/providence-analytics/src/dashboard/app/utils/GlobalDecorator.js diff --git a/packages-node/providence-analytics/dashboard/app/utils/downloadFile.js b/packages-node/providence-analytics/src/dashboard/app/utils/downloadFile.js similarity index 100% rename from packages-node/providence-analytics/dashboard/app/utils/downloadFile.js rename to packages-node/providence-analytics/src/dashboard/app/utils/downloadFile.js diff --git a/packages-node/providence-analytics/dashboard/index.html b/packages-node/providence-analytics/src/dashboard/index.html similarity index 100% rename from packages-node/providence-analytics/dashboard/index.html rename to packages-node/providence-analytics/src/dashboard/index.html diff --git a/packages-node/providence-analytics/dashboard/server.js b/packages-node/providence-analytics/src/dashboard/server.js similarity index 92% rename from packages-node/providence-analytics/dashboard/server.js rename to packages-node/providence-analytics/src/dashboard/server.js index de551c36e..1c69d216c 100644 --- a/packages-node/providence-analytics/dashboard/server.js +++ b/packages-node/providence-analytics/src/dashboard/server.js @@ -1,14 +1,14 @@ import fs from 'fs'; import pathLib from 'path'; import { startDevServer } from '@web/dev-server'; -import { ReportService } from '../src/program/core/ReportService.js'; -import { providenceConfUtil } from '../src/program/utils/providence-conf-util.js'; -import { getCurrentDir } from '../src/program/utils/get-current-dir.js'; +import { ReportService } from '../program/core/ReportService.js'; +import { providenceConfUtil } from '../program/utils/providence-conf-util.js'; +import { getCurrentDir } from '../program/utils/get-current-dir.js'; /** - * @typedef {import('../types/index.js').PathFromSystemRoot} PathFromSystemRoot - * @typedef {import('../types/index.js').GatherFilesConfig} GatherFilesConfig - * @typedef {import('../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../types/index.js').PathFromSystemRoot} PathFromSystemRoot + * @typedef {import('../../types/index.js').GatherFilesConfig} GatherFilesConfig + * @typedef {import('../../types/index.js').AnalyzerName} AnalyzerName */ /** 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 f9c9290d7..3ab8ee7c9 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-classes.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-classes.js @@ -1,9 +1,10 @@ /* eslint-disable no-shadow, no-param-reassign */ import path from 'path'; import t from '@babel/types'; +// @ts-ignore import babelTraverse from '@babel/traverse'; import { Analyzer } from '../core/Analyzer.js'; -import { trackDownIdentifierFromScope } from './helpers/track-down-identifier.js'; +import { trackDownIdentifierFromScope } from './helpers/track-down-identifier--legacy.js'; /** * @typedef {import('@babel/types').File} File @@ -227,7 +228,7 @@ export default class FindClassesAnalyzer extends Analyzer { static analyzerName = 'find-classes'; /** @type {'babel'|'swc-to-babel'} */ - requiredAst = 'babel'; + static requiredAst = 'babel'; /** * 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 cc3eb0293..5d6d3a348 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-customelements.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-customelements.js @@ -2,7 +2,7 @@ import path from 'path'; import t from '@babel/types'; import babelTraverse from '@babel/traverse'; import { Analyzer } from '../core/Analyzer.js'; -import { trackDownIdentifierFromScope } from './helpers/track-down-identifier.js'; +import { trackDownIdentifierFromScope } from './helpers/track-down-identifier--legacy.js'; /** * @typedef {import('@babel/types').File} File @@ -94,7 +94,7 @@ export default class FindCustomelementsAnalyzer extends Analyzer { static analyzerName = 'find-customelements'; /** @type {'babel'|'swc-to-babel'} */ - requiredAst = 'swc-to-babel'; + static requiredAst = 'swc-to-babel'; /** * Finds export specifiers and sources diff --git a/packages-node/providence-analytics/src/program/analyzers/find-exports--legacy.js b/packages-node/providence-analytics/src/program/analyzers/find-exports--legacy.js new file mode 100644 index 000000000..b7f8f8d6d --- /dev/null +++ b/packages-node/providence-analytics/src/program/analyzers/find-exports--legacy.js @@ -0,0 +1,272 @@ +/* eslint-disable no-shadow, no-param-reassign */ +import pathLib from 'path'; +import babelTraverse from '@babel/traverse'; +import { Analyzer } from '../core/Analyzer.js'; +import { trackDownIdentifier } from './helpers/track-down-identifier--legacy.js'; +import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; +import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-declaration.js'; +import { LogService } from '../core/LogService.js'; + +/** + * @typedef {import('@babel/types').File} File + * @typedef {import('@babel/types').Node} Node + * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult + * @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry + * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot + * @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile + * @typedef {object} RootFileMapEntry + * @typedef {string} currentFileSpecifier this is the local name in the file we track from + * @typedef {RootFile} rootFile contains file(filePath) and specifier + * @typedef {RootFileMapEntry[]} RootFileMap + * @typedef {{ exportSpecifiers:string[]; localMap: object; source:string, __tmp: { path:string } }} FindExportsSpecifierObj + */ + +/** + * @param {FindExportsSpecifierObj[]} transformedFile + */ +async function trackdownRoot(transformedFile, relativePath, projectPath) { + const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath); + for (const specObj of transformedFile) { + /** @type {RootFileMap} */ + const rootFileMap = []; + if (specObj.exportSpecifiers[0] === '[file]') { + rootFileMap.push(undefined); + } else { + /** + * './src/origin.js': `export class MyComp {}` + * './index.js:' `export { MyComp as RenamedMyComp } from './src/origin'` + * + * Goes from specifier like 'RenamedMyComp' to object for rootFileMap like: + * { + * currentFileSpecifier: 'RenamedMyComp', + * rootFile: { + * file: './src/origin.js', + * specifier: 'MyCompDefinition', + * } + * } + */ + for (const specifier of specObj.exportSpecifiers) { + let rootFile; + let localMapMatch; + if (specObj.localMap) { + localMapMatch = specObj.localMap.find(m => m.exported === specifier); + } + + // TODO: find out if possible to use trackDownIdentifierFromScope + if (specObj.source) { + // TODO: see if still needed: && (localMapMatch || specifier === '[default]') + const importedIdentifier = localMapMatch?.local || specifier; + + rootFile = await trackDownIdentifier( + specObj.source, + importedIdentifier, + fullCurrentFilePath, + projectPath, + ); + + /** @type {RootFileMapEntry} */ + const entry = { + currentFileSpecifier: specifier, + rootFile, + }; + rootFileMap.push(entry); + } else { + /** @type {RootFileMapEntry} */ + const entry = { + currentFileSpecifier: specifier, + rootFile: { file: '[current]', specifier }, + }; + rootFileMap.push(entry); + } + } + } + specObj.rootFileMap = rootFileMap; + } + return transformedFile; +} + +function cleanup(transformedFile) { + transformedFile.forEach(specObj => { + if (specObj.__tmp) { + delete specObj.__tmp; + } + }); + return transformedFile; +} + +/** + * @returns {string[]} + */ +function getExportSpecifiers(node) { + // handles default [export const g = 4]; + if (node.declaration) { + if (node.declaration.declarations) { + return [node.declaration.declarations[0].id.name]; + } + if (node.declaration.id) { + return [node.declaration.id.name]; + } + } + + // handles (re)named specifiers [export { x (as y)} from 'y']; + return node.specifiers.map(s => { + let specifier; + if (s.exported) { + // { x as y } + specifier = s.exported.name === 'default' ? '[default]' : s.exported.name; + } else { + // { x } + specifier = s.local.name; + } + return specifier; + }); +} + +/** + * @returns {object[]} + */ +function getLocalNameSpecifiers(node) { + return node.specifiers + .map(s => { + if (s.exported && s.local && s.exported.name !== s.local.name) { + return { + // if reserved keyword 'default' is used, translate it into 'providence keyword' + local: s.local.name === 'default' ? '[default]' : s.local.name, + exported: s.exported.name, + }; + } + return undefined; + }) + .filter(s => s); +} + +const isImportingSpecifier = pathOrNode => + pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier'; + +/** + * Finds import specifiers and sources for a given ast result + * @param {File} babelAst + * @param {FindExportsConfig} config + */ +function findExportsPerAstFile(babelAst, { skipFileImports }) { + LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`); + + // Visit AST... + + /** @type {FindExportsSpecifierObj[]} */ + const transformedFile = []; + // Unfortunately, we cannot have async functions in babel traverse. + // Therefore, we store a temp reference to path that we use later for + // async post processing (tracking down original export Identifier) + let globalScopeBindings; + + babelTraverse.default(babelAst, { + Program(babelPath) { + // enter(babelPath) { + const body = babelPath.get('body'); + if (body.length) { + globalScopeBindings = body[0].scope.bindings; + } + // }, + }, + ExportNamedDeclaration(astPath) { + const exportSpecifiers = getExportSpecifiers(astPath.node); + const localMap = getLocalNameSpecifiers(astPath.node); + const source = astPath.node.source?.value; + const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } }; + if (astPath.node.assertions?.length) { + entry.assertionType = astPath.node.assertions[0].value?.value; + } + transformedFile.push(entry); + }, + ExportDefaultDeclaration(defaultExportPath) { + const exportSpecifiers = ['[default]']; + let source; + if (defaultExportPath.node.declaration?.type !== 'Identifier') { + source = defaultExportPath.node.declaration.name; + } else { + const importOrDeclPath = getReferencedDeclaration({ + referencedIdentifierName: defaultExportPath.node.declaration.name, + globalScopeBindings, + }); + if (isImportingSpecifier(importOrDeclPath)) { + source = importOrDeclPath.parentPath.node.source.value; + } + } + transformedFile.push({ exportSpecifiers, source, __tmp: { astPath: defaultExportPath } }); + }, + }); + + if (!skipFileImports) { + // Always add an entry for just the file 'relativePath' + // (since this also can be imported directly from a search target project) + transformedFile.push({ + exportSpecifiers: ['[file]'], + // source: relativePath, + }); + } + + return transformedFile; +} + +export default class FindExportsAnalyzer extends Analyzer { + /** @type {AnalyzerName} */ + static analyzerName = 'find-exports'; + + /** @type {'babel'|'swc-to-babel'} */ + static requiredAst = 'swc-to-babel'; + + /** + * Finds export specifiers and sources + * @param {FindExportsConfig} customConfig + */ + async execute(customConfig = {}) { + /** + * @typedef FindExportsConfig + * @property {boolean} [onlyInternalSources=false] + * @property {boolean} [skipFileImports=false] Instead of both focusing on specifiers like + * [import {specifier} 'lion-based-ui/foo.js'], and [import 'lion-based-ui/foo.js'] as a result, + * not list file exports + */ + const cfg = { + targetProjectPath: null, + skipFileImports: false, + ...customConfig, + }; + + /** + * Prepare + */ + const cachedAnalyzerResult = this._prepare(cfg); + if (cachedAnalyzerResult) { + return cachedAnalyzerResult; + } + + /** + * Traverse + */ + const projectPath = cfg.targetProjectPath; + + const traverseEntryFn = async (ast, { relativePath }) => { + let transformedFile = findExportsPerAstFile(ast, cfg); + + transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath); + transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath); + transformedFile = cleanup(transformedFile); + + return { result: transformedFile }; + }; + + const queryOutput = await this._traverse({ + traverseEntryFn, + filePaths: cfg.targetFilePaths, + projectPath: cfg.targetProjectPath, + }); + + /** + * Finalize + */ + return this._finalize(queryOutput, cfg); + } +} 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 22a1e4ab1..49acf8db3 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-exports.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-exports.js @@ -1,6 +1,6 @@ /* eslint-disable no-shadow, no-param-reassign */ -import pathLib from 'path'; -import babelTraverse from '@babel/traverse'; +import path from 'path'; +import { swcTraverse } from '../utils/swc-traverse.js'; import { Analyzer } from '../core/Analyzer.js'; import { trackDownIdentifier } from './helpers/track-down-identifier.js'; import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; @@ -8,12 +8,18 @@ import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-d import { LogService } from '../core/LogService.js'; /** - * @typedef {import('@babel/types').File} File - * @typedef {import('@babel/types').Node} Node + * @typedef {import("@swc/core").Module} SwcAstModule + * @typedef {import("@swc/core").Node} SwcNode + * @typedef {import("@swc/core").VariableDeclaration} SwcVariableDeclaration * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst * @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult * @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot + * @typedef {import('../../../types/index.js').SwcScope} SwcScope + * @typedef {import('../../../types/index.js').SwcBinding} SwcBinding + * @typedef {import('../../../types/index.js').SwcPath} SwcPath + * @typedef {import('../../../types/index.js').SwcVisitor} SwcVisitor * @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile * @typedef {object} RootFileMapEntry * @typedef {string} currentFileSpecifier this is the local name in the file we track from @@ -26,7 +32,7 @@ import { LogService } from '../core/LogService.js'; * @param {FindExportsSpecifierObj[]} transformedFile */ async function trackdownRoot(transformedFile, relativePath, projectPath) { - const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath); + const fullCurrentFilePath = path.resolve(projectPath, relativePath); for (const specObj of transformedFile) { /** @type {RootFileMap} */ const rootFileMap = []; @@ -96,49 +102,47 @@ function cleanup(transformedFile) { } /** + * @param {*} node * @returns {string[]} */ function getExportSpecifiers(node) { // handles default [export const g = 4]; if (node.declaration) { if (node.declaration.declarations) { - return [node.declaration.declarations[0].id.name]; + return [node.declaration.declarations[0].id.value]; } - if (node.declaration.id) { - return [node.declaration.id.name]; + if (node.declaration.identifier) { + return [node.declaration.identifier.value]; } } // handles (re)named specifiers [export { x (as y)} from 'y']; - return node.specifiers.map(s => { - let specifier; + return (node.specifiers || []).map(s => { if (s.exported) { // { x as y } - specifier = s.exported.name === 'default' ? '[default]' : s.exported.name; - } else { - // { x } - specifier = s.local.name; + return s.exported.value === 'default' ? '[default]' : s.exported.value; } - return specifier; + // { x } + return s.orig.value; }); } /** - * @returns {object[]} + * @returns {{local:string;exported:string;}|undefined[]} */ function getLocalNameSpecifiers(node) { - return node.specifiers + return (node.declaration?.declarations || node.specifiers || []) .map(s => { - if (s.exported && s.local && s.exported.name !== s.local.name) { + if (s.exported && s.orig && s.exported.value !== s.orig.value) { return { // if reserved keyword 'default' is used, translate it into 'providence keyword' - local: s.local.name === 'default' ? '[default]' : s.local.name, - exported: s.exported.name, + local: s.orig.value === 'default' ? '[default]' : s.orig.value, + exported: s.exported.value, }; } return undefined; }) - .filter(s => s); + .filter(Boolean); } const isImportingSpecifier = pathOrNode => @@ -146,10 +150,10 @@ const isImportingSpecifier = pathOrNode => /** * Finds import specifiers and sources for a given ast result - * @param {File} babelAst + * @param {SwcAstModule} swcAst * @param {FindExportsConfig} config */ -function findExportsPerAstFile(babelAst, { skipFileImports }) { +function findExportsPerAstFile(swcAst, { skipFileImports }) { LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`); // Visit AST... @@ -159,44 +163,52 @@ function findExportsPerAstFile(babelAst, { skipFileImports }) { // Unfortunately, we cannot have async functions in babel traverse. // Therefore, we store a temp reference to path that we use later for // async post processing (tracking down original export Identifier) + /** @type {{[key:string]:SwcBinding}} */ let globalScopeBindings; - babelTraverse.default(babelAst, { - Program: { - enter(babelPath) { - const body = babelPath.get('body'); - if (body.length) { - globalScopeBindings = body[0].scope.bindings; - } - }, - }, - ExportNamedDeclaration(astPath) { - const exportSpecifiers = getExportSpecifiers(astPath.node); - const localMap = getLocalNameSpecifiers(astPath.node); - const source = astPath.node.source?.value; - const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } }; - if (astPath.node.assertions?.length) { - entry.assertionType = astPath.node.assertions[0].value?.value; + const exportHandler = (/** @type {SwcPath} */ astPath) => { + const exportSpecifiers = getExportSpecifiers(astPath.node); + const localMap = getLocalNameSpecifiers(astPath.node); + const source = astPath.node.source?.value; + const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } }; + if (astPath.node.asserts) { + entry.assertionType = astPath.node.asserts.properties[0].value?.value; + } + transformedFile.push(entry); + }; + + const exportDefaultHandler = (/** @type {SwcPath} */ astPath) => { + const exportSpecifiers = ['[default]']; + let source; + // Is it an inline declaration like "export default class X {};" ? + if ( + astPath.node.decl?.type === 'Identifier' || + astPath.node.expression?.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, + globalScopeBindings, + }); + if (isImportingSpecifier(importOrDeclPath)) { + source = importOrDeclPath.parentPath.node.source.value; } - transformedFile.push(entry); + } + transformedFile.push({ exportSpecifiers, source, __tmp: { astPath } }); + }; + + /** @type {SwcVisitor} */ + const visitor = { + Module({ scope }) { + globalScopeBindings = scope.bindings; }, - ExportDefaultDeclaration(defaultExportPath) { - const exportSpecifiers = ['[default]']; - let source; - if (defaultExportPath.node.declaration?.type !== 'Identifier') { - source = defaultExportPath.node.declaration.name; - } else { - const importOrDeclPath = getReferencedDeclaration({ - referencedIdentifierName: defaultExportPath.node.declaration.name, - globalScopeBindings, - }); - if (isImportingSpecifier(importOrDeclPath)) { - source = importOrDeclPath.parentPath.node.source.value; - } - } - transformedFile.push({ exportSpecifiers, source, __tmp: { astPath: defaultExportPath } }); - }, - }); + ExportDeclaration: exportHandler, + ExportNamedDeclaration: exportHandler, + ExportDefaultDeclaration: exportDefaultHandler, + ExportDefaultExpression: exportDefaultHandler, + }; + + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); if (!skipFileImports) { // Always add an entry for just the file 'relativePath' @@ -211,62 +223,34 @@ function findExportsPerAstFile(babelAst, { skipFileImports }) { } export default class FindExportsAnalyzer extends Analyzer { - /** @type {AnalyzerName} */ - static analyzerName = 'find-exports'; + static analyzerName = /** @type {AnalyzerName} */ ('find-exports'); - /** @type {'babel'|'swc-to-babel'} */ - requiredAst = 'swc-to-babel'; + static requiredAst = /** @type {AnalyzerAst} */ ('swc'); /** - * Finds export specifiers and sources - * @param {FindExportsConfig} customConfig + * @typedef FindExportsConfig + * @property {boolean} [onlyInternalSources=false] + * @property {boolean} [skipFileImports=false] Instead of both focusing on specifiers like + * [import {specifier} 'lion-based-ui/foo.js'], and [import 'lion-based-ui/foo.js'] as a result, + * not list file exports */ - async execute(customConfig = {}) { - /** - * @typedef FindExportsConfig - * @property {boolean} [onlyInternalSources=false] - * @property {boolean} [skipFileImports=false] Instead of both focusing on specifiers like - * [import {specifier} 'lion-based-ui/foo.js'], and [import 'lion-based-ui/foo.js'] as a result, - * not list file exports - */ - const cfg = { + get config() { + return { targetProjectPath: null, skipFileImports: false, - ...customConfig, + ...this._customConfig, }; + } - /** - * Prepare - */ - const cachedAnalyzerResult = this._prepare(cfg); - if (cachedAnalyzerResult) { - return cachedAnalyzerResult; - } + static async analyzeFile(ast, { relativePath, analyzerCfg }) { + const projectPath = analyzerCfg.targetProjectPath; - /** - * Traverse - */ - const projectPath = cfg.targetProjectPath; + let transformedFile = findExportsPerAstFile(ast, analyzerCfg); - const traverseEntryFn = async (ast, { relativePath }) => { - let transformedFile = findExportsPerAstFile(ast, cfg); + transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath); + transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath); + transformedFile = cleanup(transformedFile); - transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath); - transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath); - transformedFile = cleanup(transformedFile); - - return { result: transformedFile }; - }; - - const queryOutput = await this._traverse({ - traverseEntryFn, - filePaths: cfg.targetFilePaths, - projectPath: cfg.targetProjectPath, - }); - - /** - * Finalize - */ - return this._finalize(queryOutput, cfg); + return { result: transformedFile }; } } diff --git a/packages-node/providence-analytics/src/program/analyzers/find-imports--legacy.js b/packages-node/providence-analytics/src/program/analyzers/find-imports--legacy.js new file mode 100644 index 000000000..cff31fe5b --- /dev/null +++ b/packages-node/providence-analytics/src/program/analyzers/find-imports--legacy.js @@ -0,0 +1,189 @@ +/* eslint-disable no-shadow, no-param-reassign */ +import babelTraverse from '@babel/traverse'; +import { isRelativeSourcePath } from '../utils/relative-source-path.js'; +import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; +import { Analyzer } from '../core/Analyzer.js'; +import { LogService } from '../core/LogService.js'; + +/** + * @typedef {import('@babel/types').File} File + * @typedef {import('@babel/types').Node} Node + * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig + * @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult + * @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry + * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot + */ + +/** + * Options that allow to filter 'on a file basis'. + * We can also filter on the total result + */ +const /** @type {AnalyzerConfig} */ options = { + /** + * Only leaves entries with external sources: + * - keeps: '@open-wc/testing' + * - drops: '../testing' + * @param {FindImportsAnalyzerQueryOutput} result + * @param {string} targetSpecifier for instance 'LitElement' + */ + onlyExternalSources(result) { + return result.filter(entry => !isRelativeSourcePath(entry.source)); + }, + }; + +/** + * @param {Node} node + */ +function getImportOrReexportsSpecifiers(node) { + return node.specifiers.map(s => { + if ( + s.type === 'ImportDefaultSpecifier' || + s.type === 'ExportDefaultSpecifier' || + (s.type === 'ExportSpecifier' && s.exported?.name === 'default') + ) { + return '[default]'; + } + if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') { + return '[*]'; + } + if ((s.imported && s.type === 'ImportNamespaceSpecifier') || s.type === 'ImportSpecifier') { + return s.imported.name; + } + if (s.exported && s.type === 'ExportNamespaceSpecifier') { + return s.exported.name; + } + return s.local.name; + }); +} + +/** + * Finds import specifiers and sources + * @param {File} babelAst + */ +function findImportsPerAstFile(babelAst, context) { + 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 = []; + babelTraverse.default(babelAst, { + ImportDeclaration(path) { + const importSpecifiers = getImportOrReexportsSpecifiers(path.node); + if (!importSpecifiers.length) { + importSpecifiers.push('[file]'); // apparently, there was just a file import + } + const source = path.node.source.value; + const entry = /** @type {Partial} */ ({ importSpecifiers, source }); + if (path.node.assertions?.length) { + entry.assertionType = path.node.assertions[0].value?.value; + } + transformedFile.push(entry); + }, + // Dynamic imports + CallExpression(path) { + if (path.node.callee?.type !== 'Import') { + return; + } + // TODO: check for specifiers catched via obj destructuring? + // TODO: also check for ['file'] + const importSpecifiers = ['[default]']; + let source = path.node.arguments[0].value; + if (!source) { + // TODO: with advanced retrieval, we could possibly get the value + source = '[variable]'; + } + transformedFile.push({ importSpecifiers, source }); + }, + ExportNamedDeclaration(path) { + if (!path.node.source) { + return; // we are dealing with a regular export, not a reexport + } + const importSpecifiers = getImportOrReexportsSpecifiers(path.node); + const source = path.node.source.value; + const entry = /** @type {Partial} */ ({ importSpecifiers, source }); + if (path.node.assertions?.length) { + entry.assertionType = path.node.assertions[0].value?.value; + } + transformedFile.push(entry); + }, + // ExportAllDeclaration(path) { + // if (!path.node.source) { + // return; // we are dealing with a regular export, not a reexport + // } + // const importSpecifiers = ['[*]']; + // const source = path.node.source.value; + // transformedFile.push({ importSpecifiers, source }); + // }, + }); + + return transformedFile; +} + +export default class FindImportsAnalyzer extends Analyzer { + /** @type {AnalyzerName} */ + static analyzerName = 'find-imports'; + + /** @type {'babel'|'swc-to-babel'} */ + requiredAst = 'swc-to-babel'; + + /** + * Finds import specifiers and sources + * @param {FindImportsConfig} customConfig + */ + async execute(customConfig = {}) { + /** + * @typedef FindImportsConfig + * @property {boolean} [keepInternalSources=false] by default, relative paths like '../x.js' are + * filtered out. This option keeps them. + * means that 'external-dep/file' will be resolved to 'external-dep/file.js' will both be stored + * as the latter + */ + const cfg = { + targetProjectPath: null, + // post process file + keepInternalSources: false, + ...customConfig, + }; + + /** + * Prepare + */ + const cachedAnalyzerResult = this._prepare(cfg); + if (cachedAnalyzerResult) { + return cachedAnalyzerResult; + } + + /** + * Traverse + */ + const queryOutput = await this._traverse(async (ast, context) => { + let transformedFile = findImportsPerAstFile(ast, context); + // Post processing based on configuration... + transformedFile = await normalizeSourcePaths( + transformedFile, + context.relativePath, + cfg.targetProjectPath, + ); + + if (!cfg.keepInternalSources) { + transformedFile = options.onlyExternalSources(transformedFile); + } + + return { result: transformedFile }; + }); + + // if (cfg.sortBySpecifier) { + // queryOutput = sortBySpecifier.execute(queryOutput, { + // ...cfg, + // specifiersKey: 'importSpecifiers', + // }); + // } + + /** + * Finalize + */ + return this._finalize(queryOutput, cfg); + } +} 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 cff31fe5b..efa010ae2 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-imports.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-imports.js @@ -1,14 +1,15 @@ /* eslint-disable no-shadow, no-param-reassign */ -import babelTraverse from '@babel/traverse'; import { isRelativeSourcePath } from '../utils/relative-source-path.js'; +import { swcTraverse } from '../utils/swc-traverse.js'; import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; import { Analyzer } from '../core/Analyzer.js'; import { LogService } from '../core/LogService.js'; /** - * @typedef {import('@babel/types').File} File - * @typedef {import('@babel/types').Node} Node + * @typedef {import("@swc/core").Module} SwcAstModule + * @typedef {import("@swc/core").Node} SwcNode * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst * @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig * @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult * @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry @@ -16,117 +17,86 @@ import { LogService } from '../core/LogService.js'; */ /** - * Options that allow to filter 'on a file basis'. - * We can also filter on the total result - */ -const /** @type {AnalyzerConfig} */ options = { - /** - * Only leaves entries with external sources: - * - keeps: '@open-wc/testing' - * - drops: '../testing' - * @param {FindImportsAnalyzerQueryOutput} result - * @param {string} targetSpecifier for instance 'LitElement' - */ - onlyExternalSources(result) { - return result.filter(entry => !isRelativeSourcePath(entry.source)); - }, - }; - -/** - * @param {Node} node + * @param {SwcNode} node */ function getImportOrReexportsSpecifiers(node) { return node.specifiers.map(s => { if ( s.type === 'ImportDefaultSpecifier' || s.type === 'ExportDefaultSpecifier' || - (s.type === 'ExportSpecifier' && s.exported?.name === 'default') + (s.type === 'ExportSpecifier' && s.exported?.value === 'default') ) { return '[default]'; } if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') { return '[*]'; } - if ((s.imported && s.type === 'ImportNamespaceSpecifier') || s.type === 'ImportSpecifier') { - return s.imported.name; - } - if (s.exported && s.type === 'ExportNamespaceSpecifier') { - return s.exported.name; - } - return s.local.name; + const importedValue = s.imported?.value || s.orig?.value || s.exported?.value || s.local?.value; + return importedValue; }); } /** * Finds import specifiers and sources - * @param {File} babelAst + * @param {SwcAstModule} swcAst */ -function findImportsPerAstFile(babelAst, context) { +function findImportsPerAstFile(swcAst, context) { 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 = []; - babelTraverse.default(babelAst, { - ImportDeclaration(path) { - const importSpecifiers = getImportOrReexportsSpecifiers(path.node); + + swcTraverse(swcAst, { + ImportDeclaration({ node }) { + const importSpecifiers = getImportOrReexportsSpecifiers(node); if (!importSpecifiers.length) { importSpecifiers.push('[file]'); // apparently, there was just a file import } - const source = path.node.source.value; + const source = node.source.value; const entry = /** @type {Partial} */ ({ importSpecifiers, source }); - if (path.node.assertions?.length) { - entry.assertionType = path.node.assertions[0].value?.value; + if (node.asserts) { + entry.assertionType = node.asserts.properties[0].value?.value; + } + transformedFile.push(entry); + }, + ExportNamedDeclaration({ node }) { + if (!node.source) { + return; // we are dealing with a regular export, not a reexport + } + const importSpecifiers = getImportOrReexportsSpecifiers(node); + const source = node.source.value; + const entry = /** @type {Partial} */ ({ importSpecifiers, source }); + if (node.asserts) { + entry.assertionType = node.asserts.properties[0].value?.value; } transformedFile.push(entry); }, // Dynamic imports - CallExpression(path) { - if (path.node.callee?.type !== 'Import') { + CallExpression({ node }) { + if (node.callee?.type !== 'Import') { return; } // TODO: check for specifiers catched via obj destructuring? // TODO: also check for ['file'] const importSpecifiers = ['[default]']; - let source = path.node.arguments[0].value; - if (!source) { - // TODO: with advanced retrieval, we could possibly get the value - source = '[variable]'; - } + const dynamicImportExpression = node.arguments[0].expression; + const source = + dynamicImportExpression.type === 'StringLiteral' + ? dynamicImportExpression.value + : '[variable]'; transformedFile.push({ importSpecifiers, source }); }, - ExportNamedDeclaration(path) { - if (!path.node.source) { - return; // we are dealing with a regular export, not a reexport - } - const importSpecifiers = getImportOrReexportsSpecifiers(path.node); - const source = path.node.source.value; - const entry = /** @type {Partial} */ ({ importSpecifiers, source }); - if (path.node.assertions?.length) { - entry.assertionType = path.node.assertions[0].value?.value; - } - transformedFile.push(entry); - }, - // ExportAllDeclaration(path) { - // if (!path.node.source) { - // return; // we are dealing with a regular export, not a reexport - // } - // const importSpecifiers = ['[*]']; - // const source = path.node.source.value; - // transformedFile.push({ importSpecifiers, source }); - // }, }); return transformedFile; } -export default class FindImportsAnalyzer extends Analyzer { - /** @type {AnalyzerName} */ - static analyzerName = 'find-imports'; +export default class FindImportsSwcAnalyzer extends Analyzer { + static analyzerName = /** @type {AnalyzerName} */ ('find-imports'); - /** @type {'babel'|'swc-to-babel'} */ - requiredAst = 'swc-to-babel'; + static requiredAst = /** @type {AnalyzerAst} */ ('swc'); /** * Finds import specifiers and sources @@ -158,8 +128,8 @@ export default class FindImportsAnalyzer extends Analyzer { /** * Traverse */ - const queryOutput = await this._traverse(async (ast, context) => { - let transformedFile = findImportsPerAstFile(ast, context); + const queryOutput = await this._traverse(async (swcAst, context) => { + let transformedFile = findImportsPerAstFile(swcAst, context); // Post processing based on configuration... transformedFile = await normalizeSourcePaths( transformedFile, @@ -168,19 +138,12 @@ export default class FindImportsAnalyzer extends Analyzer { ); if (!cfg.keepInternalSources) { - transformedFile = options.onlyExternalSources(transformedFile); + transformedFile = transformedFile.filter(entry => !isRelativeSourcePath(entry.source)); } return { result: transformedFile }; }); - // if (cfg.sortBySpecifier) { - // queryOutput = sortBySpecifier.execute(queryOutput, { - // ...cfg, - // specifiersKey: 'importSpecifiers', - // }); - // } - /** * Finalize */ diff --git a/packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier--legacy.js b/packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier--legacy.js new file mode 100644 index 000000000..4b53cc48f --- /dev/null +++ b/packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier--legacy.js @@ -0,0 +1,330 @@ +import fs from 'fs'; +import pathLib from 'path'; +import babelTraverse from '@babel/traverse'; +import { isRelativeSourcePath, toRelativeSourcePath } from '../../utils/relative-source-path.js'; +import { InputDataService } from '../../core/InputDataService.js'; +import { resolveImportPath } from '../../utils/resolve-import-path.js'; +import { AstService } from '../../core/AstService.js'; +import { LogService } from '../../core/LogService.js'; +import { memoize } from '../../utils/memoize.js'; + +/** + * @typedef {import('../../../../types/index.js').RootFile} RootFile + * @typedef {import('../../../../types/index.js').SpecifierSource} SpecifierSource + * @typedef {import('../../../../types/index.js').IdentifierName} IdentifierName + * @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot + * @typedef {import('@babel/traverse').NodePath} NodePath + */ + +/** + * @param {string} source + * @param {string} projectName + */ +function isSelfReferencingProject(source, projectName) { + return source.startsWith(`${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(pathLib.extname(resolvedSourcePath))) { + // We have an import assertion + return /** @type { RootFile } */ { + file: toRelativeSourcePath(resolvedSourcePath, rootPath), + specifier: '[default]', + }; + } + const code = 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/analyzers/helpers/track-down-identifier.js b/packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier.js index c8cced2e3..da4a41eef 100644 --- a/packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier.js +++ b/packages-node/providence-analytics/src/program/analyzers/helpers/track-down-identifier.js @@ -1,11 +1,10 @@ import fs from 'fs'; -import pathLib from 'path'; -import babelTraverse from '@babel/traverse'; +import path from 'path'; +import { swcTraverse } from '../../utils/swc-traverse.js'; import { isRelativeSourcePath, toRelativeSourcePath } from '../../utils/relative-source-path.js'; import { InputDataService } from '../../core/InputDataService.js'; import { resolveImportPath } from '../../utils/resolve-import-path.js'; import { AstService } from '../../core/AstService.js'; -import { LogService } from '../../core/LogService.js'; import { memoize } from '../../utils/memoize.js'; /** @@ -13,7 +12,7 @@ import { memoize } from '../../utils/memoize.js'; * @typedef {import('../../../../types/index.js').SpecifierSource} SpecifierSource * @typedef {import('../../../../types/index.js').IdentifierName} IdentifierName * @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot - * @typedef {import('@babel/traverse').NodePath} NodePath + * @typedef {import('../../../../types/index.js').SwcPath} SwcPath */ /** @@ -40,7 +39,7 @@ function isExternalProject(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 {SwcPath} 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) { @@ -54,18 +53,21 @@ function getBindingAndSourceReexports(astPath, identifierName) { curPath = curPath.parentPath; } const rootPath = curPath; - rootPath.traverse({ - ExportSpecifier(path) { + + swcTraverse(rootPath.node, { + ExportSpecifier(astPath) { // eslint-disable-next-line arrow-body-style const found = - // @ts-expect-error - path.node.exported.name === identifierName || path.node.local.name === identifierName; + astPath.node.orig?.value === identifierName || + astPath.node.exported?.value === identifierName || + astPath.node.local?.value === identifierName; if (found) { - bindingPath = path; + bindingPath = astPath; bindingType = 'ExportSpecifier'; - // @ts-expect-error - source = path.parentPath.node.source ? path.parentPath.node.source.value : '[current]'; - path.stop(); + source = astPath.parentPath.node.source + ? astPath.parentPath.node.source.value + : '[current]'; + astPath.stop(); } }, }); @@ -78,7 +80,7 @@ function getBindingAndSourceReexports(astPath, identifierName) { * 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 {SwcPath} 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 }} */ @@ -86,13 +88,12 @@ export function getImportSourceFromAst(astPath, identifierName) { let source; let importedIdentifierName; - const binding = astPath.scope.getBinding(identifierName); + const binding = astPath.scope.bindings[identifierName]; let bindingType = binding?.path.type; let bindingPath = binding?.path; const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier']; if (bindingType && matchingTypes.includes(bindingType)) { - // @ts-expect-error source = binding?.path?.parentPath?.node?.source?.value; } else { // no binding @@ -103,10 +104,10 @@ export function getImportSourceFromAst(astPath, identifierName) { if (shouldLookForDefaultExport) { importedIdentifierName = '[default]'; } else if (source) { - // @ts-expect-error const { node } = bindingPath; - importedIdentifierName = (node.imported && node.imported.name) || node.local.name; + importedIdentifierName = node.orig?.value || node.imported?.value || node.local?.value; } + return { source, importedIdentifierName }; } @@ -174,17 +175,26 @@ async function trackDownIdentifierFn( const resolvedSourcePath = await resolveImportPath(source, currentFilePath); - LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`); + // if (resolvedSourcePath === null) { + // LogService.error(`[trackDownIdentifier] ${resolvedSourcePath} not found`); + + // } + // if (resolvedSourcePath === '[node-builtin]') { + // LogService.error(`[trackDownIdentifier] ${resolvedSourcePath} not found`); + // } + const allowedJsModuleExtensions = ['.mjs', '.js']; - if (!allowedJsModuleExtensions.includes(pathLib.extname(resolvedSourcePath))) { + if ( + !allowedJsModuleExtensions.includes(path.extname(/** @type {string} */ (resolvedSourcePath))) + ) { // We have an import assertion return /** @type { RootFile } */ { - file: toRelativeSourcePath(resolvedSourcePath, rootPath), + file: toRelativeSourcePath(/** @type {string} */ (resolvedSourcePath), rootPath), specifier: '[default]', }; } - const code = fs.readFileSync(resolvedSourcePath, 'utf8'); - const babelAst = AstService.getAst(code, 'swc-to-babel', { filePath: resolvedSourcePath }); + const code = fs.readFileSync(/** @type {string} */ (resolvedSourcePath), 'utf8'); + const swcAst = AstService._getSwcAst(code); const shouldLookForDefaultExport = identifierName === '[default]'; @@ -192,96 +202,111 @@ async function trackDownIdentifierFn( let exportMatch; let pendingTrackDownPromise; - babelTraverse.default(babelAst, { - ExportDefaultDeclaration(path) { - if (!shouldLookForDefaultExport) { + const handleExportDefaultDeclOrExpr = astPath => { + if (!shouldLookForDefaultExport) { + return; + } + + let newSource; + if ( + astPath.node.expression?.type === 'Identifier' || + astPath.node.declaration?.type === 'Identifier' + ) { + newSource = getImportSourceFromAst(astPath, astPath.node.expression.value).source; + } + + if (newSource) { + pendingTrackDownPromise = trackDownIdentifier( + newSource, + '[default]', + /** @type {PathFromSystemRoot} */ (resolvedSourcePath), + rootPath, + projectName, + depth + 1, + ); + } else { + // We found our file! + rootSpecifier = identifierName; + rootFilePath = toRelativeSourcePath( + /** @type {PathFromSystemRoot} */ (resolvedSourcePath), + rootPath, + ); + } + astPath.stop(); + }; + const handleExportDeclOrNamedDecl = { + enter(astPath) { + if (reexportMatch || shouldLookForDefaultExport) { return; } - let newSource; - if (path.node.declaration.type === 'Identifier') { - newSource = getImportSourceFromAst(path, path.node.declaration.name).source; - } - - if (newSource) { - pendingTrackDownPromise = trackDownIdentifier( - newSource, - '[default]', - resolvedSourcePath, - rootPath, - projectName, - depth + 1, + // 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, ); - } else { - // We found our file! - rootSpecifier = identifierName; - rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath); - } - path.stop(); - }, - ExportNamedDeclaration: { - enter(path) { - if (reexportMatch || shouldLookForDefaultExport) { - return; - } - // Are we dealing with a re-export ? - if (path.node.specifiers?.length) { - exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); + 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; - if (exportMatch) { - const localName = exportMatch.local.name; - let newSource; - if (path.node.source) { + if (!newSource || newSource === '[current]') { /** * @example - * export { x } from 'y' - */ - newSource = path.node.source.value; - } else { - /** - * @example - * import { x } from 'y' + * const x = 12; * export { x } */ - newSource = getImportSourceFromAst(path, identifierName).source; - - if (!newSource || newSource === '[current]') { - /** - * @example - * const x = 12; - * export { x } - */ - return; - } + return; } - reexportMatch = true; - pendingTrackDownPromise = trackDownIdentifier( - newSource, - localName, - resolvedSourcePath, - rootPath, - projectName, - depth + 1, - ); - path.stop(); } + reexportMatch = true; + pendingTrackDownPromise = trackDownIdentifier( + newSource, + localName, + resolvedSourcePath, + rootPath, + projectName, + depth + 1, + ); + astPath.stop(); } - }, - exit(path) { - 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) { - path.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(); + } + } + }, + }; + + const visitor = { + ExportDefaultDeclaration: handleExportDefaultDeclOrExpr, + ExportDefaultExpression: handleExportDefaultDeclOrExpr, + ExportNamedDeclaration: handleExportDeclOrNamedDecl, + ExportDeclaration: handleExportDeclOrNamedDecl, + }; + + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); if (pendingTrackDownPromise) { // We can't handle promises inside Babel traverse, so we do it here... @@ -296,7 +321,7 @@ async function trackDownIdentifierFn( trackDownIdentifier = memoize(trackDownIdentifierFn); /** - * @param {NodePath} astPath + * @param {SwcPath} astPath * @param {string} identifierNameInScope * @param {PathFromSystemRoot} fullCurrentFilePath * @param {PathFromSystemRoot} projectPath 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 5bef11886..3a2bd2cba 100644 --- a/packages-node/providence-analytics/src/program/analyzers/match-imports.js +++ b/packages-node/providence-analytics/src/program/analyzers/match-imports.js @@ -19,6 +19,7 @@ import { transformIntoIterableFindImportsOutput } from './helpers/transform-into * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst */ /** @@ -155,8 +156,9 @@ async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerRes } export default class MatchImportsAnalyzer extends Analyzer { - /** @type {AnalyzerName} */ - static analyzerName = 'match-imports'; + static analyzerName = /** @type {AnalyzerName} */ ('match-imports'); + + static requiredAst = /** @type {AnalyzerAst} */ ('swc'); static requiresReference = true; diff --git a/packages-node/providence-analytics/src/program/core/Analyzer.js b/packages-node/providence-analytics/src/program/core/Analyzer.js index 61e446fe9..f6137fb5b 100644 --- a/packages-node/providence-analytics/src/program/core/Analyzer.js +++ b/packages-node/providence-analytics/src/program/core/Analyzer.js @@ -6,11 +6,12 @@ import { QueryService } from './QueryService.js'; import { ReportService } from './ReportService.js'; import { InputDataService } from './InputDataService.js'; import { toPosixPath } from '../utils/to-posix-path.js'; -import { memoize } from '../utils/memoize.js'; import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-from-root.js'; /** + * @typedef {import("@swc/core").Module} SwcAstModule * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../types/index.js').QueryOutput} QueryOutput * @typedef {import('../../../types/index.js').ProjectInputData} ProjectInputData @@ -25,12 +26,13 @@ import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-fro * Analyzes one entry: the callback can traverse a given ast for each entry * @param {ProjectInputDataWithMeta} projectData * @param {function} astAnalysis + * @param {object} analyzerCfg */ -async function analyzePerAstFile(projectData, astAnalysis) { +async function analyzePerAstFile(projectData, astAnalysis, analyzerCfg) { const entries = []; for (const { file, ast, context: astContext } of projectData.entries) { const relativePath = getFilePathRelativeFromRoot(file, projectData.project.path); - const context = { code: astContext.code, relativePath, projectData }; + const context = { code: astContext.code, relativePath, projectData, analyzerCfg }; LogService.debug(`${pathLib.resolve(projectData.project.path, file)}`); const { result, meta } = await astAnalysis(ast, context); entries.push({ file: relativePath, meta, result }); @@ -86,8 +88,8 @@ function ensureAnalyzerResultFormat(queryOutput, cfg, analyzer) { const aResult = { queryOutput, analyzerMeta: { - name: analyzer.name, - requiredAst: analyzer.requiredAst, + name: analyzer.constructor.analyzerName, + requiredAst: analyzer.constructor.requiredAst, identifier, ...optional, configuration: cfg, @@ -131,30 +133,28 @@ function ensureAnalyzerResultFormat(queryOutput, cfg, analyzer) { * @typedef {(referencePath:PathFromSystemRoot,targetPath:PathFromSystemRoot) => {compatible:boolean; reason?:string}} CheckForMatchCompatibilityFn * @type {CheckForMatchCompatibilityFn} */ -const checkForMatchCompatibility = memoize( - ( - /** @type {PathFromSystemRoot} */ referencePath, - /** @type {PathFromSystemRoot} */ targetPath, - ) => { - // const refFile = pathLib.resolve(referencePath, 'package.json'); - const referencePkg = InputDataService.getPackageJson(referencePath); - // const targetFile = pathLib.resolve(targetPath, 'package.json'); - const targetPkg = InputDataService.getPackageJson(targetPath); +const checkForMatchCompatibility = ( + /** @type {PathFromSystemRoot} */ referencePath, + /** @type {PathFromSystemRoot} */ targetPath, +) => { + // const refFile = pathLib.resolve(referencePath, 'package.json'); + const referencePkg = InputDataService.getPackageJson(referencePath); + // const targetFile = pathLib.resolve(targetPath, 'package.json'); + const targetPkg = InputDataService.getPackageJson(targetPath); - const allTargetDeps = [ - ...Object.entries(targetPkg?.devDependencies || {}), - ...Object.entries(targetPkg?.dependencies || {}), - ]; - const importEntry = allTargetDeps.find(([name]) => referencePkg?.name === name); - if (!importEntry) { - return { compatible: false, reason: 'no-dependency' }; - } - if (referencePkg?.version && !semver.satisfies(referencePkg.version, importEntry[1])) { - return { compatible: false, reason: 'no-matched-version' }; - } - return { compatible: true }; - }, -); + const allTargetDeps = [ + ...Object.entries(targetPkg?.devDependencies || {}), + ...Object.entries(targetPkg?.dependencies || {}), + ]; + const importEntry = allTargetDeps.find(([name]) => referencePkg?.name === name); + if (!importEntry) { + return { compatible: false, reason: 'no-dependency' }; + } + if (referencePkg?.version && !semver.satisfies(referencePkg.version, importEntry[1])) { + return { compatible: false, reason: 'no-matched-version' }; + } + return { compatible: true }; +}; /** * If in json format, 'unwind' to be compatible for analysis... @@ -169,13 +169,21 @@ function unwindJsonResult(targetOrReferenceProjectResult) { export class Analyzer { static requiresReference = false; + /** @type {AnalyzerAst} */ + static requiredAst = 'babel'; + /** @type {AnalyzerName} */ static analyzerName = ''; name = /** @type {typeof Analyzer} */ (this.constructor).analyzerName; - /** @type {'babel'|'swc-to-babel'} */ - requiredAst = 'babel'; + _customConfig = {}; + + get config() { + return { + ...this._customConfig, + }; + } /** * In a MatchAnalyzer, two Analyzers (a reference and targer) are run. @@ -335,33 +343,36 @@ export class Analyzer { */ const astDataProjects = await QueryService.addAstToProjectsData( finalTargetData, - this.requiredAst, + this.constructor.requiredAst, ); - return analyzePerAstFile(astDataProjects[0], traverseEntryFn); + return analyzePerAstFile(astDataProjects[0], traverseEntryFn, this.config); } - async execute(customConfig = {}) { - LogService.debug(`Analyzer "${this.name}": started execute method`); - - const cfg = { - targetProjectPath: null, - referenceProjectPath: null, - suppressNonCriticalLogs: false, - ...customConfig, - }; + /** + * Finds export specifiers and sources + * @param {FindExportsConfig} customConfig + */ + async execute(customConfig) { + this._customConfig = customConfig; + const cfg = this.config; /** * Prepare */ - const analyzerResult = this._prepare(cfg); - if (analyzerResult) { - return analyzerResult; + const cachedAnalyzerResult = this._prepare(cfg); + if (cachedAnalyzerResult) { + return cachedAnalyzerResult; } /** * Traverse */ - const queryOutput = await this._traverse(() => {}); + const queryOutput = await this._traverse({ + // @ts-ignore + traverseEntryFn: this.constructor.analyzeFile, + filePaths: cfg.targetFilePaths, + projectPath: cfg.targetProjectPath, + }); /** * Finalize diff --git a/packages-node/providence-analytics/src/program/core/AstService.js b/packages-node/providence-analytics/src/program/core/AstService.js index a911e04b1..c18346ba6 100644 --- a/packages-node/providence-analytics/src/program/core/AstService.js +++ b/packages-node/providence-analytics/src/program/core/AstService.js @@ -52,6 +52,29 @@ export class AstService { return guardedSwcToBabel(ast, code); } + /** + * Compiles an array of file paths using swc. + * @param {string} code + * @param {ParserOptions} parserOptions + * @returns {SwcAstModule} + */ + static _getSwcAst(code, parserOptions = {}) { + const ast = swc.parseSync(code, { + syntax: 'typescript', + target: 'es2022', + ...parserOptions, + }); + return ast; + } + + /** + * Compensates for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812 + * @returns {number} + */ + static _getSwcOffset() { + return swc.parseSync('').span.end; + } + /** * Combines all script tags as if it were one js file. * @param {string} htmlCode @@ -77,9 +100,9 @@ export class AstService { /** * Returns the Babel AST * @param { string } code - * @param { 'babel'|'swc-to-babel'} astType + * @param { 'babel'|'swc-to-babel'|'swc'} astType * @param { {filePath?: PathFromSystemRoot} } options - * @returns {File|undefined} + * @returns {File|undefined|SwcAstModule} */ // eslint-disable-next-line consistent-return static getAst(code, astType, { filePath } = {}) { @@ -91,6 +114,9 @@ export class AstService { if (astType === 'swc-to-babel') { return this._getSwcToBabelAst(code); } + if (astType === 'swc') { + return this._getSwcAst(code); + } throw new Error(`astType "${astType}" not supported.`); } catch (e) { LogService.error(`Error when parsing "${filePath}":/n${e}`); diff --git a/packages-node/providence-analytics/src/program/core/InputDataService.js b/packages-node/providence-analytics/src/program/core/InputDataService.js index 538df2ce7..d41f2e3f9 100644 --- a/packages-node/providence-analytics/src/program/core/InputDataService.js +++ b/packages-node/providence-analytics/src/program/core/InputDataService.js @@ -12,6 +12,8 @@ import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-fro import { toPosixPath } from '../utils/to-posix-path.js'; import { memoize } from '../utils/memoize.js'; +// const memoize = fn => fn; + /** * @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult * @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry @@ -309,13 +311,13 @@ export class InputDataService { try { const pkgJson = getPackageJson(projectPath); // eslint-disable-next-line no-param-reassign - project.mainEntry = this.__normalizeMainEntry(pkgJson.main || './index.js'); + project.mainEntry = this.__normalizeMainEntry(pkgJson?.main || './index.js'); // eslint-disable-next-line no-param-reassign - project.name = pkgJson.name; + project.name = pkgJson?.name; // TODO: also add meta info whether we are in a monorepo or not. // We do this by checking whether there is a lerna.json on root level. // eslint-disable-next-line no-empty - project.version = pkgJson.version; + project.version = pkgJson?.version; } catch (e) { LogService.warn(/** @type {string} */ (e)); } @@ -422,6 +424,10 @@ export class InputDataService { .filter(dirPath => fs.lstatSync(dirPath).isDirectory()); } + static set targetProjectPaths(v) { + this.__targetProjectPaths = ensureArray(v); + } + /** * @type {PathFromSystemRoot[]} a list of strings representing all entry paths for projects we want to query */ @@ -446,10 +452,6 @@ export class InputDataService { this.__referenceProjectPaths = ensureArray(v); } - static set targetProjectPaths(v) { - this.__targetProjectPaths = ensureArray(v); - } - /** * @type {GatherFilesConfig} */ @@ -620,12 +622,12 @@ export class InputDataService { static getMonoRepoPackages(rootPath) { // [1] Look for npm/yarn workspaces const pkgJson = getPackageJson(rootPath); - if (pkgJson && pkgJson.workspaces) { + if (pkgJson?.workspaces) { return getPathsFromGlobList(pkgJson.workspaces, rootPath); } // [2] Look for lerna packages const lernaJson = getLernaJson(rootPath); - if (lernaJson && lernaJson.packages) { + if (lernaJson?.packages) { return getPathsFromGlobList(lernaJson.packages, rootPath); } // TODO: support forward compatibility for npm? diff --git a/packages-node/providence-analytics/src/program/core/QueryService.js b/packages-node/providence-analytics/src/program/core/QueryService.js index d79ffe588..8be55ad69 100644 --- a/packages-node/providence-analytics/src/program/core/QueryService.js +++ b/packages-node/providence-analytics/src/program/core/QueryService.js @@ -3,8 +3,10 @@ import path from 'path'; import { AstService } from './AstService.js'; import { LogService } from './LogService.js'; import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-from-root.js'; -import { memoize } from '../utils/memoize.js'; import { getCurrentDir } from '../utils/get-current-dir.js'; +// import { memoize } from '../utils/memoize.js'; + +const memoize = fn => fn; /** * @typedef {import('./Analyzer.js').Analyzer} Analyzer @@ -20,6 +22,7 @@ import { getCurrentDir } from '../utils/get-current-dir.js'; * @typedef {import('../../../types/index.js').ProjectInputData} ProjectInputData * @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName + * @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../types/index.js').GatherFilesConfig} GatherFilesConfig * @typedef {import('../../../types/index.js').AnalyzerQueryResult} AnalyzerQueryResult @@ -239,7 +242,7 @@ export class QueryService { /** * @param {ProjectInputData[]} projectsData - * @param {'babel'|'swc-to-babel'} requiredAst + * @param {AnalyzerAst} requiredAst */ static async addAstToProjectsData(projectsData, requiredAst) { return projectsData.map(projectData => { diff --git a/packages-node/providence-analytics/src/program/core/ReportService.js b/packages-node/providence-analytics/src/program/core/ReportService.js index a14a3857a..19c0b80dc 100644 --- a/packages-node/providence-analytics/src/program/core/ReportService.js +++ b/packages-node/providence-analytics/src/program/core/ReportService.js @@ -1,7 +1,8 @@ import fs from 'fs'; import pathLib from 'path'; import { getHash } from '../utils/get-hash.js'; -import { memoize } from '../utils/memoize.js'; +// import { memoize } from '../utils/memoize.js'; +const memoize = fn => fn; /** * @typedef {import('../../../types/index.js').Project} Project 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 new file mode 100644 index 000000000..d11e76374 --- /dev/null +++ b/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration--legacy.js @@ -0,0 +1,188 @@ +import fs from 'fs'; +import path from 'path'; +import babelTraversePkg from '@babel/traverse'; +import { AstService } from '../core/AstService.js'; +import { trackDownIdentifier } from '../analyzers/helpers/track-down-identifier.js'; +import { toPosixPath } from './to-posix-path.js'; + +/** + * @typedef {import('@babel/types').Node} Node + * @typedef {import('@babel/traverse').NodePath} NodePath + * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot + * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot + */ + +/** + * @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 = 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 d11e76374..e16923f4c 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,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import babelTraversePkg from '@babel/traverse'; +import { swcTraverse, getPathFromNode } from './swc-traverse.js'; import { AstService } from '../core/AstService.js'; import { trackDownIdentifier } from '../analyzers/helpers/track-down-identifier.js'; import { toPosixPath } from './to-posix-path.js'; @@ -8,6 +8,7 @@ import { toPosixPath } from './to-posix-path.js'; /** * @typedef {import('@babel/types').Node} Node * @typedef {import('@babel/traverse').NodePath} NodePath + * @typedef {import('../../../types/index.js').SwcBinding} SwcBinding * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot */ @@ -33,28 +34,32 @@ export function getFilePathOrExternalSource({ rootPath, localPath }) { * export const myIdentifier = y; * ``` * - We started in getSourceCodeFragmentOfDeclaration (looking for 'myIdentifier'), which found VariableDeclarator of export myIdentifier - * - getReferencedDeclaration is called with { referencedIdentifierName: 'y', ... } + * - getReferencedDeclaration is called with { referencedIdentifierName: 'y', globalScopeBindings: {x: SwcBinding; y: SwcBinding} } * - 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 + * @param {{ referencedIdentifierName:string, globalScopeBindings:{[key:string]:SwcBinding}; }} opts * @returns {NodePath} */ export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) { - const [, refDeclaratorBinding] = Object.entries(globalScopeBindings).find( - ([key]) => key === referencedIdentifierName, - ); + // We go from referencedIdentifierName 'y' to binding (VariableDeclarator path) 'y'; + // const [, refDeclaratorBinding] = + // Object.entries(globalScopeBindings).find(([key]) => key === referencedIdentifierName) || []; - if ( - refDeclaratorBinding.path.type === 'ImportSpecifier' || - refDeclaratorBinding.path.type === 'ImportDefaultSpecifier' - ) { + const refDeclaratorBinding = globalScopeBindings[referencedIdentifierName]; + + // We provided a referencedIdentifierName that is not in the globalScopeBindings + if (!refDeclaratorBinding) { + return null; + } + + if (['ImportSpecifier', 'ImportDefaultSpecifier'].includes(refDeclaratorBinding.path.node.type)) { return refDeclaratorBinding.path; } - if (refDeclaratorBinding.path.node.init.type === 'Identifier') { + if (refDeclaratorBinding.identifier.init.type === 'Identifier') { return getReferencedDeclaration({ - referencedIdentifierName: refDeclaratorBinding.path.node.init.name, + referencedIdentifierName: refDeclaratorBinding.identifier.init.value, globalScopeBindings, }); } @@ -83,70 +88,78 @@ export async function getSourceCodeFragmentOfDeclaration({ projectRootPath, }) { const code = fs.readFileSync(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 babelAst = AstService.getAst(code, 'babel', { filePath }); + const swcAst = AstService._getSwcAst(code); /** @type {NodePath} */ let finalNodePath; - babelTraversePkg.default(babelAst, { - Program(astPath) { - astPath.stop(); + swcTraverse( + swcAst, + { + 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 + // 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; + const globalScopeBindings = getPathFromNode(astPath.node.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 (exportedIdentifier === '[default]') { + const defaultExportPath = getPathFromNode( + astPath.node.body.find(child => + ['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type), + ), + ); + const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier'; - if (!isReferenced) { - finalNodePath = defaultExportPath.get('declaration'); + if (!isReferenced) { + finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression'); + } else { + finalNodePath = getReferencedDeclaration({ + referencedIdentifierName: defaultExportPath.node.expression.value, + globalScopeBindings, + }); + } } 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 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.name - : varDeclNode.id?.name || varDeclNode.imported.name; + 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 = getReferencedDeclaration({ - referencedIdentifierName: name, - globalScopeBindings, - }); + if (!isReferenced) { + // it must be an exported declaration + finalNodePath = contentPath; + } else { + finalNodePath = getReferencedDeclaration({ + referencedIdentifierName: name, + globalScopeBindings, + }); + } } - } + }, }, - }); + { needsAdvancedPaths: true }, + ); if (finalNodePath.type === 'ImportSpecifier') { const importDeclNode = finalNodePath.parentPath.node; const source = importDeclNode.source.value; - const identifierName = finalNodePath.node.imported.name; + const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value; const currentFilePath = filePath; const rootFile = await trackDownIdentifier( @@ -180,9 +193,10 @@ export async function getSourceCodeFragmentOfDeclaration({ return { sourceNodePath: finalNodePath, sourceFragment: code.slice( - finalNodePath.node?.loc?.start.index, - finalNodePath.node?.loc?.end.index, + finalNodePath.node?.span?.start - 1 - offset, + finalNodePath.node?.span?.end - 1 - offset, ), + // sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value, externalImportSource: null, }; } diff --git a/packages-node/providence-analytics/src/program/utils/resolve-import-path.js b/packages-node/providence-analytics/src/program/utils/resolve-import-path.js index b130d070a..112625879 100644 --- a/packages-node/providence-analytics/src/program/utils/resolve-import-path.js +++ b/packages-node/providence-analytics/src/program/utils/resolve-import-path.js @@ -66,9 +66,9 @@ async function resolveImportPathFn(importee, importer, opts) { ); if (!result?.id) { - LogService.warn( - `[resolveImportPath] importee ${importee} not found in filesystem for importer '${importer}'.`, - ); + // LogService.warn( + // `[resolveImportPath] importee ${importee} not found in filesystem for importer '${importer}'.`, + // ); return null; } return toPosixPath(result.id); diff --git a/packages-node/providence-analytics/src/program/utils/swc-traverse.js b/packages-node/providence-analytics/src/program/utils/swc-traverse.js new file mode 100644 index 000000000..bee5091ef --- /dev/null +++ b/packages-node/providence-analytics/src/program/utils/swc-traverse.js @@ -0,0 +1,359 @@ +/** + * @typedef {import('@swc/core').Module} SwcAstModule + * @typedef {import('@swc/core').Node} SwcNode + * @typedef {import('@swc/core').VariableDeclarator} SwcVariableDeclarator + * @typedef {import('@swc/core').Identifier} SwcIdentifierNode + * @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 + */ + +/** + * Contains all node info, to create paths from + * @type {WeakMap} + */ +const swcPathCache = new WeakMap(); + +const fnTypes = [ + 'FunctionDeclaration', + 'FunctionExpression', + 'ArrowFunctionExpression', + 'ClassMethod', + 'Constructor', +]; + +const nonBlockParentTypes = [...fnTypes, 'SwitchStatement', 'ClassDeclaration']; + +/** + * @param {SwcPath} swcPath + * @param {SwcScope} currentScope + * @param {SwcTraversalContext} traversalContext + * @returns {SwcScope|null} + */ +function getNewScope(swcPath, currentScope, traversalContext) { + const { node, parent } = swcPath; + // const hasNonBlockParent = (/** @type {SwcNode} */ nd) => nonBlockParentTypes.includes(nd.type); + const isFn = (/** @type {SwcNode} */ nd) => nd && fnTypes.includes(nd.type); + + const isIsolatedBlockStatement = !isFn(parent) && node.type === 'BlockStatement'; + + // Create new scope... + if (nonBlockParentTypes.includes(node.type) || isIsolatedBlockStatement) { + // eslint-disable-next-line no-param-reassign + traversalContext.scopeId += 1; + return { + id: traversalContext.scopeId, + parentScope: currentScope, + path: swcPath, + bindings: {}, + _pendingRefsWithoutBinding: [], + _isIsolatedBlockStatement: isIsolatedBlockStatement, + }; + } + + return null; +} + +/** + * @param {SwcNode} node + */ +export function getPathFromNode(node) { + return swcPathCache.get(node); +} + +/** + * @param {SwcNode} node + * @param {SwcNode|null} parent + * @param {Function} stop + * @param {SwcScope} [scope] + * @returns {SwcPath} + */ +function createSwcPath(node, parent, stop, scope) { + const swcPath = { + node, + parent, + stop, + // TODO: "pre-traverse" the missing scope parts instead via getter that adds refs and bindings for current scope + scope, + parentPath: parent ? getPathFromNode(parent) : null, + get(/** @type {string} */ name) { + const swcPathForNode = getPathFromNode(node[name]); + if (node[name] && !swcPathForNode) { + // throw new Error( + // `[swcTraverse]: Use {needsAdvancedPaths: true} to find path for node: ${node[name]}`, + // ); + // TODO: "pre-traverse" the missing path parts instead + } + return swcPathForNode; + }, + get type() { + return node.type; + }, + }; + swcPathCache.set(node, swcPath); + return swcPath; +} + +/** + * Is the node: + * - a declaration (like "const a = 1")? + * - 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 + */ +function isBindingNode(parent, identifierValue) { + if (parent.type === 'VariableDeclarator') { + // @ts-ignore + return parent.id.value === identifierValue; + } + return [ + 'ClassDeclaration', + 'FunctionDeclaration', + 'ArrowFunctionExpression', + 'ImportSpecifier', + 'ImportDefaultSpecifier', + ].includes(parent.type); +} + +/** + * Is the node: + * - a declaration (like "const a = 1")? + * - an import specifier (like "import { a } from 'b'")? + * Handy to know if the parents of Identifiers mark a binding + * @param {SwcNode} parent + */ +function isBindingRefNode(parent) { + return ![ + 'ClassMethod', + 'Constructor', + 'MemberExpression', + 'KeyValueProperty', + 'SwitchStatement', + 'MethodProperty', + ].includes(parent.type); +} + +/** + * @param {SwcPath} swcPathForIdentifier + * @returns {void} + */ +function addPotentialBindingOrRefToScope(swcPathForIdentifier) { + const { node, parent, scope, parentPath } = swcPathForIdentifier; + + if (node.type !== 'Identifier') { + return; + } + + // const parentPath = getPathFromNode(parent); + if (isBindingNode(parent, node.value)) { + /** @type {SwcBinding} */ + const binding = { + identifier: parent, + // kind: 'var', + refs: [], + path: swcPathForIdentifier.parentPath, + }; + let scopeBindingBelongsTo = scope; + const isVarInIsolatedBlock = + scope._isIsolatedBlockStatement && + swcPathForIdentifier.parentPath.parentPath.node.kind === 'var'; + const hasNonBlockParent = nonBlockParentTypes.includes(parent.type); + + if (isVarInIsolatedBlock || hasNonBlockParent) { + scopeBindingBelongsTo = scope.parentScope || scope; + } + if (scopeBindingBelongsTo._pendingRefsWithoutBinding.includes(parentPath)) { + binding.refs.push(parentPath); + scopeBindingBelongsTo._pendingRefsWithoutBinding.splice( + scopeBindingBelongsTo._pendingRefsWithoutBinding.indexOf(parentPath), + 1, + ); + } + const idName = node.value || node.local?.value || node.orig?.value; + // 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') { + scope.bindings[idName] = binding; + } + } + // In other cases, we are dealing with a reference that must be bound to a binding + else if (isBindingRefNode(parent)) { + const binding = scope.bindings[node.value]; + if (binding) { + binding.refs.push(parentPath); + } else { + // we are referencing a variable that is not declared in this scope or any parent scope + // It might be hoisted, so we might find it later. For now, store it as a pending reference + scope._pendingRefsWithoutBinding.push(parentPath); + } + } +} + +/** + * Is the node is the root of the ast? + * in Babel, this is the equivalent of Program + * @param {SwcNode} node + * @returns {boolean} + */ +function isRootNode(node) { + return node.type === 'Module' || node.type === 'Script'; +} + +/** + * @param {{node: SwcNode; }} node + * @param {(data:{child:SwcNode}) => void} callback + */ +const loopChildren = ({ node }, callback) => { + for (const [childKey, childVal] of Object.entries(node)) { + if (childKey === 'span') { + // eslint-disable-next-line no-continue + continue; + } + + if (Array.isArray(childVal)) { + for (const childValElem of childVal) { + callback({ child: childValElem }); + } + } else if (typeof childVal === 'object') { + callback({ child: childVal }); + } + } +}; + +/** + * @param {SwcPath} swcPath + * @param {SwcVisitor} visitor + * @param {SwcTraversalContext} traversalContext + */ +function visit(swcPath, visitor, traversalContext) { + if (visitor.enter) { + // @ts-expect-error + visitor.enter(swcPath); + } + + if (isRootNode(swcPath.node) && visitor.root) { + // @ts-expect-error + visitor.root(swcPath); + } + + // Later, consider https://github.com/babel/babel/blob/b1e73d6f961065c56427ffa89c130beea8321d3b/packages/babel-traverse/src/traverse-node.ts#L28 + if (typeof visitor[swcPath.node.type] === 'function') { + // @ts-expect-error + visitor[swcPath.node.type](swcPath); + } + // @ts-expect-error + else if (visitor[swcPath.node.type]?.enter) { + // @ts-expect-error + visitor[swcPath.node.type].enter(swcPath); + } + // @ts-expect-error + if (visitor[swcPath.node.type]?.exit) { + // Let visitTree know that we should visit on exit + // @ts-expect-error + traversalContext.visitOnExitFns.push(() => visitor[swcPath.node.type].exit(swcPath)); + } +} + +/** + * Simple traversal for swc ast. + * @param {SwcAstModule} swcAst + * @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 } = {}) { + /** + * For performance, the author of a visitor can call this to stop further traversal + */ + let isStopped = false; + const stop = () => { + isStopped = true; + }; + + /** + * @param {SwcNode} node + * @param {SwcNode|null} parent + * @param {SwcScope} scope + * @param {boolean} hasPreparedTree + * @param {SwcTraversalContext} traversalContext + */ + const handlePathAndScope = (node, parent, scope, hasPreparedTree, traversalContext) => { + if (hasPreparedTree) { + const swcPath = /** @type {SwcPath} */ (swcPathCache.get(node)); + return { + swcPath, + newOrCurScope: getNewScope(swcPath, scope, traversalContext) || scope, + }; + } + // `needsAdvancedPaths` was false + const swcPath = createSwcPath(node, parent, stop); + // We create scopes ourselves, since paths are not prepared yet... + const newOrCurScope = getNewScope(swcPath, scope, traversalContext) || scope; + swcPath.scope = newOrCurScope; + addPotentialBindingOrRefToScope(swcPath); + return { newOrCurScope, swcPath }; + }; + + /** + * @param {SwcNode} node + * @param {SwcNode|null} parent + * @param {SwcScope} scope + * @param {SwcTraversalContext} traversalContext + * @param {{haltCondition?: (node: SwcNode) => boolean;}} [config] + */ + const prepareTree = (node, parent, scope, traversalContext, { haltCondition } = {}) => { + if (!node?.type) { + return; + } + + const { newOrCurScope } = handlePathAndScope(node, parent, scope, false, traversalContext); + loopChildren({ node }, ({ child }) => { + prepareTree(child, node, newOrCurScope, traversalContext, { haltCondition }); + }); + }; + + /** + * @param {SwcNode} node + * @param {SwcNode|null} parent + * @param {SwcScope} scope + * @param {{hasPreparedTree?: boolean;}} config + * @param {SwcTraversalContext} traversalContext + */ + const visitTree = (node, parent, scope, config, traversalContext) => { + if (!node?.type || isStopped) { + return; + } + + const { hasPreparedTree = false } = config || {}; + + const { swcPath } = handlePathAndScope(node, parent, scope, hasPreparedTree, traversalContext); + visit(swcPath, visitor, traversalContext); + loopChildren({ node }, ({ child }) => { + visitTree(child, node, swcPath.scope, config, traversalContext); + }); + }; + + const traversalContext = { visitOnExitFns: [], scopeId: 0 }; + // https://developer.mozilla.org/en-US/docs/Glossary/Scope + /** @type {SwcScope} */ + const initialScope = { + id: traversalContext.scopeId, + bindings: {}, + path: null, + _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); + } + visitTree(swcAst, null, initialScope, { hasPreparedTree: needsAdvancedPaths }, traversalContext); + // @ts-ignore + traversalContext.visitOnExitFns.reverse().forEach(fn => fn()); +} diff --git a/packages-node/providence-analytics/test-node/cli/cli.test.js b/packages-node/providence-analytics/test-node/cli/cli.test.js index f1edf5f96..c33173680 100644 --- a/packages-node/providence-analytics/test-node/cli/cli.test.js +++ b/packages-node/providence-analytics/test-node/cli/cli.test.js @@ -13,7 +13,7 @@ import { cli } from '../../src/cli/cli.js'; import { _promptAnalyzerMenuModule } from '../../src/cli/prompt-analyzer-menu.js'; import { memoizeConfig } from '../../src/program/utils/memoize.js'; import { _extendDocsModule } from '../../src/cli/launch-providence-with-extend-docs.js'; -import { dashboardServer } from '../../dashboard/server.js'; +import { dashboardServer } from '../../src/dashboard/server.js'; import { setupAnalyzerTest } from '../../test-helpers/setup-analyzer-test.js'; /** diff --git a/packages-node/providence-analytics/test-node/dashboard/dashboard-server.test.js b/packages-node/providence-analytics/test-node/dashboard/dashboard-server.test.js index 08a795d86..cb4fa9ad6 100644 --- a/packages-node/providence-analytics/test-node/dashboard/dashboard-server.test.js +++ b/packages-node/providence-analytics/test-node/dashboard/dashboard-server.test.js @@ -4,9 +4,10 @@ import pathLib from 'path'; import sinon from 'sinon'; import { fileURLToPath } from 'url'; import { expect } from 'chai'; +import { it } from 'mocha'; import fetch from 'node-fetch'; import { createTestServer } from '@web/dev-server-core/test-helpers'; -import { createDashboardServerConfig } from '../../dashboard/server.js'; +import { createDashboardServerConfig } from '../../src/dashboard/server.js'; import { ReportService } from '../../src/program/core/ReportService.js'; import { providenceConfUtil } from '../../src/program/utils/providence-conf-util.js'; @@ -57,7 +58,7 @@ describe('Dashboard Server', () => { describe('Index', () => { it(`returns an index on '/'`, async () => { - const response = await fetch(`${host}/dashboard`); + const response = await fetch(`${host}/src/dashboard`); const responseText = await response.text(); expect(response.status).to.equal(200); expect(responseText).to.include('Providence dashboard'); @@ -66,7 +67,7 @@ describe('Dashboard Server', () => { describe('App assets', () => { it(`returns (static) js assets via app/*`, async () => { - const response = await fetch(`${host}/dashboard/app/p-board.js`); + const response = await fetch(`${host}/src/dashboard/app/p-board.js`); expect(response.status).to.equal(200); }); }); 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 new file mode 100644 index 000000000..28c1b6ab3 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-classes_-_importing-target-project_0.0.2-target-mock__-905964591.json @@ -0,0 +1,220 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "find-classes", + "requiredAst": "babel", + "identifier": "importing-target-project_0.0.2-target-mock__-905964591", + "targetProject": { + "mainEntry": "./target-src/match-imports/root-level-imports.js", + "name": "importing-target-project", + "version": "0.0.2-target-mock", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "gatherFilesConfig": {}, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "file": "./target-src/find-customelements/multiple.js", + "result": [ + { + "name": null, + "isMixin": true, + "superClasses": [ + { + "name": "HTMLElement", + "isMixin": false, + "rootFile": { + "file": "[current]", + "specifier": "HTMLElement" + } + } + ], + "members": { + "props": [], + "methods": [] + } + }, + { + "name": "ExtendedOnTheFly", + "isMixin": false, + "superClasses": [ + { + "isMixin": true, + "rootFile": { + "file": "[current]" + } + }, + { + "isMixin": false, + "rootFile": { + "file": "[current]" + } + } + ], + "members": { + "props": [], + "methods": [] + } + } + ] + }, + { + "file": "./target-src/match-subclasses/ExtendedComp.js", + "result": [ + { + "name": "ExtendedComp", + "isMixin": false, + "superClasses": [ + { + "name": "MyCompMixin", + "isMixin": true, + "rootFile": { + "file": "exporting-ref-project", + "specifier": "[default]" + } + }, + { + "name": "RefClass", + "isMixin": false, + "rootFile": { + "file": "exporting-ref-project", + "specifier": "RefClass" + } + } + ], + "members": { + "props": [ + { + "name": "getterSetter", + "accessType": "public", + "kind": [ + "get", + "set" + ] + }, + { + "name": "staticGetterSetter", + "accessType": "public", + "static": true, + "kind": [ + "get", + "set" + ] + }, + { + "name": "attributes", + "accessType": "public", + "static": true, + "kind": [ + "get" + ] + }, + { + "name": "styles", + "accessType": "public", + "static": true, + "kind": [ + "get" + ] + }, + { + "name": "updateComplete", + "accessType": "public", + "kind": [ + "get" + ] + }, + { + "name": "localizeNamespaces", + "accessType": "public", + "static": true, + "kind": [ + "get" + ] + }, + { + "name": "slots", + "accessType": "public", + "kind": [ + "get" + ] + } + ], + "methods": [ + { + "name": "method", + "accessType": "public" + }, + { + "name": "_protectedMethod", + "accessType": "protected" + }, + { + "name": "__privateMethod", + "accessType": "private" + }, + { + "name": "$protectedMethod", + "accessType": "protected" + }, + { + "name": "$$privateMethod", + "accessType": "private" + }, + { + "name": "constructor", + "accessType": "public" + }, + { + "name": "connectedCallback", + "accessType": "public" + }, + { + "name": "disconnectedCallback", + "accessType": "public" + }, + { + "name": "requestUpdate", + "accessType": "public" + }, + { + "name": "createRenderRoot", + "accessType": "public" + }, + { + "name": "render", + "accessType": "public" + }, + { + "name": "updated", + "accessType": "public" + }, + { + "name": "firstUpdated", + "accessType": "public" + }, + { + "name": "update", + "accessType": "public" + }, + { + "name": "shouldUpdate", + "accessType": "public" + }, + { + "name": "onLocaleUpdated", + "accessType": "public" + } + ] + } + } + ] + } + ] +} \ No newline at end of file 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 new file mode 100644 index 000000000..341216313 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-customelements_-_importing-target-project_0.0.2-target-mock__61665553.json @@ -0,0 +1,52 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "find-customelements", + "requiredAst": "swc-to-babel", + "identifier": "importing-target-project_0.0.2-target-mock__61665553", + "targetProject": { + "mainEntry": "./target-src/match-imports/root-level-imports.js", + "name": "importing-target-project", + "version": "0.0.2-target-mock", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "gatherFilesConfig": {}, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "file": "./target-src/find-customelements/multiple.js", + "result": [ + { + "tagName": "ref-class", + "constructorIdentifier": "RefClass", + "rootFile": { + "file": "exporting-ref-project", + "specifier": "RefClass" + } + }, + { + "tagName": "extended-comp", + "constructorIdentifier": "ExtendedComp", + "rootFile": { + "file": "./target-src/match-subclasses/ExtendedComp.js", + "specifier": "ExtendedComp" + } + }, + { + "tagName": "on-the-fly", + "constructorIdentifier": "[inline]", + "rootFile": { + "file": "[current]", + "specifier": "[inline]" + } + } + ] + } + ] +} \ No newline at end of file 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 new file mode 100644 index 000000000..9908ee979 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-exports_-_exporting-ref-project_1.0.0__-42206859.json @@ -0,0 +1,194 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "find-exports", + "requiredAst": "swc-to-babel", + "identifier": "exporting-ref-project_1.0.0__-42206859", + "targetProject": { + "mainEntry": "./index.js", + "name": "exporting-ref-project", + "version": "1.0.0", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "skipFileImports": false, + "gatherFilesConfig": {}, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "file": "./index.js", + "result": [ + { + "exportSpecifiers": [ + "[default]" + ], + "source": "./ref-src/core.js", + "normalizedSource": "./ref-src/core.js", + "rootFileMap": [ + { + "currentFileSpecifier": "[default]", + "rootFile": { + "file": "./ref-src/core.js", + "specifier": "[default]" + } + } + ] + }, + { + "exportSpecifiers": [ + "RefClass", + "RefRenamedClass" + ], + "localMap": [ + { + "local": "RefClass", + "exported": "RefRenamedClass" + } + ], + "source": "./ref-src/core.js", + "normalizedSource": "./ref-src/core.js", + "rootFileMap": [ + { + "currentFileSpecifier": "RefClass", + "rootFile": { + "file": "./ref-src/core.js", + "specifier": "RefClass" + } + }, + { + "currentFileSpecifier": "RefRenamedClass", + "rootFile": { + "file": "./ref-src/core.js", + "specifier": "RefClass" + } + } + ] + }, + { + "exportSpecifiers": [ + "[file]" + ], + "rootFileMap": [ + null + ] + } + ] + }, + { + "file": "./not-imported.js", + "result": [ + { + "exportSpecifiers": [ + "notImported" + ], + "localMap": [], + "rootFileMap": [ + { + "currentFileSpecifier": "notImported", + "rootFile": { + "file": "[current]", + "specifier": "notImported" + } + } + ] + }, + { + "exportSpecifiers": [ + "[file]" + ], + "rootFileMap": [ + null + ] + } + ] + }, + { + "file": "./ref-component.js", + "result": [ + { + "exportSpecifiers": [ + "[file]" + ], + "rootFileMap": [ + null + ] + } + ] + }, + { + "file": "./ref-src/core.js", + "result": [ + { + "exportSpecifiers": [ + "RefClass" + ], + "localMap": [], + "rootFileMap": [ + { + "currentFileSpecifier": "RefClass", + "rootFile": { + "file": "[current]", + "specifier": "RefClass" + } + } + ] + }, + { + "exportSpecifiers": [ + "[default]" + ], + "rootFileMap": [ + { + "currentFileSpecifier": "[default]", + "rootFile": { + "file": "[current]", + "specifier": "[default]" + } + } + ] + }, + { + "exportSpecifiers": [ + "[file]" + ], + "rootFileMap": [ + null + ] + } + ] + }, + { + "file": "./ref-src/folder/index.js", + "result": [ + { + "exportSpecifiers": [ + "resolvePathCorrect" + ], + "localMap": [], + "rootFileMap": [ + { + "currentFileSpecifier": "resolvePathCorrect", + "rootFile": { + "file": "[current]", + "specifier": "resolvePathCorrect" + } + } + ] + }, + { + "exportSpecifiers": [ + "[file]" + ], + "rootFileMap": [ + null + ] + } + ] + } + ] +} \ No newline at end of file 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 new file mode 100644 index 000000000..3cc567826 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/find-imports_-_importing-target-project_0.0.2-target-mock__349742630.json @@ -0,0 +1,204 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "find-imports", + "requiredAst": "swc-to-babel", + "identifier": "importing-target-project_0.0.2-target-mock__349742630", + "targetProject": { + "mainEntry": "./target-src/match-imports/root-level-imports.js", + "name": "importing-target-project", + "version": "0.0.2-target-mock", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "keepInternalSources": false, + "gatherFilesConfig": {}, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "file": "./target-src/find-customelements/multiple.js", + "result": [ + { + "importSpecifiers": [ + "RefClass" + ], + "source": "exporting-ref-project", + "normalizedSource": "exporting-ref-project" + } + ] + }, + { + "file": "./target-src/find-imports/all-notations.js", + "result": [ + { + "importSpecifiers": [ + "[file]" + ], + "source": "imported/source", + "normalizedSource": "imported/source" + }, + { + "importSpecifiers": [ + "[default]" + ], + "source": "imported/source-a", + "normalizedSource": "imported/source-a" + }, + { + "importSpecifiers": [ + "b" + ], + "source": "imported/source-b", + "normalizedSource": "imported/source-b" + }, + { + "importSpecifiers": [ + "c", + "d" + ], + "source": "imported/source-c", + "normalizedSource": "imported/source-c" + }, + { + "importSpecifiers": [ + "[default]", + "f", + "g" + ], + "source": "imported/source-d", + "normalizedSource": "imported/source-d" + }, + { + "importSpecifiers": [ + "[default]" + ], + "source": "my/source-e", + "normalizedSource": "my/source-e" + }, + { + "importSpecifiers": [ + "[default]" + ], + "source": "[variable]", + "normalizedSource": "[variable]" + }, + { + "importSpecifiers": [ + "[*]" + ], + "source": "imported/source-g", + "normalizedSource": "imported/source-g" + } + ] + }, + { + "file": "./target-src/match-imports/deep-imports.js", + "result": [ + { + "importSpecifiers": [ + "RefClass" + ], + "source": "exporting-ref-project/ref-src/core.js", + "normalizedSource": "exporting-ref-project/ref-src/core.js" + }, + { + "importSpecifiers": [ + "[default]" + ], + "source": "exporting-ref-project/ref-src/core.js", + "normalizedSource": "exporting-ref-project/ref-src/core.js" + }, + { + "importSpecifiers": [ + "nonMatched" + ], + "source": "unknown-project/xyz.js", + "normalizedSource": "unknown-project/xyz.js" + }, + { + "importSpecifiers": [ + "[file]" + ], + "source": "exporting-ref-project/ref-component", + "normalizedSource": "exporting-ref-project/ref-component" + }, + { + "importSpecifiers": [ + "resolvePathCorrect" + ], + "source": "exporting-ref-project/ref-src/folder", + "normalizedSource": "exporting-ref-project/ref-src/folder" + }, + { + "importSpecifiers": [ + "[*]" + ], + "source": "exporting-ref-project/ref-src/core.js", + "normalizedSource": "exporting-ref-project/ref-src/core.js" + } + ] + }, + { + "file": "./target-src/match-imports/root-level-imports.js", + "result": [ + { + "importSpecifiers": [ + "RefClass" + ], + "source": "exporting-ref-project", + "normalizedSource": "exporting-ref-project" + }, + { + "importSpecifiers": [ + "RefRenamedClass" + ], + "source": "exporting-ref-project", + "normalizedSource": "exporting-ref-project" + }, + { + "importSpecifiers": [ + "[default]" + ], + "source": "exporting-ref-project", + "normalizedSource": "exporting-ref-project" + }, + { + "importSpecifiers": [ + "nonMatched" + ], + "source": "unknown-project", + "normalizedSource": "unknown-project" + } + ] + }, + { + "file": "./target-src/match-subclasses/ExtendedComp.js", + "result": [ + { + "importSpecifiers": [ + "RefClass" + ], + "source": "exporting-ref-project", + "normalizedSource": "exporting-ref-project" + } + ] + }, + { + "file": "./target-src/match-subclasses/internalProxy.js", + "result": [ + { + "importSpecifiers": [ + "[default]" + ], + "source": "exporting-ref-project", + "normalizedSource": "exporting-ref-project" + } + ] + } + ] +} \ No newline at end of file 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__142861209.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__142861209.json new file mode 100644 index 000000000..8fc92eb6c --- /dev/null +++ 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__142861209.json @@ -0,0 +1,94 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "match-imports", + "requiredAst": "babel", + "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209", + "targetProject": { + "mainEntry": "./target-src/match-imports/root-level-imports.js", + "name": "importing-target-project", + "version": "0.0.2-target-mock", + "commitHash": "[not-a-git-root]" + }, + "referenceProject": { + "mainEntry": "./index.js", + "name": "exporting-ref-project", + "version": "1.0.0", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "gatherFilesConfig": {}, + "prefix": null, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "name": "[default]", + "variable": { + "from": "[default]", + "to": "ExtendedComp", + "paths": [ + { + "from": "./index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "./ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + } + ] + } + }, + { + "name": "RefClass", + "variable": { + "from": "RefClass", + "to": "ExtendedComp", + "paths": [ + { + "from": "./index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "./ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + } + ] + }, + "tag": { + "from": "ref-component", + "to": "extended-comp", + "paths": [ + { + "from": "./ref-component.js", + "to": "./target-src/find-customelements/multiple.js" + }, + { + "from": "exporting-ref-project/ref-component.js", + "to": "./target-src/find-customelements/multiple.js" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-paths_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209.json b/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-paths_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209.json new file mode 100644 index 000000000..274163089 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/analyzers/e2e/match-paths_-_importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209.json @@ -0,0 +1,94 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "match-paths", + "requiredAst": "babel", + "identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209", + "targetProject": { + "mainEntry": "./target-src/match-imports/root-level-imports.js", + "name": "importing-target-project", + "version": "0.0.2-target-mock", + "commitHash": "[not-a-git-root]" + }, + "referenceProject": { + "mainEntry": "./index.js", + "name": "exporting-ref-project", + "version": "1.0.0", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "gatherFilesConfig": {}, + "prefix": null, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "name": "[default]", + "variable": { + "from": "[default]", + "to": "ExtendedComp", + "paths": [ + { + "from": "./index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "./ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + } + ] + } + }, + { + "name": "RefClass", + "variable": { + "from": "RefClass", + "to": "ExtendedComp", + "paths": [ + { + "from": "./index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "./ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/index.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + }, + { + "from": "exporting-ref-project/ref-src/core.js", + "to": "./target-src/match-subclasses/ExtendedComp.js" + } + ] + }, + "tag": { + "from": "ref-component", + "to": "extended-comp", + "paths": [ + { + "from": "./ref-component.js", + "to": "./target-src/find-customelements/multiple.js" + }, + { + "from": "exporting-ref-project/ref-component.js", + "to": "./target-src/find-customelements/multiple.js" + } + ] + } + } + ] +} \ No newline at end of file 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 new file mode 100644 index 000000000..f28cc6a6a --- /dev/null +++ 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 @@ -0,0 +1,67 @@ +{ + "meta": { + "searchType": "ast-analyzer", + "analyzerMeta": { + "name": "match-subclasses", + "requiredAst": "babel", + "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", + "name": "importing-target-project", + "version": "0.0.2-target-mock", + "commitHash": "[not-a-git-root]" + }, + "referenceProject": { + "mainEntry": "./index.js", + "name": "exporting-ref-project", + "version": "1.0.0", + "commitHash": "[not-a-git-root]" + }, + "configuration": { + "gatherFilesConfig": {}, + "skipCheckMatchCompatibility": false, + "addSystemPathsInResult": false + } + } + }, + "queryOutput": [ + { + "exportSpecifier": { + "name": "[default]", + "project": "exporting-ref-project", + "filePath": "./index.js", + "id": "[default]::./index.js::exporting-ref-project" + }, + "matchesPerProject": [ + { + "project": "importing-target-project", + "files": [ + { + "file": "./target-src/match-subclasses/ExtendedComp.js", + "identifier": "ExtendedComp" + } + ] + } + ] + }, + { + "exportSpecifier": { + "name": "RefClass", + "project": "exporting-ref-project", + "filePath": "./index.js", + "id": "RefClass::./index.js::exporting-ref-project" + }, + "matchesPerProject": [ + { + "project": "importing-target-project", + "files": [ + { + "file": "./target-src/match-subclasses/ExtendedComp.js", + "identifier": "ExtendedComp" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test--legacy.js b/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test--legacy.js new file mode 100644 index 000000000..02ca6ece5 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test--legacy.js @@ -0,0 +1,330 @@ +import { expect } from 'chai'; +import { it } from 'mocha'; +import { providence } from '../../../src/program/providence.js'; +import { QueryService } from '../../../src/program/core/QueryService.js'; +import { setupAnalyzerTest } from '../../../test-helpers/setup-analyzer-test.js'; +import { mockProject, getEntry, getEntries } from '../../../test-helpers/mock-project-helpers.js'; +import FindExportsAnalyzer from '../../../src/program/analyzers/find-exports.js'; + +/** + * @typedef {import('../../../types/index.js').ProvidenceConfig} ProvidenceConfig + */ + +setupAnalyzerTest(); + +describe('Analyzer "find-exports"', async () => { + const findExportsQueryConfig = await QueryService.getQueryConfigFromAnalyzer(FindExportsAnalyzer); + + /** @type {Partial} */ + const _providenceCfg = { + targetProjectPaths: ['/fictional/project'], // defined in mockProject + }; + + describe('Export notations', () => { + it(`supports [export const x = 0] (named specifier)`, async () => { + mockProject([`export const x = 0`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstResult = getEntry(queryResults[0]).result[0]; + + expect(firstResult.exportSpecifiers).to.eql(['x']); + expect(firstResult.source).to.be.undefined; + }); + + it(`supports [export default class X {}] (default export)`, async () => { + mockProject([`export default class X {}`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstResult = getEntry(queryResults[0]).result[0]; + expect(firstResult.exportSpecifiers).to.eql(['[default]']); + expect(firstResult.source).to.be.undefined; + }); + + it(`supports [export default fn(){}] (default export)`, async () => { + mockProject([`export default x => x * 3`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstResult = getEntry(queryResults[0]).result[0]; + + expect(firstResult.exportSpecifiers).to.eql(['[default]']); + expect(firstResult.source).to.equal(undefined); + }); + + it(`supports [export {default as x} from 'y'] (default re-export)`, async () => { + mockProject({ + './file-with-default-export.js': 'export default 1;', + './file-with-default-re-export.js': + "export { default as namedExport } from './file-with-default-export.js';", + }); + + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstResult = getEntry(queryResults[0]).result[0]; + expect(firstResult).to.eql({ + exportSpecifiers: ['[default]'], + source: undefined, + rootFileMap: [ + { + currentFileSpecifier: '[default]', + rootFile: { file: '[current]', specifier: '[default]' }, + }, + ], + }); + + const secondEntry = getEntry(queryResults[0], 1); + expect(secondEntry.result[0]).to.eql({ + exportSpecifiers: ['namedExport'], + source: './file-with-default-export.js', + localMap: [{ exported: 'namedExport', local: '[default]' }], + normalizedSource: './file-with-default-export.js', + rootFileMap: [ + { + currentFileSpecifier: 'namedExport', + rootFile: { file: './file-with-default-export.js', specifier: '[default]' }, + }, + ], + }); + }); + + it(`supports [import {x} from 'y'; export default x] (named re-export as default)`, async () => { + mockProject([`import {x} from 'y'; export default x;`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]'); + expect(firstEntry.result[0].source).to.equal('y'); + }); + + it(`supports [import x from 'y'; export default x] (default re-export as default)`, async () => { + mockProject([`import x from 'y'; export default x;`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]'); + expect(firstEntry.result[0].source).to.equal('y'); + }); + + it(`supports [export { x } from 'my/source'] (re-export named specifier)`, async () => { + mockProject([`export { x } from 'my/source'`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('x'); + expect(firstEntry.result[0].source).to.equal('my/source'); + }); + + it(`supports [export { x as y } from 'my/source'] (re-export renamed specifier)`, async () => { + mockProject([`export { x as y } from 'my/source'`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('y'); + expect(firstEntry.result[0].source).to.equal('my/source'); + }); + + it(`supports [export styles from './styles.css' assert { type: "css" }] (import assertions)`, async () => { + mockProject({ + './styles.css': '.block { display:block; };', + './x.js': `export { styles as default } from './styles.css' assert { type: "css" };`, + }); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]'); + expect(firstEntry.result[0].source).to.equal('./styles.css'); + expect(firstEntry.result[0].rootFileMap[0]).to.eql({ + currentFileSpecifier: '[default]', + rootFile: { + file: './styles.css', + specifier: '[default]', + }, + }); + }); + + it(`supports [import styles from './styles.css' assert { type: "css" }; export default styles;] (import assertions)`, async () => { + mockProject({ + './styles.css': '.block { display:block; };', + './x.js': `import styles from './styles.css' assert { type: "css" }; export default styles;`, + }); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]'); + expect(firstEntry.result[0].source).to.equal('./styles.css'); + expect(firstEntry.result[0].rootFileMap[0]).to.eql({ + currentFileSpecifier: '[default]', + rootFile: { + file: './styles.css', + specifier: '[default]', + }, + }); + }); + + it(`stores meta info(local name) of renamed specifiers`, async () => { + mockProject([`export { x as y } from 'my/source'`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + // This info will be relevant later to identify 'transitive' relations + expect(firstEntry.result[0].localMap).to.eql([ + { + local: 'x', + exported: 'y', + }, + ]); + }); + + it(`supports [export { x, y } from 'my/source'] (multiple re-exported named specifiers)`, async () => { + mockProject([`export { x, y } from 'my/source'`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(2); + expect(firstEntry.result[0].exportSpecifiers).to.eql(['x', 'y']); + expect(firstEntry.result[0].source).to.equal('my/source'); + }); + + it(`stores rootFileMap of an exported Identifier`, async () => { + mockProject({ + './src/OriginalComp.js': `export class OriginalComp {}`, + './src/inbetween.js': `export { OriginalComp as InBetweenComp } from './OriginalComp.js'`, + './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`, + }); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + const secondEntry = getEntry(queryResults[0], 1); + const thirdEntry = getEntry(queryResults[0], 2); + + expect(firstEntry.result[0].rootFileMap).to.eql([ + { + currentFileSpecifier: 'MyComp', // this is the local name in the file we track from + rootFile: { + file: './src/OriginalComp.js', // the file containing declaration + specifier: 'OriginalComp', // the specifier that was exported in file + }, + }, + ]); + expect(secondEntry.result[0].rootFileMap).to.eql([ + { + currentFileSpecifier: 'InBetweenComp', + rootFile: { + file: './src/OriginalComp.js', + specifier: 'OriginalComp', + }, + }, + ]); + expect(thirdEntry.result[0].rootFileMap).to.eql([ + { + currentFileSpecifier: 'OriginalComp', + rootFile: { + file: '[current]', + specifier: 'OriginalComp', + }, + }, + ]); + }); + + it(`stores rootFileMap of an exported Identifier`, async () => { + mockProject({ + './src/reexport.js': ` + // a direct default import + import RefDefault from 'exporting-ref-project'; + + export default RefDefault; + `, + './index.js': ` + import ExtendRefDefault from './src/reexport.js'; + + export default ExtendRefDefault; + `, + }); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + + expect(firstEntry.result[0].rootFileMap).to.eql([ + { + currentFileSpecifier: '[default]', + rootFile: { + file: 'exporting-ref-project', + specifier: '[default]', + }, + }, + ]); + }); + + it(`correctly handles empty files`, async () => { + // These can be encountered while scanning repos.. They should not break the code... + mockProject([`// some comment here...`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers).to.eql(['[file]']); + expect(firstEntry.result[0].source).to.equal(undefined); + }); + }); + + describe('Export variable types', () => { + it(`classes`, async () => { + mockProject([`export class X {}`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('X'); + expect(firstEntry.result[0].source).to.be.undefined; + }); + + it(`functions`, async () => { + mockProject([`export function y() {}`]); + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); + expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('y'); + expect(firstEntry.result[0].source).to.be.undefined; + }); + + // ...etc? + // ...TODO: create custom hooks to store meta info about types etc. + }); + + describe('Default post processing', () => { + // onlyInternalSources: false, + // keepOriginalSourcePaths: false, + // filterSpecifier: null, + }); + + describe('Options', () => { + // TODO: Move to dashboard + it.skip(`"metaConfig.categoryConfig"`, async () => { + mockProject( + [ + `export const foo = null`, // firstEntry + `export const bar = null`, // secondEntry + `export const baz = null`, // thirdEntry + ], + { + projectName: 'my-project', + filePaths: ['./foo.js', './packages/bar/test/bar.test.js', './temp/baz.js'], + }, + ); + + const findExportsCategoryQueryObj = await QueryService.getQueryConfigFromAnalyzer( + 'find-exports', + { + metaConfig: { + categoryConfig: [ + { + project: 'my-project', + categories: { + fooCategory: localFilePath => localFilePath.startsWith('./foo'), + barCategory: localFilePath => localFilePath.startsWith('./packages/bar'), + testCategory: localFilePath => localFilePath.includes('/test/'), + }, + }, + ], + }, + }, + ); + + const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const queryResult = queryResults[0]; + const [firstEntry, secondEntry, thirdEntry] = getEntries(queryResult); + expect(firstEntry.meta.categories).to.eql(['fooCategory']); + // not mutually exclusive... + expect(secondEntry.meta.categories).to.eql(['barCategory', 'testCategory']); + expect(thirdEntry.meta.categories).to.eql([]); + }); + }); +}); diff --git a/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test.js b/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test.js index 02ca6ece5..4bc6ed8b7 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test.js +++ b/packages-node/providence-analytics/test-node/program/analyzers/find-exports.test.js @@ -21,7 +21,7 @@ describe('Analyzer "find-exports"', async () => { }; describe('Export notations', () => { - it(`supports [export const x = 0] (named specifier)`, async () => { + it(`supports "export const x = 0;" (named specifier)`, async () => { mockProject([`export const x = 0`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstResult = getEntry(queryResults[0]).result[0]; @@ -30,7 +30,7 @@ describe('Analyzer "find-exports"', async () => { expect(firstResult.source).to.be.undefined; }); - it(`supports [export default class X {}] (default export)`, async () => { + it(`supports "export default class X {};" (default export)`, async () => { mockProject([`export default class X {}`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstResult = getEntry(queryResults[0]).result[0]; @@ -38,16 +38,16 @@ describe('Analyzer "find-exports"', async () => { expect(firstResult.source).to.be.undefined; }); - it(`supports [export default fn(){}] (default export)`, async () => { + it(`supports "export default x => x * 3;" (default function export)`, async () => { mockProject([`export default x => x * 3`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstResult = getEntry(queryResults[0]).result[0]; expect(firstResult.exportSpecifiers).to.eql(['[default]']); - expect(firstResult.source).to.equal(undefined); + expect(firstResult.source).to.be.undefined; }); - it(`supports [export {default as x} from 'y'] (default re-export)`, async () => { + it(`supports "export {default as x} from 'y';" (default re-export)`, async () => { mockProject({ './file-with-default-export.js': 'export default 1;', './file-with-default-re-export.js': @@ -82,7 +82,7 @@ describe('Analyzer "find-exports"', async () => { }); }); - it(`supports [import {x} from 'y'; export default x] (named re-export as default)`, async () => { + it(`supports "import {x} from 'y'; export default x;" (named re-export as default)`, async () => { mockProject([`import {x} from 'y'; export default x;`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstEntry = getEntry(queryResults[0]); @@ -91,7 +91,7 @@ describe('Analyzer "find-exports"', async () => { expect(firstEntry.result[0].source).to.equal('y'); }); - it(`supports [import x from 'y'; export default x] (default re-export as default)`, async () => { + it(`supports "import x from 'y'; export default x" (default re-export as default)`, async () => { mockProject([`import x from 'y'; export default x;`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstEntry = getEntry(queryResults[0]); @@ -100,7 +100,7 @@ describe('Analyzer "find-exports"', async () => { expect(firstEntry.result[0].source).to.equal('y'); }); - it(`supports [export { x } from 'my/source'] (re-export named specifier)`, async () => { + it(`supports "export { x } from 'my/source'" (re-export named specifier)`, async () => { mockProject([`export { x } from 'my/source'`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstEntry = getEntry(queryResults[0]); @@ -169,7 +169,7 @@ describe('Analyzer "find-exports"', async () => { ]); }); - it(`supports [export { x, y } from 'my/source'] (multiple re-exported named specifiers)`, async () => { + it(`supports "export { x, y } from 'my/source';" (multiple re-exported named specifiers)`, async () => { mockProject([`export { x, y } from 'my/source'`]); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const firstEntry = getEntry(queryResults[0]); @@ -185,6 +185,7 @@ describe('Analyzer "find-exports"', async () => { './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`, }); const queryResults = await providence(findExportsQueryConfig, _providenceCfg); + const firstEntry = getEntry(queryResults[0]); const secondEntry = getEntry(queryResults[0], 1); const thirdEntry = getEntry(queryResults[0], 2); 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/analyzers/helpers/track-down-identifier.test.js index 6eeab68eb..2edfbe1ff 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/helpers/track-down-identifier.test.js +++ b/packages-node/providence-analytics/test-node/program/analyzers/helpers/track-down-identifier.test.js @@ -1,15 +1,12 @@ import { expect } from 'chai'; import { it } from 'mocha'; -import babelTraverse from '@babel/traverse'; +import { swcTraverse } from '../../../../src/program/utils/swc-traverse.js'; import { trackDownIdentifier, trackDownIdentifierFromScope, } from '../../../../src/program/analyzers/helpers/track-down-identifier.js'; import { AstService } from '../../../../src/program/core/AstService.js'; -import { - mockProject, - restoreMockedProjects, -} from '../../../../test-helpers/mock-project-helpers.js'; +import { mockProject } from '../../../../test-helpers/mock-project-helpers.js'; import { setupAnalyzerTest } from '../../../../test-helpers/setup-analyzer-test.js'; /** @@ -285,9 +282,6 @@ describe('trackdownIdentifier', () => { specifier: 'IngAccordionInvokerButton', }); }); - - // TODO: improve perf - describe.skip('Caching', () => {}); }); describe('trackDownIdentifierFromScope', () => { @@ -299,7 +293,8 @@ describe('trackDownIdentifierFromScope', () => { }; mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); - const ast = AstService._getBabelAst(projectFiles['./src/declarationOfMyClass.js']); + // const ast = AstService._getBabelAst(projectFiles['./src/declarationOfMyClass.js']); + const ast = AstService._getSwcAst(projectFiles['./src/declarationOfMyClass.js']); // Let's say we want to track down 'MyClass' in the code above const identifierNameInScope = 'MyClass'; @@ -308,7 +303,12 @@ describe('trackDownIdentifierFromScope', () => { /** @type {NodePath} */ let astPath; - babelTraverse.default(ast, { + // babelTraverse.default(ast, { + // ClassDeclaration(path) { + // astPath = path; + // }, + // }); + swcTraverse(ast, { ClassDeclaration(path) { astPath = path; }, @@ -344,7 +344,8 @@ describe('trackDownIdentifierFromScope', () => { }; mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); - const ast = AstService._getBabelAst(projectFiles['./imported.js']); + // const ast = AstService._getBabelAst(projectFiles['./imported.js']); + const ast = AstService._getSwcAst(projectFiles['./imported.js']); // Let's say we want to track down 'MyClass' in the code above const identifierNameInScope = 'MyClass'; @@ -353,7 +354,12 @@ describe('trackDownIdentifierFromScope', () => { /** @type {NodePath} */ let astPath; - babelTraverse.default(ast, { + // babelTraverse.default(ast, { + // ImportDeclaration(path) { + // astPath = path; + // }, + // }); + swcTraverse(ast, { ImportDeclaration(path) { astPath = path; }, @@ -372,7 +378,7 @@ describe('trackDownIdentifierFromScope', () => { }); }); - it(`tracks down extended classes from a reexport`, async () => { + it(`tracks down extended classes from a re-export`, async () => { const projectFiles = { './src/classes.js': ` export class El1 extends HTMLElement {} @@ -386,7 +392,8 @@ describe('trackDownIdentifierFromScope', () => { }; mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); - const ast = AstService._getBabelAst(projectFiles['./imported.js']); + // const ast = AstService._getBabelAst(projectFiles['./imported.js']); + const ast = AstService._getSwcAst(projectFiles['./imported.js']); // Let's say we want to track down 'MyClass' in the code above const identifierNameInScope = 'El1'; @@ -395,7 +402,12 @@ describe('trackDownIdentifierFromScope', () => { /** @type {NodePath} */ let astPath; - babelTraverse.default(ast, { + // babelTraverse.default(ast, { + // ClassDeclaration(path) { + // astPath = path; + // }, + // }); + swcTraverse(ast, { ClassDeclaration(path) { astPath = path; }, diff --git a/packages-node/providence-analytics/test-node/program/core/Analyzer.test.js b/packages-node/providence-analytics/test-node/program/core/Analyzer.test.js index f6b5f6cc2..4d00d6580 100644 --- a/packages-node/providence-analytics/test-node/program/core/Analyzer.test.js +++ b/packages-node/providence-analytics/test-node/program/core/Analyzer.test.js @@ -25,9 +25,9 @@ describe('Analyzer', async () => { }); it('has a "requiredAst" string', async () => { - expect(typeof dummyAnalyzer.requiredAst).to.equal('string'); + expect(typeof dummyAnalyzer.constructor.requiredAst).to.equal('string'); const allowedAsts = ['babel']; - expect(allowedAsts).to.include(dummyAnalyzer.requiredAst); + expect(allowedAsts).to.include(dummyAnalyzer.constructor.requiredAst); }); it('has a "requiresReference" boolean', async () => { 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 45d5c4e62..7aa38d3e0 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 @@ -88,8 +88,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => { it('handles class declarations', async () => { const fakeFs = { '/my/proj/exports/ajax.js': ` - import { AjaxClass as LionAjaxClass } from '../_legacy/ajax/index.js'; - + import { AjaxClass as LionAjaxClass } from 'some-external-package'; export class AjaxClass extends LionAjaxClass {} `, }; 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 new file mode 100644 index 000000000..719dd8c1c --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/utils/swc-traverse.test.js @@ -0,0 +1,454 @@ +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 { AstService } from '../../../src/program/core/AstService.js'; + +/** + * @typedef {import('@swc/core').Module} SwcAstModule + * @typedef {import('../../../types/index.js').SwcPath} SwcPath + * @typedef {import('../../../types/index.js').SwcScope} SwcScope + */ + +/** + * @param {SwcAstModule} swcAst + */ +function gatherAllScopes(swcAst) { + /** @type {SwcScope[]} */ + const swcScopes = []; + swcTraverse(swcAst, { + enter({ scope }) { + if (!swcScopes.includes(scope)) { + swcScopes.push(scope); + } + }, + }); + return swcScopes; +} + +describe('swcTraverse', () => { + describe('Visitor', () => { + it('traverses an swc AST based on visitor', async () => { + const code = `import x from 'y';`; + const swcAst = await AstService._getSwcAst(code); + + let foundImportDeclarationPath; + const visitor = { + ImportDeclaration(/** @type {SwcPath} */ path) { + foundImportDeclarationPath = path; + }, + }; + swcTraverse(swcAst, 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); + + /** @type {string[]} */ + const foundTypes = []; + const visitor = { + /** + * @param {any} path + */ + enter(path) { + foundTypes.push(path.node.type); + }, + }; + swcTraverse(swcAst, visitor); + + expect(foundTypes).to.eql([ + 'Module', + 'ImportDeclaration', + 'ImportDefaultSpecifier', + 'Identifier', + 'StringLiteral', + ]); + }); + + it('supports "enter" and "exit" as generic handlers inside handlers', async () => { + const code = `import x from 'y';`; + const swcAst = await AstService._getSwcAst(code); + + /** @type {string[]} */ + const visitedPaths = []; + const visitor = { + /** + * @param {any} path + */ + ImportDeclaration: { + enter(path) { + visitedPaths.push({ path, phase: 'enter' }); + }, + exit(path) { + visitedPaths.push({ path, phase: 'exit' }); + }, + }, + }; + swcTraverse(swcAst, visitor); + + expect(visitedPaths[0].path).to.equal(visitedPaths[1].path); + expect(visitedPaths[0].phase).to.equal('enter'); + expect(visitedPaths[1].phase).to.equal('exit'); + }); + + it('supports "root" as alternative for Program', async () => { + const code = `import x from 'y';`; + const swcAst = await AstService._getSwcAst(code); + + let rootPath; + const visitor = { + /** + * @param {any} path + */ + root(path) { + rootPath = path; + }, + }; + swcTraverse(swcAst, visitor); + + // TODO: also add case for Script + expect(rootPath.node.type).to.equal('Module'); + }); + }); + + describe.skip('Paths', () => { + it(`adds { + node: SwcNode; + parent: SwcNode; + stop: function; + scope: SwcScope; + parentPath: SwcPath; + }`, async () => {}); + + it('supports getPathFromNode', async () => {}); + }); + + describe('Scopes', () => { + describe('Lexical scoping', () => { + it('creates scopes for blocks', async () => { + const code = ` + const globalScope = 0; + { + const middleScope = 1; + { + const deepestScope = 2; + } + } + const alsoGlobalScope = 3; + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + /** + * @param {any} path + */ + VariableDeclarator(path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, 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(Object.keys(declaratorPaths[0].scope.bindings)).to.eql([ + 'globalScope', + 'alsoGlobalScope', + ]); + // 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( + declaratorPaths[0].node, + ); + expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.identifier).to.equal( + declaratorPaths[3].node, + ); + + expect(Object.keys(declaratorPaths[1].scope.bindings)).to.eql(['middleScope']); + expect(Object.keys(declaratorPaths[2].scope.bindings)).to.eql(['deepestScope']); + }); + + it('creates scopes for nested FunctionDeclaration', async () => { + const code = ` + function globalFn() { + function middleScope() { + function deepestScope() { + + } + } + } + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + FunctionDeclaration(/** @type {any} */ path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + const scopes = gatherAllScopes(swcAst); + + expect(scopes[1].path?.node).to.equal(declaratorPaths[0].node); + expect(scopes[2].path?.node).to.equal(declaratorPaths[1].node); + expect(scopes[3].path?.node).to.equal(declaratorPaths[2].node); + }); + + it('creates scopes for ClassDeclaration', async () => { + const code = ` + class X extends HTMLElement { + constructor() { + var x = 1; + } + + method() { + window.alert('hi'); + } + } + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + VariableDeclarator(/** @type {any} */ path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + + expect(declaratorPaths[0].scope.id).to.equal(2); + }); + + it('creates scopes SwitchStatement', async () => { + const code = ` + const myCases = { a: true }; + switch (myCases) { + case myCases.a: + const x = 1; + break; + default: + }`; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + VariableDeclarator(/** @type {any} */ path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + + expect(declaratorPaths[0].node.id.value).to.equal('myCases'); + expect(declaratorPaths[1].node.id.value).to.equal('x'); + expect(declaratorPaths[0].scope.id).to.equal(0); + expect(declaratorPaths[1].scope.id).to.equal(1); + }); + + it('creates scopes for ObjectExpression', async () => { + const code = ` + export default { + toString(dateObj, opt = {}) {}, + }; + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const results = []; + const visitor = { + MethodProperty(/** @type {any} */ path) { + results.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + + expect(results[0].node.key.value).to.equal('toString'); + expect(results[0].scope.id).to.equal(0); + }); + + it('works for KeyValueProperty', async () => { + const code = ` + export const x = { + y:() => { + const z = 1; + }, + }; + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + VariableDeclarator(/** @type {any} */ path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + + expect(declaratorPaths[0].node.id.value).to.equal('x'); + expect(declaratorPaths[1].node.id.value).to.equal('z'); + expect(declaratorPaths[0].scope.id).to.equal(0); + expect(declaratorPaths[1].scope.id).to.equal(1); + }); + }); + + describe('Bindings', () => { + it('binds const and lets to block scope', async () => { + const code = ` + const globalScope = 0; + { + let middleScope = 1; + { + const deepestScope = 2; + } + } + let alsoGlobalScope = 3; + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + VariableDeclarator(/** @type {SwcPath} */ path) { + declaratorPaths.push(path); + }, + FunctionDeclaration(/** @type {SwcPath} */ path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + + expect(Object.keys(declaratorPaths[0].scope.bindings)).to.eql([ + 'globalScope', + 'alsoGlobalScope', + ]); + // Scope bindings refer to Declarator nodes + expect(declaratorPaths[0].scope.bindings.globalScope.identifier).to.equal( + declaratorPaths[0].node, + ); + expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.identifier).to.equal( + declaratorPaths[3].node, + ); + }); + + it('binds vars to function scope', async () => { + const code = ` + var globalScope = 0; + { + var stillGlobalScope = 1; + function middleScope() { + var insideFnScope = 2; + } + } + `; + const swcAst = await AstService._getSwcAst(code); + + /** @type {SwcPath[]} */ + const declaratorPaths = []; + const visitor = { + VariableDeclarator(/** @type {SwcPath} */ path) { + declaratorPaths.push(path); + }, + }; + swcTraverse(swcAst, visitor, { needsAdvancedPaths: true }); + + expect(Object.keys(declaratorPaths[0].scope.bindings)).to.eql([ + 'globalScope', + 'stillGlobalScope', + ]); + expect(Object.keys(declaratorPaths[1].scope.bindings)).to.eql(['middleScope']); + expect(Object.keys(declaratorPaths[2].scope.bindings)).to.eql(['insideFnScope']); + }); + }); + + describe.skip('References', () => {}); + }); + + describe('Babel compatibility', () => { + const babelTraverse = babelTraversePkg.default; + + /** + * @param {string} code + */ + async function compareScopeResultsWithBabel(code) { + const swcAst = await AstService._getSwcAst(code); + const babelAst = await AstService._getBabelAst(code); + + /** + * @type {any[]} + */ + const babelScopes = []; + babelTraverse(babelAst, { + // @ts-ignore + enter({ scope }) { + if (!babelScopes.includes(scope)) { + babelScopes.push(scope); + } + }, + }); + + /** @type {SwcScope[]} */ + const swcScopes = []; + swcTraverse(swcAst, { + enter({ scope }) { + if (!swcScopes.includes(scope)) { + swcScopes.push(scope); + } + }, + }); + + const babelRootScopeIdOffset = babelScopes[0].uid; + + 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.eql(Object.keys(swcScopes[i].bindings)); + // expect(babelScopes[i].references).to.eql(swcResults[i].references); + } + } + + it('handles all kinds of lexical scopes and bindings in a similar way', async () => { + const code = ` + const globalScope = 0; + function fn() { + let middleScope = 2; + function fn() { + var parentScope = 3; + } + } + const alsoGlobalScope = 4; + + { + const myCases = { a: true }; + { + switch (myCases) { + case myCases.a: + const x = 1; + break; + default: + }; + } + } + + class Q { + constructor() { + + } + } + `; + + await compareScopeResultsWithBabel(code); + }); + }); +}); diff --git a/packages-node/providence-analytics/tsconfig.json b/packages-node/providence-analytics/tsconfig.json index a61cb2cb8..db4176df6 100644 --- a/packages-node/providence-analytics/tsconfig.json +++ b/packages-node/providence-analytics/tsconfig.json @@ -4,6 +4,6 @@ "outDir": "./dist-types", "rootDir": "." }, - "include": ["src", "dashboard", "types"], + "include": ["src", "types"], "exclude": ["dist-types"] } diff --git a/packages-node/providence-analytics/types/core/Analyzer.d.ts b/packages-node/providence-analytics/types/core/Analyzer.d.ts index 1c64cbf1d..f4bba1058 100644 --- a/packages-node/providence-analytics/types/core/Analyzer.d.ts +++ b/packages-node/providence-analytics/types/core/Analyzer.d.ts @@ -3,7 +3,7 @@ import { PathFromSystemRoot, QueryType, QueryResult, - RequiredAst, + AnalyzerAst, ImportOrExportId, Project, GatherFilesConfig, @@ -16,6 +16,8 @@ import { */ export type AnalyzerName = `${'find' | 'match'}-${string}` | ''; +export type AnalyzerAst = 'babel' | 'swc-to-babel' | 'swc'; + // 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) // so that these type definitions can be used to generate a json schema: https://www.npmjs.com/package/typescript-json-schema @@ -27,7 +29,7 @@ export interface Meta { export interface AnalyzerMeta { name: AnalyzerName; - requiredAst: RequiredAst; + requiredAst: AnalyzerAst; /* a unique hash based on target, reference and configuration */ identifier: ImportOrExportId; /* target project meta object */ diff --git a/packages-node/providence-analytics/types/core/core.d.ts b/packages-node/providence-analytics/types/core/core.d.ts index cc76cbc8d..7823604cd 100644 --- a/packages-node/providence-analytics/types/core/core.d.ts +++ b/packages-node/providence-analytics/types/core/core.d.ts @@ -53,11 +53,6 @@ export type RootFile = { specifier: SpecifierName; }; -/** - * Required ast for the analysis. Currently, only Babel is supported - */ -export type RequiredAst = 'babel'; - /** * Name entry found in package.json */ @@ -186,4 +181,9 @@ export type PackageJson = { dependencies?: { [dependency: string]: string }; devDependencies?: { [dependency: string]: string }; workspaces?: string[]; + main?: string; +}; + +export type LernaJson = { + packages: string[]; }; diff --git a/packages-node/providence-analytics/types/index.ts b/packages-node/providence-analytics/types/index.ts index c18b7c8ad..dbcad9034 100644 --- a/packages-node/providence-analytics/types/index.ts +++ b/packages-node/providence-analytics/types/index.ts @@ -1,3 +1,4 @@ export * from './core/index.js'; export * from './analyzers/index.js'; export * from './misc.js'; +export * from './utils/index.js'; diff --git a/packages-node/providence-analytics/types/utils/index.d.ts b/packages-node/providence-analytics/types/utils/index.d.ts new file mode 100644 index 000000000..bea813e9d --- /dev/null +++ b/packages-node/providence-analytics/types/utils/index.d.ts @@ -0,0 +1,31 @@ +export type SwcScope = { + id: number; + parentScope?: Scope; + bindings: { [key: string]: Binding }; + path: SwcPath | null; + _pendingRefsWithoutBinding: SwcNode[]; + _isIsolatedBlockStatement: boolean; +}; + +/* Binding https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-bindings */ +export type SwcBinding = { + identifier: SwcNode; + // kind: string; + refs: SwcNode[]; + path: SwcPath; +}; + +export type SwcPath = { + node: SwcNode; + parent: SwcNode; + stop: function; + scope: SwcScope; + parentPath: SwcPath; +}; + +type SwcVisitorFn = (swcPath: SwcPath) => void; +export type SwcVisitor = { + [key: string]: SwcVisitorFn | { enter?: SwcVisitorFn; leave?: SwcVisitorFn }; +}; + +export type SwcTraversalContext = { visitOnExitFns: (() => void)[]; scopeId: number };