core/parsers/definition.parser.ts
core/parsers/definition.parser.ts is a file in GovLab Patterns. 123 lines of code and 43 definitions.
import { ROLE_CALL, ROLE_DEFINITION, ROLE_NODE, roleOf } from "#core/classifiers/syntax.classifier";
import { childNodes, fieldText, fieldType, lineOf } from "#core/selectors/syntax.selector";
import type { CodeSymbol } from "#types/code.types";
import type { CstNode } from "@govlab/code-parse";
import { MODULE_SCOPE } from "#configuration/constants/report.constants";
import { bodyHash } from "#core/converters/code.converter";
const NAME_FIELD = "name";
const ALIAS_FIELD = "alias";
const CALL_FUNCTION_FIELD = "function";
const VALUE_FIELD = "value";
const IMPORT_SPECIFIER = "import_specifier";
const EXPORT_TOKEN = "export";
const EXPORT_SPECIFIER = "export_specifier";
const SEGMENT = ".";
const NAME_FIELD_BY_ROLE: ReadonlyMap<string, string> = new Map([
[ROLE_DEFINITION, NAME_FIELD],
[ROLE_CALL, CALL_FUNCTION_FIELD],
]);
const MEMBER_CALL_TYPES: ReadonlySet<string> = new Set([
"member_expression",
"selector_expression",
"attribute",
"field_expression",
]);
const FUNCTION_KIND_TOKENS: readonly string[] = ["function", "method"];
const FUNCTION_VALUE_TYPES: ReadonlySet<string> = new Set([
"arrow_function",
"function",
"function_expression",
"generator_function",
"function_definition",
]);
interface SymbolSite {
enclosing: string;
name: string;
role: string;
}
const descendName = function descendName(node: CstNode): string {
return (
childNodes(node)
.map((child) => fieldText(child, NAME_FIELD))
.find((nested) => nested.length > 0) ?? ""
);
};
const lastSegment = function lastSegment(name: string): string {
const dot = name.lastIndexOf(SEGMENT);
return dot === -1 ? name : name.slice(dot + 1);
};
const nameOf = function nameOf(node: CstNode, role: string): string {
const field = NAME_FIELD_BY_ROLE.get(role);
if (field === undefined) {
return "";
}
const direct = fieldText(node, field);
return lastSegment(direct.length > 0 ? direct : descendName(node));
};
const collectAliases = function collectAliases(node: CstNode): Map<string, string> {
const aliases = new Map<string, string>();
const imported = node.type === IMPORT_SPECIFIER ? fieldText(node, NAME_FIELD) : "";
const local = node.type === IMPORT_SPECIFIER ? fieldText(node, ALIAS_FIELD) : "";
if (imported.length > 0 && local.length > 0) {
aliases.set(local, imported);
}
for (const child of childNodes(node)) {
for (const [key, value] of collectAliases(child)) {
aliases.set(key, value);
}
}
return aliases;
};
const isMemberCall = function isMemberCall(node: CstNode): boolean {
const fn = fieldType(node, CALL_FUNCTION_FIELD);
return fn !== undefined && MEMBER_CALL_TYPES.has(fn);
};
const isCallableDef = function isCallableDef(node: CstNode): boolean {
const lower = node.type.toLowerCase();
const hasFunctionValue = childNodes(node).some((child) =>
FUNCTION_VALUE_TYPES.has(fieldType(child, VALUE_FIELD) ?? ""),
);
return FUNCTION_KIND_TOKENS.some((token) => lower.includes(token)) || hasFunctionValue;
};
const symbolAt = function symbolAt(node: CstNode, site: SymbolSite): CodeSymbol {
const isDefinition = site.role === ROLE_DEFINITION;
return {
callable: isDefinition && isCallableDef(node),
enclosing: site.enclosing,
exported: false,
file: "",
hash: isDefinition ? bodyHash(node.text ?? "") : "",
kind: node.type,
line: lineOf(node),
member: site.role === ROLE_CALL && isMemberCall(node),
name: site.name,
role: site.role,
size: isDefinition ? (node.text ?? "").length : 0,
};
};
const symbolWalk = function symbolWalk(
node: CstNode,
enclosing: string,
aliases: ReadonlyMap<string, string>,
): CodeSymbol[] {
const role = node.isNamed ? roleOf(node.type) : ROLE_NODE;
const found = nameOf(node, role);
const name = role === ROLE_CALL ? (aliases.get(found) ?? found) : found;
const here = name.length > 0 && role !== ROLE_NODE ? [symbolAt(node, { enclosing, name, role })] : [];
const childEnclosing = role === ROLE_DEFINITION && name.length > 0 ? name : enclosing;
return [...here, ...childNodes(node).flatMap((child) => symbolWalk(child, childEnclosing, aliases))];
};
const exportNamesAt = function exportNamesAt(node: CstNode, underExport: boolean): string[] {
const active = underExport || node.type.includes(EXPORT_TOKEN);
const isDefinition = active && node.isNamed && roleOf(node.type) === ROLE_DEFINITION;
const defName = isDefinition ? nameOf(node, ROLE_DEFINITION) : "";
const specName = node.type === EXPORT_SPECIFIER ? fieldText(node, NAME_FIELD) : "";
const nested = childNodes(node).flatMap((child) => exportNamesAt(child, active));
return [defName, specName, ...nested].filter((name) => name.length > 0);
};
export const codeSymbols = function codeSymbols(root: CstNode): CodeSymbol[] {
const exported = new Set(exportNamesAt(root, false));
return symbolWalk(root, MODULE_SCOPE, collectAliases(root)).map((symbol) =>
symbol.role === ROLE_DEFINITION && exported.has(symbol.name) ? { ...symbol, exported: true } : symbol,
);
};