@titan-design/code-graph 0.4.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 +7 -4
- package/dist/index.d.ts +42 -6
- package/dist/index.js +314 -80
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
-
|
|
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(...
|
|
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
|
|
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
|
-
|
|
2307
|
+
const counts = deadCodeOf(file);
|
|
2308
|
+
if (!counts) continue;
|
|
2158
2309
|
const id = fileIdOf(file.filePath);
|
|
2159
|
-
const unreachable =
|
|
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 +=
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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",
|
|
@@ -4177,11 +4353,13 @@ function describeMetrics(names) {
|
|
|
4177
4353
|
|
|
4178
4354
|
// src/check/context.ts
|
|
4179
4355
|
function buildRuleContext(store, snapshotId) {
|
|
4180
|
-
const
|
|
4356
|
+
const all = store.listNodes(snapshotId, { includeSymbols: true });
|
|
4357
|
+
const nodes = all.filter((n) => n.kind !== "symbol");
|
|
4358
|
+
const symbolNodes2 = all.filter((n) => n.kind === "symbol");
|
|
4181
4359
|
const edges = store.listEdges(snapshotId);
|
|
4182
4360
|
const nodesById = /* @__PURE__ */ new Map();
|
|
4183
4361
|
for (const n of nodes) nodesById.set(n.id, n);
|
|
4184
|
-
return { nodes, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
|
|
4362
|
+
return { nodes, symbolNodes: symbolNodes2, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
|
|
4185
4363
|
}
|
|
4186
4364
|
function indexMetrics(metrics) {
|
|
4187
4365
|
const metricsByNode = /* @__PURE__ */ new Map();
|
|
@@ -4339,6 +4517,18 @@ function barrelMessage(barrelPkg, importerCount) {
|
|
|
4339
4517
|
return importerCount === 0 ? `barrel in package "${barrelPkg}" has no importers` : `barrel in package "${barrelPkg}" has ${importerCount} internal importer(s) but zero external`;
|
|
4340
4518
|
}
|
|
4341
4519
|
|
|
4520
|
+
// src/check/violation-location.ts
|
|
4521
|
+
var CODE_GRAPH_TOOL = "code-graph";
|
|
4522
|
+
function locateNode(node) {
|
|
4523
|
+
if (node.kind !== "symbol") return { path: node.id };
|
|
4524
|
+
const location = { path: node.parentId ?? node.id, symbol: node.name };
|
|
4525
|
+
const startLine = node.attrs?.startLine;
|
|
4526
|
+
const endLine = node.attrs?.endLine;
|
|
4527
|
+
if (typeof startLine === "number") location.lineStart = startLine;
|
|
4528
|
+
if (typeof endLine === "number") location.lineEnd = endLine;
|
|
4529
|
+
return location;
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4342
4532
|
// src/check/metric-rules.ts
|
|
4343
4533
|
function nodeFilter(rule) {
|
|
4344
4534
|
return {
|
|
@@ -4353,71 +4543,76 @@ function passesFilter(node, filter) {
|
|
|
4353
4543
|
if (node.role && filter.excludedRoles.has(node.role)) return false;
|
|
4354
4544
|
return true;
|
|
4355
4545
|
}
|
|
4356
|
-
function
|
|
4546
|
+
function candidateNodes(rule, ctx) {
|
|
4357
4547
|
const filter = nodeFilter(rule);
|
|
4548
|
+
const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
|
|
4549
|
+
return pool.filter((node) => passesFilter(node, filter));
|
|
4550
|
+
}
|
|
4551
|
+
function toViolation(rule, breach) {
|
|
4552
|
+
return {
|
|
4553
|
+
ruleId: rule.id,
|
|
4554
|
+
severity: severityOf(rule),
|
|
4555
|
+
nodeId: breach.node.id,
|
|
4556
|
+
metric: breach.metric,
|
|
4557
|
+
value: breach.value,
|
|
4558
|
+
threshold: breach.threshold,
|
|
4559
|
+
message: breach.message,
|
|
4560
|
+
...locateNode(breach.node),
|
|
4561
|
+
evidence: breach.evidence,
|
|
4562
|
+
tool: CODE_GRAPH_TOOL
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
function thresholdBreach(node, metric, value, bound, threshold) {
|
|
4566
|
+
const op = bound === "max" ? ">" : "<";
|
|
4567
|
+
return {
|
|
4568
|
+
node,
|
|
4569
|
+
metric,
|
|
4570
|
+
value,
|
|
4571
|
+
threshold,
|
|
4572
|
+
message: `${metric}=${formatNumber(value)} ${op} ${formatNumber(threshold)}`,
|
|
4573
|
+
evidence: `${metric}=${formatNumber(value)} (${bound} ${formatNumber(threshold)})`
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
function runMetricMaxRule(rule, ctx) {
|
|
4358
4577
|
const out = [];
|
|
4359
|
-
for (const node of ctx
|
|
4360
|
-
if (!passesFilter(node, filter)) continue;
|
|
4578
|
+
for (const node of candidateNodes(rule, ctx)) {
|
|
4361
4579
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4362
|
-
if (value === void 0) continue;
|
|
4363
|
-
|
|
4364
|
-
out.push({
|
|
4365
|
-
ruleId: rule.id,
|
|
4366
|
-
severity: severityOf(rule),
|
|
4367
|
-
nodeId: node.id,
|
|
4368
|
-
metric: rule.metric,
|
|
4369
|
-
value,
|
|
4370
|
-
threshold: rule.max,
|
|
4371
|
-
message: `${rule.metric}=${formatNumber(value)} > ${formatNumber(rule.max)}`
|
|
4372
|
-
});
|
|
4580
|
+
if (value === void 0 || value <= rule.max) continue;
|
|
4581
|
+
out.push(toViolation(rule, thresholdBreach(node, rule.metric, value, "max", rule.max)));
|
|
4373
4582
|
}
|
|
4374
4583
|
return out;
|
|
4375
4584
|
}
|
|
4376
4585
|
function runMetricMinRule(rule, ctx) {
|
|
4377
|
-
const filter = nodeFilter(rule);
|
|
4378
4586
|
const out = [];
|
|
4379
|
-
for (const node of ctx
|
|
4380
|
-
if (!passesFilter(node, filter)) continue;
|
|
4587
|
+
for (const node of candidateNodes(rule, ctx)) {
|
|
4381
4588
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4382
|
-
if (value === void 0) continue;
|
|
4383
|
-
|
|
4384
|
-
out.push({
|
|
4385
|
-
ruleId: rule.id,
|
|
4386
|
-
severity: severityOf(rule),
|
|
4387
|
-
nodeId: node.id,
|
|
4388
|
-
metric: rule.metric,
|
|
4389
|
-
value,
|
|
4390
|
-
threshold: rule.min,
|
|
4391
|
-
message: `${rule.metric}=${formatNumber(value)} < ${formatNumber(rule.min)}`
|
|
4392
|
-
});
|
|
4589
|
+
if (value === void 0 || value >= rule.min) continue;
|
|
4590
|
+
out.push(toViolation(rule, thresholdBreach(node, rule.metric, value, "min", rule.min)));
|
|
4393
4591
|
}
|
|
4394
4592
|
return out;
|
|
4395
4593
|
}
|
|
4396
4594
|
function runMetricProductMaxRule(rule, ctx) {
|
|
4397
|
-
const filter = nodeFilter(rule);
|
|
4398
4595
|
const out = [];
|
|
4399
|
-
for (const node of ctx
|
|
4400
|
-
if (!passesFilter(node, filter)) continue;
|
|
4596
|
+
for (const node of candidateNodes(rule, ctx)) {
|
|
4401
4597
|
const inner = ctx.metricsByNode.get(node.id);
|
|
4402
4598
|
if (!inner) continue;
|
|
4403
4599
|
const components = collectComponents(rule.metrics, inner);
|
|
4404
4600
|
if (!components) continue;
|
|
4405
4601
|
const product = components.reduce((a, b) => a * b, 1);
|
|
4406
4602
|
if (product <= rule.max) continue;
|
|
4407
|
-
out.push(
|
|
4603
|
+
out.push(toViolation(rule, productBreach(rule, node, components, product)));
|
|
4408
4604
|
}
|
|
4409
4605
|
return out;
|
|
4410
4606
|
}
|
|
4411
|
-
function
|
|
4607
|
+
function productBreach(rule, node, components, product) {
|
|
4412
4608
|
const detail = rule.metrics.map((m, i) => `${m}=${formatNumber(components[i])}`).join(" * ");
|
|
4413
4609
|
return {
|
|
4414
|
-
|
|
4415
|
-
severity: severityOf(rule),
|
|
4416
|
-
nodeId,
|
|
4610
|
+
node,
|
|
4417
4611
|
metric: rule.metrics.join(" * "),
|
|
4418
4612
|
value: product,
|
|
4419
4613
|
threshold: rule.max,
|
|
4420
|
-
message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}
|
|
4614
|
+
message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`,
|
|
4615
|
+
evidence: `${detail} = ${formatNumber(product)} (max ${formatNumber(rule.max)})`
|
|
4421
4616
|
};
|
|
4422
4617
|
}
|
|
4423
4618
|
function collectComponents(metrics, values) {
|
|
@@ -4507,6 +4702,43 @@ function countByOriginAndSeverity(violations) {
|
|
|
4507
4702
|
return counts;
|
|
4508
4703
|
}
|
|
4509
4704
|
|
|
4705
|
+
// src/check/findings.ts
|
|
4706
|
+
function toFindings(result) {
|
|
4707
|
+
return result.violations.map(violationToFinding);
|
|
4708
|
+
}
|
|
4709
|
+
function violationToFinding(v) {
|
|
4710
|
+
const tool = v.tool ?? CODE_GRAPH_TOOL;
|
|
4711
|
+
const target = v.destinationId ? `${v.nodeId}->${v.destinationId}` : v.nodeId;
|
|
4712
|
+
return withoutUndefined({
|
|
4713
|
+
id: `${tool}:${v.ruleId}:${target}`,
|
|
4714
|
+
path: v.path ?? v.nodeId,
|
|
4715
|
+
lineStart: v.lineStart,
|
|
4716
|
+
lineEnd: v.lineEnd,
|
|
4717
|
+
symbol: v.symbol,
|
|
4718
|
+
signal: v.ruleId,
|
|
4719
|
+
value: v.value,
|
|
4720
|
+
threshold: v.threshold,
|
|
4721
|
+
severity: v.severity,
|
|
4722
|
+
evidence: v.evidence ?? v.message,
|
|
4723
|
+
tool
|
|
4724
|
+
});
|
|
4725
|
+
}
|
|
4726
|
+
function externalToFinding(input) {
|
|
4727
|
+
return withoutUndefined({
|
|
4728
|
+
id: `${input.tool}:${input.rule}:${input.file}:${input.line}`,
|
|
4729
|
+
path: input.file,
|
|
4730
|
+
lineStart: input.line,
|
|
4731
|
+
lineEnd: input.endLine,
|
|
4732
|
+
signal: input.rule,
|
|
4733
|
+
severity: input.severity,
|
|
4734
|
+
evidence: input.message,
|
|
4735
|
+
tool: input.tool
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
function withoutUndefined(finding) {
|
|
4739
|
+
return Object.fromEntries(Object.entries(finding).filter(([, v]) => v !== void 0));
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4510
4742
|
// src/check/validate.ts
|
|
4511
4743
|
function validateRules(input, options = {}) {
|
|
4512
4744
|
if (!input || typeof input !== "object") {
|
|
@@ -5414,6 +5646,7 @@ export {
|
|
|
5414
5646
|
embedSnapshot,
|
|
5415
5647
|
embedTextsCached,
|
|
5416
5648
|
externalId,
|
|
5649
|
+
externalToFinding,
|
|
5417
5650
|
fileId,
|
|
5418
5651
|
findConventions,
|
|
5419
5652
|
findSimilarCapability,
|
|
@@ -5472,6 +5705,7 @@ export {
|
|
|
5472
5705
|
summarizeConventions,
|
|
5473
5706
|
symbolId,
|
|
5474
5707
|
testCoverageCountMetrics,
|
|
5708
|
+
toFindings,
|
|
5475
5709
|
tryEmbedSnapshot,
|
|
5476
5710
|
validateRules,
|
|
5477
5711
|
violationKey,
|