feat(providence-analytics): add util "getSourceCodeFragmentOfDeclaration"

This commit is contained in:
Thijs Louisse 2022-09-14 16:45:24 +02:00 committed by Thijs Louisse
parent 9593c45695
commit a849f09fa9
5 changed files with 252 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
'providence-analytics': patch
---
add posibility to provide a 'custom defined project' (array of filePaths) to Analyzer

View file

@ -0,0 +1,5 @@
---
'providence-analytics': patch
---
add util "getSourceCodeFragmentOfDeclaration"

View file

@ -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 = {}) {

View file

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

View file

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