codemetrics-ai 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +114 -8
  2. package/dist/analyzer.js +37 -27
  3. package/dist/async-population.d.ts +12 -0
  4. package/dist/async-population.js +53 -0
  5. package/dist/async-probe.d.ts +8 -0
  6. package/dist/async-probe.js +52 -0
  7. package/dist/async-scoring.d.ts +3 -0
  8. package/dist/async-scoring.js +33 -0
  9. package/dist/complexity-breakdown.d.ts +21 -0
  10. package/dist/complexity-breakdown.js +30 -0
  11. package/dist/decomposition-evidence.d.ts +91 -0
  12. package/dist/decomposition-evidence.js +71 -0
  13. package/dist/error-handling-scoring.d.ts +1 -0
  14. package/dist/error-handling-scoring.js +15 -0
  15. package/dist/error-handling.d.ts +33 -0
  16. package/dist/error-handling.js +147 -0
  17. package/dist/failure-contracts.d.ts +19 -0
  18. package/dist/failure-contracts.js +261 -0
  19. package/dist/function-measurements.d.ts +17 -0
  20. package/dist/function-measurements.js +85 -0
  21. package/dist/function-nodes.d.ts +3 -0
  22. package/dist/function-nodes.js +5 -0
  23. package/dist/function-scoring.d.ts +5 -0
  24. package/dist/function-scoring.js +50 -0
  25. package/dist/handler-disposition.d.ts +9 -0
  26. package/dist/handler-disposition.js +156 -0
  27. package/dist/handler-resolution.d.ts +26 -0
  28. package/dist/handler-resolution.js +99 -0
  29. package/dist/metrics.d.ts +3 -0
  30. package/dist/metrics.js +23 -45
  31. package/dist/module-forwarding.d.ts +4 -0
  32. package/dist/module-forwarding.js +153 -0
  33. package/dist/module-graph.d.ts +34 -0
  34. package/dist/module-graph.js +122 -0
  35. package/dist/module-references.d.ts +8 -0
  36. package/dist/module-references.js +51 -0
  37. package/dist/module-resolution.d.ts +10 -0
  38. package/dist/module-resolution.js +76 -0
  39. package/dist/module-topology.d.ts +72 -0
  40. package/dist/module-topology.js +72 -0
  41. package/dist/react-probe.d.ts +6 -0
  42. package/dist/react-probe.js +60 -0
  43. package/dist/score-arithmetic.d.ts +12 -0
  44. package/dist/score-arithmetic.js +40 -0
  45. package/package.json +4 -3
