feat(providence-analytics): improvements find-exports, trackdown-identifier, get-source-code-fragment-of-declaration
This commit is contained in:
parent
dd3458af70
commit
f7fc7df349
7 changed files with 137 additions and 9 deletions
5
.changeset/great-jokes-travel.md
Normal file
5
.changeset/great-jokes-travel.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'providence-analytics': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
improvements find-exports, trackdown-identifier, get-source-code-fragment-of-declaration
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,7 +219,10 @@ 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);
|
||||||
path.stop();
|
|
||||||
|
if (exportMatch) {
|
||||||
|
path.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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', () => {});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue