@titan-design/code-graph 0.5.0 → 0.6.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 CHANGED
@@ -27,8 +27,8 @@ In: the parser (tree-sitter WASM for TypeScript, TSX and Python, since moved to
27
27
  `@titan-design/code-parser` and still re-exported here), the walk, the ts-morph
28
28
  extractor and its symbol layer, role classification, generated-file detection, id aliasing
29
29
  across git renames, the three-tier incremental reuse, and the metrics computed at index time
30
- (degree, utilization, loc, cyclomatic, cognitive, nesting, class count, lcom4, per-symbol
31
- complexity). `lcom.ts` came along despite being an analysis: `source-metrics.ts` calls it
30
+ (degree, utilization, loc, cyclomatic, cognitive, nesting, class count, lcom4, and per
31
+ symbol `symbol_cognitive`, `symbol_cyclomatic`, `symbol_loc` and `symbol_max_nesting`). `lcom.ts` came along despite being an analysis: `source-metrics.ts` calls it
32
32
  directly and lcom4 is a pure function of a file's bytes, so it belongs with the metrics that
33
33
  carry forward under reuse.
34
34
 
@@ -307,8 +307,11 @@ Dead-code and growth-risk metrics are computed at index time, like the source me
307
307
  carry forward for unchanged files. Both are sparse: a file gets a row only when a count is
308
308
  above zero.
309
309
 
310
- - Dead code, TypeScript only: `unreachable_statements` (after a `return`, `throw`, `break`
311
- or `continue` in the same block), `unused_locals`, and `unused_params` (trailing run only).
310
+ - Dead code, TypeScript and Python: `unreachable_statements` (after a `return`, `throw` or
311
+ `raise`, `break` or `continue` in the same block), `unused_locals`, and `unused_params`
312
+ (trailing run only). Python skips `self`, `cls`, `_`-prefixed names, `global` and
313
+ `nonlocal` names, and the parameters of stub bodies such as `@overload` signatures; a
314
+ `*args` or `**kwargs` ends the trailing run.
312
315
  - Growth risk, TypeScript and Python: `loop_depth` (at 2 or more), `recursive_functions`,
313
316
  and `search_in_loop` (`.includes`, `.find` and similar inside a loop). These are smells,
314
317
  not complexity bounds. Recursion and search match TypeScript call nodes only, so Python
