diff --git a/.changeset/brown-pets-accept.md b/.changeset/brown-pets-accept.md new file mode 100644 index 000000000..2c6f8ecc8 --- /dev/null +++ b/.changeset/brown-pets-accept.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +add posibility to provide a 'custom defined project' (array of filePaths) to Analyzer diff --git a/.changeset/nine-fishes-fly.md b/.changeset/nine-fishes-fly.md new file mode 100644 index 000000000..02c33ddd6 --- /dev/null +++ b/.changeset/nine-fishes-fly.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +add util "getSourceCodeFragmentOfDeclaration" diff --git a/packages-node/providence-analytics/src/program/analyzers/helpers/Analyzer.js b/packages-node/providence-analytics/src/program/analyzers/helpers/Analyzer.js index e132942fd..f723bcc11 100644 --- a/packages-node/providence-analytics/src/program/analyzers/helpers/Analyzer.js +++ b/packages-node/providence-analytics/src/program/analyzers/helpers/Analyzer.js @@ -13,10 +13,13 @@ const { getFilePathRelativeFromRoot } = require('../../utils/get-file-path-relat * @typedef {import('../../types/core').AnalyzerName} AnalyzerName * @typedef {import('../../types/core').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../types/core').QueryOutput} QueryOutput + * @typedef {import('../../types/core').QueryOutputEntry} QueryOutputEntry * @typedef {import('../../types/core').ProjectInputData} ProjectInputData * @typedef {import('../../types/core').ProjectInputDataWithMeta} ProjectInputDataWithMeta * @typedef {import('../../types/core').AnalyzerQueryResult} AnalyzerQueryResult * @typedef {import('../../types/core').MatchAnalyzerConfig} MatchAnalyzerConfig + * + * @typedef {(ast: object, { relativePath: PathRelative }) => {result: QueryOutputEntry}} TraversEntryFn */ /** @@ -171,10 +174,10 @@ class Analyzer { } /** - * In a MatchAnalyzer, two Analyzers (a reference and targer) are run. - * For instance, in a MatchImportsAnalyzer, a FindExportsAnalyzer and FinImportsAnalyzer are run. + * In a MatchAnalyzer, two Analyzers (a reference and target) are run. + * For instance: a FindExportsAnalyzer and FindImportsAnalyzer are run. * Their results can be provided as config params. - * If they are stored in json format, 'unwind' them to be compatible for analysis... + * When they were stored in json format in the filesystem, 'unwind' them to be compatible for analysis... * @param {MatchAnalyzerConfig} cfg */ static __unwindProvidedResults(cfg) { @@ -284,16 +287,43 @@ class Analyzer { } /** - * @param {function} traverseEntry + * @param {function|{traverseEntryFn: function: filePaths:string[]; projectPath: string}} traverseEntryOrConfig */ - async _traverse(traverseEntry) { + async _traverse(traverseEntryOrConfig) { LogService.debug(`Analyzer "${this.name}": started _traverse method`); + let traverseEntryFn; + let finalTargetData; + + if (typeof traverseEntryOrConfig === 'function') { + traverseEntryFn = traverseEntryOrConfig; + finalTargetData = this.targetData; + } else { + traverseEntryFn = traverseEntryOrConfig.traverseEntryFn; + if (!traverseEntryOrConfig.filePaths) { + finalTargetData = this.targetData; + } else { + const { projectPath, projectName } = traverseEntryOrConfig; + if (!projectPath) { + LogService.error(`[Analyzer._traverse]: you must provide a projectPath`); + } + finalTargetData = InputDataService.createDataObject([ + { + project: { + name: projectName || '[n/a]', + path: projectPath, + }, + entries: traverseEntryOrConfig.filePaths, + }, + ]); + } + } + /** * Create ASTs for our inputData */ - const astDataProjects = await QueryService.addAstToProjectsData(this.targetData, 'babel'); - return analyzePerAstEntry(astDataProjects[0], traverseEntry); + const astDataProjects = await QueryService.addAstToProjectsData(finalTargetData, 'babel'); + return analyzePerAstEntry(astDataProjects[0], traverseEntryFn); } async execute(customConfig = {}) { 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 new file mode 100644 index 000000000..ab8ae07f1 --- /dev/null +++ b/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration.js @@ -0,0 +1,98 @@ +const fs = require('fs'); +const babelTraversePkg = require('@babel/traverse'); +const { AstService } = require('../services/AstService.js'); + +/** + * Assume we had: + * ```js + * const x = 88; + * const y = x; + * export const myIdentifier = y; + * ``` + * - We started in getSourceCodeFragmentOfDeclaration (looing 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 {BabelNodePath} + */ +function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) { + const [, refDeclaratorBinding] = Object.entries(globalScopeBindings).find( + ([key]) => key === referencedIdentifierName, + ); + + if (refDeclaratorBinding.path.node.init.type === 'Identifier') { + return getReferencedDeclaration({ + referencedIdentifierName: refDeclaratorBinding.path.node.init.name, + globalScopeBindings, + }); + } + + return refDeclaratorBinding.path.get('init'); +} + +/** + * + * @param {{ filePath: string; exportedIdentifier: string; }} opts + */ +async function getSourceCodeFragmentOfDeclaration({ filePath, exportedIdentifier }) { + const code = fs.readFileSync(filePath, 'utf-8'); + const ast = AstService.getAst(code, 'babel'); + + let finalNodePath; + + babelTraversePkg.default(ast, { + Program(babelPath) { + babelPath.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 = babelPath.get('body')[0].scope.bindings; + + if (exportedIdentifier === '[default]') { + const defaultExportPath = babelPath + .get('body') + .find(child => child.node.type === 'ExportDefaultDeclaration'); + 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 = babelPath.scope.getBinding(exportedIdentifier).path; + const isReferenced = variableDeclaratorPath.node.init?.type === 'Identifier'; + + if (!isReferenced) { + // it must be an exported declaration + finalNodePath = variableDeclaratorPath.get('init'); + } else { + finalNodePath = getReferencedDeclaration({ + referencedIdentifierName: variableDeclaratorPath.node.init.name, + globalScopeBindings, + }); + } + } + }, + }); + + return { + sourceNodePath: finalNodePath, + sourceFragment: code.slice(finalNodePath.node.start, finalNodePath.node.end), + }; +} + +module.exports = { + getSourceCodeFragmentOfDeclaration, +}; diff --git a/packages-node/providence-analytics/test-node/program/utils/getSourceCodeFragmentOfDeclaration.test.js b/packages-node/providence-analytics/test-node/program/utils/getSourceCodeFragmentOfDeclaration.test.js new file mode 100644 index 000000000..faa361ff7 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/utils/getSourceCodeFragmentOfDeclaration.test.js @@ -0,0 +1,107 @@ +const { expect } = require('chai'); +const { mock } = require('../../../test-helpers/mock-project-helpers.js'); +const { getSourceCodeFragmentOfDeclaration } = require('../../../src/program/utils/index.js'); + +describe('getSourceCodeFragmentOfDeclaration', () => { + describe('Named specifiers', () => { + it('finds source code for directly declared specifiers', async () => { + const fakeFs = { + '/my/proj/exports/file.js': 'export const x = 0;', + }; + mock(fakeFs); + + const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ + filePath: '/my/proj/exports/file.js', + exportedIdentifier: 'x', + }); + + expect(sourceFragment).to.equal('0'); + }); + + it('finds source code for referenced specifiers', async () => { + const fakeFs = { + '/my/proj/exports/file.js': ` + const y = 0; + export const x = y; + `, + }; + mock(fakeFs); + + const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ + filePath: '/my/proj/exports/file.js', + exportedIdentifier: 'x', + }); + + expect(sourceFragment).to.equal('0'); + }); + + it('finds source code for rereferenced specifiers', async () => { + const fakeFs = { + '/my/proj/exports/file.js': ` + const x = 88; + const y = x; + export const myIdentifier = y; + `, + }; + mock(fakeFs); + + const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ + filePath: '/my/proj/exports/file.js', + exportedIdentifier: 'myIdentifier', + }); + + expect(sourceFragment).to.equal('88'); + }); + }); + + describe('[default] specifiers', () => { + it('finds source code for directly declared specifiers', async () => { + const fakeFs = { + '/my/proj/exports/file.js': 'export default class {};', + }; + mock(fakeFs); + + const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ + filePath: '/my/proj/exports/file.js', + exportedIdentifier: '[default]', + }); + + expect(sourceFragment).to.equal('class {}'); + }); + + it('finds source code for referenced specifiers', async () => { + const fakeFs = { + '/my/proj/exports/file.js': ` + const myIdentifier = 0; + export default myIdentifier; + `, + }; + mock(fakeFs); + + const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ + filePath: '/my/proj/exports/file.js', + exportedIdentifier: '[default]', + }); + + expect(sourceFragment).to.equal('0'); + }); + + it('finds source code for rereferenced specifiers', async () => { + const fakeFs = { + '/my/proj/exports/file.js': ` + const x = 88; + const myIdentifier = x; + export default myIdentifier; + `, + }; + mock(fakeFs); + + const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({ + filePath: '/my/proj/exports/file.js', + exportedIdentifier: '[default]', + }); + + expect(sourceFragment).to.equal('88'); + }); + }); +});