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/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",

View file

@ -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 } });
},
});

View file

@ -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,

View file

@ -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;
}

View file

@ -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,
};

View file

@ -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', () => {