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

251 lines (223 loc) 8.77 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 */ /** * @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, }; }