250 lines
8.6 KiB
JavaScript
250 lines
8.6 KiB
JavaScript
import path from 'path';
|
|
|
|
import { oxcTraverse, getPathFromNode, nameOf } from './oxc-traverse.js';
|
|
import { trackDownIdentifier } from './track-down-identifier.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').AnalyzerAst} AnalyzerAst
|
|
* @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))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks whether we are a Declaration (like class X {}) or Declarator (like const x = 88)
|
|
* @param {SwcNode} node
|
|
* @returns {boolean}
|
|
*/
|
|
function containsIdentifier(node) {
|
|
// @ts-expect-error
|
|
return node.id || node.identifier;
|
|
}
|
|
|
|
/**
|
|
* 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 identifierBinding = /** @type {SwcBinding} */ (
|
|
globalScopeBindings[referencedIdentifierName]
|
|
);
|
|
|
|
// We provided a referencedIdentifierName that is not in the globalScopeBindings
|
|
if (!identifierBinding) return null;
|
|
|
|
const { type } = identifierBinding.path.node;
|
|
const isNonRefDeclaration = type.endsWith('Declaration');
|
|
if (isNonRefDeclaration && !containsIdentifier(identifierBinding.path.node)) {
|
|
throw new Error('Make sure entries added to globalScopeBindings contains an identifier');
|
|
}
|
|
|
|
const isImportingSpecifier = ['ImportSpecifier', 'ImportDefaultSpecifier'].includes(type);
|
|
if (isImportingSpecifier || isNonRefDeclaration) {
|
|
return identifierBinding.path;
|
|
}
|
|
|
|
const isRefDeclarator = identifierBinding.path.node.init.type === 'Identifier';
|
|
if (isRefDeclarator) {
|
|
return getReferencedDeclaration({
|
|
referencedIdentifierName: nameOf(identifierBinding.path.node.init),
|
|
globalScopeBindings,
|
|
});
|
|
}
|
|
|
|
return /** @type {SwcPath} */ (identifierBinding.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; parser: AnalyzerAst }} opts
|
|
* @returns {Promise<{ sourceNodePath: SwcPath; sourceFragment: string|null; externalImportSource: string|null; }>}
|
|
*/
|
|
export async function getSourceCodeFragmentOfDeclaration({
|
|
exportedIdentifier,
|
|
projectRootPath,
|
|
parser = 'oxc',
|
|
filePath,
|
|
}) {
|
|
const code = await fsAdapter.fs.promises.readFile(filePath, 'utf8');
|
|
|
|
// compensate for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812
|
|
const offset = parser === 'swc' ? await AstService._getSwcOffset() : -1;
|
|
const ast = await AstService.getAst(code, parser);
|
|
|
|
/** @type {SwcPath} */
|
|
let finalNodePath;
|
|
|
|
const moduleOrProgramHandler = 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.declaration?.type || defaultExportPath?.node.expression?.type) ===
|
|
'Identifier';
|
|
|
|
if (!isReferenced) {
|
|
finalNodePath =
|
|
defaultExportPath.get('declaration') ||
|
|
defaultExportPath.get('decl') ||
|
|
defaultExportPath.get('expression');
|
|
} else {
|
|
finalNodePath = /** @type {SwcPath} */ (
|
|
getReferencedDeclaration({
|
|
referencedIdentifierName: nameOf(
|
|
defaultExportPath.node.declaration || defaultExportPath.node.expression,
|
|
),
|
|
// @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
|
|
? nameOf(varDeclNode.init)
|
|
: nameOf(varDeclNode.id) || nameOf(varDeclNode.imported) || nameOf(varDeclNode.orig);
|
|
|
|
if (!isReferenced) {
|
|
// it must be an exported declaration
|
|
finalNodePath = contentPath;
|
|
} else {
|
|
finalNodePath = /** @type {SwcPath} */ (
|
|
getReferencedDeclaration({
|
|
referencedIdentifierName: name,
|
|
// @ts-expect-error
|
|
globalScopeBindings,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
oxcTraverse(
|
|
ast,
|
|
{
|
|
Module: moduleOrProgramHandler,
|
|
Program: moduleOrProgramHandler,
|
|
},
|
|
{ needsAdvancedPaths: true },
|
|
);
|
|
|
|
// @ts-expect-error
|
|
if (finalNodePath.type === 'ImportSpecifier') {
|
|
// @ts-expect-error
|
|
const importDeclNode = finalNodePath.parentPath.node;
|
|
const source = nameOf(importDeclNode.source);
|
|
// @ts-expect-error
|
|
const identifierName = nameOf(finalNodePath.node.imported) || nameOf(finalNodePath.node.local);
|
|
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,
|
|
parser,
|
|
});
|
|
}
|
|
|
|
const startOf = node => node.start || node.span.start;
|
|
const endOf = node => node.end || node.span.end;
|
|
return {
|
|
// @ts-expect-error
|
|
sourceNodePath: finalNodePath,
|
|
sourceFragment: code.slice(
|
|
// @ts-expect-error
|
|
startOf(finalNodePath.node) - 1 - offset,
|
|
// @ts-expect-error
|
|
endOf(finalNodePath.node) - 1 - offset,
|
|
),
|
|
// sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value,
|
|
externalImportSource: null,
|
|
};
|
|
}
|