@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
JavaScript
;
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;