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

217 lines
7.6 KiB
JavaScript

import path from 'path';
import { trackDownIdentifier } from './track-down-identifier.js';
import { swcTraverse, getPathFromNode } from './swc-traverse.js';
import { AstService } from '../core/AstService.js';
import { toPosixPath } from './to-posix-path.js';
import { fsAdapter } from './fs-adapter.js';
/**
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../../types/index.js').SwcBinding} SwcBinding
* @typedef {import('../../../types/index.js').SwcPath} SwcPath
* @typedef {import('@swc/core').Node} SwcNode
*/
/**
* @param {{rootPath:PathFromSystemRoot; localPath:PathRelativeFromProjectRoot}} opts
* @returns {PathRelativeFromProjectRoot}
*/
export 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 /** @type {PathRelativeFromProjectRoot} */ (
toPosixPath(path.resolve(rootPath, localPath))
);
}
/**
* Assume we had:
* ```js
* const x = 88;
* const y = x;
* export const myIdentifier = y;
* ```
* - We started in getSourceCodeFragmentOfDeclaration (looking for 'myIdentifier'), which found VariableDeclarator of export myIdentifier
* - getReferencedDeclaration is called with { referencedIdentifierName: 'y', globalScopeBindings: {x: SwcBinding; y: SwcBinding} }
* - 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:{[key:string]:SwcBinding}; }} opts
* @returns {SwcPath|null}
*/
export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) {
// We go from referencedIdentifierName 'y' to binding (VariableDeclarator path) 'y';
const refDeclaratorBinding = globalScopeBindings[referencedIdentifierName];
// We provided a referencedIdentifierName that is not in the globalScopeBindings
if (!refDeclaratorBinding) {
return null;
}
if (['ImportSpecifier', 'ImportDefaultSpecifier'].includes(refDeclaratorBinding.path.node.type)) {
return refDeclaratorBinding.path;
}
if (refDeclaratorBinding.identifier.init.type === 'Identifier') {
return getReferencedDeclaration({
referencedIdentifierName: refDeclaratorBinding.identifier.init.value,
globalScopeBindings,
});
}
return refDeclaratorBinding.path.get('init');
}
/**
* @example
* ```js
* // ------ input file --------
* const x = 88;
* const y = x;
* export const myIdentifier = y;
* // --------------------------
*
* await getSourceCodeFragmentOfDeclaration(code) // finds "88"
* ```
*
* @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts
* @returns {Promise<{ sourceNodePath: SwcPath; sourceFragment: string|null; externalImportSource: string|null; }>}
*/
export async function getSourceCodeFragmentOfDeclaration({
exportedIdentifier,
projectRootPath,
filePath,
}) {
const code = fsAdapter.fs.readFileSync(filePath, 'utf8');
// compensate for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812
const offset = AstService._getSwcOffset();
// TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst
const swcAst = AstService._getSwcAst(code);
/** @type {SwcPath} */
let finalNodePath;
swcTraverse(
swcAst,
{
Module(astPath) {
astPath.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 = getPathFromNode(astPath.node.body?.[0])?.scope.bindings;
if (exportedIdentifier === '[default]') {
const defaultExportPath = /** @type {SwcPath} */ (
getPathFromNode(
astPath.node.body.find((/** @type {{ type: string; }} */ child) =>
['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type),
),
)
);
const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier';
if (!isReferenced) {
finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression');
} else {
finalNodePath = /** @type {SwcPath} */ (
getReferencedDeclaration({
referencedIdentifierName: defaultExportPath.node.expression.value,
// @ts-expect-error
globalScopeBindings,
})
);
}
} else {
const variableDeclaratorPath = astPath.scope.bindings[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.value
: varDeclNode.id?.value || varDeclNode.imported?.value || varDeclNode.orig?.value;
if (!isReferenced) {
// it must be an exported declaration
finalNodePath = contentPath;
} else {
finalNodePath = /** @type {SwcPath} */ (
getReferencedDeclaration({
referencedIdentifierName: name,
// @ts-expect-error
globalScopeBindings,
})
);
}
}
},
},
{ needsAdvancedPaths: true },
);
// @ts-expect-error
if (finalNodePath.type === 'ImportSpecifier') {
// @ts-expect-error
const importDeclNode = finalNodePath.parentPath.node;
const source = importDeclNode.source.value;
// @ts-expect-error
const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value;
const currentFilePath = filePath;
const rootFile = await trackDownIdentifier(
source,
identifierName,
currentFilePath,
projectRootPath,
);
const filePathOrSrc = getFilePathOrExternalSource({
rootPath: projectRootPath,
localPath: /** @type {PathRelativeFromProjectRoot} */ (rootFile.file),
});
// TODO: allow resolving external project file paths
if (!filePathOrSrc.startsWith('/')) {
// So we have external project; smth like '@lion/input/x.js'
return {
// @ts-expect-error
sourceNodePath: finalNodePath,
sourceFragment: null,
externalImportSource: filePathOrSrc,
};
}
return getSourceCodeFragmentOfDeclaration({
filePath: /** @type {PathFromSystemRoot} */ (filePathOrSrc),
exportedIdentifier: rootFile.specifier,
projectRootPath,
});
}
return {
// @ts-expect-error
sourceNodePath: finalNodePath,
sourceFragment: code.slice(
// @ts-expect-error
finalNodePath.node.span.start - 1 - offset,
// @ts-expect-error
finalNodePath.node.span.end - 1 - offset,
),
// sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value,
externalImportSource: null,
};
}