package/dist/index.d.ts CHANGED
@@ -205,7 +205,7 @@ declare function runPrune(store: CodeGraphStore, options?: PruneOptions & {
205
205
  * index version is never reused, so a change to node/edge shape or to a metric's
206
206
  * value for the same bytes can never be carried forward from an incompatible graph.
207
207
  */
208
- declare const INDEX_VERSION = "0.15.0";
208
+ declare const INDEX_VERSION = "0.16.0";
209
209
  interface IndexOptions {
210
210
  /** Roots to walk. Node ids are still rooted at the git toplevel, so importers across roots share an id space. */
211
211
  paths: string[];
@@ -779,10 +779,10 @@ declare function buildIndexerMetrics(input: IndexerMetricsInput): GraphMetric[];
779
779
  */
780
780
  declare const DEAD_CODE_METRIC_NAMES: ReadonlySet<string>;
781
781
  /**
782
- * Per-file dead-code metrics for TypeScript files. Emitted sparsely — only when
783
- * a count is > 0 — so a clean file adds no rows (a full index and an incremental
784
- * re-index therefore produce the identical metric set). Non-TypeScript files are
785
- * skipped for now.
782
+ * Per-file dead-code metrics for TypeScript and Python files (Python since
783
+ * TP-318, in dead-code-python.ts). Emitted sparsely — only when a count is > 0 —
784
+ * so a clean file adds no rows (a full index and an incremental re-index
785
+ * therefore produce the identical metric set). Other languages are skipped.
786
786
  */
787
787
  declare function computeDeadCodeMetrics(files: readonly ParsedFile[], fileIdOf: (filePath: string) => string): GraphMetric[];
788
788
 
package/dist/index.js CHANGED
@@ -1937,6 +1937,36 @@ function areLinked(a, b) {
1937
1937
  return false;
1938
1938
  }
1939
1939
 
1940
+ // src/symbol-metrics.ts
1941
+ var SYMBOL_METRICS = [
1942
+ { name: "symbol_cognitive", stat: "cognitive", unit: "count" },
1943
+ { name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
1944
+ { name: "symbol_loc", stat: "loc", unit: "lines" },
1945
+ { name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" }
1946
+ ];
1947
+ var SYMBOL_METRIC_NAMES = SYMBOL_METRICS.map((m) => m.name);
1948
+ function symbolMetrics(fileId2, stats, symbolNames) {
1949
+ if (symbolNames.size === 0) return [];
1950
+ const byName = /* @__PURE__ */ new Map();
1951
+ for (const s of stats) {
1952
+ if (!s.name || !symbolNames.has(s.name)) continue;
1953
+ const prev = byName.get(s.name);
1954
+ if (!prev) {
1955
+ byName.set(s.name, { ...s });
1956
+ continue;
1957
+ }
1958
+ for (const { stat } of SYMBOL_METRICS) prev[stat] = Math.max(prev[stat], s[stat]);
1959
+ }
1960
+ const out = [];
1961
+ for (const [name, values] of byName) {
1962
+ const nodeId = symbolId(fileId2, name);
1963
+ for (const m of SYMBOL_METRICS) {
1964
+ out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
1965
+ }
1966
+ }
1967
+ return out;
1968
+ }
1969
+
1940
1970
  // src/source-metrics.ts
1941
1971
  var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
1942
1972
  "function_declaration",
@@ -1986,12 +2016,11 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
1986
2016
  "max_nesting_depth",
1987
2017
  "class_count",
1988
2018
  "lcom4_max",
1989
- // Per-symbol complexity (C-58; C-64 extends it to non-exported symbols), keyed
2019
+ // Per-symbol complexity, loc and nesting (C-58, C-64, TP-317), keyed
1990
2020
  // to `symbol` node ids. Source-local like the file-level metrics above, so an
1991
2021
  // unchanged file carries them forward — but their nodeId is `<fileId>#<name>`,
1992
2022
  // so the reuse basis buckets them under the symbol's parent file (incremental.ts).
1993
- "symbol_cognitive",
1994
- "symbol_cyclomatic"
2023
+ ...SYMBOL_METRIC_NAMES
1995
2024
  ]);
1996
2025
  var EMPTY_NAMES = /* @__PURE__ */ new Set();
1997
2026
  function computeSourceMetrics(files, fileIdOf, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
@@ -2045,30 +2074,10 @@ function metricsForFile(nodeId, file, symbolNames) {
2045
2074
  unit: "count"
2046
2075
  });
2047
2076
  }
2048
- out.push(...symbolComplexityMetrics(nodeId, stats, symbolNames));
2077
+ out.push(...symbolMetrics(nodeId, stats, symbolNames));
2049
2078
  out.push(...computeLcomMetrics(file, nodeId));
2050
2079
  return out;
2051
2080
  }
2052
- function symbolComplexityMetrics(fileId2, stats, symbolNames) {
2053
- if (symbolNames.size === 0) return [];
2054
- const byName = /* @__PURE__ */ new Map();
2055
- for (const s of stats) {
2056
- if (!s.name || !symbolNames.has(s.name)) continue;
2057
- const prev = byName.get(s.name);
2058
- if (!prev) byName.set(s.name, { cognitive: s.cognitive, cyclomatic: s.cyclomatic });
2059
- else {
2060
- prev.cognitive = Math.max(prev.cognitive, s.cognitive);
2061
- prev.cyclomatic = Math.max(prev.cyclomatic, s.cyclomatic);
2062
- }
2063
- }
2064
- const out = [];
2065
- for (const [name, m] of byName) {
2066
- const sid = symbolId(fileId2, name);
2067
- out.push({ nodeId: sid, name: "symbol_cognitive", value: m.cognitive, unit: "count" });
2068
- out.push({ nodeId: sid, name: "symbol_cyclomatic", value: m.cyclomatic, unit: "count" });
2069
- }
2070
- return out;
2071
- }
2072
2081
  function countLoc(content) {
2073
2082
  return content.split("\n").filter((l) => l.trim() !== "").length;
2074
2083
  }
@@ -2082,7 +2091,8 @@ function analyzeFunctions(file) {
2082
2091
  name: fn.name === null ? null : qualify(scope, fn.name),
2083
2092
  cyclomatic: cyclomaticOf(fn.body, file.language),
2084
2093
  cognitive: cognitiveComplexityOf(fn.body, file.language),
2085
- nestingDepth: nestingDepthOf(fn.body, file.language, 0)
2094
+ nestingDepth: nestingDepthOf(fn.body, file.language, 0),
2095
+ loc: fn.node.endPosition.row - fn.node.startPosition.row + 1
2086
2096
  });
