@titan-design/code-graph 0.6.0 → 0.8.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.
- package/README.md +48 -6
- package/dist/index.d.ts +43 -12
- package/dist/index.js +986 -83
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -189,6 +189,9 @@ function readMetricAggregates(stmts, snapshotId, name) {
|
|
|
189
189
|
|
|
190
190
|
// src/store.ts
|
|
191
191
|
var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
|
|
192
|
+
function isStructuralLayer(edge) {
|
|
193
|
+
return edge.kind !== "references" && edge.kind !== "calls";
|
|
194
|
+
}
|
|
192
195
|
var CodeGraphStore = class {
|
|
193
196
|
constructor(db) {
|
|
194
197
|
this.db = db;
|
|
@@ -307,10 +310,10 @@ var CodeGraphStore = class {
|
|
|
307
310
|
const nodes = this.statements.listNodes.all(snapshotId).map(rowToNode);
|
|
308
311
|
return opts?.includeSymbols ? nodes : nodes.filter((n) => n.kind !== "symbol");
|
|
309
312
|
}
|
|
310
|
-
/** See {@link listNodes}: `references` edges are the symbol layer, excluded by default. */
|
|
313
|
+
/** See {@link listNodes}: `references` and `calls` edges are the symbol layer, excluded by default. */
|
|
311
314
|
listEdges(snapshotId, opts) {
|
|
312
315
|
const edges = this.statements.listEdges.all(snapshotId).map(rowToEdge);
|
|
313
|
-
return opts?.includeReferences ? edges : edges.filter(
|
|
316
|
+
return opts?.includeReferences ? edges : edges.filter(isStructuralLayer);
|
|
314
317
|
}
|
|
315
318
|
listMetrics(snapshotId) {
|
|
316
319
|
return this.statements.listMetrics.all(snapshotId).map(rowToMetric);
|
|
@@ -327,10 +330,10 @@ var CodeGraphStore = class {
|
|
|
327
330
|
topByMetric(opts) {
|
|
328
331
|
return readTopByMetric(this.targeted, opts);
|
|
329
332
|
}
|
|
330
|
-
/** Edges into or out of one node;
|
|
333
|
+
/** Edges into or out of one node; symbol-layer edges are excluded unless asked, as in {@link listEdges}. */
|
|
331
334
|
listEdgesTouching(snapshotId, nodeId, opts) {
|
|
332
335
|
const edges = readEdgesTouching(this.targeted, snapshotId, nodeId);
|
|
333
|
-
return opts?.includeReferences ? edges : edges.filter(
|
|
336
|
+
return opts?.includeReferences ? edges : edges.filter(isStructuralLayer);
|
|
334
337
|
}
|
|
335
338
|
/** Count, sum, min, and max per metric name and node kind; pass `name` for one metric. */
|
|
336
339
|
aggregateMetrics(snapshotId, opts) {
|
|
@@ -447,12 +450,10 @@ function runPrune(store, options = {}) {
|
|
|
447
450
|
|
|
448
451
|
// src/indexer.ts
|
|
449
452
|
import { realpathSync } from "fs";
|
|
450
|
-
import * as
|
|
453
|
+
import * as path8 from "path";
|
|
451
454
|
import { parseFile } from "@titan-design/code-parser";
|
|
452
455
|
|
|
453
456
|
// src/extractors/ts-morph-extractor.ts
|
|
454
|
-
import * as path4 from "path";
|
|
455
|
-
import { existsSync } from "fs";
|
|
456
457
|
import {
|
|
457
458
|
Project,
|
|
458
459
|
ScriptTarget,
|
|
@@ -664,20 +665,24 @@ var TS_DECL_TYPES = /* @__PURE__ */ new Set([
|
|
|
664
665
|
]);
|
|
665
666
|
var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
|
|
666
667
|
function collectDeclarations(file) {
|
|
667
|
-
const python = file.language === "python";
|
|
668
|
-
const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
|
|
669
668
|
const out = [];
|
|
670
|
-
|
|
671
|
-
const named = declaredNodeAt(node, declTypes);
|
|
672
|
-
if (!named) return;
|
|
669
|
+
forEachDeclaration(file, (name, qualifiedName, node) => {
|
|
673
670
|
out.push({
|
|
674
|
-
name
|
|
675
|
-
qualifiedName
|
|
676
|
-
span: { startLine:
|
|
671
|
+
name,
|
|
672
|
+
qualifiedName,
|
|
673
|
+
span: { startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }
|
|
677
674
|
});
|
|
678
675
|
});
|
|
679
676
|
return out;
|
|
680
677
|
}
|
|
678
|
+
function forEachDeclaration(file, visit) {
|
|
679
|
+
const python = file.language === "python";
|
|
680
|
+
const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
|
|
681
|
+
walkScopes(file.tree.rootNode, python, (node, scope) => {
|
|
682
|
+
const named = declaredNodeAt(node, declTypes);
|
|
683
|
+
if (named) visit(named.name, qualify(scope, named.name), named.node);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
681
686
|
function collectDeclaredSpans(file) {
|
|
682
687
|
const spans = /* @__PURE__ */ new Map();
|
|
683
688
|
for (const d of collectDeclarations(file)) spans.set(d.qualifiedName, d.span);
|
|
@@ -698,6 +703,80 @@ function declaredNodeAt(node, declTypes) {
|
|
|
698
703
|
return null;
|
|
699
704
|
}
|
|
700
705
|
|
|
706
|
+
// src/extractors/call-sites.ts
|
|
707
|
+
function addCallSite(agg, srcId, dstId, site) {
|
|
708
|
+
const key = JSON.stringify([srcId, dstId]);
|
|
709
|
+
const existing = agg.get(key);
|
|
710
|
+
if (existing) existing.attrs.sites.push(site);
|
|
711
|
+
else agg.set(key, { srcId, dstId, kind: "calls", attrs: { sites: [site] } });
|
|
712
|
+
}
|
|
713
|
+
function paramAttrs(shape) {
|
|
714
|
+
if (!shape) return {};
|
|
715
|
+
return shape.positional === shape.params.length ? { params: shape.params } : { params: shape.params, positionalParams: shape.positional };
|
|
716
|
+
}
|
|
717
|
+
function readParamShape(attrs) {
|
|
718
|
+
const params = attrs?.params;
|
|
719
|
+
if (!Array.isArray(params)) return void 0;
|
|
720
|
+
const positional = typeof attrs?.positionalParams === "number" ? attrs.positionalParams : params.length;
|
|
721
|
+
return { params, positional };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/extractors/callable-params.ts
|
|
725
|
+
var TS_PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
|
|
726
|
+
function collectParamShapes(file) {
|
|
727
|
+
const out = /* @__PURE__ */ new Map();
|
|
728
|
+
forEachDeclaration(file, (_name, qualifiedName, node) => {
|
|
729
|
+
const shape = file.language === "python" ? pythonShape(node) : tsShape(node);
|
|
730
|
+
if (shape) out.set(qualifiedName, shape);
|
|
731
|
+
});
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
function tsShape(fn) {
|
|
735
|
+
if (fn.type === "class_declaration") return null;
|
|
736
|
+
const single = fn.childForFieldName("parameter");
|
|
737
|
+
if (single) return { params: [single.text], positional: 1 };
|
|
738
|
+
const params = [];
|
|
739
|
+
for (const param of fn.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
740
|
+
if (!param || !TS_PARAM_TYPES.has(param.type)) continue;
|
|
741
|
+
const pattern = param.childForFieldName("pattern");
|
|
742
|
+
if (!pattern || pattern.type === "this") continue;
|
|
743
|
+
if (pattern.type === "rest_pattern") break;
|
|
744
|
+
params.push(pattern.type === "identifier" ? pattern.text : `$${params.length}`);
|
|
745
|
+
}
|
|
746
|
+
return { params, positional: params.length };
|
|
747
|
+
}
|
|
748
|
+
function pythonShape(fn) {
|
|
749
|
+
if (fn.type !== "function_definition") return null;
|
|
750
|
+
const names = [];
|
|
751
|
+
let positionalEnd = null;
|
|
752
|
+
for (const param of fn.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
753
|
+
if (!param) continue;
|
|
754
|
+
if (param.type === "keyword_separator" || param.type === "list_splat_pattern") positionalEnd ??= names.length;
|
|
755
|
+
const name = pythonParamName(param);
|
|
756
|
+
if (name) names.push(name);
|
|
757
|
+
}
|
|
758
|
+
const receiver = hasReceiver(fn) && names.length > 0 ? 1 : 0;
|
|
759
|
+
return { params: names.slice(receiver), positional: Math.max((positionalEnd ?? names.length) - receiver, 0) };
|
|
760
|
+
}
|
|
761
|
+
function pythonParamName(param) {
|
|
762
|
+
if (param.type === "identifier") return param.text;
|
|
763
|
+
if (param.type === "default_parameter" || param.type === "typed_default_parameter") {
|
|
764
|
+
return param.childForFieldName("name")?.text ?? null;
|
|
765
|
+
}
|
|
766
|
+
if (param.type === "typed_parameter") {
|
|
767
|
+
const first = param.namedChildren[0];
|
|
768
|
+
return first?.type === "identifier" ? first.text : null;
|
|
769
|
+
}
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
function hasReceiver(fn) {
|
|
773
|
+
const decorated = fn.parent?.type === "decorated_definition" ? fn.parent : null;
|
|
774
|
+
const block = (decorated ?? fn).parent;
|
|
775
|
+
if (block?.type !== "block" || block.parent?.type !== "class_definition") return false;
|
|
776
|
+
const decorators = decorated?.namedChildren.filter((c) => c?.type === "decorator") ?? [];
|
|
777
|
+
return !decorators.some((d) => d?.text === "@staticmethod");
|
|
778
|
+
}
|
|
779
|
+
|
|
701
780
|
// src/extractors/symbol-signature.ts
|
|
702
781
|
import {
|
|
703
782
|
Node as Node2
|
|
@@ -776,8 +855,8 @@ function clamp(s, max = MAX_SIGNATURE) {
|
|
|
776
855
|
}
|
|
777
856
|
|
|
778
857
|
// src/extractors/symbol-nodes.ts
|
|
779
|
-
function makeSymbolNode(fileId2, name, exported, span, text) {
|
|
780
|
-
const attrs = { exported };
|
|
858
|
+
function makeSymbolNode(fileId2, name, exported, span, text, shape) {
|
|
859
|
+
const attrs = { exported, ...paramAttrs(shape) };
|
|
781
860
|
if (span) {
|
|
782
861
|
attrs.startLine = span.startLine;
|
|
783
862
|
attrs.endLine = span.endLine;
|
|
@@ -796,18 +875,19 @@ function makeSymbolNode(fileId2, name, exported, span, text) {
|
|
|
796
875
|
function buildSymbolNodes(repoRoot, sourceFile, file) {
|
|
797
876
|
const fId = fileId(repoRoot, sourceFile.getFilePath());
|
|
798
877
|
const spans = collectDeclaredSpans(file);
|
|
878
|
+
const shapes = collectParamShapes(file);
|
|
799
879
|
const out = [];
|
|
800
880
|
const exported = /* @__PURE__ */ new Set();
|
|
801
881
|
for (const [name, decls] of sourceFile.getExportedDeclarations()) {
|
|
802
882
|
const own = decls.find((d) => d.getSourceFile() === sourceFile);
|
|
803
883
|
if (!own) continue;
|
|
804
884
|
exported.add(name);
|
|
805
|
-
out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own)));
|
|
885
|
+
out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own), shapes.get(name)));
|
|
806
886
|
}
|
|
807
887
|
for (const [name, span] of spans) {
|
|
808
888
|
if (exported.has(name)) continue;
|
|
809
889
|
const decl = lookupDeclaration(sourceFile, name);
|
|
810
|
-
out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}));
|
|
890
|
+
out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}, shapes.get(name)));
|
|
811
891
|
}
|
|
812
892
|
return out;
|
|
813
893
|
}
|
|
@@ -879,6 +959,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
|
|
|
879
959
|
|
|
880
960
|
// src/extractors/module-resolution.ts
|
|
881
961
|
import * as path3 from "path";
|
|
962
|
+
import { existsSync } from "fs";
|
|
882
963
|
var RESOLVABLE_EXTS = [
|
|
883
964
|
".ts",
|
|
884
965
|
".tsx",
|
|
@@ -916,6 +997,17 @@ function inRepoFileId(repoRoot, abs) {
|
|
|
916
997
|
if (relative4.split(path3.sep).includes("node_modules")) return null;
|
|
917
998
|
return fileId(repoRoot, abs);
|
|
918
999
|
}
|
|
1000
|
+
function remapDistToSrc(abs) {
|
|
1001
|
+
const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
|
|
1002
|
+
if (!m) return abs;
|
|
1003
|
+
const base = m[1];
|
|
1004
|
+
const sub = m[2];
|
|
1005
|
+
for (const ext of [".ts", ".tsx"]) {
|
|
1006
|
+
const candidate = path3.join(base, "src", sub + ext);
|
|
1007
|
+
if (existsSync(candidate)) return candidate;
|
|
1008
|
+
}
|
|
1009
|
+
return abs;
|
|
1010
|
+
}
|
|
919
1011
|
|
|
920
1012
|
// src/extractors/reexport-resolve.ts
|
|
921
1013
|
function resolveReExportOrigin(ctx, importerAbs, specifier, name) {
|
|
@@ -962,6 +1054,125 @@ function reExportInwardName(ed, name) {
|
|
|
962
1054
|
return name;
|
|
963
1055
|
}
|
|
964
1056
|
|
|
1057
|
+
// src/extractors/ts-calls.ts
|
|
1058
|
+
import { Node as Node5, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
1059
|
+
|
|
1060
|
+
// src/extractors/ts-scope.ts
|
|
1061
|
+
import { Node as Node4 } from "ts-morph";
|
|
1062
|
+
function bindingName2(node) {
|
|
1063
|
+
const parent = node.getParent();
|
|
1064
|
+
if (Node4.isVariableDeclaration(parent) || Node4.isPropertyDeclaration(parent)) return parent.getName();
|
|
1065
|
+
if (Node4.isPropertyAssignment(parent)) return parent.getNameNode().getText();
|
|
1066
|
+
if (Node4.isExportAssignment(parent)) return "default";
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
function declaredName(node) {
|
|
1070
|
+
const name = node.getName();
|
|
1071
|
+
if (name) return name;
|
|
1072
|
+
return Node4.isExportable(node) && node.isDefaultExport() ? "default" : null;
|
|
1073
|
+
}
|
|
1074
|
+
function scopeSegment2(node) {
|
|
1075
|
+
if (Node4.isClassDeclaration(node) || Node4.isFunctionDeclaration(node)) return declaredName(node);
|
|
1076
|
+
if (Node4.isMethodDeclaration(node) || Node4.isGetAccessorDeclaration(node) || Node4.isSetAccessorDeclaration(node)) {
|
|
1077
|
+
return node.getName();
|
|
1078
|
+
}
|
|
1079
|
+
if (Node4.isConstructorDeclaration(node)) return "constructor";
|
|
1080
|
+
if (Node4.isModuleDeclaration(node)) return node.hasNamespaceKeyword() ? node.getName() : null;
|
|
1081
|
+
if (Node4.isClassExpression(node) && node.getName()) return node.getName() ?? null;
|
|
1082
|
+
const bound = Node4.isArrowFunction(node) || Node4.isFunctionExpression(node) || Node4.isClassExpression(node) || Node4.isObjectLiteralExpression(node);
|
|
1083
|
+
return bound ? bindingName2(node) ?? ANONYMOUS_SCOPE : null;
|
|
1084
|
+
}
|
|
1085
|
+
function scopeOf(node) {
|
|
1086
|
+
const segments = node.getAncestors().map(scopeSegment2).filter((s) => s !== null).reverse();
|
|
1087
|
+
let scope = "";
|
|
1088
|
+
for (const segment of segments) {
|
|
1089
|
+
const repeatsAnonymous = segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`));
|
|
1090
|
+
if (!repeatsAnonymous) scope = qualify(scope, segment);
|
|
1091
|
+
}
|
|
1092
|
+
return scope;
|
|
1093
|
+
}
|
|
1094
|
+
function declarationQualifiedName(decl) {
|
|
1095
|
+
if (Node4.isVariableDeclaration(decl)) {
|
|
1096
|
+
const init = decl.getInitializer();
|
|
1097
|
+
const callable = Node4.isArrowFunction(init) || Node4.isFunctionExpression(init);
|
|
1098
|
+
return callable ? qualify(scopeOf(decl), decl.getName()) : null;
|
|
1099
|
+
}
|
|
1100
|
+
if (Node4.isArrowFunction(decl) || Node4.isFunctionExpression(decl)) {
|
|
1101
|
+
const parent = decl.getParent();
|
|
1102
|
+
return Node4.isVariableDeclaration(parent) ? qualify(scopeOf(parent), parent.getName()) : null;
|
|
1103
|
+
}
|
|
1104
|
+
const isDeclaration = Node4.isFunctionDeclaration(decl) || Node4.isClassDeclaration(decl) || Node4.isMethodDeclaration(decl) || Node4.isGetAccessorDeclaration(decl) || Node4.isSetAccessorDeclaration(decl) || Node4.isConstructorDeclaration(decl);
|
|
1105
|
+
const own = isDeclaration ? scopeSegment2(decl) : null;
|
|
1106
|
+
return own ? qualify(scopeOf(decl), own) : null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/extractors/ts-calls.ts
|
|
1110
|
+
function collectTsCallEdges(sourceFile, ctx) {
|
|
1111
|
+
const agg = /* @__PURE__ */ new Map();
|
|
1112
|
+
sourceFile.forEachDescendant((node) => {
|
|
1113
|
+
if (!Node5.isCallExpression(node) && !Node5.isNewExpression(node)) return;
|
|
1114
|
+
const dstId = resolveCallee(node, ctx);
|
|
1115
|
+
if (dstId) addCallSite(agg, callerId(node, ctx), dstId, tsCallSite(node));
|
|
1116
|
+
});
|
|
1117
|
+
return [...agg.values()];
|
|
1118
|
+
}
|
|
1119
|
+
function resolveCallee(call, ctx) {
|
|
1120
|
+
const callee = call.getExpression();
|
|
1121
|
+
if (callee.getKind() === SyntaxKind3.SuperKeyword) return null;
|
|
1122
|
+
const symbol = callee.getSymbol();
|
|
1123
|
+
if (!symbol) return null;
|
|
1124
|
+
const target = symbol.isAlias() ? symbol.getAliasedSymbol() : symbol;
|
|
1125
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1126
|
+
for (const decl of target?.getDeclarations() ?? []) {
|
|
1127
|
+
const id = declarationId(decl, ctx);
|
|
1128
|
+
if (id === null) return null;
|
|
1129
|
+
ids.add(id);
|
|
1130
|
+
}
|
|
1131
|
+
return ids.size === 1 ? [...ids][0] : null;
|
|
1132
|
+
}
|
|
1133
|
+
function declarationId(decl, ctx) {
|
|
1134
|
+
const sf = decl.getSourceFile();
|
|
1135
|
+
const abs = remapDistToSrc(sf.getFilePath());
|
|
1136
|
+
if (sf.isDeclarationFile() && abs === sf.getFilePath()) return null;
|
|
1137
|
+
const fileId2 = inRepoFileId(ctx.repoRoot, abs);
|
|
1138
|
+
const name = fileId2 ? declarationQualifiedName(decl) : null;
|
|
1139
|
+
return fileId2 && name ? symbolId(fileId2, name) : null;
|
|
1140
|
+
}
|
|
1141
|
+
function callerId(call, ctx) {
|
|
1142
|
+
for (const ancestor of call.getAncestors()) {
|
|
1143
|
+
const name = declarationQualifiedName(ancestor);
|
|
1144
|
+
if (name && ctx.localSymbols.has(name)) return symbolId(ctx.srcFileId, name);
|
|
1145
|
+
}
|
|
1146
|
+
return ctx.srcFileId;
|
|
1147
|
+
}
|
|
1148
|
+
function tsCallSite(call) {
|
|
1149
|
+
const site = { args: [] };
|
|
1150
|
+
for (const arg of call.getArguments()) {
|
|
1151
|
+
if (Node5.isSpreadElement(arg)) {
|
|
1152
|
+
site.spreadFrom ??= site.args.length;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
site.args.push(literalText(arg));
|
|
1156
|
+
}
|
|
1157
|
+
return site;
|
|
1158
|
+
}
|
|
1159
|
+
var LITERAL_KINDS = /* @__PURE__ */ new Set([
|
|
1160
|
+
SyntaxKind3.NumericLiteral,
|
|
1161
|
+
SyntaxKind3.StringLiteral,
|
|
1162
|
+
SyntaxKind3.NoSubstitutionTemplateLiteral,
|
|
1163
|
+
SyntaxKind3.TrueKeyword,
|
|
1164
|
+
SyntaxKind3.FalseKeyword,
|
|
1165
|
+
SyntaxKind3.NullKeyword
|
|
1166
|
+
]);
|
|
1167
|
+
function literalText(arg) {
|
|
1168
|
+
if (LITERAL_KINDS.has(arg.getKind())) return arg.getText();
|
|
1169
|
+
if (Node5.isIdentifier(arg) && arg.getText() === "undefined") return "undefined";
|
|
1170
|
+
if (Node5.isPrefixUnaryExpression(arg) && arg.getOperatorToken() === SyntaxKind3.MinusToken) {
|
|
1171
|
+
return Node5.isNumericLiteral(arg.getOperand()) ? arg.getText() : null;
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
965
1176
|
// src/extractors/ts-morph-extractor.ts
|
|
966
1177
|
var TsMorphGraphExtractor = class {
|
|
967
1178
|
name = "ts-morph-graph";
|
|
@@ -980,7 +1191,12 @@ var TsMorphGraphExtractor = class {
|
|
|
980
1191
|
const nodes = this.buildFileAndModuleNodes(sourceFile);
|
|
981
1192
|
const symbolNodes2 = buildSymbolNodes(this.repoRoot, sourceFile, file);
|
|
982
1193
|
const { edges, externalNodes } = this.collectEdges(sourceFile);
|
|
983
|
-
|
|
1194
|
+
const calls = collectTsCallEdges(sourceFile, {
|
|
1195
|
+
repoRoot: this.repoRoot,
|
|
1196
|
+
srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
|
|
1197
|
+
localSymbols: new Set(symbolNodes2.map((n) => n.name))
|
|
1198
|
+
});
|
|
1199
|
+
return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges: [...edges, ...calls] }];
|
|
984
1200
|
}
|
|
985
1201
|
ensureProject() {
|
|
986
1202
|
if (this.project) return this.project;
|
|
@@ -1146,27 +1362,129 @@ var TsMorphGraphExtractor = class {
|
|
|
1146
1362
|
return inRepoFileId(this.repoRoot, abs);
|
|
1147
1363
|
}
|
|
1148
1364
|
};
|
|
1149
|
-
function remapDistToSrc(abs) {
|
|
1150
|
-
const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
|
|
1151
|
-
if (!m) return abs;
|
|
1152
|
-
const base = m[1];
|
|
1153
|
-
const sub = m[2];
|
|
1154
|
-
for (const ext of [".ts", ".tsx"]) {
|
|
1155
|
-
const candidate = path4.join(base, "src", sub + ext);
|
|
1156
|
-
if (existsSync(candidate)) return candidate;
|
|
1157
|
-
}
|
|
1158
|
-
return abs;
|
|
1159
|
-
}
|
|
1160
1365
|
function isTypeScriptFile(file) {
|
|
1161
1366
|
return file.language === "typescript" || file.language === "tsx";
|
|
1162
1367
|
}
|
|
1163
1368
|
|
|
1164
1369
|
// src/extractors/python-extractor.ts
|
|
1165
|
-
import * as
|
|
1370
|
+
import * as path4 from "path";
|
|
1166
1371
|
import { existsSync as existsSync2 } from "fs";
|
|
1372
|
+
|
|
1373
|
+
// src/extractors/python-calls.ts
|
|
1374
|
+
function collectPythonCallEdges(file, fileId2, resolve4) {
|
|
1375
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
1376
|
+
forEachDeclaration(file, (_name, qualifiedName, node) => declarations.set(qualifiedName, node));
|
|
1377
|
+
const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve4) };
|
|
1378
|
+
const agg = /* @__PURE__ */ new Map();
|
|
1379
|
+
walkScopes(file.tree.rootNode, true, (node, scope) => {
|
|
1380
|
+
if (node.type !== "call") return;
|
|
1381
|
+
const dst = resolveCallee2(node.childForFieldName("function"), scope, ctx);
|
|
1382
|
+
const args = node.childForFieldName("arguments");
|
|
1383
|
+
if (!dst || args?.type !== "argument_list") return;
|
|
1384
|
+
const srcId = declarations.has(scope) ? symbolId(fileId2, scope) : fileId2;
|
|
1385
|
+
addCallSite(agg, srcId, dst, pythonCallSite(args));
|
|
1386
|
+
});
|
|
1387
|
+
return [...agg.values()];
|
|
1388
|
+
}
|
|
1389
|
+
function resolveCallee2(callee, scope, ctx) {
|
|
1390
|
+
if (callee?.type === "identifier") return resolveBareName(callee.text, scope, ctx);
|
|
1391
|
+
if (callee?.type === "attribute") return resolveSelfMethod(callee, scope, ctx);
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
function resolveBareName(name, scope, ctx) {
|
|
1395
|
+
if (isShadowed(name, scope, ctx)) return null;
|
|
1396
|
+
const local = ctx.declarations.has(name);
|
|
1397
|
+
const imported = ctx.imports.get(name);
|
|
1398
|
+
if (local && imported) return null;
|
|
1399
|
+
if (local) return symbolId(ctx.fileId, name);
|
|
1400
|
+
return imported ? symbolId(imported.fileId, imported.name) : null;
|
|
1401
|
+
}
|
|
1402
|
+
function isShadowed(name, scope, ctx) {
|
|
1403
|
+
const segments = scope ? scope.split(".") : [];
|
|
1404
|
+
for (let depth = segments.length; depth > 0; depth--) {
|
|
1405
|
+
const enclosing = segments.slice(0, depth).join(".");
|
|
1406
|
+
if (ctx.declarations.has(`${enclosing}.${name}`)) return true;
|
|
1407
|
+
if (parameterNames(ctx.declarations.get(enclosing)).includes(name)) return true;
|
|
1408
|
+
}
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
function resolveSelfMethod(callee, scope, ctx) {
|
|
1412
|
+
const receiver = callee.childForFieldName("object");
|
|
1413
|
+
const attr = callee.childForFieldName("attribute")?.text;
|
|
1414
|
+
const method = ctx.declarations.get(scope);
|
|
1415
|
+
if (receiver?.type !== "identifier" || !attr || !method || !hasReceiver(method)) return null;
|
|
1416
|
+
if (parameterNames(method)[0] !== receiver.text) return null;
|
|
1417
|
+
const owner = findMethodOwner(scope.slice(0, scope.lastIndexOf(".")), attr, ctx, /* @__PURE__ */ new Set());
|
|
1418
|
+
return owner ? symbolId(ctx.fileId, `${owner}.${attr}`) : null;
|
|
1419
|
+
}
|
|
1420
|
+
function findMethodOwner(className, attr, ctx, seen) {
|
|
1421
|
+
const cls = ctx.declarations.get(className);
|
|
1422
|
+
if (cls?.type !== "class_definition" || seen.has(className)) return null;
|
|
1423
|
+
seen.add(className);
|
|
1424
|
+
if (ctx.declarations.get(`${className}.${attr}`)?.type === "function_definition") return className;
|
|
1425
|
+
for (const base of cls.childForFieldName("superclasses")?.namedChildren ?? []) {
|
|
1426
|
+
if (base?.type !== "identifier" || !ctx.declarations.has(base.text)) return null;
|
|
1427
|
+
const owner = findMethodOwner(base.text, attr, ctx, seen);
|
|
1428
|
+
if (owner) return owner;
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
function parameterNames(def) {
|
|
1433
|
+
if (def?.type !== "function_definition") return [];
|
|
1434
|
+
const out = [];
|
|
1435
|
+
for (const param of def.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
1436
|
+
const id = param?.type === "identifier" ? param : param?.descendantsOfType("identifier")[0];
|
|
1437
|
+
if (id) out.push(id.text);
|
|
1438
|
+
}
|
|
1439
|
+
return out;
|
|
1440
|
+
}
|
|
1441
|
+
function fromImports(root, resolve4) {
|
|
1442
|
+
const out = /* @__PURE__ */ new Map();
|
|
1443
|
+
for (const stmt of root.descendantsOfType("import_from_statement")) {
|
|
1444
|
+
const moduleName = stmt?.childForFieldName("module_name")?.text;
|
|
1445
|
+
const moduleFile = moduleName ? resolve4(moduleName) : null;
|
|
1446
|
+
if (!stmt || !moduleFile) continue;
|
|
1447
|
+
for (const imported of stmt.childrenForFieldName("name")) {
|
|
1448
|
+
const alias = imported?.type === "aliased_import" ? imported.childForFieldName("alias")?.text : void 0;
|
|
1449
|
+
const name = imported?.type === "aliased_import" ? imported.childForFieldName("name")?.text : imported?.text;
|
|
1450
|
+
if (name && !name.includes(".")) out.set(alias ?? name, { fileId: moduleFile, name });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return out;
|
|
1454
|
+
}
|
|
1455
|
+
var LITERAL_TYPES = /* @__PURE__ */ new Set(["integer", "float", "true", "false", "none"]);
|
|
1456
|
+
function literalText2(arg) {
|
|
1457
|
+
if (LITERAL_TYPES.has(arg.type)) return arg.text;
|
|
1458
|
+
if (arg.type === "string") return arg.namedChildren.some((c) => c?.type === "interpolation") ? null : arg.text;
|
|
1459
|
+
if (arg.type === "unary_operator" && arg.text.startsWith("-")) {
|
|
1460
|
+
const operand = arg.childForFieldName("argument");
|
|
1461
|
+
return operand && (operand.type === "integer" || operand.type === "float") ? arg.text : null;
|
|
1462
|
+
}
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
function pythonCallSite(argumentList) {
|
|
1466
|
+
const site = { args: [] };
|
|
1467
|
+
for (const arg of argumentList.namedChildren) {
|
|
1468
|
+
if (!arg || arg.type === "comment") continue;
|
|
1469
|
+
if (arg.type === "list_splat") site.spreadFrom ??= site.args.length;
|
|
1470
|
+
else if (arg.type === "dictionary_splat") site.kwSplat = true;
|
|
1471
|
+
else if (arg.type === "keyword_argument") addKeyword(site, arg);
|
|
1472
|
+
else if (site.spreadFrom === void 0) site.args.push(literalText2(arg));
|
|
1473
|
+
}
|
|
1474
|
+
return site;
|
|
1475
|
+
}
|
|
1476
|
+
function addKeyword(site, arg) {
|
|
1477
|
+
const name = arg.childForFieldName("name")?.text;
|
|
1478
|
+
const value = arg.childForFieldName("value");
|
|
1479
|
+
if (!name || !value) return;
|
|
1480
|
+
site.kwargs ??= {};
|
|
1481
|
+
site.kwargs[name] = literalText2(value);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/extractors/python-extractor.ts
|
|
1167
1485
|
function* candidates(base) {
|
|
1168
1486
|
yield `${base}.py`;
|
|
1169
|
-
yield
|
|
1487
|
+
yield path4.join(base, "__init__.py");
|
|
1170
1488
|
}
|
|
1171
1489
|
var PythonGraphExtractor = class {
|
|
1172
1490
|
constructor(repoRoot) {
|
|
@@ -1180,7 +1498,8 @@ var PythonGraphExtractor = class {
|
|
|
1180
1498
|
const nodes = buildFileModuleNodes(this.repoRoot, file.filePath);
|
|
1181
1499
|
nodes.push(...symbolNodes(fId, file));
|
|
1182
1500
|
const { edges, externals } = this.collectImports(fId, file);
|
|
1183
|
-
|
|
1501
|
+
const calls = collectPythonCallEdges(file, fId, (specifier) => this.resolveDotted(file.filePath, specifier));
|
|
1502
|
+
return [{ nodes: [...nodes, ...externals], edges: [...edges, ...calls] }];
|
|
1184
1503
|
}
|
|
1185
1504
|
collectImports(srcId, file) {
|
|
1186
1505
|
const edges = /* @__PURE__ */ new Map();
|
|
@@ -1203,10 +1522,10 @@ var PythonGraphExtractor = class {
|
|
|
1203
1522
|
resolveDotted(fromAbs, specifier) {
|
|
1204
1523
|
const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
|
|
1205
1524
|
const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
|
|
1206
|
-
const relativeBase =
|
|
1207
|
-
const roots = leadingDots > 0 ? [relativeBase] : [
|
|
1525
|
+
const relativeBase = path4.resolve(path4.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
|
|
1526
|
+
const roots = leadingDots > 0 ? [relativeBase] : [path4.dirname(fromAbs), this.repoRoot];
|
|
1208
1527
|
for (const root of roots) {
|
|
1209
|
-
for (const candidate of candidates(
|
|
1528
|
+
for (const candidate of candidates(path4.join(root, ...parts))) {
|
|
1210
1529
|
const id = inRepoFileId(this.repoRoot, candidate);
|
|
1211
1530
|
if (id !== null && existsSync2(candidate)) return id;
|
|
1212
1531
|
}
|
|
@@ -1218,13 +1537,19 @@ function isPublicName(qualifiedName) {
|
|
|
1218
1537
|
return !qualifiedName.slice(qualifiedName.lastIndexOf(".") + 1).startsWith("_");
|
|
1219
1538
|
}
|
|
1220
1539
|
function symbolNodes(fId, file) {
|
|
1540
|
+
const shapes = collectParamShapes(file);
|
|
1221
1541
|
return [...collectDeclaredSpans(file)].map(([name, span]) => ({
|
|
1222
1542
|
id: symbolId(fId, name),
|
|
1223
1543
|
kind: "symbol",
|
|
1224
1544
|
name,
|
|
1225
1545
|
parentId: fId,
|
|
1226
1546
|
language: "python",
|
|
1227
|
-
attrs: {
|
|
1547
|
+
attrs: {
|
|
1548
|
+
exported: isPublicName(name),
|
|
1549
|
+
startLine: span.startLine,
|
|
1550
|
+
endLine: span.endLine,
|
|
1551
|
+
...paramAttrs(shapes.get(name))
|
|
1552
|
+
}
|
|
1228
1553
|
}));
|
|
1229
1554
|
}
|
|
1230
1555
|
function importSpecifiers(root) {
|
|
@@ -1257,7 +1582,7 @@ var LanguageExtractor = class {
|
|
|
1257
1582
|
};
|
|
1258
1583
|
|
|
1259
1584
|
// src/git-renames.ts
|
|
1260
|
-
import * as
|
|
1585
|
+
import * as path5 from "path";
|
|
1261
1586
|
function detectGitHead(repoRoot) {
|
|
1262
1587
|
return runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1263
1588
|
}
|
|
@@ -1298,12 +1623,12 @@ function parseRenameOutput(text) {
|
|
|
1298
1623
|
return out;
|
|
1299
1624
|
}
|
|
1300
1625
|
function classifyRename(oldPath, newPath) {
|
|
1301
|
-
return
|
|
1626
|
+
return path5.posix.dirname(oldPath) === path5.posix.dirname(newPath) ? "rename" : "move";
|
|
1302
1627
|
}
|
|
1303
1628
|
function renameToAliases(repoRoot, pair) {
|
|
1304
1629
|
const reason = classifyRename(pair.oldPath, pair.newPath);
|
|
1305
|
-
const oldAbs =
|
|
1306
|
-
const newAbs =
|
|
1630
|
+
const oldAbs = path5.join(repoRoot, pair.oldPath);
|
|
1631
|
+
const newAbs = path5.join(repoRoot, pair.newPath);
|
|
1307
1632
|
return [
|
|
1308
1633
|
{ oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
|
|
1309
1634
|
{ oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
|
|
@@ -1396,8 +1721,8 @@ function walk(requestedId, maps) {
|
|
|
1396
1721
|
}
|
|
1397
1722
|
function chainSteps(input) {
|
|
1398
1723
|
if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
|
|
1399
|
-
const
|
|
1400
|
-
if (
|
|
1724
|
+
const path11 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
|
|
1725
|
+
if (path11) return { steps: path11, connected: true };
|
|
1401
1726
|
return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
|
|
1402
1727
|
}
|
|
1403
1728
|
function createAliasChain(input) {
|
|
@@ -1478,7 +1803,7 @@ function computeAliasBridge(store, input) {
|
|
|
1478
1803
|
|
|
1479
1804
|
// src/generated.ts
|
|
1480
1805
|
import { readFileSync } from "fs";
|
|
1481
|
-
import * as
|
|
1806
|
+
import * as path6 from "path";
|
|
1482
1807
|
var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
|
|
1483
1808
|
var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
|
|
1484
1809
|
function isGeneratedByHeuristic(id) {
|
|
@@ -1490,7 +1815,7 @@ function isGeneratedFile(id, patterns) {
|
|
|
1490
1815
|
}
|
|
1491
1816
|
function loadGeneratedPatterns(rootDir) {
|
|
1492
1817
|
try {
|
|
1493
|
-
const content = readFileSync(
|
|
1818
|
+
const content = readFileSync(path6.join(rootDir, ".gitattributes"), "utf-8");
|
|
1494
1819
|
return parseGeneratedPatterns(content);
|
|
1495
1820
|
} catch {
|
|
1496
1821
|
return [];
|
|
@@ -1593,7 +1918,7 @@ function annotateRoles(nodes, options) {
|
|
|
1593
1918
|
|
|
1594
1919
|
// src/file-walk.ts
|
|
1595
1920
|
import * as fs from "fs/promises";
|
|
1596
|
-
import * as
|
|
1921
|
+
import * as path7 from "path";
|
|
1597
1922
|
import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
|
|
1598
1923
|
async function walkSourceFiles(rootDirs, languages) {
|
|
1599
1924
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1606,12 +1931,12 @@ async function walkSourceFiles(rootDirs, languages) {
|
|
|
1606
1931
|
async function collectFiles(dir, rootDir, languages, seen, out) {
|
|
1607
1932
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1608
1933
|
for (const entry of entries) {
|
|
1609
|
-
const full =
|
|
1934
|
+
const full = path7.join(dir, entry.name);
|
|
1610
1935
|
if (entry.isDirectory()) {
|
|
1611
1936
|
if (isExcludedDir(entry.name)) continue;
|
|
1612
1937
|
await collectFiles(full, rootDir, languages, seen, out);
|
|
1613
1938
|
} else if (entry.isFile() && !seen.has(full)) {
|
|
1614
|
-
const relative4 =
|
|
1939
|
+
const relative4 = path7.relative(rootDir, full);
|
|
1615
1940
|
if (shouldIncludeFile(relative4, [...languages])) {
|
|
1616
1941
|
seen.add(full);
|
|
1617
1942
|
out.push(full);
|
|
@@ -1627,7 +1952,8 @@ function edgeWeight(e) {
|
|
|
1627
1952
|
}
|
|
1628
1953
|
function pruneDanglingReferences(nodes, edges) {
|
|
1629
1954
|
for (const [key, edge] of edges) {
|
|
1630
|
-
|
|
1955
|
+
const symbolLayer = edge.kind === "references" || edge.kind === "calls";
|
|
1956
|
+
if (symbolLayer && (!nodes.has(edge.dstId) || !nodes.has(edge.srcId))) edges.delete(key);
|
|
1631
1957
|
}
|
|
1632
1958
|
}
|
|
1633
1959
|
function resolveBarrelEdges(nodes, edges) {
|
|
@@ -1685,6 +2011,234 @@ import { createHash } from "crypto";
|
|
|
1685
2011
|
import * as fs2 from "fs/promises";
|
|
1686
2012
|
import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
|
|
1687
2013
|
|
|
2014
|
+
// src/analysis/comment-lines.ts
|
|
2015
|
+
function commentLineStats(fn, body, lines) {
|
|
2016
|
+
const docstring = pythonDocstring(body);
|
|
2017
|
+
const comments = functionComments(fn, body);
|
|
2018
|
+
const commentLines = rowsOf(comments).size;
|
|
2019
|
+
const bodyLines = codeRows(body, docstring, comments, lines);
|
|
2020
|
+
return {
|
|
2021
|
+
commentLines,
|
|
2022
|
+
docstringLines: docstring ? rowSpan(docstring) : jsDocLines(fn),
|
|
2023
|
+
bodyLines,
|
|
2024
|
+
commentRatio: commentLines / Math.max(bodyLines, 1)
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function pythonDocstring(body) {
|
|
2028
|
+
if (body.type !== "block") return null;
|
|
2029
|
+
const first = body.namedChildren.find((c) => c !== null && c.type !== "comment");
|
|
2030
|
+
if (first?.type !== "expression_statement" || first.namedChildCount !== 1) return null;
|
|
2031
|
+
return first.namedChildren[0]?.type === "string" ? first : null;
|
|
2032
|
+
}
|
|
2033
|
+
function bodyStatements(body, docstring = pythonDocstring(body)) {
|
|
2034
|
+
if (body.type !== "block" && body.type !== "statement_block") return [body];
|
|
2035
|
+
return body.namedChildren.filter(
|
|
2036
|
+
(c) => c !== null && c.type !== "comment" && c.id !== docstring?.id
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
function functionComments(fn, body) {
|
|
2040
|
+
if (body.type !== "block") return descendantsOfType(body, "comment");
|
|
2041
|
+
const signatureEnd = fn.childForFieldName("parameters")?.endIndex ?? body.startIndex;
|
|
2042
|
+
return descendantsOfType(fn, "comment").filter((c) => c.startIndex >= signatureEnd);
|
|
2043
|
+
}
|
|
2044
|
+
function descendantsOfType(root, type) {
|
|
2045
|
+
const out = [];
|
|
2046
|
+
const visit = (node) => {
|
|
2047
|
+
if (node.type === type) out.push(node);
|
|
2048
|
+
for (const child of node.namedChildren) if (child) visit(child);
|
|
2049
|
+
};
|
|
2050
|
+
visit(root);
|
|
2051
|
+
return out;
|
|
2052
|
+
}
|
|
2053
|
+
function rowSpan(node) {
|
|
2054
|
+
return node.endPosition.row - node.startPosition.row + 1;
|
|
2055
|
+
}
|
|
2056
|
+
function rowsOf(nodes) {
|
|
2057
|
+
const rows = /* @__PURE__ */ new Set();
|
|
2058
|
+
for (const n of nodes) {
|
|
2059
|
+
for (let r = n.startPosition.row; r <= n.endPosition.row; r++) rows.add(r);
|
|
2060
|
+
}
|
|
2061
|
+
return rows;
|
|
2062
|
+
}
|
|
2063
|
+
function codeRows(body, docstring, comments, lines) {
|
|
2064
|
+
let count = 0;
|
|
2065
|
+
for (const row of rowsOf(bodyStatements(body, docstring))) {
|
|
2066
|
+
if (codeOnRow(row, lines[row] ?? "", comments).trim() !== "") count++;
|
|
2067
|
+
}
|
|
2068
|
+
return count;
|
|
2069
|
+
}
|
|
2070
|
+
function codeOnRow(row, text, comments) {
|
|
2071
|
+
let out = text;
|
|
2072
|
+
for (const c of comments) {
|
|
2073
|
+
if (row < c.startPosition.row || row > c.endPosition.row) continue;
|
|
2074
|
+
const from = row === c.startPosition.row ? c.startPosition.column : 0;
|
|
2075
|
+
const to = row === c.endPosition.row ? c.endPosition.column : out.length;
|
|
2076
|
+
out = out.slice(0, from) + " ".repeat(Math.max(to - from, 0)) + out.slice(to);
|
|
2077
|
+
}
|
|
2078
|
+
return out;
|
|
2079
|
+
}
|
|
2080
|
+
function jsDocLines(fn) {
|
|
2081
|
+
const anchor = declarationAnchor(fn);
|
|
2082
|
+
const prev = anchor.previousNamedSibling;
|
|
2083
|
+
if (prev?.type !== "comment" || !prev.text.startsWith("/**")) return 0;
|
|
2084
|
+
const gap = anchor.startPosition.row - prev.endPosition.row;
|
|
2085
|
+
return gap === 0 || gap === 1 ? rowSpan(prev) : 0;
|
|
2086
|
+
}
|
|
2087
|
+
function declarationAnchor(fn) {
|
|
2088
|
+
let anchor = fn;
|
|
2089
|
+
if (fn.parent?.type === "variable_declarator") anchor = fn.parent.parent ?? fn;
|
|
2090
|
+
while (anchor.parent?.type === "export_statement") anchor = anchor.parent;
|
|
2091
|
+
return anchor;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// src/analysis/narrating-comments.ts
|
|
2095
|
+
var MIN_SHARED = 2;
|
|
2096
|
+
var MIN_SHARE_OF_COMMENT = 0.5;
|
|
2097
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2098
|
+
"the",
|
|
2099
|
+
"and",
|
|
2100
|
+
"for",
|
|
2101
|
+
"this",
|
|
2102
|
+
"that",
|
|
2103
|
+
"with",
|
|
2104
|
+
"from",
|
|
2105
|
+
"into",
|
|
2106
|
+
"then",
|
|
2107
|
+
"else",
|
|
2108
|
+
"when",
|
|
2109
|
+
"not",
|
|
2110
|
+
"are",
|
|
2111
|
+
"was",
|
|
2112
|
+
"has",
|
|
2113
|
+
"have",
|
|
2114
|
+
"its",
|
|
2115
|
+
"our",
|
|
2116
|
+
"all",
|
|
2117
|
+
"any",
|
|
2118
|
+
"can",
|
|
2119
|
+
"will",
|
|
2120
|
+
"just",
|
|
2121
|
+
"now",
|
|
2122
|
+
"return",
|
|
2123
|
+
"const",
|
|
2124
|
+
"let",
|
|
2125
|
+
"var",
|
|
2126
|
+
"def",
|
|
2127
|
+
"self",
|
|
2128
|
+
"none",
|
|
2129
|
+
"true",
|
|
2130
|
+
"false",
|
|
2131
|
+
"null",
|
|
2132
|
+
"undefined",
|
|
2133
|
+
"new",
|
|
2134
|
+
"await",
|
|
2135
|
+
"async",
|
|
2136
|
+
"function"
|
|
2137
|
+
]);
|
|
2138
|
+
var IDENTIFIER_TYPES = /* @__PURE__ */ new Set([
|
|
2139
|
+
"identifier",
|
|
2140
|
+
"property_identifier",
|
|
2141
|
+
"shorthand_property_identifier",
|
|
2142
|
+
"type_identifier"
|
|
2143
|
+
]);
|
|
2144
|
+
function countNarratingComments(fn, body) {
|
|
2145
|
+
let count = 0;
|
|
2146
|
+
for (const comment of functionComments(fn, body)) {
|
|
2147
|
+
const target = trailedStatement(comment) ?? nextStatement(comment);
|
|
2148
|
+
if (target && narrates(comment.text, target)) count++;
|
|
2149
|
+
}
|
|
2150
|
+
return count;
|
|
2151
|
+
}
|
|
2152
|
+
function trailedStatement(comment) {
|
|
2153
|
+
const prev = comment.previousNamedSibling;
|
|
2154
|
+
if (!prev || prev.type === "comment") return null;
|
|
2155
|
+
return prev.endPosition.row === comment.startPosition.row ? prev : null;
|
|
2156
|
+
}
|
|
2157
|
+
function identifierTokens(text) {
|
|
2158
|
+
const words = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/);
|
|
2159
|
+
const out = /* @__PURE__ */ new Set();
|
|
2160
|
+
for (const w of words) {
|
|
2161
|
+
const t = w.toLowerCase();
|
|
2162
|
+
if (t.length >= 3 && !STOPWORDS.has(t)) out.add(t);
|
|
2163
|
+
}
|
|
2164
|
+
return out;
|
|
2165
|
+
}
|
|
2166
|
+
function nextStatement(comment) {
|
|
2167
|
+
let next = comment.nextNamedSibling;
|
|
2168
|
+
while (next?.type === "comment") next = next.nextNamedSibling;
|
|
2169
|
+
if (next?.type !== "block") return next;
|
|
2170
|
+
return next.namedChildren.find((c) => c !== null && c.type !== "comment") ?? null;
|
|
2171
|
+
}
|
|
2172
|
+
function narrates(commentText, statement) {
|
|
2173
|
+
const said = identifierTokens(commentText);
|
|
2174
|
+
if (said.size === 0) return false;
|
|
2175
|
+
const code = identifierTokens(statementIdentifiers(statement).join(" "));
|
|
2176
|
+
let shared = 0;
|
|
2177
|
+
for (const t of said) if (code.has(t)) shared++;
|
|
2178
|
+
return shared >= MIN_SHARED || shared / said.size >= MIN_SHARE_OF_COMMENT;
|
|
2179
|
+
}
|
|
2180
|
+
function statementIdentifiers(statement) {
|
|
2181
|
+
const out = [];
|
|
2182
|
+
const visit = (node) => {
|
|
2183
|
+
if (IDENTIFIER_TYPES.has(node.type)) out.push(node.text);
|
|
2184
|
+
for (const child of node.namedChildren) if (child) visit(child);
|
|
2185
|
+
};
|
|
2186
|
+
visit(statement);
|
|
2187
|
+
return out;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/analysis/exception-handling.ts
|
|
2191
|
+
var EXCEPTION_METRIC_NAMES = ["except_count", "except_density", "swallowed_except"];
|
|
2192
|
+
var HANDLER_TYPES = ["except_clause", "except_group_clause", "catch_clause"];
|
|
2193
|
+
var SILENT_STATEMENTS = /* @__PURE__ */ new Set(["pass_statement", "continue_statement", "empty_statement"]);
|
|
2194
|
+
var EMPTY_VALUES = /* @__PURE__ */ new Set(["None", "null", "undefined"]);
|
|
2195
|
+
var LOGGING_TOKENS = /* @__PURE__ */ new Set(["log", "logger", "logging", "warn", "warning", "warnings", "print", "console"]);
|
|
2196
|
+
var EXCEPT_CLAUSE_TYPES = /* @__PURE__ */ new Set(["except_clause", "except_group_clause"]);
|
|
2197
|
+
var OPTIONAL_IMPORT_EXCEPT_TYPES = /* @__PURE__ */ new Set(["ImportError", "ModuleNotFoundError"]);
|
|
2198
|
+
function exceptionMetrics(nodeId, root, loc) {
|
|
2199
|
+
const handlers = HANDLER_TYPES.flatMap((type) => descendantsOfType(root, type));
|
|
2200
|
+
const swallowed = handlers.filter((h) => isSwallowed(h)).length;
|
|
2201
|
+
return [
|
|
2202
|
+
{ nodeId, name: "except_count", value: handlers.length, unit: "count" },
|
|
2203
|
+
{ nodeId, name: "except_density", value: loc === 0 ? 0 : handlers.length * 100 / loc, unit: "per100loc" },
|
|
2204
|
+
{ nodeId, name: "swallowed_except", value: swallowed, unit: "count" }
|
|
2205
|
+
];
|
|
2206
|
+
}
|
|
2207
|
+
function isSwallowed(handler) {
|
|
2208
|
+
if (isOptionalImportExcept(handler)) return false;
|
|
2209
|
+
const body = handler.childForFieldName("body") ?? handler.namedChildren.find((c) => c?.type === "block");
|
|
2210
|
+
if (!body) return false;
|
|
2211
|
+
const statements = bodyStatements(body, null);
|
|
2212
|
+
if (statements.length === 0) return true;
|
|
2213
|
+
return statements.length === 1 && isSilent(statements[0]);
|
|
2214
|
+
}
|
|
2215
|
+
function isOptionalImportExcept(handler) {
|
|
2216
|
+
if (!EXCEPT_CLAUSE_TYPES.has(handler.type)) return false;
|
|
2217
|
+
const value = handler.childForFieldName("value");
|
|
2218
|
+
if (!value) return false;
|
|
2219
|
+
const typeExpr = value.type === "as_pattern" ? value.namedChild(0) : value;
|
|
2220
|
+
if (!typeExpr) return false;
|
|
2221
|
+
if (typeExpr.type === "identifier") return OPTIONAL_IMPORT_EXCEPT_TYPES.has(typeExpr.text);
|
|
2222
|
+
if (typeExpr.type !== "tuple") return false;
|
|
2223
|
+
const names = typeExpr.namedChildren.filter((c) => c !== null);
|
|
2224
|
+
return names.length > 0 && names.every((n) => n.type === "identifier" && OPTIONAL_IMPORT_EXCEPT_TYPES.has(n.text));
|
|
2225
|
+
}
|
|
2226
|
+
function isSilent(statement) {
|
|
2227
|
+
if (SILENT_STATEMENTS.has(statement.type)) return true;
|
|
2228
|
+
const value = statement.namedChildren[0];
|
|
2229
|
+
if (statement.type === "return_statement") return !value || EMPTY_VALUES.has(value.text);
|
|
2230
|
+
if (statement.type !== "expression_statement" || !value) return false;
|
|
2231
|
+
if (value.type === "ellipsis") return true;
|
|
2232
|
+
return isLoggingCall(value);
|
|
2233
|
+
}
|
|
2234
|
+
function isLoggingCall(node) {
|
|
2235
|
+
if (node.type !== "call" && node.type !== "call_expression") return false;
|
|
2236
|
+
const callee = node.childForFieldName("function");
|
|
2237
|
+
if (!callee) return false;
|
|
2238
|
+
for (const token of identifierTokens(callee.text)) if (LOGGING_TOKENS.has(token)) return true;
|
|
2239
|
+
return false;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
1688
2242
|
// src/cognitive-complexity.ts
|
|
1689
2243
|
function cognitiveComplexityOf(body, language) {
|
|
1690
2244
|
const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
|
|
@@ -1937,12 +2491,97 @@ function areLinked(a, b) {
|
|
|
1937
2491
|
return false;
|
|
1938
2492
|
}
|
|
1939
2493
|
|
|
2494
|
+
// src/analysis/pass-through.ts
|
|
2495
|
+
var RECEIVERS = /* @__PURE__ */ new Set(["self", "cls"]);
|
|
2496
|
+
var CALL_TYPES = /* @__PURE__ */ new Set(["call", "call_expression"]);
|
|
2497
|
+
var WRAPPER_TYPES = /* @__PURE__ */ new Set(["return_statement", "expression_statement"]);
|
|
2498
|
+
var PY_NAMED_PARAMS = /* @__PURE__ */ new Set(["default_parameter", "typed_default_parameter"]);
|
|
2499
|
+
var PY_SEPARATORS = /* @__PURE__ */ new Set(["keyword_separator", "positional_separator"]);
|
|
2500
|
+
function isPassThrough(fn, body) {
|
|
2501
|
+
const call = soleCall(body);
|
|
2502
|
+
if (!call) return false;
|
|
2503
|
+
const params = parameterShapes(fn);
|
|
2504
|
+
if (!params || params.length === 0) return false;
|
|
2505
|
+
const args = argumentShapes(call);
|
|
2506
|
+
return args !== null && args.length === params.length && args.every((a, i) => a === params[i]);
|
|
2507
|
+
}
|
|
2508
|
+
function soleCall(body) {
|
|
2509
|
+
const statements = bodyStatements(body);
|
|
2510
|
+
if (statements.length !== 1) return null;
|
|
2511
|
+
const only = statements[0];
|
|
2512
|
+
if (CALL_TYPES.has(only.type)) return only;
|
|
2513
|
+
if (!WRAPPER_TYPES.has(only.type) || only.namedChildCount !== 1) return null;
|
|
2514
|
+
const value = only.namedChildren[0];
|
|
2515
|
+
return CALL_TYPES.has(value.type) ? value : null;
|
|
2516
|
+
}
|
|
2517
|
+
function parameterShapes(fn) {
|
|
2518
|
+
const bare = fn.childForFieldName("parameter");
|
|
2519
|
+
if (bare) return [bare.text];
|
|
2520
|
+
const list = fn.childForFieldName("parameters");
|
|
2521
|
+
if (!list) return null;
|
|
2522
|
+
const shapes = [];
|
|
2523
|
+
for (const p of list.namedChildren) {
|
|
2524
|
+
if (!p || p.type === "comment" || PY_SEPARATORS.has(p.type)) continue;
|
|
2525
|
+
const shape = parameterShape(p);
|
|
2526
|
+
if (shape === null) return null;
|
|
2527
|
+
shapes.push(shape);
|
|
2528
|
+
}
|
|
2529
|
+
return RECEIVERS.has(shapes[0] ?? "") ? shapes.slice(1) : shapes;
|
|
2530
|
+
}
|
|
2531
|
+
function parameterShape(p) {
|
|
2532
|
+
if (p.type === "identifier") return p.text;
|
|
2533
|
+
if (p.type === "list_splat_pattern" || p.type === "dictionary_splat_pattern") return p.text;
|
|
2534
|
+
if (PY_NAMED_PARAMS.has(p.type)) return p.childForFieldName("name")?.text ?? null;
|
|
2535
|
+
if (p.type === "typed_parameter") return parameterShape(p.namedChildren[0]);
|
|
2536
|
+
if (p.type === "required_parameter" || p.type === "optional_parameter") {
|
|
2537
|
+
const pattern = p.childForFieldName("pattern");
|
|
2538
|
+
return pattern ? tsPatternShape(pattern) : null;
|
|
2539
|
+
}
|
|
2540
|
+
return tsPatternShape(p);
|
|
2541
|
+
}
|
|
2542
|
+
function tsPatternShape(pattern) {
|
|
2543
|
+
if (pattern.type === "identifier") return pattern.text;
|
|
2544
|
+
if (pattern.type === "rest_pattern") return `...${pattern.namedChildren[0]?.text ?? ""}`;
|
|
2545
|
+
if (pattern.type === "assignment_pattern") {
|
|
2546
|
+
const left = pattern.childForFieldName("left");
|
|
2547
|
+
return left ? tsPatternShape(left) : null;
|
|
2548
|
+
}
|
|
2549
|
+
return null;
|
|
2550
|
+
}
|
|
2551
|
+
function argumentShapes(call) {
|
|
2552
|
+
const list = call.childForFieldName("arguments");
|
|
2553
|
+
if (!list || list.type !== "argument_list" && list.type !== "arguments") return null;
|
|
2554
|
+
const shapes = [];
|
|
2555
|
+
for (const a of list.namedChildren) {
|
|
2556
|
+
if (!a || a.type === "comment") continue;
|
|
2557
|
+
const shape = argumentShape(a);
|
|
2558
|
+
if (shape === null) return null;
|
|
2559
|
+
shapes.push(shape);
|
|
2560
|
+
}
|
|
2561
|
+
return shapes;
|
|
2562
|
+
}
|
|
2563
|
+
function argumentShape(a) {
|
|
2564
|
+
if (a.type === "identifier") return a.text;
|
|
2565
|
+
const inner = a.namedChildren[0];
|
|
2566
|
+
if (inner?.type !== "identifier") return null;
|
|
2567
|
+
if (a.type === "list_splat") return `*${inner.text}`;
|
|
2568
|
+
if (a.type === "dictionary_splat") return `**${inner.text}`;
|
|
2569
|
+
if (a.type === "spread_element") return `...${inner.text}`;
|
|
2570
|
+
return null;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
1940
2573
|
// src/symbol-metrics.ts
|
|
1941
2574
|
var SYMBOL_METRICS = [
|
|
1942
2575
|
{ name: "symbol_cognitive", stat: "cognitive", unit: "count" },
|
|
1943
2576
|
{ name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
|
|
1944
2577
|
{ name: "symbol_loc", stat: "loc", unit: "lines" },
|
|
1945
|
-
{ name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" }
|
|
2578
|
+
{ name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" },
|
|
2579
|
+
{ name: "symbol_comment_lines", stat: "commentLines", unit: "lines" },
|
|
2580
|
+
{ name: "symbol_docstring_lines", stat: "docstringLines", unit: "lines" },
|
|
2581
|
+
{ name: "symbol_body_lines", stat: "bodyLines", unit: "lines" },
|
|
2582
|
+
{ name: "symbol_comment_ratio", stat: "commentRatio", unit: "ratio" },
|
|
2583
|
+
{ name: "symbol_narrating_comments", stat: "narratingComments", unit: "count" },
|
|
2584
|
+
{ name: "symbol_pass_through", stat: "passThrough", unit: "count" }
|
|
1946
2585
|
];
|
|
1947
2586
|
var SYMBOL_METRIC_NAMES = SYMBOL_METRICS.map((m) => m.name);
|
|
1948
2587
|
function symbolMetrics(fileId2, stats, symbolNames) {
|
|
@@ -1966,6 +2605,13 @@ function symbolMetrics(fileId2, stats, symbolNames) {
|
|
|
1966
2605
|
}
|
|
1967
2606
|
return out;
|
|
1968
2607
|
}
|
|
2608
|
+
function functionShapeStats(fn, body, lines) {
|
|
2609
|
+
return {
|
|
2610
|
+
...commentLineStats(fn, body, lines),
|
|
2611
|
+
narratingComments: countNarratingComments(fn, body),
|
|
2612
|
+
passThrough: isPassThrough(fn, body) ? 1 : 0
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
1969
2615
|
|
|
1970
2616
|
// src/source-metrics.ts
|
|
1971
2617
|
var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -2016,7 +2662,8 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
2016
2662
|
"max_nesting_depth",
|
|
2017
2663
|
"class_count",
|
|
2018
2664
|
"lcom4_max",
|
|
2019
|
-
|
|
2665
|
+
...EXCEPTION_METRIC_NAMES,
|
|
2666
|
+
// Per-symbol complexity, size, comments and shape (C-58, C-64, TP-317, TP-322), keyed
|
|
2020
2667
|
// to `symbol` node ids. Source-local like the file-level metrics above, so an
|
|
2021
2668
|
// unchanged file carries them forward — but their nodeId is `<fileId>#<name>`,
|
|
2022
2669
|
// so the reuse basis buckets them under the symbol's parent file (incremental.ts).
|
|
@@ -2076,6 +2723,7 @@ function metricsForFile(nodeId, file, symbolNames) {
|
|
|
2076
2723
|
}
|
|
2077
2724
|
out.push(...symbolMetrics(nodeId, stats, symbolNames));
|
|
2078
2725
|
out.push(...computeLcomMetrics(file, nodeId));
|
|
2726
|
+
out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
|
|
2079
2727
|
return out;
|
|
2080
2728
|
}
|
|
2081
2729
|
function countLoc(content) {
|
|
@@ -2084,6 +2732,7 @@ function countLoc(content) {
|
|
|
2084
2732
|
function analyzeFunctions(file) {
|
|
2085
2733
|
const stats = [];
|
|
2086
2734
|
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
|
|
2735
|
+
const lines = file.content.split("\n");
|
|
2087
2736
|
walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
|
|
2088
2737
|
const fn = functionAt(node, fnTypes);
|
|
2089
2738
|
if (!fn) return;
|
|
@@ -2092,7 +2741,8 @@ function analyzeFunctions(file) {
|
|
|
2092
2741
|
cyclomatic: cyclomaticOf(fn.body, file.language),
|
|
2093
2742
|
cognitive: cognitiveComplexityOf(fn.body, file.language),
|
|
2094
2743
|
nestingDepth: nestingDepthOf(fn.body, file.language, 0),
|
|
2095
|
-
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1
|
|
2744
|
+
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
|
|
2745
|
+
...functionShapeStats(fn.node, fn.body, lines)
|
|
2096
2746
|
});
|
|
2097
2747
|
});
|
|
2098
2748
|
return stats;
|
|
@@ -2149,7 +2799,7 @@ var TERMINALS = /* @__PURE__ */ new Set([
|
|
|
2149
2799
|
]);
|
|
2150
2800
|
var SCOPE_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda", "class_definition"]);
|
|
2151
2801
|
var SPLAT_TYPES = /* @__PURE__ */ new Set(["list_splat_pattern", "dictionary_splat_pattern"]);
|
|
2152
|
-
var
|
|
2802
|
+
var RECEIVERS2 = /* @__PURE__ */ new Set(["self", "cls"]);
|
|
2153
2803
|
function pythonDeadCode(root) {
|
|
2154
2804
|
const out = { unreachable: 0, locals: 0, params: 0 };
|
|
2155
2805
|
const visit = (node) => {
|
|
@@ -2211,7 +2861,7 @@ function ownParamNames(fn) {
|
|
|
2211
2861
|
for (const p of params.namedChildren) {
|
|
2212
2862
|
if (!p || p.type === "keyword_separator" || p.type === "positional_separator") continue;
|
|
2213
2863
|
const id = paramIdentifier(p);
|
|
2214
|
-
out.push(id && !
|
|
2864
|
+
out.push(id && !RECEIVERS2.has(id.text) ? id.text : null);
|
|
2215
2865
|
}
|
|
2216
2866
|
return out;
|
|
2217
2867
|
}
|
|
@@ -2680,9 +3330,10 @@ function loadReuseBasis(db, indexVersion) {
|
|
|
2680
3330
|
}
|
|
2681
3331
|
const edgesBySrc = /* @__PURE__ */ new Map();
|
|
2682
3332
|
for (const e of db.listEdges(snap.id, { includeReferences: true })) {
|
|
2683
|
-
const
|
|
3333
|
+
const fileKey = parseSymbolId(e.srcId)?.fileId ?? e.srcId;
|
|
3334
|
+
const bucket = edgesBySrc.get(fileKey);
|
|
2684
3335
|
if (bucket) bucket.push(e);
|
|
2685
|
-
else edgesBySrc.set(
|
|
3336
|
+
else edgesBySrc.set(fileKey, [e]);
|
|
2686
3337
|
}
|
|
2687
3338
|
const sourceMetricsByFile = /* @__PURE__ */ new Map();
|
|
2688
3339
|
for (const m of db.listMetrics(snap.id)) {
|
|
@@ -2801,6 +3452,7 @@ function weightedInbound(nodes, edges) {
|
|
|
2801
3452
|
const util = /* @__PURE__ */ new Map();
|
|
2802
3453
|
for (const n of nodes) util.set(n.id, 0);
|
|
2803
3454
|
for (const e of resolveBarrelEdges(nodes, edges)) {
|
|
3455
|
+
if (e.kind === "calls") continue;
|
|
2804
3456
|
if (util.has(e.dstId)) util.set(e.dstId, util.get(e.dstId) + edgeWeight(e));
|
|
2805
3457
|
}
|
|
2806
3458
|
return util;
|
|
@@ -2831,6 +3483,58 @@ function computeMetrics(nodes, edges) {
|
|
|
2831
3483
|
return out;
|
|
2832
3484
|
}
|
|
2833
3485
|
|
|
3486
|
+
// src/analysis/call-metrics.ts
|
|
3487
|
+
var ABSENT = "<absent>";
|
|
3488
|
+
function computeCallMetrics(nodes, edges) {
|
|
3489
|
+
const inbound = collectInbound(edges);
|
|
3490
|
+
const out = [];
|
|
3491
|
+
for (const node of nodes) {
|
|
3492
|
+
if (node.kind !== "symbol" || node.attrs?.startLine === void 0) continue;
|
|
3493
|
+
const { callers, sites } = inbound.get(node.id) ?? { callers: /* @__PURE__ */ new Set(), sites: [] };
|
|
3494
|
+
out.push({ nodeId: node.id, name: "symbol_caller_count", value: callers.size, unit: "count" });
|
|
3495
|
+
const single = isPrivateHelper(node) && callers.size === 1 && isSymbolCaller(callers);
|
|
3496
|
+
out.push({ nodeId: node.id, name: "symbol_single_caller_helper", value: single ? 1 : 0, unit: "count" });
|
|
3497
|
+
const shape = readParamShape(node.attrs);
|
|
3498
|
+
if (shape && sites.length >= 2) {
|
|
3499
|
+
out.push({ nodeId: node.id, name: "symbol_constant_params", value: constantParams(shape, sites), unit: "count" });
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
return out;
|
|
3503
|
+
}
|
|
3504
|
+
function collectInbound(edges) {
|
|
3505
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
3506
|
+
for (const edge of edges) {
|
|
3507
|
+
if (edge.kind !== "calls") continue;
|
|
3508
|
+
let entry = inbound.get(edge.dstId);
|
|
3509
|
+
if (!entry) inbound.set(edge.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
|
|
3510
|
+
if (edge.srcId !== edge.dstId) entry.callers.add(edge.srcId);
|
|
3511
|
+
entry.sites.push(...edge.attrs?.sites ?? []);
|
|
3512
|
+
}
|
|
3513
|
+
return inbound;
|
|
3514
|
+
}
|
|
3515
|
+
function isPrivateHelper(node) {
|
|
3516
|
+
const own = node.name.slice(node.name.lastIndexOf(".") + 1);
|
|
3517
|
+
return node.attrs?.exported === false && !(own.startsWith("__") && own.endsWith("__"));
|
|
3518
|
+
}
|
|
3519
|
+
function isSymbolCaller(callers) {
|
|
3520
|
+
return [...callers].every((id) => parseSymbolId(id) !== null);
|
|
3521
|
+
}
|
|
3522
|
+
function constantParams(shape, sites) {
|
|
3523
|
+
let count = 0;
|
|
3524
|
+
shape.params.forEach((name, index) => {
|
|
3525
|
+
const values = sites.map((site) => argumentAt(site, name, index < shape.positional ? index : -1));
|
|
3526
|
+
if (values.every((v) => v !== null && v === values[0])) count++;
|
|
3527
|
+
});
|
|
3528
|
+
return count;
|
|
3529
|
+
}
|
|
3530
|
+
function argumentAt(site, name, position) {
|
|
3531
|
+
if (position >= 0 && position < site.args.length) return site.args[position] ?? null;
|
|
3532
|
+
if (site.kwargs && Object.hasOwn(site.kwargs, name)) return site.kwargs[name] ?? null;
|
|
3533
|
+
if (site.kwSplat) return null;
|
|
3534
|
+
if (position >= 0 && site.spreadFrom !== void 0 && position >= site.spreadFrom) return null;
|
|
3535
|
+
return ABSENT;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
2834
3538
|
// src/analysis/test-linker.ts
|
|
2835
3539
|
var DEFAULT_MIN_CO_EDIT_COUNT = 2;
|
|
2836
3540
|
var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
|
|
@@ -3040,6 +3744,7 @@ function buildIndexerMetrics(input) {
|
|
|
3040
3744
|
const history = input.history ? loadHistoryMetrics(nodeList, input.idRoot, input.history) : null;
|
|
3041
3745
|
return [
|
|
3042
3746
|
...computeMetrics(nodeList, [...input.edges.values()]),
|
|
3747
|
+
...computeCallMetrics(nodeList, input.edges.values()),
|
|
3043
3748
|
...computeSourceMetrics(
|
|
3044
3749
|
input.parsedFiles,
|
|
3045
3750
|
(p) => fileId(input.idRoot, p),
|
|
@@ -3118,7 +3823,7 @@ function qualifiedByName(file) {
|
|
|
3118
3823
|
}
|
|
3119
3824
|
|
|
3120
3825
|
// src/indexer.ts
|
|
3121
|
-
var INDEX_VERSION = "0.
|
|
3826
|
+
var INDEX_VERSION = "0.18.0";
|
|
3122
3827
|
var LANGUAGES = ["typescript", "python"];
|
|
3123
3828
|
function canonicalizePath(p) {
|
|
3124
3829
|
try {
|
|
@@ -3129,7 +3834,7 @@ function canonicalizePath(p) {
|
|
|
3129
3834
|
}
|
|
3130
3835
|
function normalizePaths(paths) {
|
|
3131
3836
|
if (paths.length === 0) throw new Error("indexPaths requires at least one path");
|
|
3132
|
-
return [...new Set(paths.map((p) =>
|
|
3837
|
+
return [...new Set(paths.map((p) => path8.resolve(p)))];
|
|
3133
3838
|
}
|
|
3134
3839
|
function countByKind(items) {
|
|
3135
3840
|
const out = {};
|
|
@@ -3233,9 +3938,9 @@ import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, par
|
|
|
3233
3938
|
|
|
3234
3939
|
// src/extractors/deep-ast.ts
|
|
3235
3940
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3236
|
-
import * as
|
|
3941
|
+
import * as path9 from "path";
|
|
3237
3942
|
import {
|
|
3238
|
-
Node as
|
|
3943
|
+
Node as Node6,
|
|
3239
3944
|
Project as Project2,
|
|
3240
3945
|
ScriptTarget as ScriptTarget2
|
|
3241
3946
|
} from "ts-morph";
|
|
@@ -3252,7 +3957,7 @@ function loadSourceFile(absPath) {
|
|
|
3252
3957
|
useInMemoryFileSystem: true,
|
|
3253
3958
|
compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
|
|
3254
3959
|
});
|
|
3255
|
-
return project.createSourceFile(`f${
|
|
3960
|
+
return project.createSourceFile(`f${path9.extname(absPath) || ".ts"}`, text);
|
|
3256
3961
|
} catch {
|
|
3257
3962
|
return null;
|
|
3258
3963
|
}
|
|
@@ -3294,10 +3999,10 @@ function empty(target, kind) {
|
|
|
3294
3999
|
};
|
|
3295
4000
|
}
|
|
3296
4001
|
function callableOf(decl) {
|
|
3297
|
-
if (
|
|
3298
|
-
if (
|
|
4002
|
+
if (Node6.isFunctionDeclaration(decl) || Node6.isMethodDeclaration(decl)) return decl;
|
|
4003
|
+
if (Node6.isVariableDeclaration(decl)) {
|
|
3299
4004
|
const init = decl.getInitializer();
|
|
3300
|
-
if (init && (
|
|
4005
|
+
if (init && (Node6.isArrowFunction(init) || Node6.isFunctionExpression(init))) return init;
|
|
3301
4006
|
}
|
|
3302
4007
|
return null;
|
|
3303
4008
|
}
|
|
@@ -3313,8 +4018,8 @@ function returnTypeOf(decl) {
|
|
|
3313
4018
|
return callableOf(decl)?.getReturnTypeNode()?.getText() ?? null;
|
|
3314
4019
|
}
|
|
3315
4020
|
function membersOf(decl) {
|
|
3316
|
-
if (
|
|
3317
|
-
if (
|
|
4021
|
+
if (Node6.isClassDeclaration(decl)) return classMembers(decl);
|
|
4022
|
+
if (Node6.isInterfaceDeclaration(decl)) return interfaceMembers(decl);
|
|
3318
4023
|
return [];
|
|
3319
4024
|
}
|
|
3320
4025
|
function classMembers(cls) {
|
|
@@ -3322,7 +4027,7 @@ function classMembers(cls) {
|
|
|
3322
4027
|
name: memberName(m),
|
|
3323
4028
|
memberKind: m.getKindName(),
|
|
3324
4029
|
signature: memberSignature(m),
|
|
3325
|
-
isStatic:
|
|
4030
|
+
isStatic: Node6.isStaticable(m) ? m.isStatic() : false
|
|
3326
4031
|
}));
|
|
3327
4032
|
}
|
|
3328
4033
|
function interfaceMembers(iface) {
|
|
@@ -3348,17 +4053,17 @@ function exportedDeclarations(sf) {
|
|
|
3348
4053
|
return out;
|
|
3349
4054
|
}
|
|
3350
4055
|
function memberName(m) {
|
|
3351
|
-
if (
|
|
3352
|
-
return
|
|
4056
|
+
if (Node6.isConstructorDeclaration(m)) return "constructor";
|
|
4057
|
+
return Node6.hasName(m) ? m.getName() : "";
|
|
3353
4058
|
}
|
|
3354
4059
|
function memberSignature(m) {
|
|
3355
|
-
if (
|
|
4060
|
+
if (Node6.isMethodDeclaration(m) || Node6.isMethodSignature(m) || Node6.isConstructorDeclaration(m)) {
|
|
3356
4061
|
const params = m.getParameters().map((p) => oneLine2(p.getText())).join(", ");
|
|
3357
4062
|
const ret = m.getReturnTypeNode()?.getText();
|
|
3358
|
-
const name =
|
|
4063
|
+
const name = Node6.isConstructorDeclaration(m) ? "constructor" : memberName(m);
|
|
3359
4064
|
return oneLine2(`${name}(${params})${ret ? `: ${ret}` : ""}`);
|
|
3360
4065
|
}
|
|
3361
|
-
if (
|
|
4066
|
+
if (Node6.isPropertyDeclaration(m) || Node6.isPropertySignature(m)) {
|
|
3362
4067
|
const type = m.getTypeNode()?.getText();
|
|
3363
4068
|
return oneLine2(`${memberName(m)}${type ? `: ${type}` : ""}`);
|
|
3364
4069
|
}
|
|
@@ -4106,6 +4811,126 @@ var SOURCE = [
|
|
|
4106
4811
|
source: "source-metrics",
|
|
4107
4812
|
description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
|
|
4108
4813
|
},
|
|
4814
|
+
{
|
|
4815
|
+
name: "symbol_comment_lines",
|
|
4816
|
+
unit: "lines",
|
|
4817
|
+
appliesTo: SYMBOL,
|
|
4818
|
+
rollup: "max",
|
|
4819
|
+
direction: "neutral",
|
|
4820
|
+
absent: "exclude",
|
|
4821
|
+
source: "source-metrics",
|
|
4822
|
+
description: "Rows holding a comment inside the function a symbol names, the docstring excluded; the max when several share the name."
|
|
4823
|
+
},
|
|
4824
|
+
{
|
|
4825
|
+
name: "symbol_docstring_lines",
|
|
4826
|
+
unit: "lines",
|
|
4827
|
+
appliesTo: SYMBOL,
|
|
4828
|
+
rollup: "max",
|
|
4829
|
+
direction: "neutral",
|
|
4830
|
+
absent: "exclude",
|
|
4831
|
+
source: "source-metrics",
|
|
4832
|
+
description: "Rows of the function's docstring: the Python docstring, or the JSDoc block attached to a TypeScript declaration."
|
|
4833
|
+
},
|
|
4834
|
+
{
|
|
4835
|
+
name: "symbol_body_lines",
|
|
4836
|
+
unit: "lines",
|
|
4837
|
+
appliesTo: SYMBOL,
|
|
4838
|
+
rollup: "max",
|
|
4839
|
+
direction: "higher-worse",
|
|
4840
|
+
absent: "exclude",
|
|
4841
|
+
source: "source-metrics",
|
|
4842
|
+
description: "Non-blank rows of the function's body that hold code, comments and the docstring excluded."
|
|
4843
|
+
},
|
|
4844
|
+
{
|
|
4845
|
+
name: "symbol_comment_ratio",
|
|
4846
|
+
unit: "ratio",
|
|
4847
|
+
appliesTo: SYMBOL,
|
|
4848
|
+
rollup: "max",
|
|
4849
|
+
direction: "neutral",
|
|
4850
|
+
absent: "exclude",
|
|
4851
|
+
source: "source-metrics",
|
|
4852
|
+
description: "symbol_comment_lines / max(symbol_body_lines, 1). High values suggest narration; zero is not a defect."
|
|
4853
|
+
},
|
|
4854
|
+
{
|
|
4855
|
+
name: "symbol_narrating_comments",
|
|
4856
|
+
unit: "count",
|
|
4857
|
+
appliesTo: SYMBOL,
|
|
4858
|
+
rollup: "max",
|
|
4859
|
+
direction: "higher-worse",
|
|
4860
|
+
absent: "exclude",
|
|
4861
|
+
source: "source-metrics",
|
|
4862
|
+
description: "Comments in the body whose words overlap the next statement's identifiers by 2 tokens or half the comment. A candidate signal."
|
|
4863
|
+
},
|
|
4864
|
+
{
|
|
4865
|
+
name: "symbol_pass_through",
|
|
4866
|
+
unit: "count",
|
|
4867
|
+
appliesTo: SYMBOL,
|
|
4868
|
+
rollup: "max",
|
|
4869
|
+
direction: "higher-worse",
|
|
4870
|
+
absent: "exclude",
|
|
4871
|
+
source: "source-metrics",
|
|
4872
|
+
description: "1 when the function's body is one call forwarding every parameter, in order, as a bare argument; else 0."
|
|
4873
|
+
},
|
|
4874
|
+
{
|
|
4875
|
+
name: "symbol_caller_count",
|
|
4876
|
+
unit: "count",
|
|
4877
|
+
appliesTo: SYMBOL,
|
|
4878
|
+
rollup: "max",
|
|
4879
|
+
direction: "neutral",
|
|
4880
|
+
absent: "exclude",
|
|
4881
|
+
source: "call-graph",
|
|
4882
|
+
description: "Distinct symbols, or files at module level, with a resolved call to this function, method or class. Unresolved calls are not counted."
|
|
4883
|
+
},
|
|
4884
|
+
{
|
|
4885
|
+
name: "symbol_single_caller_helper",
|
|
4886
|
+
unit: "count",
|
|
4887
|
+
appliesTo: SYMBOL,
|
|
4888
|
+
rollup: "sum",
|
|
4889
|
+
direction: "higher-worse",
|
|
4890
|
+
absent: "exclude",
|
|
4891
|
+
source: "call-graph",
|
|
4892
|
+
description: "1 when a non-exported symbol has exactly one resolved caller and that caller is a symbol; else 0."
|
|
4893
|
+
},
|
|
4894
|
+
{
|
|
4895
|
+
name: "symbol_constant_params",
|
|
4896
|
+
unit: "count",
|
|
4897
|
+
appliesTo: SYMBOL,
|
|
4898
|
+
rollup: "max",
|
|
4899
|
+
direction: "higher-worse",
|
|
4900
|
+
absent: "exclude",
|
|
4901
|
+
source: "call-graph",
|
|
4902
|
+
description: "Parameters that every one of at least 2 resolved call sites passes the same literal, or never passes. Rest and keyword-splat parameters are ignored."
|
|
4903
|
+
},
|
|
4904
|
+
{
|
|
4905
|
+
name: "except_count",
|
|
4906
|
+
unit: "count",
|
|
4907
|
+
appliesTo: FILE,
|
|
4908
|
+
rollup: "sum",
|
|
4909
|
+
direction: "neutral",
|
|
4910
|
+
absent: "zero",
|
|
4911
|
+
source: "exception-handling",
|
|
4912
|
+
description: "Python except clauses and TypeScript catch clauses in the file."
|
|
4913
|
+
},
|
|
4914
|
+
{
|
|
4915
|
+
name: "except_density",
|
|
4916
|
+
unit: "per100loc",
|
|
4917
|
+
appliesTo: FILE,
|
|
4918
|
+
rollup: "none",
|
|
4919
|
+
direction: "higher-worse",
|
|
4920
|
+
absent: "zero",
|
|
4921
|
+
source: "exception-handling",
|
|
4922
|
+
description: "except_count per 100 non-blank lines. Recompute from the sums for a group."
|
|
4923
|
+
},
|
|
4924
|
+
{
|
|
4925
|
+
name: "swallowed_except",
|
|
4926
|
+
unit: "count",
|
|
4927
|
+
appliesTo: FILE,
|
|
4928
|
+
rollup: "sum",
|
|
4929
|
+
direction: "higher-worse",
|
|
4930
|
+
absent: "zero",
|
|
4931
|
+
source: "exception-handling",
|
|
4932
|
+
description: "Handlers whose body is empty, pass, ..., continue, a bare or empty return, or a single logging call. Exempts Python's `except ImportError`/`ModuleNotFoundError` (alone or paired), the standard optional-dependency idiom."
|
|
4933
|
+
},
|
|
4109
4934
|
{
|
|
4110
4935
|
name: "class_count",
|
|
4111
4936
|
unit: "count",
|
|
@@ -4529,6 +5354,45 @@ function locateNode(node) {
|
|
|
4529
5354
|
return location;
|
|
4530
5355
|
}
|
|
4531
5356
|
|
|
5357
|
+
// src/check/outlier-rule.ts
|
|
5358
|
+
var DEFAULT_OUTLIER_MIN_SAMPLE = 20;
|
|
5359
|
+
function runMetricOutlierRule(rule, ctx) {
|
|
5360
|
+
const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
|
|
5361
|
+
const carriers = [];
|
|
5362
|
+
for (const node of pool) {
|
|
5363
|
+
if (node.kind !== rule.kind) continue;
|
|
5364
|
+
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
5365
|
+
if (value !== void 0) carriers.push({ node, value });
|
|
5366
|
+
}
|
|
5367
|
+
const ranked = rule.rankNonZero ? carriers.filter((c) => c.value > 0) : carriers;
|
|
5368
|
+
if (ranked.length < (rule.minSample ?? DEFAULT_OUTLIER_MIN_SAMPLE)) return [];
|
|
5369
|
+
const threshold = percentileOf(ranked.map((c) => c.value), rule.percentile);
|
|
5370
|
+
return ranked.filter((c) => c.value > threshold && (rule.floor === void 0 || c.value > rule.floor)).map((c) => toViolation(rule, c.node, c.value, threshold));
|
|
5371
|
+
}
|
|
5372
|
+
function percentileOf(values, percentile) {
|
|
5373
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5374
|
+
const rank = percentile / 100 * (sorted.length - 1);
|
|
5375
|
+
const lower = Math.floor(rank);
|
|
5376
|
+
const upper = Math.min(lower + 1, sorted.length - 1);
|
|
5377
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (rank - lower);
|
|
5378
|
+
}
|
|
5379
|
+
function toViolation(rule, node, value, threshold) {
|
|
5380
|
+
const floorSuffix = rule.floor === void 0 ? "" : `, floor ${formatNumber(rule.floor)}`;
|
|
5381
|
+
const bound = `p${formatNumber(rule.percentile)} ${formatNumber(threshold)}${floorSuffix}`;
|
|
5382
|
+
return {
|
|
5383
|
+
ruleId: rule.id,
|
|
5384
|
+
severity: severityOf(rule),
|
|
5385
|
+
nodeId: node.id,
|
|
5386
|
+
metric: rule.metric,
|
|
5387
|
+
value,
|
|
5388
|
+
threshold,
|
|
5389
|
+
message: `${rule.metric}=${formatNumber(value)} > ${bound}`,
|
|
5390
|
+
...locateNode(node),
|
|
5391
|
+
evidence: `${rule.metric}=${formatNumber(value)} (${bound})`,
|
|
5392
|
+
tool: CODE_GRAPH_TOOL
|
|
5393
|
+
};
|
|
5394
|
+
}
|
|
5395
|
+
|
|
4532
5396
|
// src/check/metric-rules.ts
|
|
4533
5397
|
function nodeFilter(rule) {
|
|
4534
5398
|
return {
|
|
@@ -4548,7 +5412,7 @@ function candidateNodes(rule, ctx) {
|
|
|
4548
5412
|
const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
|
|
4549
5413
|
return pool.filter((node) => passesFilter(node, filter));
|
|
4550
5414
|
}
|
|
4551
|
-
function
|
|
5415
|
+
function toViolation2(rule, breach) {
|
|
4552
5416
|
return {
|
|
4553
5417
|
ruleId: rule.id,
|
|
4554
5418
|
severity: severityOf(rule),
|
|
@@ -4578,7 +5442,7 @@ function runMetricMaxRule(rule, ctx) {
|
|
|
4578
5442
|
for (const node of candidateNodes(rule, ctx)) {
|
|
4579
5443
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4580
5444
|
if (value === void 0 || value <= rule.max) continue;
|
|
4581
|
-
out.push(
|
|
5445
|
+
out.push(toViolation2(rule, thresholdBreach(node, rule.metric, value, "max", rule.max)));
|
|
4582
5446
|
}
|
|
4583
5447
|
return out;
|
|
4584
5448
|
}
|
|
@@ -4587,7 +5451,7 @@ function runMetricMinRule(rule, ctx) {
|
|
|
4587
5451
|
for (const node of candidateNodes(rule, ctx)) {
|
|
4588
5452
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4589
5453
|
if (value === void 0 || value >= rule.min) continue;
|
|
4590
|
-
out.push(
|
|
5454
|
+
out.push(toViolation2(rule, thresholdBreach(node, rule.metric, value, "min", rule.min)));
|
|
4591
5455
|
}
|
|
4592
5456
|
return out;
|
|
4593
5457
|
}
|
|
@@ -4600,7 +5464,7 @@ function runMetricProductMaxRule(rule, ctx) {
|
|
|
4600
5464
|
if (!components) continue;
|
|
4601
5465
|
const product = components.reduce((a, b) => a * b, 1);
|
|
4602
5466
|
if (product <= rule.max) continue;
|
|
4603
|
-
out.push(
|
|
5467
|
+
out.push(toViolation2(rule, productBreach(rule, node, components, product)));
|
|
4604
5468
|
}
|
|
4605
5469
|
return out;
|
|
4606
5470
|
}
|
|
@@ -4634,6 +5498,8 @@ function runRule(rule, ctx) {
|
|
|
4634
5498
|
return runMetricMinRule(rule, ctx);
|
|
4635
5499
|
case "metric-product-max":
|
|
4636
5500
|
return runMetricProductMaxRule(rule, ctx);
|
|
5501
|
+
case "metric-outlier":
|
|
5502
|
+
return runMetricOutlierRule(rule, ctx);
|
|
4637
5503
|
case "forbid-import":
|
|
4638
5504
|
return runForbidImportRule(rule, ctx);
|
|
4639
5505
|
case "layered-deps":
|
|
@@ -4778,6 +5644,8 @@ function validateRule(raw, index, warn) {
|
|
|
4778
5644
|
return assertMetricMin(r, warn);
|
|
4779
5645
|
case "metric-product-max":
|
|
4780
5646
|
return assertMetricProductMax(r, warn);
|
|
5647
|
+
case "metric-outlier":
|
|
5648
|
+
return assertMetricOutlier(r, warn);
|
|
4781
5649
|
case "forbid-import":
|
|
4782
5650
|
return assertForbidImport(r);
|
|
4783
5651
|
case "layered-deps":
|
|
@@ -4887,6 +5755,37 @@ function assertMetricMin(r, warn) {
|
|
|
4887
5755
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
4888
5756
|
};
|
|
4889
5757
|
}
|
|
5758
|
+
var NODE_KINDS = /* @__PURE__ */ new Set(["package", "module", "file", "symbol", "external"]);
|
|
5759
|
+
function assertMetricOutlier(r, warn) {
|
|
5760
|
+
if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
|
|
5761
|
+
if (typeof r.kind !== "string" || !NODE_KINDS.has(r.kind)) {
|
|
5762
|
+
throw new Error(`${r.id}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
|
|
5763
|
+
}
|
|
5764
|
+
if (typeof r.percentile !== "number" || r.percentile < 50 || r.percentile > 100) {
|
|
5765
|
+
throw new Error(`${r.id}: percentile must be a number from 50 to 100`);
|
|
5766
|
+
}
|
|
5767
|
+
if (r.minSample !== void 0 && (!Number.isInteger(r.minSample) || r.minSample < 1)) {
|
|
5768
|
+
throw new Error(`${r.id}: minSample must be a positive integer`);
|
|
5769
|
+
}
|
|
5770
|
+
if (r.floor !== void 0 && (typeof r.floor !== "number" || !Number.isFinite(r.floor) || r.floor < 0)) {
|
|
5771
|
+
throw new Error(`${r.id}: floor must be a non-negative finite number`);
|
|
5772
|
+
}
|
|
5773
|
+
if (r.rankNonZero !== void 0 && typeof r.rankNonZero !== "boolean") {
|
|
5774
|
+
throw new Error(`${r.id}: rankNonZero must be a boolean`);
|
|
5775
|
+
}
|
|
5776
|
+
const ruleId = r.id;
|
|
5777
|
+
return {
|
|
5778
|
+
type: "metric-outlier",
|
|
5779
|
+
id: ruleId,
|
|
5780
|
+
metric: healMetricName(r.metric, ruleId, warn),
|
|
5781
|
+
kind: r.kind,
|
|
5782
|
+
percentile: r.percentile,
|
|
5783
|
+
minSample: r.minSample,
|
|
5784
|
+
floor: r.floor,
|
|
5785
|
+
rankNonZero: r.rankNonZero,
|
|
5786
|
+
severity: r.severity
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
4890
5789
|
function assertForbidImport(r) {
|
|
4891
5790
|
if (typeof r.from !== "string") throw new Error(`${r.id}: from must be a string`);
|
|
4892
5791
|
if (typeof r.to !== "string") throw new Error(`${r.id}: to must be a string`);
|
|
@@ -4955,18 +5854,18 @@ function checkSnapshot(store, options) {
|
|
|
4955
5854
|
});
|
|
4956
5855
|
return { snapshot, baselineSnapshot, result };
|
|
4957
5856
|
}
|
|
4958
|
-
async function loadCheckRules(
|
|
5857
|
+
async function loadCheckRules(path11, options = {}) {
|
|
4959
5858
|
let raw;
|
|
4960
5859
|
try {
|
|
4961
|
-
raw = await readFile2(
|
|
5860
|
+
raw = await readFile2(path11, "utf8");
|
|
4962
5861
|
} catch (err) {
|
|
4963
|
-
throw new Error(`Cannot read rules file at ${
|
|
5862
|
+
throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
|
|
4964
5863
|
}
|
|
4965
5864
|
let parsed;
|
|
4966
5865
|
try {
|
|
4967
5866
|
parsed = JSON.parse(raw);
|
|
4968
5867
|
} catch (err) {
|
|
4969
|
-
throw new Error(`Invalid JSON in ${
|
|
5868
|
+
throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
|
|
4970
5869
|
}
|
|
4971
5870
|
return validateRules(parsed, options);
|
|
4972
5871
|
}
|
|
@@ -5378,7 +6277,7 @@ function pushMulti(map, key, value) {
|
|
|
5378
6277
|
}
|
|
5379
6278
|
|
|
5380
6279
|
// src/conventions/areas.ts
|
|
5381
|
-
import * as
|
|
6280
|
+
import * as path10 from "path";
|
|
5382
6281
|
import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
|
|
5383
6282
|
var DEFAULT_MIN_SIZE = 3;
|
|
5384
6283
|
var PROMPT_FILE_CAP = 30;
|
|
@@ -5462,7 +6361,7 @@ function rankByDegree(members, edges) {
|
|
|
5462
6361
|
function dominantDirectory(files) {
|
|
5463
6362
|
const counts = /* @__PURE__ */ new Map();
|
|
5464
6363
|
for (const f of files) {
|
|
5465
|
-
const dir =
|
|
6364
|
+
const dir = path10.posix.dirname(f);
|
|
5466
6365
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
5467
6366
|
}
|
|
5468
6367
|
const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
@@ -5587,7 +6486,9 @@ export {
|
|
|
5587
6486
|
CodeGraphStore,
|
|
5588
6487
|
DEAD_CODE_METRIC_NAMES,
|
|
5589
6488
|
DEFAULT_CHURN_WINDOWS,
|
|
6489
|
+
DEFAULT_OUTLIER_MIN_SAMPLE,
|
|
5590
6490
|
DOMAIN_DDL,
|
|
6491
|
+
EXCEPTION_METRIC_NAMES,
|
|
5591
6492
|
GROWTH_RISK_METRIC_NAMES,
|
|
5592
6493
|
INDEX_VERSION,
|
|
5593
6494
|
KIT,
|
|
@@ -5600,6 +6501,7 @@ export {
|
|
|
5600
6501
|
SOURCE_METRIC_NAMES,
|
|
5601
6502
|
SYMBOL_EMBEDDING_NAMESPACE,
|
|
5602
6503
|
SYMBOL_ID_SEP,
|
|
6504
|
+
SYMBOL_METRIC_NAMES,
|
|
5603
6505
|
TsMorphGraphExtractor,
|
|
5604
6506
|
aggregateMetrics,
|
|
5605
6507
|
aliasChain,
|
|
@@ -5682,6 +6584,7 @@ export {
|
|
|
5682
6584
|
parseFile2 as parseFile,
|
|
5683
6585
|
parseSymbolId,
|
|
5684
6586
|
patternToRegex,
|
|
6587
|
+
percentileOf,
|
|
5685
6588
|
planPrune,
|
|
5686
6589
|
priorSnapshotForRef,
|
|
5687
6590
|
pruneDanglingReferences,
|