feat(providence-analytics): improvements find-exports, trackdown-identifier, get-source-code-fragment-of-declaration

This commit is contained in:
Thijs Louisse 2022-09-23 13:34:54 +02:00 committed by Thomas Allmer
parent dd3458af70
commit f7fc7df349
7 changed files with 137 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'providence-analytics': patch
---
improvements find-exports, trackdown-identifier, get-source-code-fragment-of-declaration

View file

@ -46,16 +46,19 @@ async function trackdownRoot(transformedEntry, relativePath, projectPath) {
if (specObj.localMap) { if (specObj.localMap) {
localMapMatch = specObj.localMap.find(m => m.exported === specifier); localMapMatch = specObj.localMap.find(m => m.exported === specifier);
} }
// TODO: find out if possible to use trackDownIdentifierFromScope // TODO: find out if possible to use trackDownIdentifierFromScope
if (specObj.source) { if (specObj.source) {
// TODO: see if still needed: && (localMapMatch || specifier === '[default]') // TODO: see if still needed: && (localMapMatch || specifier === '[default]')
const importedIdentifier = localMapMatch?.local || specifier; const importedIdentifier = localMapMatch?.local || specifier;
rootFile = await trackDownIdentifier( rootFile = await trackDownIdentifier(
specObj.source, specObj.source,
importedIdentifier, importedIdentifier,
fullCurrentFilePath, fullCurrentFilePath,
projectPath, projectPath,
); );
/** @type {RootFileMapEntry} */ /** @type {RootFileMapEntry} */
const entry = { const entry = {
currentFileSpecifier: specifier, currentFileSpecifier: specifier,
@ -122,7 +125,8 @@ function getLocalNameSpecifiers(node) {
.map(s => { .map(s => {
if (s.exported && s.local && s.exported.name !== s.local.name) { if (s.exported && s.local && s.exported.name !== s.local.name) {
return { 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, exported: s.exported.name,
}; };
} }

View file

@ -138,6 +138,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
let reexportMatch = false; // named specifier declaration let reexportMatch = false; // named specifier declaration
let pendingTrackDownPromise; let pendingTrackDownPromise;
let exportMatch;
traverse(ast, { traverse(ast, {
ExportDefaultDeclaration(path) { ExportDefaultDeclaration(path) {
@ -172,7 +173,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
} }
// 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) {
const exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName);
if (exportMatch) { if (exportMatch) {
const localName = exportMatch.local.name; const localName = exportMatch.local.name;
@ -218,8 +219,11 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
// in current file... // in current file...
rootSpecifier = identifierName; rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath); rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
if (exportMatch) {
path.stop(); path.stop();
} }
}
}, },
}, },
}); });

View file

@ -3,6 +3,16 @@ const path = require('path');
const babelTraversePkg = require('@babel/traverse'); const babelTraversePkg = require('@babel/traverse');
const { AstService } = require('../services/AstService.js'); const { AstService } = require('../services/AstService.js');
const { trackDownIdentifier } = require('../analyzers/helpers/track-down-identifier.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: * Assume we had:
@ -82,13 +92,15 @@ async function getSourceCodeFragmentOfDeclaration({
} }
} else { } else {
const variableDeclaratorPath = babelPath.scope.getBinding(exportedIdentifier).path; const variableDeclaratorPath = babelPath.scope.getBinding(exportedIdentifier).path;
const isReferenced = variableDeclaratorPath.node.init?.type === 'Identifier'; const varDeclNode = variableDeclaratorPath.node;
const contentPath = variableDeclaratorPath.node.init const isReferenced = varDeclNode.init?.type === 'Identifier';
const contentPath = varDeclNode.init
? variableDeclaratorPath.get('init') ? variableDeclaratorPath.get('init')
: variableDeclaratorPath; : variableDeclaratorPath;
const name = variableDeclaratorPath.node.init
? variableDeclaratorPath.node.init.name const name = varDeclNode.init
: variableDeclaratorPath.node.id.name; ? varDeclNode.init.name
: varDeclNode.id?.name || varDeclNode.imported.name;
if (!isReferenced) { if (!isReferenced) {
// it must be an exported declaration // it must be an exported declaration
@ -115,19 +127,36 @@ async function getSourceCodeFragmentOfDeclaration({
currentFilePath, currentFilePath,
projectRootPath, 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({ return getSourceCodeFragmentOfDeclaration({
filePath: path.resolve(projectRootPath, rootFile.file), filePath: filePathOrSrc,
exportedIdentifier: rootFile.specifier, exportedIdentifier: rootFile.specifier,
projectRootPath,
}); });
} }
return { return {
sourceNodePath: finalNodePath, sourceNodePath: finalNodePath,
sourceFragment: code.slice(finalNodePath.node?.start, finalNodePath.node?.end), sourceFragment: code.slice(finalNodePath.node?.start, finalNodePath.node?.end),
externalImportSource: null,
}; };
} }
module.exports = { module.exports = {
getSourceCodeFragmentOfDeclaration, getSourceCodeFragmentOfDeclaration,
getFilePathOrExternalSource,
}; };

View file

@ -1,9 +1,11 @@
const { const {
getSourceCodeFragmentOfDeclaration, getSourceCodeFragmentOfDeclaration,
getFilePathOrExternalSource,
} = require('./get-source-code-fragment-of-declaration.js'); } = require('./get-source-code-fragment-of-declaration.js');
// TODO: move trackdownIdentifier to utils as well // TODO: move trackdownIdentifier to utils as well
module.exports = { module.exports = {
getSourceCodeFragmentOfDeclaration, getSourceCodeFragmentOfDeclaration,
getFilePathOrExternalSource,
}; };

View file

@ -66,6 +66,45 @@ describe('Analyzer "find-exports"', () => {
expect(firstEntry.result[0].source).to.equal(undefined); 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 () => { 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);

View file

@ -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 // TODO: improve perf
describe.skip('Caching', () => {}); describe.skip('Caching', () => {});
}); });