UNPKG

providence-analytics

Version:

Providence is the 'All Seeing Eye' that measures effectivity and popularity of software. Release management will become highly efficient due to an accurate impact analysis of (breaking) changes

355 lines (315 loc) 13.3 kB
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 * @typedef {import("oxc-parser").ParseResult} OxcParseResult * @typedef {import('@swc/core').Module} SwcAstModule * @typedef {OxcParseResult|SwcAstModule|SwcNode} ParsedAst */ /** * @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 {{ code?: string; ast?: ParsedAst; filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot; parser?: AnalyzerAst }} opts * @returns {Promise<{ sourceNodeType: string; sourceFragment: string|null; externalImportSource: string|null; }>} */ export async function getSourceCodeFragmentOfDeclaration({ exportedIdentifier, projectRootPath, parser = 'oxc', filePath, code, ast, }) { if (!code) { // eslint-disable-next-line no-param-reassign code = await fsAdapter.fs.promises.readFile(filePath, 'utf8'); } if (!ast) { // eslint-disable-next-line no-param-reassign ast = await AstService.getAst(code, parser); } // Type guard: ensure ast is defined if (!ast) throw new Error('Failed to generate AST'); // compensate for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812 const offset = parser === 'swc' ? await AstService._getSwcOffset() : -1; /** @type {SwcPath} */ let finalNodePath; const moduleOrProgramHandler = ( /** @type {{ stop: () => void; node: { body: any[]; }; scope: { bindings: { [x: string]: { path: any; }; }; }; }} */ 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 = /** @type {SwcPath} */ ( 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 globalBindingForIdentifier = astPath.scope.bindings[exportedIdentifier]; // If the identifier is not in root scope, search nested scopes if (!globalBindingForIdentifier) { // Traverse to find the binding in any nested scope oxcTraverse( ast, { VariableDeclarator(/** @type {SwcPath} */ varPath) { if (nameOf(varPath.node.id) === exportedIdentifier) { const varDeclNode = varPath.node; const initType = varDeclNode.init?.type; if (!varDeclNode.init) { // No init value finalNodePath = /** @type {SwcPath} */ (varPath.get('init') || varPath); } else if (initType === 'Literal') { // Simple literal value - return as-is (covers both string and numeric literals) finalNodePath = /** @type {SwcPath} */ (varPath.get('init') || varPath); } else if (initType === 'Identifier') { // References another identifier - try to resolve it const referencedName = nameOf(varDeclNode.init); // Get the global scope to look up the binding if (globalScopeBindings) { const resolvedPath = /** @type {SwcPath} */ ( getReferencedDeclaration({ referencedIdentifierName: referencedName, globalScopeBindings, }) ); // Only follow the reference if it resolves to a Literal or ImportSpecifier // Otherwise return the identifier itself (don't resolve to expressions) const resolvedType = resolvedPath?.node?.type; if (resolvedType === 'Literal' || resolvedType === 'ImportSpecifier') { finalNodePath = resolvedPath; } else { // Return the identifier itself, not what it references finalNodePath = /** @type {SwcPath} */ (varPath.get('init') || varPath); } } else { finalNodePath = /** @type {SwcPath} */ (varPath.get('init') || varPath); } } else { // Expression or other type - return as-is (source code will be extracted) finalNodePath = /** @type {SwcPath} */ (varPath.get('init') || varPath); } varPath.stop(); } }, }, { needsAdvancedPaths: true }, ); return; } const variableDeclaratorPath = globalBindingForIdentifier.path; const varDeclNode = variableDeclaratorPath.node; const initType = varDeclNode.init?.type; const contentPath = /** @type {SwcPath} */ ( varDeclNode.init ? variableDeclaratorPath.get('init') : variableDeclaratorPath ); const name = varDeclNode.init ? nameOf(varDeclNode.init) : nameOf(varDeclNode.id) || nameOf(varDeclNode.imported) || nameOf(varDeclNode.orig); if (!varDeclNode.init) { // No init value finalNodePath = contentPath; } else if (initType === 'Literal') { // Simple literal value - return as-is (covers both string and numeric literals) finalNodePath = contentPath; } else if (initType === 'Identifier') { // References another identifier - resolve it const resolvedPath = /** @type {SwcPath} */ ( getReferencedDeclaration({ referencedIdentifierName: name, // @ts-expect-error globalScopeBindings, }) ); // Only follow the reference if it resolves to a Literal or ImportSpecifier // Otherwise return the identifier itself (don't resolve to expressions) const resolvedType = resolvedPath?.node?.type; if (resolvedType === 'Literal' || resolvedType === 'ImportSpecifier') { finalNodePath = resolvedPath; } else { // Return the identifier itself, not what it references finalNodePath = contentPath; } } else { // Expression or other type - return as-is (source code will be extracted) finalNodePath = contentPath; } } }; 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 { sourceNodeType: 'ImportSpecifier', // External import, return the import type sourceFragment: null, externalImportSource: filePathOrSrc, }; } return getSourceCodeFragmentOfDeclaration({ filePath: /** @type {PathFromSystemRoot} */ (filePathOrSrc), exportedIdentifier: rootFile.specifier, projectRootPath, parser, }); } // Guard against finalNodePath not being found // @ts-expect-error - finalNodePath is assigned within oxcTraverse callback if (!finalNodePath) { return { sourceNodeType: 'Unknown', sourceFragment: null, externalImportSource: null, }; } const startOf = (/** @type {{ start: number; span: { start: number; }; }} */ node) => node.start || node.span.start; const endOf = (/** @type {{ end: number; span: { end: number; }; }} */ node) => node.end || node.span.end; const sourceFragment = code.slice( startOf(finalNodePath.node) - 1 - offset, endOf(finalNodePath.node) - 1 - offset, ); return { sourceNodeType: finalNodePath.node.type, sourceFragment, // sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value, externalImportSource: null, }; }