lion/packages-node/providence-analytics/src/program/utils/get-source-code-fragment-of-declaration.js

162 lines
5.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const babelTraversePkg = require('@babel/traverse');
const { AstService } = require('../services/AstService.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:
* ```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.type === 'ImportSpecifier') {
return refDeclaratorBinding.path;
}
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,
projectRootPath,
}) {
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 varDeclNode = variableDeclaratorPath.node;
const isReferenced = varDeclNode.init?.type === 'Identifier';
const contentPath = varDeclNode.init
? variableDeclaratorPath.get('init')
: variableDeclaratorPath;
const name = varDeclNode.init
? varDeclNode.init.name
: varDeclNode.id?.name || varDeclNode.imported.name;
if (!isReferenced) {
// it must be an exported declaration
finalNodePath = contentPath;
} else {
finalNodePath = getReferencedDeclaration({
referencedIdentifierName: name,
globalScopeBindings,
});
}
}
},
});
if (finalNodePath.type === 'ImportSpecifier') {
const importDeclNode = finalNodePath.parentPath.node;
const source = importDeclNode.source.value;
const identifierName = finalNodePath.node.imported.name;
const currentFilePath = filePath;
const rootFile = await trackDownIdentifier(
source,
identifierName,
currentFilePath,
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({
filePath: filePathOrSrc,
exportedIdentifier: rootFile.specifier,
projectRootPath,
});
}
return {
sourceNodePath: finalNodePath,
sourceFragment: code.slice(finalNodePath.node?.start, finalNodePath.node?.end),
externalImportSource: null,
};
}
module.exports = {
getSourceCodeFragmentOfDeclaration,
getFilePathOrExternalSource,
};