162 lines
5.3 KiB
JavaScript
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,
|
|
};
|