2087
2097
  });
2088
2098
  return stats;
@@ -2090,12 +2100,12 @@ function analyzeFunctions(file) {
2090
2100
  function functionAt(node, fnTypes) {
2091
2101
  if (fnTypes.has(node.type)) {
2092
2102
  const body = node.childForFieldName("body");
2093
- return body ? { name: node.childForFieldName("name")?.text ?? null, body } : null;
2103
+ return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
2094
2104
  }
2095
2105
  if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
2096
2106
  const body = node.childForFieldName("body");
2097
2107
  if (!body) return null;
2098
- return { name: node.parent.childForFieldName("name")?.text ?? null, body };
2108
+ return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
2099
2109
  }
2100
2110
  return null;
2101
2111
  }
@@ -2130,6 +2140,146 @@ function cyclomaticOf(body, language) {
2130
2140
  return complexity;
2131
2141
  }
2132
2142
 
2143
+ // src/analysis/dead-code-python.ts
2144
+ var TERMINALS = /* @__PURE__ */ new Set([
2145
+ "return_statement",
2146
+ "raise_statement",
2147
+ "break_statement",
2148
+ "continue_statement"
2149
+ ]);
2150
+ var SCOPE_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda", "class_definition"]);
2151
+ var SPLAT_TYPES = /* @__PURE__ */ new Set(["list_splat_pattern", "dictionary_splat_pattern"]);
2152
+ var RECEIVERS = /* @__PURE__ */ new Set(["self", "cls"]);
2153
+ function pythonDeadCode(root) {
2154
+ const out = { unreachable: 0, locals: 0, params: 0 };
2155
+ const visit = (node) => {
2156
+ if (node.type === "block") out.unreachable += unreachableInBlock(node);
2157
+ if (node.type === "function_definition") {
2158
+ const r = analyzeFunction(node);
2159
+ out.locals += r.locals;
2160
+ out.params += r.params;
2161
+ }
2162
+ for (const child of node.namedChildren) {
2163
+ if (child) visit(child);
2164
+ }
2165
+ };
2166
+ visit(root);
2167
+ return out;
2168
+ }
2169
+ function unreachableInBlock(block) {
2170
+ let seenTerminal = false;
2171
+ let count = 0;
2172
+ for (const child of block.namedChildren) {
2173
+ if (!child) continue;
2174
+ if (seenTerminal && child.type !== "comment") count++;
2175
+ if (TERMINALS.has(child.type)) seenTerminal = true;
2176
+ }
2177
+ return count;
2178
+ }
2179
+ function analyzeFunction(fn) {
2180
+ const own = ownBindings(fn);
2181
+ const nested = nestedBindings(fn);
2182
+ const refs = countReferences(fn, own.declIds);
2183
+ const isUnused = (name) => !name.startsWith("_") && !nested.has(name) && !own.declared.has(name) && (refs.get(name) ?? 0) === 0;
2184
+ let params = 0;
2185
+ const paramNames = ownParamNames(fn);
2186
+ for (let i = isStub(fn) ? -1 : paramNames.length - 1; i >= 0; i--) {
2187
+ const name = paramNames[i] ?? null;
2188
+ if (name !== null && isUnused(name)) params++;
2189
+ else break;
2190
+ }
2191
+ const paramSet = new Set(paramNames);
2192
+ let locals = 0;
2193
+ for (const name of own.locals) {
2194
+ if (!paramSet.has(name) && isUnused(name)) locals++;
2195
+ }
2196
+ return { locals, params };
2197
+ }
2198
+ function isStub(fn) {
2199
+ const body = fn.childForFieldName("body");
2200
+ return (body?.namedChildren ?? []).every((stmt) => {
2201
+ if (!stmt || stmt.type === "comment" || stmt.type === "pass_statement") return true;
2202
+ if (stmt.type === "raise_statement") return /^raise NotImplementedError\b/.test(stmt.text);
2203
+ const expr = stmt.type === "expression_statement" ? stmt.namedChild(0) : null;
2204
+ return expr?.type === "ellipsis" || expr?.type === "string";
2205
+ });
2206
+ }
2207
+ function ownParamNames(fn) {
2208
+ const params = fn.childForFieldName("parameters");
2209
+ if (!params) return [];
2210
+ const out = [];
2211
+ for (const p of params.namedChildren) {
2212
+ if (!p || p.type === "keyword_separator" || p.type === "positional_separator") continue;
2213
+ const id = paramIdentifier(p);
2214
+ out.push(id && !RECEIVERS.has(id.text) ? id.text : null);
2215
+ }
2216
+ return out;
2217
+ }
2218
+ function paramIdentifier(p) {
2219
+ if (p.type === "identifier") return p;
2220
+ if (SPLAT_TYPES.has(p.type)) return null;
2221
+ const inner = p.childForFieldName("name") ?? p.namedChild(0);
2222
+ return inner?.type === "identifier" ? inner : null;
2223
+ }
2224
+ function assignedIdentifier(node) {
2225
+ if (node.type !== "assignment" || !node.childForFieldName("right")) return null;
2226
+ const left = node.childForFieldName("left");
2227
+ return left?.type === "identifier" ? left : null;
2228
+ }
2229
+ function ownBindings(fn) {
2230
+ const out = { locals: /* @__PURE__ */ new Set(), declared: /* @__PURE__ */ new Set(), declIds: /* @__PURE__ */ new Set() };
2231
+ for (const p of fn.childForFieldName("parameters")?.namedChildren ?? []) {
2232
+ const id = p ? paramIdentifier(p) : null;
2233
+ if (id) out.declIds.add(id.id);
2234
+ }
2235
+ const walk2 = (node) => {
2236
+ if (SCOPE_TYPES.has(node.type)) return;
2237
+ const id = assignedIdentifier(node);
2238
+ if (id) {
2239
+ out.locals.add(id.text);
2240
+ out.declIds.add(id.id);
2241
+ }
2242
+ if (node.type === "global_statement" || node.type === "nonlocal_statement") {
2243
+ for (const n of node.namedChildren) if (n) out.declared.add(n.text);
2244
+ }
2245
+ for (const child of node.namedChildren) if (child) walk2(child);
2246
+ };
2247
+ const body = fn.childForFieldName("body");
2248
+ if (body) walk2(body);
2249
+ return out;
2250
+ }
2251
+ function nestedBindings(fn) {
2252
+ const out = /* @__PURE__ */ new Set();
2253
+ const walk2 = (node, nested) => {
2254
+ const inner = nested || node !== fn && SCOPE_TYPES.has(node.type);
2255
+ if (inner) {
2256
+ const id = assignedIdentifier(node);
2257
+ if (id) out.add(id.text);
2258
+ if (node.type === "parameters" || node.type === "lambda_parameters") {
2259
+ for (const p of node.namedChildren) {
2260
+ const pid = p ? paramIdentifier(p) : null;
2261
+ if (pid) out.add(pid.text);
2262
+ }
2263
+ }
2264
+ }
2265
+ for (const child of node.namedChildren) if (child) walk2(child, inner);
2266
+ };
2267
+ walk2(fn, false);
2268
+ return out;
2269
+ }
2270
+ function countReferences(fn, declIds) {
2271
+ const out = /* @__PURE__ */ new Map();
2272
+ const walk2 = (node) => {
2273
+ if (node.type === "identifier" && !declIds.has(node.id)) {
2274
+ out.set(node.text, (out.get(node.text) ?? 0) + 1);
2275
+ }
2276
+ for (const child of node.namedChildren) if (child) walk2(child);
2277
+ };
2278
+ const body = fn.childForFieldName("body");
2279
+ if (body) walk2(body);
2280
+ return out;
2281
+ }
2282
+
2133
2283
  // src/analysis/dead-code.ts
