feat(providence-analytics): support "exportDefaultFrom" in find-exports analyzer

This commit is contained in:
Thijs Louisse 2022-09-26 18:14:07 +02:00 committed by Thomas Allmer
parent 5925364ffe
commit 78697d3b00
6 changed files with 85 additions and 11 deletions

View file

@ -32,6 +32,7 @@
"@babel/core": "^7.10.1", "@babel/core": "^7.10.1",
"@babel/parser": "^7.5.5", "@babel/parser": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-syntax-export-default-from": "^7.18.6",
"@babel/register": "^7.5.5", "@babel/register": "^7.5.5",
"@babel/traverse": "^7.5.5", "@babel/traverse": "^7.5.5",
"@babel/types": "^7.9.0", "@babel/types": "^7.9.0",

View file

@ -4,6 +4,8 @@ const { default: traverse } = require('@babel/traverse');
const { Analyzer } = require('./helpers/Analyzer.js'); const { Analyzer } = require('./helpers/Analyzer.js');
const { trackDownIdentifier } = require('./helpers/track-down-identifier.js'); const { trackDownIdentifier } = require('./helpers/track-down-identifier.js');
const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.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'); const { LogService } = require('../services/LogService.js');
/** /**
@ -135,6 +137,9 @@ function getLocalNameSpecifiers(node) {
.filter(s => s); .filter(s => s);
} }
const isImportingSpecifier = pathOrNode =>
pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier';
/** /**
* @desc Finds import specifiers and sources for a given ast result * @desc Finds import specifiers and sources for a given ast result
* @param {BabelAst} ast * @param {BabelAst} ast
@ -150,17 +155,38 @@ function findExportsPerAstEntry(ast, { skipFileImports }) {
// Unfortunately, we cannot have async functions in babel traverse. // Unfortunately, we cannot have async functions in babel traverse.
// Therefore, we store a temp reference to path that we use later for // Therefore, we store a temp reference to path that we use later for
// async post processing (tracking down original export Identifier) // async post processing (tracking down original export Identifier)
let globalScopeBindings;
traverse(ast, { traverse(ast, {
Program: {
enter(babelPath) {
const body = babelPath.get('body');
if (body.length) {
globalScopeBindings = body[0].scope.bindings;
}
},
},
ExportNamedDeclaration(path) { ExportNamedDeclaration(path) {
const exportSpecifiers = getExportSpecifiers(path.node); const exportSpecifiers = getExportSpecifiers(path.node);
const localMap = getLocalNameSpecifiers(path.node); const localMap = getLocalNameSpecifiers(path.node);
const source = path.node.source?.value; const source = path.node.source?.value;
transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } }); transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } });
}, },
ExportDefaultDeclaration(path) { ExportDefaultDeclaration(defaultExportPath) {
const exportSpecifiers = ['[default]']; const exportSpecifiers = ['[default]'];
const source = path.node.declaration.name; let source;
transformedEntry.push({ exportSpecifiers, source, __tmp: { path } }); 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 } });
}, },
}); });

View file

@ -171,6 +171,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
if (reexportMatch || shouldLookForDefaultExport) { if (reexportMatch || shouldLookForDefaultExport) {
return; return;
} }
// Are we dealing with a re-export ? // Are we dealing with a re-export ?
if (path.node.specifiers && path.node.specifiers.length) { if (path.node.specifiers && path.node.specifiers.length) {
exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName);
@ -202,6 +203,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
} }
} }
reexportMatch = true; reexportMatch = true;
pendingTrackDownPromise = trackDownIdentifier( pendingTrackDownPromise = trackDownIdentifier(
newSource, newSource,
localName, localName,

View file

@ -61,7 +61,7 @@ class AstService {
static _getBabelAst(code) { static _getBabelAst(code) {
const ast = babelParser.parse(code, { const ast = babelParser.parse(code, {
sourceType: 'module', sourceType: 'module',
plugins: ['importMeta', 'dynamicImport', 'classProperties'], plugins: ['importMeta', 'dynamicImport', 'classProperties', 'exportDefaultFrom'],
}); });
return ast; return ast;
} }

View file

@ -21,7 +21,7 @@ function getFilePathOrExternalSource({ rootPath, localPath }) {
* const y = x; * const y = x;
* export const myIdentifier = y; * 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', ... } * - getReferencedDeclaration is called with { referencedIdentifierName: 'y', ... }
* - now we will look in globalScopeBindings, till we find declaration of '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 ref? Call ourselves with referencedIdentifierName ('x' in example above)
@ -34,7 +34,10 @@ function getReferencedDeclaration({ referencedIdentifierName, globalScopeBinding
([key]) => key === referencedIdentifierName, ([key]) => key === referencedIdentifierName,
); );
if (refDeclaratorBinding.path.type === 'ImportSpecifier') { if (
refDeclaratorBinding.path.type === 'ImportSpecifier' ||
refDeclaratorBinding.path.type === 'ImportDefaultSpecifier'
) {
return refDeclaratorBinding.path; return refDeclaratorBinding.path;
} }
@ -159,4 +162,5 @@ async function getSourceCodeFragmentOfDeclaration({
module.exports = { module.exports = {
getSourceCodeFragmentOfDeclaration, getSourceCodeFragmentOfDeclaration,
getFilePathOrExternalSource, getFilePathOrExternalSource,
getReferencedDeclaration,
}; };

View file

@ -66,6 +66,16 @@ describe('Analyzer "find-exports"', () => {
expect(firstEntry.result[0].source).to.equal(undefined); 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 () => { it(`supports [export {default as x} from 'y'] (default re-export)`, async () => {
mockProject({ mockProject({
'./file-with-default-export.js': 'export default 1;', './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 () => { it(`supports [export { x } from 'my/source'] (re-export named specifier)`, async () => {
mockProject([`export { x } from 'my/source'`]); mockProject([`export { x } from 'my/source'`]);
await providence(findExportsQueryConfig, _providenceCfg); 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(`stores rootFileMap of an exported Identifier`, async () => {
it.skip(`stores rootFileMap of an exported Identifier`, async () => {
mockProject({ mockProject({
'./src/reexport.js': ` './src/reexport.js': `
// a direct default import // a direct default import
import RefDefault from 'exporting-ref-project'; import RefDefault from 'exporting-ref-project';
export RefDefault; export default RefDefault;
`, `,
'./index.js': ` './index.js': `
export { ExtendRefDefault } from './src/reexport.js'; import ExtendRefDefault from './src/reexport.js';
export default ExtendRefDefault;
`, `,
}); });
await providence(findExportsQueryConfig, _providenceCfg); await providence(findExportsQueryConfig, _providenceCfg);
@ -210,7 +241,7 @@ describe('Analyzer "find-exports"', () => {
expect(firstEntry.result[0].rootFileMap).to.eql([ expect(firstEntry.result[0].rootFileMap).to.eql([
{ {
currentFileSpecifier: 'ExtendRefDefault', currentFileSpecifier: '[default]',
rootFile: { rootFile: {
file: 'exporting-ref-project', file: 'exporting-ref-project',
specifier: '[default]', 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', () => { describe('Export variable types', () => {