UNPKG

@jqassistant/ts-lce

Version:

Tool to extract language concepts from a TypeScript codebase and export them to a JSON file.

268 lines (267 loc) 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DependencyResolutionProcessor = void 0; const utils_1 = require("@typescript-eslint/utils"); const concept_1 = require("../concept"); const dependency_concept_1 = require("../concepts/dependency.concept"); const context_1 = require("../context"); const execution_condition_1 = require("../execution-condition"); const modulepath_utils_1 = require("../utils/modulepath.utils"); const processor_1 = require("../processor"); const processor_utils_1 = require("../utils/processor.utils"); const program_traverser_1 = require("../traversers/program.traverser"); const context_keys_1 = require("../context.keys"); /** * Manages FQN contexts, provides index for registering declarations and resolves FQN references. */ class DependencyResolutionProcessor extends processor_1.Processor { executionCondition = new execution_condition_1.ExecutionCondition([utils_1.AST_NODE_TYPES.Program], () => true); preChildrenProcessing({ localContexts, globalContext, node }) { localContexts.currentContexts.set(context_keys_1.CoreContextKeys.DECLARATION_INDEX, new Map()); const scopeIdentifier = modulepath_utils_1.ModulePathUtils.toFQN(globalContext.sourceFilePathAbsolute, globalContext.sourceFilePathRelative); localContexts.currentContexts.set(context_keys_1.CoreContextKeys.FQN_SCOPE, { globalIdentifier: scopeIdentifier.globalFqn, localIdentifier: scopeIdentifier.localFqn, internalScopeId: 0, }); localContexts.currentContexts.set(context_keys_1.CoreContextKeys.FQN_RESOLVER, []); localContexts.currentContexts.set(context_keys_1.CoreContextKeys.DEPENDENCY_GLOBAL_SOURCE_FQN, modulepath_utils_1.ModulePathUtils.toFQN(globalContext.sourceFilePathAbsolute).globalFqn); localContexts.currentContexts.set(context_keys_1.CoreContextKeys.DEPENDENCY_INDEX, []); // if a declaration is default-exported: register its name if (node.type === utils_1.AST_NODE_TYPES.Program) { for (const statement of node.body) { if (statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration && statement.declaration.type === utils_1.AST_NODE_TYPES.Identifier) { localContexts.currentContexts.set(context_keys_1.CoreContextKeys.DEFAULT_EXPORT_IDENTIFIER, statement.declaration.name); break; } } } } postChildrenProcessing({ localContexts }, childConcepts) { const [declIndex] = localContexts.getNextContext(context_keys_1.CoreContextKeys.DECLARATION_INDEX); const [resolutionList] = localContexts.getNextContext(context_keys_1.CoreContextKeys.FQN_RESOLVER); // resolve FQNs for (const [namespaces, identifier, concept] of resolutionList) { if (identifier.includes(".")) { // complex identifier: multiple tries const identifiers = identifier.split("."); let resultFound = false; // test full identifier names from bottom to top (e.g. "a.b.c" => "a.b.c", "a.b", "a") for (let i = identifiers.length; i > 0; i--) { const testIdentifier = identifiers.slice(0, i).join("."); if (this.resolveFQN(declIndex, namespaces, testIdentifier, concept)) { resultFound = true; break; } } if (resultFound) continue; // test partial identifier names from bottom to top (e.g. "a.b.c" => "c", "b.c", "a.b.c") for (let i = identifiers.length - 1; i > 0; i--) { const testIdentifier = identifiers.slice(i).join("."); if (this.resolveFQN(declIndex, namespaces, testIdentifier, concept)) { resultFound = true; break; } } } else { // simple identifier: resolve it this.resolveFQN(declIndex, namespaces, identifier, concept); } } // merge dependencies const dependencies = (0, processor_utils_1.getAndDeleteChildConcepts)(program_traverser_1.ProgramTraverser.PROGRAM_BODY_PROP, dependency_concept_1.LCEDependency.conceptId, childConcepts).concat(localContexts.currentContexts.get(context_keys_1.CoreContextKeys.DEPENDENCY_INDEX)); const depIndex = new Map(); for (const dep of dependencies) { if (!dep.fqn.globalFqn) continue; if ((!dep.fqn.globalFqn.startsWith('"') && dep.targetType !== "module") || dep.fqn.globalFqn.startsWith(dep.globalSourceFQN)) continue; // skip invalid FQNs and dependencies on own scope if (!depIndex.has(dep.globalSourceFQN)) { depIndex.set(dep.globalSourceFQN, new Map([[dep.fqn.globalFqn, dep]])); } else if (!depIndex.get(dep.globalSourceFQN)?.has(dep.fqn.globalFqn)) { depIndex.get(dep.globalSourceFQN)?.set(dep.fqn.globalFqn, dep); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion depIndex.get(dep.globalSourceFQN).get(dep.fqn.globalFqn).cardinality += dep.cardinality; } } // return merged dependencies const concepts = []; depIndex.forEach((valMap) => { valMap.forEach((val) => { concepts.push((0, concept_1.singleEntryConceptMap)(dependency_concept_1.LCEDependency.conceptId, val)); }); }); return (0, concept_1.mergeConceptMaps)(...concepts); } /** * Tries to resolve the FQN of a concept by a given list of (global) namespace identifiers and the local name of the declaration. * * @param declIndex the {@link DeclarationIndex} containing all registered declarations * @param namespaces list of global namespace identifiers that were present during the resolution scheduling * @param identifier local name of the declaration * @param concept concept to which the FQN should be resolved and assigned to * @returns whether the resolution was successful */ resolveFQN(declIndex, namespaces, identifier, concept) { for (let i = namespaces.length; i > 0; i--) { const testNamespace = namespaces.slice(0, i).join("."); if (declIndex.has(testNamespace) && declIndex.get(testNamespace)?.has(identifier)) { concept.fqn = declIndex.get(testNamespace)?.get(identifier); return true; } } return false; } /** * Constructs the prefix for a FQN based on the current scope. * @param skipLastScope when set to true, the current scope is not included in the FQN prefix. */ static constructFQNPrefix(localContexts, skipLastScope = false) { let result = context_1.FQN.id(""); for (let i = 0; i < localContexts.contexts.length - (skipLastScope ? 1 : 0); i++) { const context = localContexts.contexts[i]; const globalName = context.get(context_keys_1.CoreContextKeys.FQN_SCOPE)?.globalIdentifier; const localName = context.get(context_keys_1.CoreContextKeys.FQN_SCOPE)?.localIdentifier; if (globalName && localName) { result.globalFqn += globalName + "."; result.localFqn += localName + "."; } } return result; } /** * Constructs the complete FQN (local and global) for a given declaration. */ static constructDeclarationFQN(localContexts, declarationNode, identifier) { const fqnPrefix = DependencyResolutionProcessor.constructFQNPrefix(localContexts); if (this.isDefaultDeclaration(localContexts, declarationNode, identifier)) { return new context_1.FQN(fqnPrefix.globalFqn + "default", fqnPrefix.localFqn + "default"); } else { return new context_1.FQN(fqnPrefix.globalFqn + identifier, fqnPrefix.localFqn + identifier); } } /** * Determines the identifier of a given declaration. Don't use on unnamed declarations that are not default-exported! */ static constructDeclarationIdentifier(localContexts, declarationNode, nodeIdentifier) { let id = DependencyResolutionProcessor.isDefaultDeclaration(localContexts, declarationNode, nodeIdentifier) ? "default" : nodeIdentifier; if (id === "default") { if (nodeIdentifier) { id = nodeIdentifier; } else { const srcPath = localContexts.contexts[0].get(context_keys_1.CoreContextKeys.FQN_SCOPE).globalIdentifier; id = srcPath.substring(srcPath.lastIndexOf("/") + 1, srcPath.includes(".") ? srcPath.indexOf(".") : (srcPath.length - 1)); } } if (!id) { throw new Error("Cannot determine identifier of declaration"); } return id; } /** * Constructs the FQN for the current scope. * @param skipLastScope when set to true, the current scope is not included in the FQN. */ static constructScopeFQN(localContexts, skipLastScope = false) { const prefix = this.constructFQNPrefix(localContexts, skipLastScope); return new context_1.FQN(prefix.globalFqn.substring(0, prefix.globalFqn.length - 1), prefix.localFqn.substring(0, prefix.localFqn.length - 1)); } /** * Register a declaration for the current scope. * This information is used later to resolve FQNs. * @param localName local name under which the declaration is used * @param fqn fully qualified name of the declaration * @param insideScopeDeclaration specifies whether the declaration is registered while traversing its own scope */ static registerDeclaration(localContexts, localName, fqn, insideScopeDeclaration = false) { const [declIndex] = localContexts.getNextContext(context_keys_1.CoreContextKeys.DECLARATION_INDEX); const scope = this.constructScopeFQN(localContexts, insideScopeDeclaration); if (!declIndex.has(scope.globalFqn)) declIndex.set(scope.globalFqn, new Map()); declIndex.get(scope.globalFqn)?.set(localName, fqn); } /** * Schedules the resolution of a FQN for a named concept. * The resolution happens after the AST has been traversed completely. * @param localName local name of the concept that will be used to resolve the FQN (e.g. variable name) * @param concept named concept with the fqn property that will be resolved */ static scheduleFqnResolution(localContexts, localName, concept) { const namespaces = []; for (const context of localContexts.contexts) { const name = context.get(context_keys_1.CoreContextKeys.FQN_SCOPE)?.globalIdentifier; if (name) { namespaces.push(name); } } const [resolutionList] = localContexts.getNextContext(context_keys_1.CoreContextKeys.FQN_RESOLVER); resolutionList.push([namespaces, localName, concept]); } /** * Introduces a new scope (e.g. for a function or a simple block statement). * This will be used to generate FQNs for declarations made within the scope. * @param scopeIdentifier can be used to identify the scope (e.g. with function name), * if undefined the scope will be identified by a unique number */ static addScopeContext(localContexts, scopeIdentifier) { if (localContexts.currentContexts.has(context_keys_1.CoreContextKeys.FQN_SCOPE)) { // if scope context is already present, ignore this call return; } if (!scopeIdentifier) { const internalScopeId = localContexts.getNextContext(context_keys_1.CoreContextKeys.FQN_SCOPE)[0].internalScopeId.toString(); scopeIdentifier = context_1.FQN.id(internalScopeId); localContexts.getNextContext(context_keys_1.CoreContextKeys.FQN_SCOPE)[0].internalScopeId++; } localContexts.currentContexts.set(context_keys_1.CoreContextKeys.FQN_SCOPE, { globalIdentifier: scopeIdentifier.globalFqn, localIdentifier: scopeIdentifier.localFqn, internalScopeId: 0, }); } /** * Creates a new dependency index for the current namespace FQN. * Use `getRegisteredDependencies()` to get all registered dependencies from children and return them in `postChildrenProcessing()`. * @param globalFqn use this to specify different global FQN than the one of the current namespace */ static createDependencyIndex(localContexts, globalFqn) { localContexts.currentContexts.set(context_keys_1.CoreContextKeys.DEPENDENCY_GLOBAL_SOURCE_FQN, globalFqn ?? DependencyResolutionProcessor.constructScopeFQN(localContexts).globalFqn); localContexts.currentContexts.set(context_keys_1.CoreContextKeys.DEPENDENCY_INDEX, []); } /** * Registers a dependency on a declaration * @param depGlobalFQN global FQN of the dependency (does not need to be resolved yet) * @param resolveFQN if set to true(default) automatically schedules resolution of the dependency FQN */ static registerDependency(localContexts, depGlobalFQN, resolveFQN = true) { const [depIndex] = localContexts.getNextContext(context_keys_1.CoreContextKeys.DEPENDENCY_INDEX); const [depSourceFQN] = localContexts.getNextContext(context_keys_1.CoreContextKeys.DEPENDENCY_GLOBAL_SOURCE_FQN); const dep = new dependency_concept_1.LCEDependency(depGlobalFQN, "declaration", depSourceFQN, modulepath_utils_1.ModulePathUtils.isFQNModule(depSourceFQN) ? "module" : "declaration", 1); if (resolveFQN) { this.scheduleFqnResolution(localContexts, depGlobalFQN, dep); } depIndex.push(dep); } /** * @returns all registered dependencies of the current dependency index as a `ConceptMap` */ static getRegisteredDependencies(localContexts) { return (0, concept_1.createConceptMap)(dependency_concept_1.LCEDependency.conceptId, localContexts.getNextContext(context_keys_1.CoreContextKeys.DEPENDENCY_INDEX)[0]); } /** * @returns whether a declaration is default-exported. */ static isDefaultDeclaration(localContexts, declarationNode, identifier) { const defaultIdentifierContext = localContexts.getNextContext(context_keys_1.CoreContextKeys.DEFAULT_EXPORT_IDENTIFIER); return declarationNode.parent?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration || (!!identifier && !!defaultIdentifierContext && defaultIdentifierContext[0] === identifier); } } exports.DependencyResolutionProcessor = DependencyResolutionProcessor;