2134
2284
  var DEAD_CODE_METRIC_NAMES = /* @__PURE__ */ new Set([
2135
2285
  "unreachable_statements",
@@ -2145,7 +2295,7 @@ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
2145
2295
  "generator_function"
2146
2296
  ]);
2147
2297
  var PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
2148
- var TERMINALS = /* @__PURE__ */ new Set([
2298
+ var TERMINALS2 = /* @__PURE__ */ new Set([
2149
2299
  "return_statement",
2150
2300
  "throw_statement",
2151
2301
  "break_statement",
@@ -2154,13 +2304,13 @@ var TERMINALS = /* @__PURE__ */ new Set([
2154
2304
  function computeDeadCodeMetrics(files, fileIdOf) {
2155
2305
  const out = [];
2156
2306
  for (const file of files) {
2157
- if (file.language !== "typescript" && file.language !== "tsx") continue;
2307
+ const counts = deadCodeOf(file);
2308
+ if (!counts) continue;
2158
2309
  const id = fileIdOf(file.filePath);
2159
- const unreachable = countUnreachable(file.tree.rootNode);
2310
+ const { unreachable, locals, params } = counts;
2160
2311
  if (unreachable > 0) {
2161
2312
  out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
2162
2313
  }
2163
- const { locals, params } = countUnusedBindings(file.tree.rootNode);
2164
2314
  if (locals > 0) {
2165
2315
  out.push({ nodeId: id, name: "unused_locals", value: locals, unit: "count" });
2166
2316
  }
@@ -2170,10 +2320,16 @@ function computeDeadCodeMetrics(files, fileIdOf) {
2170
2320
  }
2171
2321
  return out;
2172
2322
  }
2323
+ function deadCodeOf(file) {
2324
+ const root = file.tree.rootNode;
2325
+ if (file.language === "python") return pythonDeadCode(root);
2326
+ if (file.language !== "typescript" && file.language !== "tsx") return null;
2327
+ return { unreachable: countUnreachable(root), ...countUnusedBindings(root) };
2328
+ }
2173
2329
  function countUnreachable(root) {
2174
2330
  let count = 0;
2175
2331
  const visit = (node) => {
2176
- if (node.type === "statement_block") count += unreachableInBlock(node);
2332
+ if (node.type === "statement_block") count += unreachableInBlock2(node);
2177
2333
  for (const child of node.namedChildren) {
2178
2334
  if (child) visit(child);
2179
2335
  }
@@ -2181,7 +2337,7 @@ function countUnreachable(root) {
2181
2337
  visit(root);
2182
2338
  return count;
2183
2339
  }
2184
- function unreachableInBlock(block) {
2340
+ function unreachableInBlock2(block) {
2185
2341
  let seenTerminal = false;
2186
2342
  let count = 0;
2187
2343
  for (const child of block.namedChildren) {
@@ -2189,7 +2345,7 @@ function unreachableInBlock(block) {
2189
2345
  if (seenTerminal && child.type !== "comment" && child.type !== "function_declaration") {
2190
2346
  count++;
2191
2347
  }
2192
- if (TERMINALS.has(child.type)) seenTerminal = true;
2348
+ if (TERMINALS2.has(child.type)) seenTerminal = true;
2193
2349
  }
2194
2350
  return count;
2195
2351
  }
@@ -2198,7 +2354,7 @@ function countUnusedBindings(root) {
2198
2354
  let params = 0;
2199
2355
  const visit = (node) => {
2200
2356
  if (FUNCTION_TYPES.has(node.type)) {
2201
- const r = analyzeFunction(node);
2357
+ const r = analyzeFunction2(node);
2202
2358
  locals += r.locals;
2203
2359
  params += r.params;
2204
2360
  }
@@ -2209,14 +2365,14 @@ function countUnusedBindings(root) {
2209
2365
  visit(root);
2210
2366
  return { locals, params };
2211
2367
  }
2212
- function analyzeFunction(fn) {
2368
+ function analyzeFunction2(fn) {
2213
2369
  const declIds = /* @__PURE__ */ new Set();
2214
2370
  const nameCount = /* @__PURE__ */ new Map();
2215
2371
  collectBindingDecls(fn, declIds, nameCount);
2216
2372
  const refs = /* @__PURE__ */ new Map();
2217
- countReferences(fn, declIds, refs);
2373
+ countReferences2(fn, declIds, refs);
2218
2374
  const isUnused = (name) => !name.startsWith("_") && (nameCount.get(name) ?? 0) <= 1 && (refs.get(name) ?? 0) === 0;
2219
- const paramNames = ownParamNames(fn);
2375
+ const paramNames = ownParamNames2(fn);
2220
2376
  let params = 0;
2221
2377
  for (let i = paramNames.length - 1; i >= 0; i--) {
2222
2378
  const name = paramNames[i] ?? null;
@@ -2229,7 +2385,7 @@ function analyzeFunction(fn) {
2229
2385
  }
2230
2386
  return { locals, params };
2231
2387
  }
2232
- function ownParamNames(fn) {
2388
+ function ownParamNames2(fn) {
2233
2389
  const params = fn.childForFieldName("parameters");
2234
2390
  if (!params) return [];
2235
2391
  const out = [];
@@ -2272,7 +2428,7 @@ function collectBindingDecls(fn, ids, nameCount) {
2272
2428
  };
2273
2429
  walk2(fn);
2274
2430
  }
2275
- function countReferences(fn, declIds, out) {
2431
+ function countReferences2(fn, declIds, out) {
2276
2432
  const walk2 = (node) => {
2277
2433
  if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
2278
2434
  out.set(node.text, (out.get(node.text) ?? 0) + 1);
@@ -2962,7 +3118,7 @@ function qualifiedByName(file) {
2962
3118
  }
2963
3119
 
2964
3120
  // src/indexer.ts
2965
- var INDEX_VERSION = "0.15.0";
3121
+ var INDEX_VERSION = "0.16.0";
2966
3122
  var LANGUAGES = ["typescript", "python"];
2967
3123
  function canonicalizePath(p) {
2968
3124
  try {
@@ -3930,6 +4086,26 @@ var SOURCE = [
3930
4086
  source: "source-metrics",
3931
4087
  description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
3932
4088
  },
4089
+ {
4090
+ name: "symbol_loc",
4091
+ unit: "lines",
4092
+ appliesTo: SYMBOL,
4093
+ rollup: "max",
4094
+ direction: "higher-worse",
4095
+ absent: "exclude",
4096
+ source: "source-metrics",
4097
+ description: "Lines spanned by the function a symbol names, signature included; the max when several share the name."
4098
+ },
4099
+ {
4100
+ name: "symbol_max_nesting",
4101
+ unit: "count",
4102
+ appliesTo: SYMBOL,
4103
+ rollup: "max",
4104
+ direction: "higher-worse",
4105
+ absent: "exclude",
4106
+ source: "source-metrics",
4107
+ description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
4108
+ },
3933
4109
  {
3934
4110
  name: "class_count",
3935
4111
  unit: "count",