diff --git a/.changeset/great-jokes-travel.md b/.changeset/great-jokes-travel.md new file mode 100644 index 000000000..ea7c17ab2 --- /dev/null +++ b/.changeset/great-jokes-travel.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +improvements find-exports, trackdown-identifier, get-source-code-fragment-of-declaration 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 12c5b5029..b16633a78 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-exports.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-exports.js @@ -46,16 +46,19 @@ async function trackdownRoot(transformedEntry, relativePath, projectPath) { 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, @@ -122,7 +125,8 @@ function getLocalNameSpecifiers(node) { .map(s => { if (s.exported && s.local && s.exported.name !== s.local.name) { return { - local: s.local.name, + // if reserved keyword 'default' is used, translate it into 'providence keyword' + local: s.local.name === 'default' ? '[default]' : s.local.name, exported: s.exported.name, }; } 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 01edbc6bb..27248c65e 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 @@ -138,6 +138,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro let reexportMatch = false; // named specifier declaration let pendingTrackDownPromise; + let exportMatch; traverse(ast, { ExportDefaultDeclaration(path) { @@ -172,7 +173,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro } // Are we dealing with a re-export ? if (path.node.specifiers && path.node.specifiers.length) { - const exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); + exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); if (exportMatch) { const localName = exportMatch.local.name; @@ -218,7 +219,10 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro // in current file... rootSpecifier = identifierName; rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath); - path.stop(); + + if (exportMatch) { + path.stop(); + } } }, }, 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 674d7cf19..2d6bfbd1d 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 @@ -3,6 +3,16 @@ const path = require('path'); const babelTraversePkg = require('@babel/traverse'); const { AstService } = require('../services/AstService.js'); const { trackDownIdentifier } = require('../analyzers/helpers/track-down-identifier.js'); +const { toPosixPath } = require('./to-posix-path.js'); + +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: @@ -82,13 +92,15 @@ async function getSourceCodeFragmentOfDeclaration({ } } else { const variableDeclaratorPath = babelPath.scope.getBinding(exportedIdentifier).path; - const isReferenced = variableDeclaratorPath.node.init?.type === 'Identifier'; - const contentPath = variableDeclaratorPath.node.init + const varDeclNode = variableDeclaratorPath.node; + const isReferenced = varDeclNode.init?.type === 'Identifier'; + const contentPath = varDeclNode.init ? variableDeclaratorPath.get('init') : variableDeclaratorPath; - const name = variableDeclaratorPath.node.init - ? variableDeclaratorPath.node.init.name - : variableDeclaratorPath.node.id.name; + + const name = varDeclNode.init + ? varDeclNode.init.name + : varDeclNode.id?.name || varDeclNode.imported.name; if (!isReferenced) { // it must be an exported declaration @@ -115,19 +127,36 @@ async function getSourceCodeFragmentOfDeclaration({ 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: path.resolve(projectRootPath, rootFile.file), + filePath: filePathOrSrc, exportedIdentifier: rootFile.specifier, + projectRootPath, }); } return { sourceNodePath: finalNodePath, sourceFragment: code.slice(finalNodePath.node?.start, finalNodePath.node?.end), + externalImportSource: null, }; } module.exports = { getSourceCodeFragmentOfDeclaration, + getFilePathOrExternalSource, }; diff --git a/packages-node/providence-analytics/src/program/utils/index.js b/packages-node/providence-analytics/src/program/utils/index.js index dc77f82b7..b9c315b07 100644 --- a/packages-node/providence-analytics/src/program/utils/index.js +++ b/packages-node/providence-analytics/src/program/utils/index.js @@ -1,9 +1,11 @@ const { getSourceCodeFragmentOfDeclaration, + getFilePathOrExternalSource, } = require('./get-source-code-fragment-of-declaration.js'); // TODO: move trackdownIdentifier to utils as well module.exports = { getSourceCodeFragmentOfDeclaration, + getFilePathOrExternalSource, }; 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 700603fa5..fb8b99364 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 @@ -66,6 +66,45 @@ describe('Analyzer "find-exports"', () => { expect(firstEntry.result[0].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';", + }); + + await providence(findExportsQueryConfig, _providenceCfg); + const queryResult = queryResults[0]; + const firstEntry = getEntry(queryResult); + expect(firstEntry.result[0]).to.eql({ + exportSpecifiers: ['[default]'], + source: undefined, + rootFileMap: [ + { + currentFileSpecifier: '[default]', + rootFile: { file: '[current]', specifier: '[default]' }, + }, + ], + }); + + const secondEntry = getEntry(queryResult, 1); + expect(secondEntry.result[0].exportSpecifiers.length).to.equal(1); + expect(secondEntry.result[0].exportSpecifiers[0]).to.equal('namedExport'); + expect(secondEntry.result[0].source).to.equal('./file-with-default-export.js'); + 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 [export { x } from 'my/source'] (re-export named specifier)`, async () => { mockProject([`export { x } from 'my/source'`]); await providence(findExportsQueryConfig, _providenceCfg); 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 9b9a3bc68..18e619fc3 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 @@ -168,6 +168,51 @@ describe('trackdownIdentifier', () => { }); }); + it(`works with multiple re-exports in a file`, async () => { + mockProject( + { + './packages/accordion/IngAccordionContent.js': `export class IngAccordionContent { }`, + './packages/accordion/IngAccordionInvokerButton.js': `export class IngAccordionInvokerButton { }`, + './packages/accordion/index.js': ` + export { IngAccordionContent } from './IngAccordionContent.js'; + export { IngAccordionInvokerButton } from './IngAccordionInvokerButton.js';`, + }, + { + projectName: 'my-project', + projectPath: '/my/project', + }, + ); + + // Let's say we want to track down 'IngAccordionInvokerButton' in the code above + const source = './IngAccordionContent.js'; + const identifierName = 'IngAccordionContent'; + const currentFilePath = '/my/project/packages/accordion/index.js'; + const rootPath = '/my/project'; + + const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath); + expect(rootFile).to.eql({ + file: './packages/accordion/IngAccordionContent.js', + specifier: 'IngAccordionContent', + }); + + // Let's say we want to track down 'IngAccordionInvokerButton' in the code above + const source2 = './IngAccordionInvokerButton.js'; + const identifierName2 = 'IngAccordionInvokerButton'; + const currentFilePath2 = '/my/project/packages/accordion/index.js'; + const rootPath2 = '/my/project'; + + const rootFile2 = await trackDownIdentifier( + source2, + identifierName2, + currentFilePath2, + rootPath2, + ); + expect(rootFile2).to.eql({ + file: './packages/accordion/IngAccordionInvokerButton.js', + specifier: 'IngAccordionInvokerButton', + }); + }); + // TODO: improve perf describe.skip('Caching', () => {}); });