@@ -0,0 +1,71 @@
1
+ import ts from "typescript";
2
+ import { isFunction } from "./function-nodes.js";
3
+ function ownedShape(root) {
4
+ // A concise function body has an implicit return, independent of formatting.
5
+ let statements = ts.isExpression(root) ? 1 : 0, dataLiteralEntries = 0;
6
+ function visit(node) {
7
+ if (ts.isExpressionWithTypeArguments(node)) {
8
+ visit(node.expression);
9
+ return;
10
+ }
11
+ if (isFunction(node) || ts.isTypeNode(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node) ||
12
+ ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node))
13
+ return;
14
+ if (ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node) || ts.isExportDeclaration(node))
15
+ return;
16
+ if (ts.isVariableStatement(node) && !node.declarationList.declarations.some(declaration => declaration.initializer))
17
+ return;
18
+ if (ts.isStatement(node) && !ts.isBlock(node) && !ts.isEmptyStatement(node) && !ts.isLabeledStatement(node))
19
+ statements++;
20
+ if (ts.isObjectLiteralExpression(node))
21
+ dataLiteralEntries += node.properties.length;
22
+ if (ts.isArrayLiteralExpression(node))
23
+ dataLiteralEntries += node.elements.filter(element => !ts.isOmittedExpression(element)).length;
24
+ ts.forEachChild(node, visit);
25
+ }
26
+ visit(root);
27
+ return { ownedStatements: statements, dataLiteralEntries };
28
+ }
29
+ export function decompositionModule(source, file, project, functions) {
30
+ const byStart = new Map(functions.map(item => [item.sourceSpanStart, item]));
31
+ const rows = [];
32
+ function visit(node) {
33
+ if (isFunction(node) && node.body) {
34
+ const item = byStart.get(node.getStart(source));
35
+ if (item)
36
+ rows.push({ id: item.id, member: item.member, line: item.line, ownedSourceLines: item.ownSourceLines, ...ownedShape(node.body) });
37
+ }
38
+ if (!ts.isTypeNode(node) || ts.isExpressionWithTypeArguments(node))
39
+ ts.forEachChild(node, visit);
40
+ }
41
+ visit(source);
42
+ return { file, project, implementedFunctions: rows.length, sumOwnedFunctionLines: rows.reduce((sum, row) => sum + row.ownedSourceLines, 0),
43
+ sumOwnedFunctionStatements: rows.reduce((sum, row) => sum + row.ownedStatements, 0),
44
+ initializer: ownedShape(source), directReExports: source.statements.filter(node => ts.isExportDeclaration(node) && node.moduleSpecifier).length,
45
+ typeDeclarations: source.statements.filter(node => ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)).length,
46
+ enums: source.statements.filter(ts.isEnumDeclaration).length, functions: rows };
47
+ }
48
+ export function decompositionEvidence(modules, incomplete) {
49
+ const ordered = [...modules].sort((a, b) => a.file.localeCompare(b.file, "en") || a.project.localeCompare(b.project, "en"));
50
+ const functions = ordered.flatMap(module => module.functions.map(row => ({ ...row, file: module.file, project: module.project })));
51
+ const sizes = functions.map(row => row.ownedSourceLines).sort((a, b) => a - b);
52
+ const sum = sizes.reduce((total, size) => total + size, 0);
53
+ const statements = functions.map(row => row.ownedStatements).sort((a, b) => a - b);
54
+ const statementSum = statements.reduce((total, size) => total + size, 0);
55
+ return { version: 2, status: incomplete ? "failed" : "unscored", score: null,
56
+ measurement: "owned-executable-statements-v2", primaryMeasure: "ownedStatements",
57
+ basis: "Descriptive size evidence; no responsibility inference, threshold, finding or score contribution.",
58
+ exclusions: ["nested-function-double-counting", "class-and-enum-initializers", "parameter-initializers", "semantic-responsibility"],
59
+ population: { modules: ordered.length, functions: functions.length, sumOwnedFunctionLines: sum,
60
+ sumOwnedFunctionStatements: statementSum,
61
+ medianFunctionStatements: statements.length ? statements[Math.ceil(statements.length * 0.5) - 1] : null,
62
+ p90FunctionStatements: statements.length ? statements[Math.ceil(statements.length * 0.9) - 1] : null,
63
+ medianFunctionLines: sizes.length ? sizes[Math.ceil(sizes.length * 0.5) - 1] : null,
64
+ p90FunctionLines: sizes.length ? sizes[Math.ceil(sizes.length * 0.9) - 1] : null }, modules: ordered,
65
+ largestFunctions: [...functions].sort((a, b) => b.ownedStatements - a.ownedStatements || a.id.localeCompare(b.id, "en")).slice(0, 10),
66
+ largestFunctionContainers: ordered.filter(module => module.sumOwnedFunctionStatements > 0)
67
+ .sort((a, b) => b.sumOwnedFunctionStatements - a.sumOwnedFunctionStatements || a.file.localeCompare(b.file, "en")).slice(0, 10)
68
+ .map(({ functions: _functions, ...module }) => ({ ...module,
69
+ shareOfOwnedFunctionStatements: statementSum ? module.sumOwnedFunctionStatements / statementSum : null,
70
+ shareOfOwnedFunctionLines: sum ? module.sumOwnedFunctionLines / sum : null })) };
71
+ }
@@ -0,0 +1 @@
1
+ export declare function errorHandlingDecision(total: number, unexplained: number, unknown: number, unresolvedUses: number): import("./scoring-decision.js").ScoringDecision | undefined;
@@ -0,0 +1,15 @@
1
+ import { firstMatch } from "./scoring-decision.js";
2
+ import { fraction, roundScore } from "./score-arithmetic.js";
3
+ export function errorHandlingDecision(total, unexplained, unknown, unresolvedUses) {
4
+ if (![total, unexplained, unknown, unresolvedUses].every(value => Number.isSafeInteger(value) && value >= 0) || unexplained + unknown > total)
5
+ throw new Error("Invalid handler population.");
6
+ if (!total || unknown || unresolvedUses)
7
+ return undefined;
8
+ const populationScore = roundScore(fraction(10n * BigInt(total - unexplained), BigInt(total)));
9
+ const cap = unexplained ? 9 : 10;
10
+ return firstMatch("javascript-typescript/errorHandling/failure-disposition-v2", { countingUnit: "distinctHandlerBody", totalHandlers: total, assessedHandlers: total, unexplainedSwallowing: unexplained,
11
+ unknownHandlers: unknown, unresolvedCallbackUses: unresolvedUses, populationScore, cap,
12
+ formula: "min(10 - 10 * unexplainedSwallowing / assessedHandlers, unexplainedSwallowing > 0 ? 9 : 10)", rounding: "half-up-one-decimal" }, [{ id: "issueCap", condition: "unexplainedSwallowing > 0 and populationScore > 9", matched: unexplained > 0 && populationScore > 9,
13
+ score: 9, categories: ["unexplainedSwallowing"] },
14
+ { id: "population", condition: "otherwise", matched: true, score: populationScore, categories: ["unexplainedSwallowing"] }]);
15
+ }
@@ -0,0 +1,33 @@
1
+ import ts from "typescript";
2
+ import { type Dimension } from "./evidence.js";
3
+ import { HandlerResolver } from "./handler-resolution.js";
4
+ import { type HandlerDisposition } from "./handler-disposition.js";
5
+ import type { FailureContracts } from "./failure-contracts.js";
6
+ type UsageKind = "catchClause" | "promiseCatch" | "promiseThenRejection";
7
+ interface Handler {
8
+ id: string;
9
+ file: string;
10
+ project: string;
11
+ line: number;
12
+ kind: "catchClause" | "promiseRejection";
13
+ classification: "unexplainedEmpty" | "documentedEmpty" | "containsCode";
14
+ comments: string[];
15
+ disposition: HandlerDisposition;
16
+ uses: {
17
+ file: string;
18
+ line: number;
19
+ kind: UsageKind;
20
+ resolution: "inline" | "reference";
21
+ }[];
22
+ }
23
+ export declare function inspectErrorHandling(source: ts.SourceFile, checker: ts.TypeChecker, file: string, project: string, resolver: HandlerResolver, contracts: FailureContracts): {
24
+ handlers: Handler[];
25
+ uninspectedCallbacks: {
26
+ file: string;
27
+ line: number;
28
+ kind: UsageKind;
29
+ reason: string;
30
+ }[];
31
+ };
32
+ export declare function errorHandlingEvidence(results: ReturnType<typeof inspectErrorHandling>[], incomplete: boolean): Dimension;
33
+ export {};
@@ -0,0 +1,147 @@
1
+ import ts from "typescript";
2
+ import { hash, scored } from "./evidence.js";
3
+ import { unwrapHandler } from "./handler-resolution.js";
4
+ import { classifyDisposition } from "./handler-disposition.js";
5
+ import { errorHandlingDecision } from "./error-handling-scoring.js";
6
+ function undefinedValue(node, checker) {
7
+ node = unwrapHandler(node);
8
+ if (ts.isVoidExpression(node))
9
+ return ts.isNumericLiteral(node.expression);
10
+ if (!ts.isIdentifier(node) || node.text !== "undefined")
11
+ return false;
12
+ const declarations = checker.getSymbolAtLocation(node)?.declarations ?? [];
13
+ return declarations.length === 0 || declarations.every(declaration => declaration.getSourceFile().hasNoDefaultLib);
14
+ }
15
+ function emptyBody(body, checker) {
16
+ if (!ts.isBlock(body))
17
+ return undefinedValue(body, checker);
18
+ return body.statements.every(statement => ts.isEmptyStatement(statement) ||
19
+ (ts.isReturnStatement(statement) && (!statement.expression || undefinedValue(statement.expression, checker))));
20
+ }
21
+ function bodyComments(body, source) {
22
+ const nested = [];
23
+ function visit(node) {
24
+ if (node !== body && (ts.isFunctionLike(node) || ts.isLiteralExpression(node) || ts.isTemplateExpression(node))) {
25
+ nested.push({ start: ts.isFunctionLike(node) ? node.pos : node.getStart(source), end: node.end });
26
+ return;
27
+ }
28
+ ts.forEachChild(node, visit);
29
+ }
30
+ visit(body);
31
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, source.languageVariant, source.text.slice(body.pos, body.end));
32
+ const comments = [];
33
+ for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
34
+ if (token !== ts.SyntaxKind.SingleLineCommentTrivia && token !== ts.SyntaxKind.MultiLineCommentTrivia)
35
+ continue;
36
+ if (nested.some(range => body.pos + scanner.getTokenPos() >= range.start && body.pos + scanner.getTokenPos() < range.end))
37
+ continue;
38
+ const text = scanner.getTokenText().replace(/^\/\/?\*?|\*\/$/g, "").trim();
39
+ if (/[\p{L}\p{N}]/u.test(text))
40
+ comments.push(text);
41
+ }
42
+ return comments;
43
+ }
44
+ function rejectionArgument(call, checker) {
45
+ const declaration = checker.getResolvedSignature(call)?.declaration;
46
+ if (!declaration || !ts.isMethodSignature(declaration) || !ts.isIdentifier(declaration.name) ||
47
+ !ts.isInterfaceDeclaration(declaration.parent) || !["Promise", "PromiseLike"].includes(declaration.parent.name.text))
48
+ return;
49
+ const source = declaration.getSourceFile();
50
+ if (!source.hasNoDefaultLib || !/(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/.test(source.fileName))
51
+ return;
52
+ if (declaration.name.text === "catch")
53
+ return { index: 0, kind: "promiseCatch" };
54
+ if (declaration.name.text === "then" && call.arguments.length > 1)
55
+ return { index: 1, kind: "promiseThenRejection" };
56
+ }
57
+ export function inspectErrorHandling(source, checker, file, project, resolver, contracts) {
58
+ const handlers = new Map();
59
+ const uninspectedCallbacks = [];
60
+ function add(body, kind, at, resolution) {
61
+ const identity = resolver.identity(body);
62
+ const existing = handlers.get(identity.id);
63
+ const use = { file, line: source.getLineAndCharacterOfPosition(at.getStart(source)).line + 1, kind, resolution };
64
+ const empty = emptyBody(body, checker), comments = bodyComments(body, body.getSourceFile());
65
+ const disposition = classifyDisposition(body, checker, empty, comments, at, contracts);
66
+ if (existing) {
67
+ existing.uses.push(use);
68
+ if (existing.disposition.classification !== disposition.classification)
69
+ existing.disposition = { classification: "unknown", reason: "Uses of the shared handler have different failure contexts." };
70
+ return;
71
+ }
72
+ const classification = !empty ? "containsCode" : comments.length ? "documentedEmpty" : "unexplainedEmpty";
73
+ handlers.set(identity.id, { ...identity, kind: kind === "catchClause" ? "catchClause" : "promiseRejection", classification, comments,
74
+ disposition, uses: [use] });
75
+ }
76
+ function visit(node) {
77
+ if (ts.isTypeNode(node) && !ts.isExpressionWithTypeArguments(node))
78
+ return;
79
+ if (ts.isCatchClause(node))
80
+ add(node.block, "catchClause", node, "inline");
81
+ if (ts.isCallExpression(node)) {
82
+ const rejection = rejectionArgument(node, checker);
83
+ if (rejection) {
84
+ const resolved = resolver.resolve(node.arguments[rejection.index]);
85
+ if ("body" in resolved)
86
+ add(resolved.body, rejection.kind, node, resolved.resolution);
87
+ else
88
+ uninspectedCallbacks.push({ file, line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
89
+ kind: rejection.kind, reason: resolved.reason });
90
+ }
91
+ }
92
+ ts.forEachChild(node, visit);
93
+ }
94
+ visit(source);
95
+ return { handlers: [...handlers.values()], uninspectedCallbacks };
96
+ }
97
+ function handlerFinding(handler, scoreAvailable) {
98
+ const actionable = handler.disposition.classification === "unexplainedSwallowing";
99
+ const category = actionable ? "unexplainedSwallowing" : "unknownHandlerDisposition";
100
+ return { category, ruleId: `javascript-typescript/errorHandling/${category}`,
101
+ fingerprint: hash(`${handler.id}|${category}`), file: handler.file, project: handler.project, line: handler.line,
102
+ severity: actionable && scoreAvailable ? "warning" : "info", confidence: actionable ? "high" : "low",
103
+ message: handler.disposition.reason,
104
+ observations: { handlerId: handler.id, kind: handler.kind, uses: handler.uses, classification: handler.disposition.classification,
105
+ scoreDisposition: !actionable ? "excludedUnknown" : scoreAvailable ? "policyInput" : "excludedUnavailablePopulation" } };
106
+ }
107
+ export function errorHandlingEvidence(results, incomplete) {
108
+ const unique = new Map();
109
+ for (const row of results.flatMap(result => result.handlers)) {
110
+ const existing = unique.get(row.id);
111
+ if (existing) {
112
+ existing.uses.push(...row.uses);
113
+ if (existing.disposition.classification !== row.disposition.classification)
114
+ existing.disposition = { classification: "unknown", reason: "Selected project contexts disagree about the shared handler disposition." };
115
+ }
116
+ else
117
+ unique.set(row.id, { ...row, uses: [...row.uses] });
118
+ }
119
+ const handlers = [...unique.values()].sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
120
+ for (const handler of handlers)
121
+ handler.uses.sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line || a.kind.localeCompare(b.kind, "en"));
122
+ const uninspectedCallbacks = results.flatMap(result => result.uninspectedCallbacks)
123
+ .sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
124
+ const count = (classification) => handlers.filter(handler => handler.classification === classification).length;
125
+ const unexplained = count("unexplainedEmpty");
126
+ const dispositions = Object.fromEntries(["propagated", "failureResult", "reported", "fallback", "documented", "unexplainedSwallowing", "unknown"]
127
+ .map(kind => [kind, handlers.filter(handler => handler.disposition.classification === kind).length]));
128
+ const decision = incomplete ? undefined : errorHandlingDecision(handlers.length, dispositions.unexplainedSwallowing, dispositions.unknown, uninspectedCallbacks.length);
129
+ const findings = handlers.filter(handler => ["unexplainedSwallowing", "unknown"].includes(handler.disposition.classification))
130
+ .map(handler => handlerFinding(handler, !!decision));
131
+ const scope = { id: "javascript-typescript/errorHandling/failure-disposition-v2", coverage: "partial",
132
+ includes: ["catch-clauses", "inline-standard-promise-rejection-handlers", "attached-intent", "selected-source-referenced-handlers", "bounded-failure-continuations"],
133
+ excludes: ["general-exception-flow", "arbitrary-callback-value-flow", "correctness-of-recovery", "semantic-rationale-judgment"] };
134
+ const dispositionEvidence = { version: 1, countingUnit: "distinctHandlerBody", totalHandlers: handlers.length,
135
+ assessedHandlers: handlers.length - dispositions.unknown, unknownHandlers: dispositions.unknown, counts: dispositions,
136
+ assessedPercent: handlers.length ? 100 * (handlers.length - dispositions.unknown) / handlers.length : null,
137
+ unresolvedCallbackUses: uninspectedCallbacks.length, populationComplete: !incomplete && !dispositions.unknown && !uninspectedCallbacks.length };
138
+ const dimension = decision ? scored(decision, "Partial Error Handling: unexplained swallowing among distinct assessed handler bodies. Documented intent is honored; failure outcomes are signals, not proof of correct recovery.", findings, dispositionEvidence) : { status: incomplete ? "failed" : "skipped", findings,
139
+ basis: incomplete ? "Incomplete source; handler observations are diagnostic only." : !handlers.length ? "No assessed handler population; error handling is unavailable." :
140
+ "Unknown handler dispositions or unresolved callback uses prevent a reliable population score." };
141
+ return { ...dimension, scope, dispositionEvidence, handlerEvidence: { version: 2, countingUnit: "distinctHandlerBody",
142
+ totalHandlers: handlers.length, unexplainedEmptyHandlers: unexplained, documentedEmptyHandlers: count("documentedEmpty"),
143
+ handlersContainingCode: count("containsCode"), unexplainedEmptyPercent: handlers.length ? 100 * unexplained / handlers.length : null,
144
+ handlerUseSites: handlers.reduce((sum, handler) => sum + handler.uses.length, 0),
145
+ referencedCallbackUseSites: handlers.reduce((sum, handler) => sum + handler.uses.filter(use => use.resolution === "reference").length, 0),
146
+ uninspectedRejectionCallbacks: uninspectedCallbacks.length, uninspectedCallbacks, handlers } };
147
+ }
@@ -0,0 +1,19 @@
1
+ import ts from "typescript";
2
+ export declare function standardConsole(call: ts.CallExpression, checker: ts.TypeChecker): boolean;
3
+ /** Source-backed contracts, not name-based exemptions or a runtime data-flow proof. */
4
+ export declare class FailureContracts {
5
+ private readonly checker;
6
+ private readonly selected;
7
+ private readonly references;
8
+ private readonly writes;
9
+ private readonly local;
10
+ constructor(checker: ts.TypeChecker, sources: readonly ts.SourceFile[], selected: readonly ts.SourceFile[], root: string);
11
+ forwardsCaughtError(call: ts.CallExpression): boolean;
12
+ explicitResult(expression: ts.Expression | undefined, depth?: number): boolean;
13
+ reports(call: ts.CallExpression): boolean;
14
+ private reachesConsole;
15
+ guardedUndefinedResult(body: ts.ConciseBody): boolean;
16
+ private commonJsFile;
17
+ private guardedCallResult;
18
+ private truthyGuard;
19
+ }
@@ -0,0 +1,261 @@
1
+ import ts from "typescript";
2
+ import path from "node:path";
3
+ import { exclusion } from "./discovery.js";
4
+ import { unwrapHandler } from "./handler-resolution.js";
5
+ export function standardConsole(call, checker) {
6
+ const member = call.expression;
7
+ if (!ts.isPropertyAccessExpression(member) || !["error", "warn", "info", "log", "debug", "trace"].includes(member.name.text) ||
8
+ !ts.isIdentifier(member.expression) || member.expression.text !== "console" || !call.arguments.length || call.questionDotToken || member.questionDotToken)
9
+ return false;
10
+ const declarations = checker.getSymbolAtLocation(member.expression)?.declarations ?? [];
11
+ return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile &&
12
+ /(?:lib\.dom\.d\.ts|[/\\]@types[/\\]node[/\\](?:web-globals[/\\])?(?:console|globals)\.d\.ts)$/.test(declaration.getSourceFile().fileName));
13
+ }
14
+ const descendants = (node, match) => match(node) || (ts.forEachChild(node, child => descendants(child, match)) ?? false);
15
+ function enclosingFunction(node) {
16
+ for (let at = node.parent; at; at = at.parent)
17
+ if (ts.isFunctionLike(at))
18
+ return at;
19
+ }
20
+ function documentation(node) {
21
+ return (ts.getLeadingCommentRanges(node.getSourceFile().text, node.getFullStart()) ?? [])
22
+ .map(range => node.getSourceFile().text.slice(range.pos, range.end)).join("\n");
23
+ }
24
+ /** Source-backed contracts, not name-based exemptions or a runtime data-flow proof. */
25
+ export class FailureContracts {
26
+ checker;
27
+ selected;
28
+ references = new Map();
29
+ writes = new Set();
30
+ local = new Set();
31
+ constructor(checker, sources, selected, root) {
32
+ this.checker = checker;
33
+ this.selected = selected;
34
+ for (const source of sources) {
35
+ const relative = path.relative(root, source.fileName).replaceAll("\\", "/");
36
+ if (source.isDeclarationFile || relative.startsWith("../") || exclusion(relative))
37
+ continue;
38
+ this.local.add(source);
39
+ const visit = (node) => {
40
+ if (ts.isIdentifier(node)) {
41
+ const symbol = ts.isShorthandPropertyAssignment(node.parent)
42
+ ? checker.getShorthandAssignmentValueSymbol(node.parent) : checker.getSymbolAtLocation(node);
43
+ if (symbol) {
44
+ const refs = this.references.get(symbol) ?? [];
45
+ refs.push(node);
46
+ this.references.set(symbol, refs);
47
+ }
48
+ }
49
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
50
+ const record = (target) => {
51
+ if (ts.isIdentifier(target) || ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) {
52
+ const symbol = checker.getSymbolAtLocation(target);
53
+ if (symbol)
54
+ this.writes.add(symbol);
55
+ }
56
+ else
57
+ ts.forEachChild(target, record);
58
+ };
59
+ record(node.left);
60
+ }
61
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
62
+ [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
63
+ const symbol = checker.getSymbolAtLocation(node.operand);
64
+ if (symbol)
65
+ this.writes.add(symbol);
66
+ }
67
+ ts.forEachChild(node, visit);
68
+ };
69
+ visit(source);
70
+ }
71
+ }
72
+ forwardsCaughtError(call) {
73
+ if (call.questionDotToken || !ts.isIdentifier(call.expression) || !call.arguments.length)
74
+ return false;
75
+ const target = this.checker.getSymbolAtLocation(call.expression), error = unwrapHandler(call.arguments[0]);
76
+ const declaration = target?.declarations?.[0];
77
+ if (!target || this.writes.has(target) || !declaration || !ts.isParameter(declaration) || declaration.initializer ||
78
+ declaration.getSourceFile() !== call.getSourceFile() || call.pos < declaration.parent.pos || call.end > declaration.parent.end)
79
+ return false;
80
+ const caughtSymbol = (symbol) => {
81
+ const binding = symbol?.declarations?.[0];
82
+ return !!symbol && !!binding && ts.isVariableDeclaration(binding) && ts.isCatchClause(binding.parent) &&
83
+ !this.writes.has(symbol) && call.pos >= binding.parent.block.pos && call.end <= binding.parent.block.end;
84
+ };
85
+ // Only plain payload expressions, never an unrelated call taking the error.
86
+ const payload = (node) => {
87
+ if (ts.isIdentifier(node) && caughtSymbol(this.checker.getSymbolAtLocation(node)))
88
+ return true;
89
+ if (ts.isPropertyAccessExpression(node))
90
+ return payload(node.expression);
91
+ if (ts.isObjectLiteralExpression(node))
92
+ return node.properties.some(property => ts.isPropertyAssignment(property) ? payload(property.initializer) : ts.isShorthandPropertyAssignment(property) &&
93
+ caughtSymbol(this.checker.getShorthandAssignmentValueSymbol(property)));
94
+ return false;
95
+ };
96
+ return payload(error);
97
+ }
98
+ explicitResult(expression, depth = 0) {
99
+ if (!expression || depth > 4)
100
+ return false;
101
+ expression = unwrapHandler(expression);
102
+ if (ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression) || ts.isNewExpression(expression))
103
+ return true;
104
+ if (!ts.isIdentifier(expression))
105
+ return false;
106
+ const symbol = this.checker.getSymbolAtLocation(expression), declarations = symbol?.declarations ?? [];
107
+ if (!symbol || this.writes.has(symbol) || declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]))
108
+ return false;
109
+ const declaration = declarations[0];
110
+ if (!ts.isVariableDeclarationList(declaration.parent) || !(declaration.parent.flags & ts.NodeFlags.Const) || !declaration.initializer)
111
+ return false;
112
+ const value = unwrapHandler(declaration.initializer);
113
+ return ts.isLiteralExpression(value) || [ts.SyntaxKind.NullKeyword, ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(value.kind) || this.explicitResult(value, depth + 1);
114
+ }
115
+ reports(call) {
116
+ return this.reachesConsole(call, new Set(), 0);
117
+ }
118
+ reachesConsole(call, seen, depth, payloadPositions = new Set(call.arguments.map((_, index) => index))) {
119
+ if (call.questionDotToken || (ts.isPropertyAccessExpression(call.expression) && call.expression.questionDotToken))
120
+ return false;
121
+ if (standardConsole(call, this.checker))
122
+ return true;
123
+ const symbol = this.checker.getSymbolAtLocation(call.expression);
124
+ if (symbol && this.writes.has(symbol))
125
+ return false;
126
+ if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)) {
127
+ const receiver = this.checker.getSymbolAtLocation(call.expression.expression);
128
+ if (receiver && this.writes.has(receiver))
129
+ return false;
130
+ }
131
+ const declaration = this.checker.getResolvedSignature(call)?.declaration;
132
+ if (!declaration || depth >= 3 || seen.has(declaration) || !this.local.has(declaration.getSourceFile()) ||
133
+ !(ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isArrowFunction(declaration)) || !declaration.body)
134
+ return false;
135
+ seen = new Set(seen).add(declaration);
136
+ const parameters = new Set(declaration.parameters.filter((parameter, index) => payloadPositions.has(index) || parameter.dotDotDotToken && [...payloadPositions].some(position => position >= index))
137
+ .map(parameter => this.checker.getSymbolAtLocation(parameter.name)).filter(symbol => symbol && !this.writes.has(symbol)));
138
+ const declaredLogger = /\b(?:log|write)\b[^\n]*\b(?:message|exception|error|warn)\b/i.test(documentation(declaration));
139
+ // Ordinary wrappers require an unconditional forwarding call. A documented
140
+ // logger may have runtime level filters; prove its payload reaches a console sink.
141
+ const candidates = [];
142
+ const visit = (node) => {
143
+ if (node !== declaration.body && ts.isFunctionLike(node))
144
+ return;
145
+ if (ts.isCallExpression(node))
146
+ candidates.push(node);
147
+ ts.forEachChild(node, visit);
148
+ };
149
+ if (declaredLogger)
150
+ visit(declaration.body);
151
+ else if (ts.isBlock(declaration.body)) {
152
+ for (const statement of declaration.body.statements) {
153
+ if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
154
+ candidates.push(statement.expression);
155
+ else if (ts.isReturnStatement(statement) && statement.expression && ts.isCallExpression(statement.expression)) {
156
+ candidates.push(statement.expression);
157
+ break;
158
+ }
159
+ else if (!ts.isVariableStatement(statement) && !ts.isEmptyStatement(statement))
160
+ break;
161
+ }
162
+ }
163
+ else if (ts.isCallExpression(declaration.body))
164
+ candidates.push(declaration.body);
165
+ return candidates.some(candidate => {
166
+ const forwarded = new Set(candidate.arguments.flatMap((arg, index) => descendants(arg, node => ts.isIdentifier(node) && parameters.has(this.checker.getSymbolAtLocation(node))) ? [index] : []));
167
+ return forwarded.size > 0 && this.reachesConsole(candidate, seen, depth + 1, forwarded);
168
+ });
169
+ }
170
+ guardedUndefinedResult(body) {
171
+ const fn = enclosingFunction(body);
172
+ if (!fn || !ts.isFunctionDeclaration(fn) || !fn.name || ts.getCombinedModifierFlags(fn) & ts.ModifierFlags.Export)
173
+ return false;
174
+ const symbol = this.checker.getSymbolAtLocation(fn.name);
175
+ if (!symbol || this.writes.has(symbol))
176
+ return false;
177
+ const module = this.checker.getSymbolAtLocation(fn.getSourceFile());
178
+ if (!module && !this.commonJsFile(fn.getSourceFile()) && !enclosingFunction(fn))
179
+ return false;
180
+ if (module && this.checker.getExportsOfModule(module).some(exported => (exported.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(exported) : exported) === symbol))
181
+ return false;
182
+ const uses = (this.references.get(symbol) ?? []).filter(node => node !== fn.name);
183
+ return uses.length > 0 && uses.every(node => this.selected.includes(node.getSourceFile()) &&
184
+ ts.isCallExpression(node.parent) && node.parent.expression === node && this.guardedCallResult(node.parent));
185
+ }
186
+ commonJsFile(source) {
187
+ const exportRoot = (node) => {
188
+ if (!ts.isPropertyAccessExpression(node))
189
+ return;
190
+ if (ts.isIdentifier(node.expression)) {
191
+ const base = node.expression;
192
+ return base.text === "exports" || base.text === "module" && node.name.text === "exports" ? base : undefined;
193
+ }
194
+ return exportRoot(node.expression);
195
+ };
196
+ const inferredExport = (node) => {
197
+ // Depending on the assignments present, the JS binder uses either the
198
+ // assignment or its root identifier as the inferred module declaration.
199
+ while (ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)
200
+ node = node.parent;
201
+ if (ts.isBinaryExpression(node.parent) && node.parent.left === node)
202
+ node = node.parent;
203
+ return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && !!exportRoot(node.left);
204
+ };
205
+ return source.statements.some(statement => {
206
+ if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) ||
207
+ statement.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
208
+ return false;
209
+ const base = exportRoot(statement.expression.left);
210
+ if (!base)
211
+ return false;
212
+ const declarations = this.checker.getSymbolAtLocation(base)?.declarations ?? [];
213
+ // TypeScript binds bare CommonJS `module` to its export assignment when
214
+ // Node declarations are absent; that inferred declaration is not shadowing.
215
+ return declarations.every(declaration => inferredExport(declaration) ||
216
+ declaration.getSourceFile().isDeclarationFile && /[/\\]@types[/\\]node[/\\]/.test(declaration.getSourceFile().fileName));
217
+ });
218
+ }
219
+ guardedCallResult(call) {
220
+ const parent = call.parent;
221
+ let identifier, statement;
222
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
223
+ identifier = parent.name;
224
+ statement = parent.parent.parent;
225
+ }
226
+ else if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(parent.left)) {
227
+ identifier = parent.left;
228
+ statement = parent.parent;
229
+ }
230
+ else
231
+ return false;
232
+ if (!ts.isBlock(statement.parent))
233
+ return false;
234
+ const next = statement.parent.statements[statement.parent.statements.indexOf(statement) + 1];
235
+ const symbol = this.checker.getSymbolAtLocation(identifier);
236
+ if (!symbol || !next || !ts.isIfStatement(next) || !this.truthyGuard(next.expression, symbol))
237
+ return false;
238
+ return (this.references.get(symbol) ?? []).every(node => {
239
+ if (ts.isVariableDeclaration(node.parent) && node.parent.name === node)
240
+ return true;
241
+ if (ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)
242
+ return ts.isCallExpression(node.parent.right) && this.checker.getSymbolAtLocation(node.parent.right.expression) === this.checker.getSymbolAtLocation(call.expression) &&
243
+ node.parent.parent.parent === statement.parent;
244
+ if (ts.isPrefixUnaryExpression(node.parent) || ts.isPostfixUnaryExpression(node.parent))
245
+ return false;
246
+ for (let at = node; at.parent && !ts.isFunctionLike(at.parent); at = at.parent) {
247
+ const parent = at.parent;
248
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && this.truthyGuard(parent.left, symbol))
249
+ return true;
250
+ if (ts.isIfStatement(parent) && (parent.expression === at || parent.thenStatement === at) && this.truthyGuard(parent.expression, symbol))
251
+ return true;
252
+ }
253
+ return false;
254
+ });
255
+ }
256
+ truthyGuard(expression, symbol) {
257
+ expression = unwrapHandler(expression);
258
+ return ts.isIdentifier(expression) ? this.checker.getSymbolAtLocation(expression) === symbol :
259
+ ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && this.truthyGuard(expression.left, symbol);
260
+ }
261
+ }
@@ -0,0 +1,17 @@
1
+ import ts from "typescript";
2
+ import { type FunctionNode } from "./function-nodes.js";
3
+ import { type ComplexityBreakdown } from "./complexity-breakdown.js";
4
+ export interface FunctionMeasurement {
5
+ id: string;
6
+ file: string;
7
+ project: string;
8
+ member: string;
9
+ line: number;
10
+ sourceSpanStart: number;
11
+ ownComplexity: number;
12
+ ownSourceLines: number;
13
+ ownHalsteadVolume: number;
14
+ ownMaintainabilityIndex: number;
15
+ complexityBreakdown?: ComplexityBreakdown;
16
+ }
17
+ export declare function measureFunction(node: FunctionNode, source: ts.SourceFile, file: string, project: string, member: string): FunctionMeasurement;