feat(providence-analytics): add util "getSourceCodeFragmentOfDeclaration"
This commit is contained in:
parent
9593c45695
commit
a849f09fa9
5 changed files with 252 additions and 7 deletions
5
.changeset/brown-pets-accept.md
Normal file
5
.changeset/brown-pets-accept.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'providence-analytics': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add posibility to provide a 'custom defined project' (array of filePaths) to Analyzer
|
||||||
5
.changeset/nine-fishes-fly.md
Normal file
5
.changeset/nine-fishes-fly.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'providence-analytics': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add util "getSourceCodeFragmentOfDeclaration"
|
||||||
|
|
@ -13,10 +13,13 @@ const { getFilePathRelativeFromRoot } = require('../../utils/get-file-path-relat
|
||||||
* @typedef {import('../../types/core').AnalyzerName} AnalyzerName
|
* @typedef {import('../../types/core').AnalyzerName} AnalyzerName
|
||||||
* @typedef {import('../../types/core').PathFromSystemRoot} PathFromSystemRoot
|
* @typedef {import('../../types/core').PathFromSystemRoot} PathFromSystemRoot
|
||||||
* @typedef {import('../../types/core').QueryOutput} QueryOutput
|
* @typedef {import('../../types/core').QueryOutput} QueryOutput
|
||||||
|
* @typedef {import('../../types/core').QueryOutputEntry} QueryOutputEntry
|
||||||
* @typedef {import('../../types/core').ProjectInputData} ProjectInputData
|
* @typedef {import('../../types/core').ProjectInputData} ProjectInputData
|
||||||
* @typedef {import('../../types/core').ProjectInputDataWithMeta} ProjectInputDataWithMeta
|
* @typedef {import('../../types/core').ProjectInputDataWithMeta} ProjectInputDataWithMeta
|
||||||
* @typedef {import('../../types/core').AnalyzerQueryResult} AnalyzerQueryResult
|
* @typedef {import('../../types/core').AnalyzerQueryResult} AnalyzerQueryResult
|
||||||
* @typedef {import('../../types/core').MatchAnalyzerConfig} MatchAnalyzerConfig
|
* @typedef {import('../../types/core').MatchAnalyzerConfig} MatchAnalyzerConfig
|
||||||
|
*
|
||||||
|
* @typedef {(ast: object, { relativePath: PathRelative }) => {result: QueryOutputEntry}} TraversEntryFn
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -171,10 +174,10 @@ class Analyzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In a MatchAnalyzer, two Analyzers (a reference and targer) are run.
|
* In a MatchAnalyzer, two Analyzers (a reference and target) are run.
|
||||||
* For instance, in a MatchImportsAnalyzer, a FindExportsAnalyzer and FinImportsAnalyzer are run.
|
* For instance: a FindExportsAnalyzer and FindImportsAnalyzer are run.
|
||||||
* Their results can be provided as config params.
|
* Their results can be provided as config params.
|
||||||
* If they are stored in json format, 'unwind' them to be compatible for analysis...
|
* When they were stored in json format in the filesystem, 'unwind' them to be compatible for analysis...
|
||||||
* @param {MatchAnalyzerConfig} cfg
|
* @param {MatchAnalyzerConfig} cfg
|
||||||
*/
|
*/
|
||||||
static __unwindProvidedResults(cfg) {
|
static __unwindProvidedResults(cfg) {
|
||||||
|
|
@ -284,16 +287,43 @@ class Analyzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} traverseEntry
|
* @param {function|{traverseEntryFn: function: filePaths:string[]; projectPath: string}} traverseEntryOrConfig
|
||||||
*/
|
*/
|
||||||
async _traverse(traverseEntry) {
|
async _traverse(traverseEntryOrConfig) {
|
||||||
LogService.debug(`Analyzer "${this.name}": started _traverse method`);
|
LogService.debug(`Analyzer "${this.name}": started _traverse method`);
|
||||||
|
|
||||||
|
let traverseEntryFn;
|
||||||
|
let finalTargetData;
|
||||||
|
|
||||||
|
if (typeof traverseEntryOrConfig === 'function') {
|
||||||
|
traverseEntryFn = traverseEntryOrConfig;
|
||||||
|
finalTargetData = this.targetData;
|
||||||
|
} else {
|
||||||
|
traverseEntryFn = traverseEntryOrConfig.traverseEntryFn;
|
||||||
|
if (!traverseEntryOrConfig.filePaths) {
|
||||||
|
finalTargetData = this.targetData;
|
||||||
|
} else {
|
||||||
|
const { projectPath, projectName } = traverseEntryOrConfig;
|
||||||
|
if (!projectPath) {
|
||||||
|
LogService.error(`[Analyzer._traverse]: you must provide a projectPath`);
|
||||||
|
}
|
||||||
|
finalTargetData = InputDataService.createDataObject([
|
||||||
|
{
|
||||||
|
project: {
|
||||||
|
name: projectName || '[n/a]',
|
||||||
|
path: projectPath,
|
||||||
|
},
|
||||||
|
entries: traverseEntryOrConfig.filePaths,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create ASTs for our inputData
|
* Create ASTs for our inputData
|
||||||
*/
|
*/
|
||||||
const astDataProjects = await QueryService.addAstToProjectsData(this.targetData, 'babel');
|
const astDataProjects = await QueryService.addAstToProjectsData(finalTargetData, 'babel');
|
||||||
return analyzePerAstEntry(astDataProjects[0], traverseEntry);
|
return analyzePerAstEntry(astDataProjects[0], traverseEntryFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(customConfig = {}) {
|
async execute(customConfig = {}) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const babelTraversePkg = require('@babel/traverse');
|
||||||
|
const { AstService } = require('../services/AstService.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assume we had:
|
||||||
|
* ```js
|
||||||
|
* const x = 88;
|
||||||
|
* const y = x;
|
||||||
|
* export const myIdentifier = y;
|
||||||
|
* ```
|
||||||
|
* - We started in getSourceCodeFragmentOfDeclaration (looing 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)
|
||||||
|
* - is it a non ref declaration? Return the path of the node
|
||||||
|
* @param {{ referencedIdentifierName:string, globalScopeBindings:BabelBinding; }} opts
|
||||||
|
* @returns {BabelNodePath}
|
||||||
|
*/
|
||||||
|
function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) {
|
||||||
|
const [, refDeclaratorBinding] = Object.entries(globalScopeBindings).find(
|
||||||
|
([key]) => key === referencedIdentifierName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (refDeclaratorBinding.path.node.init.type === 'Identifier') {
|
||||||
|
return getReferencedDeclaration({
|
||||||
|
referencedIdentifierName: refDeclaratorBinding.path.node.init.name,
|
||||||
|
globalScopeBindings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return refDeclaratorBinding.path.get('init');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {{ filePath: string; exportedIdentifier: string; }} opts
|
||||||
|
*/
|
||||||
|
async function getSourceCodeFragmentOfDeclaration({ filePath, exportedIdentifier }) {
|
||||||
|
const code = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const ast = AstService.getAst(code, 'babel');
|
||||||
|
|
||||||
|
let finalNodePath;
|
||||||
|
|
||||||
|
babelTraversePkg.default(ast, {
|
||||||
|
Program(babelPath) {
|
||||||
|
babelPath.stop();
|
||||||
|
|
||||||
|
// Situations
|
||||||
|
// - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' )
|
||||||
|
// - declared right away (for instance a class)
|
||||||
|
// - referenced (possibly recursively) by other declaration
|
||||||
|
// - Identifier is part of a named export
|
||||||
|
// - declared right away
|
||||||
|
// - referenced (possibly recursively) by other declaration
|
||||||
|
|
||||||
|
const globalScopeBindings = babelPath.get('body')[0].scope.bindings;
|
||||||
|
|
||||||
|
if (exportedIdentifier === '[default]') {
|
||||||
|
const defaultExportPath = babelPath
|
||||||
|
.get('body')
|
||||||
|
.find(child => child.node.type === 'ExportDefaultDeclaration');
|
||||||
|
const isReferenced = defaultExportPath.node.declaration?.type === 'Identifier';
|
||||||
|
|
||||||
|
if (!isReferenced) {
|
||||||
|
finalNodePath = defaultExportPath.get('declaration');
|
||||||
|
} else {
|
||||||
|
finalNodePath = getReferencedDeclaration({
|
||||||
|
referencedIdentifierName: defaultExportPath.node.declaration.name,
|
||||||
|
globalScopeBindings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const variableDeclaratorPath = babelPath.scope.getBinding(exportedIdentifier).path;
|
||||||
|
const isReferenced = variableDeclaratorPath.node.init?.type === 'Identifier';
|
||||||
|
|
||||||
|
if (!isReferenced) {
|
||||||
|
// it must be an exported declaration
|
||||||
|
finalNodePath = variableDeclaratorPath.get('init');
|
||||||
|
} else {
|
||||||
|
finalNodePath = getReferencedDeclaration({
|
||||||
|
referencedIdentifierName: variableDeclaratorPath.node.init.name,
|
||||||
|
globalScopeBindings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceNodePath: finalNodePath,
|
||||||
|
sourceFragment: code.slice(finalNodePath.node.start, finalNodePath.node.end),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getSourceCodeFragmentOfDeclaration,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const { mock } = require('../../../test-helpers/mock-project-helpers.js');
|
||||||
|
const { getSourceCodeFragmentOfDeclaration } = require('../../../src/program/utils/index.js');
|
||||||
|
|
||||||
|
describe('getSourceCodeFragmentOfDeclaration', () => {
|
||||||
|
describe('Named specifiers', () => {
|
||||||
|
it('finds source code for directly declared specifiers', async () => {
|
||||||
|
const fakeFs = {
|
||||||
|
'/my/proj/exports/file.js': 'export const x = 0;',
|
||||||
|
};
|
||||||
|
mock(fakeFs);
|
||||||
|
|
||||||
|
const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({
|
||||||
|
filePath: '/my/proj/exports/file.js',
|
||||||
|
exportedIdentifier: 'x',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sourceFragment).to.equal('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds source code for referenced specifiers', async () => {
|
||||||
|
const fakeFs = {
|
||||||
|
'/my/proj/exports/file.js': `
|
||||||
|
const y = 0;
|
||||||
|
export const x = y;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
mock(fakeFs);
|
||||||
|
|
||||||
|
const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({
|
||||||
|
filePath: '/my/proj/exports/file.js',
|
||||||
|
exportedIdentifier: 'x',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sourceFragment).to.equal('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds source code for rereferenced specifiers', async () => {
|
||||||
|
const fakeFs = {
|
||||||
|
'/my/proj/exports/file.js': `
|
||||||
|
const x = 88;
|
||||||
|
const y = x;
|
||||||
|
export const myIdentifier = y;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
mock(fakeFs);
|
||||||
|
|
||||||
|
const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({
|
||||||
|
filePath: '/my/proj/exports/file.js',
|
||||||
|
exportedIdentifier: 'myIdentifier',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sourceFragment).to.equal('88');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('[default] specifiers', () => {
|
||||||
|
it('finds source code for directly declared specifiers', async () => {
|
||||||
|
const fakeFs = {
|
||||||
|
'/my/proj/exports/file.js': 'export default class {};',
|
||||||
|
};
|
||||||
|
mock(fakeFs);
|
||||||
|
|
||||||
|
const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({
|
||||||
|
filePath: '/my/proj/exports/file.js',
|
||||||
|
exportedIdentifier: '[default]',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sourceFragment).to.equal('class {}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds source code for referenced specifiers', async () => {
|
||||||
|
const fakeFs = {
|
||||||
|
'/my/proj/exports/file.js': `
|
||||||
|
const myIdentifier = 0;
|
||||||
|
export default myIdentifier;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
mock(fakeFs);
|
||||||
|
|
||||||
|
const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({
|
||||||
|
filePath: '/my/proj/exports/file.js',
|
||||||
|
exportedIdentifier: '[default]',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sourceFragment).to.equal('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds source code for rereferenced specifiers', async () => {
|
||||||
|
const fakeFs = {
|
||||||
|
'/my/proj/exports/file.js': `
|
||||||
|
const x = 88;
|
||||||
|
const myIdentifier = x;
|
||||||
|
export default myIdentifier;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
mock(fakeFs);
|
||||||
|
|
||||||
|
const { sourceFragment } = await getSourceCodeFragmentOfDeclaration({
|
||||||
|
filePath: '/my/proj/exports/file.js',
|
||||||
|
exportedIdentifier: '[default]',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sourceFragment).to.equal('88');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue