diff --git a/packages-node/providence-analytics/package.json b/packages-node/providence-analytics/package.json index f41c997dc..8101b8278 100644 --- a/packages-node/providence-analytics/package.json +++ b/packages-node/providence-analytics/package.json @@ -32,6 +32,7 @@ "@babel/core": "^7.10.1", "@babel/parser": "^7.5.5", "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.18.6", "@babel/register": "^7.5.5", "@babel/traverse": "^7.5.5", "@babel/types": "^7.9.0", 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 b16633a78..0abdd4363 100644 --- a/packages-node/providence-analytics/src/program/analyzers/find-exports.js +++ b/packages-node/providence-analytics/src/program/analyzers/find-exports.js @@ -4,6 +4,8 @@ const { default: traverse } = require('@babel/traverse'); const { Analyzer } = require('./helpers/Analyzer.js'); const { trackDownIdentifier } = require('./helpers/track-down-identifier.js'); const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.js'); +const { getReferencedDeclaration } = require('../utils/get-source-code-fragment-of-declaration.js'); + const { LogService } = require('../services/LogService.js'); /** @@ -135,6 +137,9 @@ function getLocalNameSpecifiers(node) { .filter(s => s); } +const isImportingSpecifier = pathOrNode => + pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier'; + /** * @desc Finds import specifiers and sources for a given ast result * @param {BabelAst} ast @@ -150,17 +155,38 @@ function findExportsPerAstEntry(ast, { 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) + let globalScopeBindings; + traverse(ast, { + Program: { + enter(babelPath) { + const body = babelPath.get('body'); + if (body.length) { + globalScopeBindings = body[0].scope.bindings; + } + }, + }, ExportNamedDeclaration(path) { const exportSpecifiers = getExportSpecifiers(path.node); const localMap = getLocalNameSpecifiers(path.node); const source = path.node.source?.value; transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } }); }, - ExportDefaultDeclaration(path) { + ExportDefaultDeclaration(defaultExportPath) { const exportSpecifiers = ['[default]']; - const source = path.node.declaration.name; - transformedEntry.push({ exportSpecifiers, source, __tmp: { path } }); + 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; + } + } + transformedEntry.push({ exportSpecifiers, source, __tmp: { path: defaultExportPath } }); }, }); 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 27248c65e..7428d6873 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 @@ -171,6 +171,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro if (reexportMatch || shouldLookForDefaultExport) { return; } + // Are we dealing with a re-export ? if (path.node.specifiers && path.node.specifiers.length) { exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); @@ -202,6 +203,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro } } reexportMatch = true; + pendingTrackDownPromise = trackDownIdentifier( newSource, localName, diff --git a/packages-node/providence-analytics/src/program/services/AstService.js b/packages-node/providence-analytics/src/program/services/AstService.js index d67455718..51a3150d9 100644 --- a/packages-node/providence-analytics/src/program/services/AstService.js +++ b/packages-node/providence-analytics/src/program/services/AstService.js @@ -61,7 +61,7 @@ class AstService { static _getBabelAst(code) { const ast = babelParser.parse(code, { sourceType: 'module', - plugins: ['importMeta', 'dynamicImport', 'classProperties'], + plugins: ['importMeta', 'dynamicImport', 'classProperties', 'exportDefaultFrom'], }); return ast; } 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 2d6bfbd1d..6edbbba78 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 @@ -21,7 +21,7 @@ function getFilePathOrExternalSource({ rootPath, localPath }) { * const y = x; * export const myIdentifier = y; * ``` - * - We started in getSourceCodeFragmentOfDeclaration (looing for 'myIdentifier'), which found VariableDeclarator of export myIdentifier + * - 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) @@ -34,7 +34,10 @@ function getReferencedDeclaration({ referencedIdentifierName, globalScopeBinding ([key]) => key === referencedIdentifierName, ); - if (refDeclaratorBinding.path.type === 'ImportSpecifier') { + if ( + refDeclaratorBinding.path.type === 'ImportSpecifier' || + refDeclaratorBinding.path.type === 'ImportDefaultSpecifier' + ) { return refDeclaratorBinding.path; } @@ -159,4 +162,5 @@ async function getSourceCodeFragmentOfDeclaration({ module.exports = { getSourceCodeFragmentOfDeclaration, getFilePathOrExternalSource, + getReferencedDeclaration, }; 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 fb8b99364..316693349 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,16 @@ describe('Analyzer "find-exports"', () => { expect(firstEntry.result[0].source).to.equal(undefined); }); + it(`supports [export default fn(){}] (default export)`, async () => { + mockProject([`export default x => x * 3`]); + await providence(findExportsQueryConfig, _providenceCfg); + const queryResult = queryResults[0]; + const firstEntry = getEntry(queryResult); + 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(undefined); + }); + it(`supports [export {default as x} from 'y'] (default re-export)`, async () => { mockProject({ './file-with-default-export.js': 'export default 1;', @@ -105,6 +115,26 @@ describe('Analyzer "find-exports"', () => { }); }); + it(`supports [import {x} from 'y'; export default x] (named re-export as default)`, async () => { + mockProject([`import {x} from 'y'; export default x;`]); + await providence(findExportsQueryConfig, _providenceCfg); + const queryResult = queryResults[0]; + const firstEntry = getEntry(queryResult); + 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;`]); + await providence(findExportsQueryConfig, _providenceCfg); + const queryResult = queryResults[0]; + const firstEntry = getEntry(queryResult); + 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'`]); await providence(findExportsQueryConfig, _providenceCfg); @@ -191,17 +221,18 @@ describe('Analyzer "find-exports"', () => { ]); }); - // TODO: myabe in the future: This experimental syntax requires enabling the parser plugin: 'exportDefaultFrom' - it.skip(`stores rootFileMap of an exported Identifier`, async () => { + it(`stores rootFileMap of an exported Identifier`, async () => { mockProject({ './src/reexport.js': ` // a direct default import import RefDefault from 'exporting-ref-project'; - export RefDefault; + export default RefDefault; `, './index.js': ` - export { ExtendRefDefault } from './src/reexport.js'; + import ExtendRefDefault from './src/reexport.js'; + + export default ExtendRefDefault; `, }); await providence(findExportsQueryConfig, _providenceCfg); @@ -210,7 +241,7 @@ describe('Analyzer "find-exports"', () => { expect(firstEntry.result[0].rootFileMap).to.eql([ { - currentFileSpecifier: 'ExtendRefDefault', + currentFileSpecifier: '[default]', rootFile: { file: 'exporting-ref-project', specifier: '[default]', @@ -218,6 +249,16 @@ describe('Analyzer "find-exports"', () => { }, ]); }); + + it(`correctly handles empty files`, async () => { + // These can be encountered while scanning repos.. They should not break the code... + mockProject([`// some comment here...`]); + await providence(findExportsQueryConfig, _providenceCfg); + const queryResult = queryResults[0]; + const firstEntry = getEntry(queryResult); + expect(firstEntry.result[0].exportSpecifiers).to.eql(['[file]']); + expect(firstEntry.result[0].source).to.equal(undefined); + }); }); describe('Export variable types', () => {