@titan-design/code-graph 0.7.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 +19 -2
- package/dist/index.d.ts +14 -9
- package/dist/index.js +514 -79
- 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) {
|
|
@@ -1867,6 +2193,8 @@ var HANDLER_TYPES = ["except_clause", "except_group_clause", "catch_clause"];
|
|
|
1867
2193
|
var SILENT_STATEMENTS = /* @__PURE__ */ new Set(["pass_statement", "continue_statement", "empty_statement"]);
|
|
1868
2194
|
var EMPTY_VALUES = /* @__PURE__ */ new Set(["None", "null", "undefined"]);
|
|
1869
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"]);
|
|
1870
2198
|
function exceptionMetrics(nodeId, root, loc) {
|
|
1871
2199
|
const handlers = HANDLER_TYPES.flatMap((type) => descendantsOfType(root, type));
|
|
1872
2200
|
const swallowed = handlers.filter((h) => isSwallowed(h)).length;
|
|
@@ -1877,12 +2205,24 @@ function exceptionMetrics(nodeId, root, loc) {
|
|
|
1877
2205
|
];
|
|
1878
2206
|
}
|
|
1879
2207
|
function isSwallowed(handler) {
|
|
2208
|
+
if (isOptionalImportExcept(handler)) return false;
|
|
1880
2209
|
const body = handler.childForFieldName("body") ?? handler.namedChildren.find((c) => c?.type === "block");
|
|
1881
2210
|
if (!body) return false;
|
|
1882
2211
|
const statements = bodyStatements(body, null);
|
|
1883
2212
|
if (statements.length === 0) return true;
|
|
1884
2213
|
return statements.length === 1 && isSilent(statements[0]);
|
|
1885
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
|
+
}
|
|
1886
2226
|
function isSilent(statement) {
|
|
1887
2227
|
if (SILENT_STATEMENTS.has(statement.type)) return true;
|
|
1888
2228
|
const value = statement.namedChildren[0];
|
|
@@ -2990,9 +3330,10 @@ function loadReuseBasis(db, indexVersion) {
|
|
|
2990
3330
|
}
|
|
2991
3331
|
const edgesBySrc = /* @__PURE__ */ new Map();
|
|
2992
3332
|
for (const e of db.listEdges(snap.id, { includeReferences: true })) {
|
|
2993
|
-
const
|
|
3333
|
+
const fileKey = parseSymbolId(e.srcId)?.fileId ?? e.srcId;
|
|
3334
|
+
const bucket = edgesBySrc.get(fileKey);
|
|
2994
3335
|
if (bucket) bucket.push(e);
|
|
2995
|
-
else edgesBySrc.set(
|
|
3336
|
+
else edgesBySrc.set(fileKey, [e]);
|
|
2996
3337
|
}
|
|
2997
3338
|
const sourceMetricsByFile = /* @__PURE__ */ new Map();
|
|
2998
3339
|
for (const m of db.listMetrics(snap.id)) {
|
|
@@ -3111,6 +3452,7 @@ function weightedInbound(nodes, edges) {
|
|
|
3111
3452
|
const util = /* @__PURE__ */ new Map();
|
|
3112
3453
|
for (const n of nodes) util.set(n.id, 0);
|
|
3113
3454
|
for (const e of resolveBarrelEdges(nodes, edges)) {
|
|
3455
|
+
if (e.kind === "calls") continue;
|
|
3114
3456
|
if (util.has(e.dstId)) util.set(e.dstId, util.get(e.dstId) + edgeWeight(e));
|
|
3115
3457
|
}
|
|
3116
3458
|
return util;
|
|
@@ -3141,6 +3483,58 @@ function computeMetrics(nodes, edges) {
|
|
|
3141
3483
|
return out;
|
|
3142
3484
|
}
|
|
3143
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
|
+
|
|
3144
3538
|
// src/analysis/test-linker.ts
|
|
3145
3539
|
var DEFAULT_MIN_CO_EDIT_COUNT = 2;
|
|
3146
3540
|
var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
|
|
@@ -3350,6 +3744,7 @@ function buildIndexerMetrics(input) {
|
|
|
3350
3744
|
const history = input.history ? loadHistoryMetrics(nodeList, input.idRoot, input.history) : null;
|
|
3351
3745
|
return [
|
|
3352
3746
|
...computeMetrics(nodeList, [...input.edges.values()]),
|
|
3747
|
+
...computeCallMetrics(nodeList, input.edges.values()),
|
|
3353
3748
|
...computeSourceMetrics(
|
|
3354
3749
|
input.parsedFiles,
|
|
3355
3750
|
(p) => fileId(input.idRoot, p),
|
|
@@ -3428,7 +3823,7 @@ function qualifiedByName(file) {
|
|
|
3428
3823
|
}
|
|
3429
3824
|
|
|
3430
3825
|
// src/indexer.ts
|
|
3431
|
-
var INDEX_VERSION = "0.
|
|
3826
|
+
var INDEX_VERSION = "0.18.0";
|
|
3432
3827
|
var LANGUAGES = ["typescript", "python"];
|
|
3433
3828
|
function canonicalizePath(p) {
|
|
3434
3829
|
try {
|
|
@@ -3439,7 +3834,7 @@ function canonicalizePath(p) {
|
|
|
3439
3834
|
}
|
|
3440
3835
|
function normalizePaths(paths) {
|
|
3441
3836
|
if (paths.length === 0) throw new Error("indexPaths requires at least one path");
|
|
3442
|
-
return [...new Set(paths.map((p) =>
|
|
3837
|
+
return [...new Set(paths.map((p) => path8.resolve(p)))];
|
|
3443
3838
|
}
|
|
3444
3839
|
function countByKind(items) {
|
|
3445
3840
|
const out = {};
|
|
@@ -3543,9 +3938,9 @@ import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, par
|
|
|
3543
3938
|
|
|
3544
3939
|
// src/extractors/deep-ast.ts
|
|
3545
3940
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3546
|
-
import * as
|
|
3941
|
+
import * as path9 from "path";
|
|
3547
3942
|
import {
|
|
3548
|
-
Node as
|
|
3943
|
+
Node as Node6,
|
|
3549
3944
|
Project as Project2,
|
|
3550
3945
|
ScriptTarget as ScriptTarget2
|
|
3551
3946
|
} from "ts-morph";
|
|
@@ -3562,7 +3957,7 @@ function loadSourceFile(absPath) {
|
|
|
3562
3957
|
useInMemoryFileSystem: true,
|
|
3563
3958
|
compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
|
|
3564
3959
|
});
|
|
3565
|
-
return project.createSourceFile(`f${
|
|
3960
|
+
return project.createSourceFile(`f${path9.extname(absPath) || ".ts"}`, text);
|
|
3566
3961
|
} catch {
|
|
3567
3962
|
return null;
|
|
3568
3963
|
}
|
|
@@ -3604,10 +3999,10 @@ function empty(target, kind) {
|
|
|
3604
3999
|
};
|
|
3605
4000
|
}
|
|
3606
4001
|
function callableOf(decl) {
|
|
3607
|
-
if (
|
|
3608
|
-
if (
|
|
4002
|
+
if (Node6.isFunctionDeclaration(decl) || Node6.isMethodDeclaration(decl)) return decl;
|
|
4003
|
+
if (Node6.isVariableDeclaration(decl)) {
|
|
3609
4004
|
const init = decl.getInitializer();
|
|
3610
|
-
if (init && (
|
|
4005
|
+
if (init && (Node6.isArrowFunction(init) || Node6.isFunctionExpression(init))) return init;
|
|
3611
4006
|
}
|
|
3612
4007
|
return null;
|
|
3613
4008
|
}
|
|
@@ -3623,8 +4018,8 @@ function returnTypeOf(decl) {
|
|
|
3623
4018
|
return callableOf(decl)?.getReturnTypeNode()?.getText() ?? null;
|
|
3624
4019
|
}
|
|
3625
4020
|
function membersOf(decl) {
|
|
3626
|
-
if (
|
|
3627
|
-
if (
|
|
4021
|
+
if (Node6.isClassDeclaration(decl)) return classMembers(decl);
|
|
4022
|
+
if (Node6.isInterfaceDeclaration(decl)) return interfaceMembers(decl);
|
|
3628
4023
|
return [];
|
|
3629
4024
|
}
|
|
3630
4025
|
function classMembers(cls) {
|
|
@@ -3632,7 +4027,7 @@ function classMembers(cls) {
|
|
|
3632
4027
|
name: memberName(m),
|
|
3633
4028
|
memberKind: m.getKindName(),
|
|
3634
4029
|
signature: memberSignature(m),
|
|
3635
|
-
isStatic:
|
|
4030
|
+
isStatic: Node6.isStaticable(m) ? m.isStatic() : false
|
|
3636
4031
|
}));
|
|
3637
4032
|
}
|
|
3638
4033
|
function interfaceMembers(iface) {
|
|
@@ -3658,17 +4053,17 @@ function exportedDeclarations(sf) {
|
|
|
3658
4053
|
return out;
|
|
3659
4054
|
}
|
|
3660
4055
|
function memberName(m) {
|
|
3661
|
-
if (
|
|
3662
|
-
return
|
|
4056
|
+
if (Node6.isConstructorDeclaration(m)) return "constructor";
|
|
4057
|
+
return Node6.hasName(m) ? m.getName() : "";
|
|
3663
4058
|
}
|
|
3664
4059
|
function memberSignature(m) {
|
|
3665
|
-
if (
|
|
4060
|
+
if (Node6.isMethodDeclaration(m) || Node6.isMethodSignature(m) || Node6.isConstructorDeclaration(m)) {
|
|
3666
4061
|
const params = m.getParameters().map((p) => oneLine2(p.getText())).join(", ");
|
|
3667
4062
|
const ret = m.getReturnTypeNode()?.getText();
|
|
3668
|
-
const name =
|
|
4063
|
+
const name = Node6.isConstructorDeclaration(m) ? "constructor" : memberName(m);
|
|
3669
4064
|
return oneLine2(`${name}(${params})${ret ? `: ${ret}` : ""}`);
|
|
3670
4065
|
}
|
|
3671
|
-
if (
|
|
4066
|
+
if (Node6.isPropertyDeclaration(m) || Node6.isPropertySignature(m)) {
|
|
3672
4067
|
const type = m.getTypeNode()?.getText();
|
|
3673
4068
|
return oneLine2(`${memberName(m)}${type ? `: ${type}` : ""}`);
|
|
3674
4069
|
}
|
|
@@ -4476,6 +4871,36 @@ var SOURCE = [
|
|
|
4476
4871
|
source: "source-metrics",
|
|
4477
4872
|
description: "1 when the function's body is one call forwarding every parameter, in order, as a bare argument; else 0."
|
|
4478
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
|
+
},
|
|
4479
4904
|
{
|
|
4480
4905
|
name: "except_count",
|
|
4481
4906
|
unit: "count",
|
|
@@ -4504,7 +4929,7 @@ var SOURCE = [
|
|
|
4504
4929
|
direction: "higher-worse",
|
|
4505
4930
|
absent: "zero",
|
|
4506
4931
|
source: "exception-handling",
|
|
4507
|
-
description: "Handlers whose body is empty, pass, ..., continue, a bare or empty return, or a single logging call."
|
|
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."
|
|
4508
4933
|
},
|
|
4509
4934
|
{
|
|
4510
4935
|
name: "class_count",
|
|
@@ -4939,9 +5364,10 @@ function runMetricOutlierRule(rule, ctx) {
|
|
|
4939
5364
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4940
5365
|
if (value !== void 0) carriers.push({ node, value });
|
|
4941
5366
|
}
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
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));
|
|
4945
5371
|
}
|
|
4946
5372
|
function percentileOf(values, percentile) {
|
|
4947
5373
|
const sorted = [...values].sort((a, b) => a - b);
|
|
@@ -4951,7 +5377,8 @@ function percentileOf(values, percentile) {
|
|
|
4951
5377
|
return sorted[lower] + (sorted[upper] - sorted[lower]) * (rank - lower);
|
|
4952
5378
|
}
|
|
4953
5379
|
function toViolation(rule, node, value, threshold) {
|
|
4954
|
-
const
|
|
5380
|
+
const floorSuffix = rule.floor === void 0 ? "" : `, floor ${formatNumber(rule.floor)}`;
|
|
5381
|
+
const bound = `p${formatNumber(rule.percentile)} ${formatNumber(threshold)}${floorSuffix}`;
|
|
4955
5382
|
return {
|
|
4956
5383
|
ruleId: rule.id,
|
|
4957
5384
|
severity: severityOf(rule),
|
|
@@ -5340,6 +5767,12 @@ function assertMetricOutlier(r, warn) {
|
|
|
5340
5767
|
if (r.minSample !== void 0 && (!Number.isInteger(r.minSample) || r.minSample < 1)) {
|
|
5341
5768
|
throw new Error(`${r.id}: minSample must be a positive integer`);
|
|
5342
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
|
+
}
|
|
5343
5776
|
const ruleId = r.id;
|
|
5344
5777
|
return {
|
|
5345
5778
|
type: "metric-outlier",
|
|
@@ -5348,6 +5781,8 @@ function assertMetricOutlier(r, warn) {
|
|
|
5348
5781
|
kind: r.kind,
|
|
5349
5782
|
percentile: r.percentile,
|
|
5350
5783
|
minSample: r.minSample,
|
|
5784
|
+
floor: r.floor,
|
|
5785
|
+
rankNonZero: r.rankNonZero,
|
|
5351
5786
|
severity: r.severity
|
|
5352
5787
|
};
|
|
5353
5788
|
}
|
|
@@ -5419,18 +5854,18 @@ function checkSnapshot(store, options) {
|
|
|
5419
5854
|
});
|
|
5420
5855
|
return { snapshot, baselineSnapshot, result };
|
|
5421
5856
|
}
|
|
5422
|
-
async function loadCheckRules(
|
|
5857
|
+
async function loadCheckRules(path11, options = {}) {
|
|
5423
5858
|
let raw;
|
|
5424
5859
|
try {
|
|
5425
|
-
raw = await readFile2(
|
|
5860
|
+
raw = await readFile2(path11, "utf8");
|
|
5426
5861
|
} catch (err) {
|
|
5427
|
-
throw new Error(`Cannot read rules file at ${
|
|
5862
|
+
throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
|
|
5428
5863
|
}
|
|
5429
5864
|
let parsed;
|
|
5430
5865
|
try {
|
|
5431
5866
|
parsed = JSON.parse(raw);
|
|
5432
5867
|
} catch (err) {
|
|
5433
|
-
throw new Error(`Invalid JSON in ${
|
|
5868
|
+
throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
|
|
5434
5869
|
}
|
|
5435
5870
|
return validateRules(parsed, options);
|
|
5436
5871
|
}
|
|
@@ -5842,7 +6277,7 @@ function pushMulti(map, key, value) {
|
|
|
5842
6277
|
}
|
|
5843
6278
|
|
|
5844
6279
|
// src/conventions/areas.ts
|
|
5845
|
-
import * as
|
|
6280
|
+
import * as path10 from "path";
|
|
5846
6281
|
import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
|
|
5847
6282
|
var DEFAULT_MIN_SIZE = 3;
|
|
5848
6283
|
var PROMPT_FILE_CAP = 30;
|
|
@@ -5926,7 +6361,7 @@ function rankByDegree(members, edges) {
|
|
|
5926
6361
|
function dominantDirectory(files) {
|
|
5927
6362
|
const counts = /* @__PURE__ */ new Map();
|
|
5928
6363
|
for (const f of files) {
|
|
5929
|
-
const dir =
|
|
6364
|
+
const dir = path10.posix.dirname(f);
|
|
5930
6365
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
5931
6366
|
}
|
|
5932
6367
|
const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|