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').PathFromSystemRoot} PathFromSystemRoot
|
||||
* @typedef {import('../../types/core').QueryOutput} QueryOutput
|
||||
* @typedef {import('../../types/core').QueryOutputEntry} QueryOutputEntry
|
||||
* @typedef {import('../../types/core').ProjectInputData} ProjectInputData
|
||||
* @typedef {import('../../types/core').ProjectInputDataWithMeta} ProjectInputDataWithMeta
|
||||
* @typedef {import('../../types/core').AnalyzerQueryResult} AnalyzerQueryResult
|
||||
* @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.
|
||||
* For instance, in a MatchImportsAnalyzer, a FindExportsAnalyzer and FinImportsAnalyzer are run.
|
||||
* In a MatchAnalyzer, two Analyzers (a reference and target) are run.
|
||||
* For instance: a FindExportsAnalyzer and FindImportsAnalyzer are run.
|
||||
* 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
|
||||
*/
|
||||
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`);
|
||||
|
||||
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
|
||||
*/
|
||||
const astDataProjects = await QueryService.addAstToProjectsData(this.targetData, 'babel');
|
||||
return analyzePerAstEntry(astDataProjects[0], traverseEntry);
|
||||
const astDataProjects = await QueryService.addAstToProjectsData(finalTargetData, 'babel');
|
||||
return analyzePerAstEntry(astDataProjects[0], traverseEntryFn);
|
||||
}
|
||||
|
||||
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