@titan-design/code-graph 0.5.0 → 0.7.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 +35 -7
- package/dist/index.d.ts +35 -9
- package/dist/index.js +689 -45
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1685,6 +1685,220 @@ import { createHash } from "crypto";
|
|
|
1685
1685
|
import * as fs2 from "fs/promises";
|
|
1686
1686
|
import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
|
|
1687
1687
|
|
|
1688
|
+
// src/analysis/comment-lines.ts
|
|
1689
|
+
function commentLineStats(fn, body, lines) {
|
|
1690
|
+
const docstring = pythonDocstring(body);
|
|
1691
|
+
const comments = functionComments(fn, body);
|
|
1692
|
+
const commentLines = rowsOf(comments).size;
|
|
1693
|
+
const bodyLines = codeRows(body, docstring, comments, lines);
|
|
1694
|
+
return {
|
|
1695
|
+
commentLines,
|
|
1696
|
+
docstringLines: docstring ? rowSpan(docstring) : jsDocLines(fn),
|
|
1697
|
+
bodyLines,
|
|
1698
|
+
commentRatio: commentLines / Math.max(bodyLines, 1)
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function pythonDocstring(body) {
|
|
1702
|
+
if (body.type !== "block") return null;
|
|
1703
|
+
const first = body.namedChildren.find((c) => c !== null && c.type !== "comment");
|
|
1704
|
+
if (first?.type !== "expression_statement" || first.namedChildCount !== 1) return null;
|
|
1705
|
+
return first.namedChildren[0]?.type === "string" ? first : null;
|
|
1706
|
+
}
|
|
1707
|
+
function bodyStatements(body, docstring = pythonDocstring(body)) {
|
|
1708
|
+
if (body.type !== "block" && body.type !== "statement_block") return [body];
|
|
1709
|
+
return body.namedChildren.filter(
|
|
1710
|
+
(c) => c !== null && c.type !== "comment" && c.id !== docstring?.id
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
function functionComments(fn, body) {
|
|
1714
|
+
if (body.type !== "block") return descendantsOfType(body, "comment");
|
|
1715
|
+
const signatureEnd = fn.childForFieldName("parameters")?.endIndex ?? body.startIndex;
|
|
1716
|
+
return descendantsOfType(fn, "comment").filter((c) => c.startIndex >= signatureEnd);
|
|
1717
|
+
}
|
|
1718
|
+
function descendantsOfType(root, type) {
|
|
1719
|
+
const out = [];
|
|
1720
|
+
const visit = (node) => {
|
|
1721
|
+
if (node.type === type) out.push(node);
|
|
1722
|
+
for (const child of node.namedChildren) if (child) visit(child);
|
|
1723
|
+
};
|
|
1724
|
+
visit(root);
|
|
1725
|
+
return out;
|
|
1726
|
+
}
|
|
1727
|
+
function rowSpan(node) {
|
|
1728
|
+
return node.endPosition.row - node.startPosition.row + 1;
|
|
1729
|
+
}
|
|
1730
|
+
function rowsOf(nodes) {
|
|
1731
|
+
const rows = /* @__PURE__ */ new Set();
|
|
1732
|
+
for (const n of nodes) {
|
|
1733
|
+
for (let r = n.startPosition.row; r <= n.endPosition.row; r++) rows.add(r);
|
|
1734
|
+
}
|
|
1735
|
+
return rows;
|
|
1736
|
+
}
|
|
1737
|
+
function codeRows(body, docstring, comments, lines) {
|
|
1738
|
+
let count = 0;
|
|
1739
|
+
for (const row of rowsOf(bodyStatements(body, docstring))) {
|
|
1740
|
+
if (codeOnRow(row, lines[row] ?? "", comments).trim() !== "") count++;
|
|
1741
|
+
}
|
|
1742
|
+
return count;
|
|
1743
|
+
}
|
|
1744
|
+
function codeOnRow(row, text, comments) {
|
|
1745
|
+
let out = text;
|
|
1746
|
+
for (const c of comments) {
|
|
1747
|
+
if (row < c.startPosition.row || row > c.endPosition.row) continue;
|
|
1748
|
+
const from = row === c.startPosition.row ? c.startPosition.column : 0;
|
|
1749
|
+
const to = row === c.endPosition.row ? c.endPosition.column : out.length;
|
|
1750
|
+
out = out.slice(0, from) + " ".repeat(Math.max(to - from, 0)) + out.slice(to);
|
|
1751
|
+
}
|
|
1752
|
+
return out;
|
|
1753
|
+
}
|
|
1754
|
+
function jsDocLines(fn) {
|
|
1755
|
+
const anchor = declarationAnchor(fn);
|
|
1756
|
+
const prev = anchor.previousNamedSibling;
|
|
1757
|
+
if (prev?.type !== "comment" || !prev.text.startsWith("/**")) return 0;
|
|
1758
|
+
const gap = anchor.startPosition.row - prev.endPosition.row;
|
|
1759
|
+
return gap === 0 || gap === 1 ? rowSpan(prev) : 0;
|
|
1760
|
+
}
|
|
1761
|
+
function declarationAnchor(fn) {
|
|
1762
|
+
let anchor = fn;
|
|
1763
|
+
if (fn.parent?.type === "variable_declarator") anchor = fn.parent.parent ?? fn;
|
|
1764
|
+
while (anchor.parent?.type === "export_statement") anchor = anchor.parent;
|
|
1765
|
+
return anchor;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/analysis/narrating-comments.ts
|
|
1769
|
+
var MIN_SHARED = 2;
|
|
1770
|
+
var MIN_SHARE_OF_COMMENT = 0.5;
|
|
1771
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1772
|
+
"the",
|
|
1773
|
+
"and",
|
|
1774
|
+
"for",
|
|
1775
|
+
"this",
|
|
1776
|
+
"that",
|
|
1777
|
+
"with",
|
|
1778
|
+
"from",
|
|
1779
|
+
"into",
|
|
1780
|
+
"then",
|
|
1781
|
+
"else",
|
|
1782
|
+
"when",
|
|
1783
|
+
"not",
|
|
1784
|
+
"are",
|
|
1785
|
+
"was",
|
|
1786
|
+
"has",
|
|
1787
|
+
"have",
|
|
1788
|
+
"its",
|
|
1789
|
+
"our",
|
|
1790
|
+
"all",
|
|
1791
|
+
"any",
|
|
1792
|
+
"can",
|
|
1793
|
+
"will",
|
|
1794
|
+
"just",
|
|
1795
|
+
"now",
|
|
1796
|
+
"return",
|
|
1797
|
+
"const",
|
|
1798
|
+
"let",
|
|
1799
|
+
"var",
|
|
1800
|
+
"def",
|
|
1801
|
+
"self",
|
|
1802
|
+
"none",
|
|
1803
|
+
"true",
|
|
1804
|
+
"false",
|
|
1805
|
+
"null",
|
|
1806
|
+
"undefined",
|
|
1807
|
+
"new",
|
|
1808
|
+
"await",
|
|
1809
|
+
"async",
|
|
1810
|
+
"function"
|
|
1811
|
+
]);
|
|
1812
|
+
var IDENTIFIER_TYPES = /* @__PURE__ */ new Set([
|
|
1813
|
+
"identifier",
|
|
1814
|
+
"property_identifier",
|
|
1815
|
+
"shorthand_property_identifier",
|
|
1816
|
+
"type_identifier"
|
|
1817
|
+
]);
|
|
1818
|
+
function countNarratingComments(fn, body) {
|
|
1819
|
+
let count = 0;
|
|
1820
|
+
for (const comment of functionComments(fn, body)) {
|
|
1821
|
+
const target = trailedStatement(comment) ?? nextStatement(comment);
|
|
1822
|
+
if (target && narrates(comment.text, target)) count++;
|
|
1823
|
+
}
|
|
1824
|
+
return count;
|
|
1825
|
+
}
|
|
1826
|
+
function trailedStatement(comment) {
|
|
1827
|
+
const prev = comment.previousNamedSibling;
|
|
1828
|
+
if (!prev || prev.type === "comment") return null;
|
|
1829
|
+
return prev.endPosition.row === comment.startPosition.row ? prev : null;
|
|
1830
|
+
}
|
|
1831
|
+
function identifierTokens(text) {
|
|
1832
|
+
const words = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/);
|
|
1833
|
+
const out = /* @__PURE__ */ new Set();
|
|
1834
|
+
for (const w of words) {
|
|
1835
|
+
const t = w.toLowerCase();
|
|
1836
|
+
if (t.length >= 3 && !STOPWORDS.has(t)) out.add(t);
|
|
1837
|
+
}
|
|
1838
|
+
return out;
|
|
1839
|
+
}
|
|
1840
|
+
function nextStatement(comment) {
|
|
1841
|
+
let next = comment.nextNamedSibling;
|
|
1842
|
+
while (next?.type === "comment") next = next.nextNamedSibling;
|
|
1843
|
+
if (next?.type !== "block") return next;
|
|
1844
|
+
return next.namedChildren.find((c) => c !== null && c.type !== "comment") ?? null;
|
|
1845
|
+
}
|
|
1846
|
+
function narrates(commentText, statement) {
|
|
1847
|
+
const said = identifierTokens(commentText);
|
|
1848
|
+
if (said.size === 0) return false;
|
|
1849
|
+
const code = identifierTokens(statementIdentifiers(statement).join(" "));
|
|
1850
|
+
let shared = 0;
|
|
1851
|
+
for (const t of said) if (code.has(t)) shared++;
|
|
1852
|
+
return shared >= MIN_SHARED || shared / said.size >= MIN_SHARE_OF_COMMENT;
|
|
1853
|
+
}
|
|
1854
|
+
function statementIdentifiers(statement) {
|
|
1855
|
+
const out = [];
|
|
1856
|
+
const visit = (node) => {
|
|
1857
|
+
if (IDENTIFIER_TYPES.has(node.type)) out.push(node.text);
|
|
1858
|
+
for (const child of node.namedChildren) if (child) visit(child);
|
|
1859
|
+
};
|
|
1860
|
+
visit(statement);
|
|
1861
|
+
return out;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/analysis/exception-handling.ts
|
|
1865
|
+
var EXCEPTION_METRIC_NAMES = ["except_count", "except_density", "swallowed_except"];
|
|
1866
|
+
var HANDLER_TYPES = ["except_clause", "except_group_clause", "catch_clause"];
|
|
1867
|
+
var SILENT_STATEMENTS = /* @__PURE__ */ new Set(["pass_statement", "continue_statement", "empty_statement"]);
|
|
1868
|
+
var EMPTY_VALUES = /* @__PURE__ */ new Set(["None", "null", "undefined"]);
|
|
1869
|
+
var LOGGING_TOKENS = /* @__PURE__ */ new Set(["log", "logger", "logging", "warn", "warning", "warnings", "print", "console"]);
|
|
1870
|
+
function exceptionMetrics(nodeId, root, loc) {
|
|
1871
|
+
const handlers = HANDLER_TYPES.flatMap((type) => descendantsOfType(root, type));
|
|
1872
|
+
const swallowed = handlers.filter((h) => isSwallowed(h)).length;
|
|
1873
|
+
return [
|
|
1874
|
+
{ nodeId, name: "except_count", value: handlers.length, unit: "count" },
|
|
1875
|
+
{ nodeId, name: "except_density", value: loc === 0 ? 0 : handlers.length * 100 / loc, unit: "per100loc" },
|
|
1876
|
+
{ nodeId, name: "swallowed_except", value: swallowed, unit: "count" }
|
|
1877
|
+
];
|
|
1878
|
+
}
|
|
1879
|
+
function isSwallowed(handler) {
|
|
1880
|
+
const body = handler.childForFieldName("body") ?? handler.namedChildren.find((c) => c?.type === "block");
|
|
1881
|
+
if (!body) return false;
|
|
1882
|
+
const statements = bodyStatements(body, null);
|
|
1883
|
+
if (statements.length === 0) return true;
|
|
1884
|
+
return statements.length === 1 && isSilent(statements[0]);
|
|
1885
|
+
}
|
|
1886
|
+
function isSilent(statement) {
|
|
1887
|
+
if (SILENT_STATEMENTS.has(statement.type)) return true;
|
|
1888
|
+
const value = statement.namedChildren[0];
|
|
1889
|
+
if (statement.type === "return_statement") return !value || EMPTY_VALUES.has(value.text);
|
|
1890
|
+
if (statement.type !== "expression_statement" || !value) return false;
|
|
1891
|
+
if (value.type === "ellipsis") return true;
|
|
1892
|
+
return isLoggingCall(value);
|
|
1893
|
+
}
|
|
1894
|
+
function isLoggingCall(node) {
|
|
1895
|
+
if (node.type !== "call" && node.type !== "call_expression") return false;
|
|
1896
|
+
const callee = node.childForFieldName("function");
|
|
1897
|
+
if (!callee) return false;
|
|
1898
|
+
for (const token of identifierTokens(callee.text)) if (LOGGING_TOKENS.has(token)) return true;
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1688
1902
|
// src/cognitive-complexity.ts
|
|
1689
1903
|
function cognitiveComplexityOf(body, language) {
|
|
1690
1904
|
const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
|
|
@@ -1937,6 +2151,128 @@ function areLinked(a, b) {
|
|
|
1937
2151
|
return false;
|
|
1938
2152
|
}
|
|
1939
2153
|
|
|
2154
|
+
// src/analysis/pass-through.ts
|
|
2155
|
+
var RECEIVERS = /* @__PURE__ */ new Set(["self", "cls"]);
|
|
2156
|
+
var CALL_TYPES = /* @__PURE__ */ new Set(["call", "call_expression"]);
|
|
2157
|
+
var WRAPPER_TYPES = /* @__PURE__ */ new Set(["return_statement", "expression_statement"]);
|
|
2158
|
+
var PY_NAMED_PARAMS = /* @__PURE__ */ new Set(["default_parameter", "typed_default_parameter"]);
|
|
2159
|
+
var PY_SEPARATORS = /* @__PURE__ */ new Set(["keyword_separator", "positional_separator"]);
|
|
2160
|
+
function isPassThrough(fn, body) {
|
|
2161
|
+
const call = soleCall(body);
|
|
2162
|
+
if (!call) return false;
|
|
2163
|
+
const params = parameterShapes(fn);
|
|
2164
|
+
if (!params || params.length === 0) return false;
|
|
2165
|
+
const args = argumentShapes(call);
|
|
2166
|
+
return args !== null && args.length === params.length && args.every((a, i) => a === params[i]);
|
|
2167
|
+
}
|
|
2168
|
+
function soleCall(body) {
|
|
2169
|
+
const statements = bodyStatements(body);
|
|
2170
|
+
if (statements.length !== 1) return null;
|
|
2171
|
+
const only = statements[0];
|
|
2172
|
+
if (CALL_TYPES.has(only.type)) return only;
|
|
2173
|
+
if (!WRAPPER_TYPES.has(only.type) || only.namedChildCount !== 1) return null;
|
|
2174
|
+
const value = only.namedChildren[0];
|
|
2175
|
+
return CALL_TYPES.has(value.type) ? value : null;
|
|
2176
|
+
}
|
|
2177
|
+
function parameterShapes(fn) {
|
|
2178
|
+
const bare = fn.childForFieldName("parameter");
|
|
2179
|
+
if (bare) return [bare.text];
|
|
2180
|
+
const list = fn.childForFieldName("parameters");
|
|
2181
|
+
if (!list) return null;
|
|
2182
|
+
const shapes = [];
|
|
2183
|
+
for (const p of list.namedChildren) {
|
|
2184
|
+
if (!p || p.type === "comment" || PY_SEPARATORS.has(p.type)) continue;
|
|
2185
|
+
const shape = parameterShape(p);
|
|
2186
|
+
if (shape === null) return null;
|
|
2187
|
+
shapes.push(shape);
|
|
2188
|
+
}
|
|
2189
|
+
return RECEIVERS.has(shapes[0] ?? "") ? shapes.slice(1) : shapes;
|
|
2190
|
+
}
|
|
2191
|
+
function parameterShape(p) {
|
|
2192
|
+
if (p.type === "identifier") return p.text;
|
|
2193
|
+
if (p.type === "list_splat_pattern" || p.type === "dictionary_splat_pattern") return p.text;
|
|
2194
|
+
if (PY_NAMED_PARAMS.has(p.type)) return p.childForFieldName("name")?.text ?? null;
|
|
2195
|
+
if (p.type === "typed_parameter") return parameterShape(p.namedChildren[0]);
|
|
2196
|
+
if (p.type === "required_parameter" || p.type === "optional_parameter") {
|
|
2197
|
+
const pattern = p.childForFieldName("pattern");
|
|
2198
|
+
return pattern ? tsPatternShape(pattern) : null;
|
|
2199
|
+
}
|
|
2200
|
+
return tsPatternShape(p);
|
|
2201
|
+
}
|
|
2202
|
+
function tsPatternShape(pattern) {
|
|
2203
|
+
if (pattern.type === "identifier") return pattern.text;
|
|
2204
|
+
if (pattern.type === "rest_pattern") return `...${pattern.namedChildren[0]?.text ?? ""}`;
|
|
2205
|
+
if (pattern.type === "assignment_pattern") {
|
|
2206
|
+
const left = pattern.childForFieldName("left");
|
|
2207
|
+
return left ? tsPatternShape(left) : null;
|
|
2208
|
+
}
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
function argumentShapes(call) {
|
|
2212
|
+
const list = call.childForFieldName("arguments");
|
|
2213
|
+
if (!list || list.type !== "argument_list" && list.type !== "arguments") return null;
|
|
2214
|
+
const shapes = [];
|
|
2215
|
+
for (const a of list.namedChildren) {
|
|
2216
|
+
if (!a || a.type === "comment") continue;
|
|
2217
|
+
const shape = argumentShape(a);
|
|
2218
|
+
if (shape === null) return null;
|
|
2219
|
+
shapes.push(shape);
|
|
2220
|
+
}
|
|
2221
|
+
return shapes;
|
|
2222
|
+
}
|
|
2223
|
+
function argumentShape(a) {
|
|
2224
|
+
if (a.type === "identifier") return a.text;
|
|
2225
|
+
const inner = a.namedChildren[0];
|
|
2226
|
+
if (inner?.type !== "identifier") return null;
|
|
2227
|
+
if (a.type === "list_splat") return `*${inner.text}`;
|
|
2228
|
+
if (a.type === "dictionary_splat") return `**${inner.text}`;
|
|
2229
|
+
if (a.type === "spread_element") return `...${inner.text}`;
|
|
2230
|
+
return null;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// src/symbol-metrics.ts
|
|
2234
|
+
var SYMBOL_METRICS = [
|
|
2235
|
+
{ name: "symbol_cognitive", stat: "cognitive", unit: "count" },
|
|
2236
|
+
{ name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
|
|
2237
|
+
{ name: "symbol_loc", stat: "loc", unit: "lines" },
|
|
2238
|
+
{ name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" },
|
|
2239
|
+
{ name: "symbol_comment_lines", stat: "commentLines", unit: "lines" },
|
|
2240
|
+
{ name: "symbol_docstring_lines", stat: "docstringLines", unit: "lines" },
|
|
2241
|
+
{ name: "symbol_body_lines", stat: "bodyLines", unit: "lines" },
|
|
2242
|
+
{ name: "symbol_comment_ratio", stat: "commentRatio", unit: "ratio" },
|
|
2243
|
+
{ name: "symbol_narrating_comments", stat: "narratingComments", unit: "count" },
|
|
2244
|
+
{ name: "symbol_pass_through", stat: "passThrough", unit: "count" }
|
|
2245
|
+
];
|
|
2246
|
+
var SYMBOL_METRIC_NAMES = SYMBOL_METRICS.map((m) => m.name);
|
|
2247
|
+
function symbolMetrics(fileId2, stats, symbolNames) {
|
|
2248
|
+
if (symbolNames.size === 0) return [];
|
|
2249
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2250
|
+
for (const s of stats) {
|
|
2251
|
+
if (!s.name || !symbolNames.has(s.name)) continue;
|
|
2252
|
+
const prev = byName.get(s.name);
|
|
2253
|
+
if (!prev) {
|
|
2254
|
+
byName.set(s.name, { ...s });
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
for (const { stat } of SYMBOL_METRICS) prev[stat] = Math.max(prev[stat], s[stat]);
|
|
2258
|
+
}
|
|
2259
|
+
const out = [];
|
|
2260
|
+
for (const [name, values] of byName) {
|
|
2261
|
+
const nodeId = symbolId(fileId2, name);
|
|
2262
|
+
for (const m of SYMBOL_METRICS) {
|
|
2263
|
+
out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return out;
|
|
2267
|
+
}
|
|
2268
|
+
function functionShapeStats(fn, body, lines) {
|
|
2269
|
+
return {
|
|
2270
|
+
...commentLineStats(fn, body, lines),
|
|
2271
|
+
narratingComments: countNarratingComments(fn, body),
|
|
2272
|
+
passThrough: isPassThrough(fn, body) ? 1 : 0
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
|
|
1940
2276
|
// src/source-metrics.ts
|
|
1941
2277
|
var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
1942
2278
|
"function_declaration",
|
|
@@ -1986,12 +2322,12 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
1986
2322
|
"max_nesting_depth",
|
|
1987
2323
|
"class_count",
|
|
1988
2324
|
"lcom4_max",
|
|
1989
|
-
|
|
2325
|
+
...EXCEPTION_METRIC_NAMES,
|
|
2326
|
+
// Per-symbol complexity, size, comments and shape (C-58, C-64, TP-317, TP-322), keyed
|
|
1990
2327
|
// to `symbol` node ids. Source-local like the file-level metrics above, so an
|
|
1991
2328
|
// unchanged file carries them forward — but their nodeId is `<fileId>#<name>`,
|
|
1992
2329
|
// so the reuse basis buckets them under the symbol's parent file (incremental.ts).
|
|
1993
|
-
|
|
1994
|
-
"symbol_cyclomatic"
|
|
2330
|
+
...SYMBOL_METRIC_NAMES
|
|
1995
2331
|
]);
|
|
1996
2332
|
var EMPTY_NAMES = /* @__PURE__ */ new Set();
|
|
1997
2333
|
function computeSourceMetrics(files, fileIdOf, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
|
|
@@ -2045,28 +2381,9 @@ function metricsForFile(nodeId, file, symbolNames) {
|
|
|
2045
2381
|
unit: "count"
|
|
2046
2382
|
});
|
|
2047
2383
|
}
|
|
2048
|
-
out.push(...
|
|
2384
|
+
out.push(...symbolMetrics(nodeId, stats, symbolNames));
|
|
2049
2385
|
out.push(...computeLcomMetrics(file, nodeId));
|
|
2050
|
-
|
|
2051
|
-
}
|
|
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
|
-
}
|
|
2386
|
+
out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
|
|
2070
2387
|
return out;
|
|
2071
2388
|
}
|
|
2072
2389
|
function countLoc(content) {
|
|
@@ -2075,6 +2392,7 @@ function countLoc(content) {
|
|
|
2075
2392
|
function analyzeFunctions(file) {
|
|
2076
2393
|
const stats = [];
|
|
2077
2394
|
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
|
|
2395
|
+
const lines = file.content.split("\n");
|
|
2078
2396
|
walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
|
|
2079
2397
|
const fn = functionAt(node, fnTypes);
|
|
2080
2398
|
if (!fn) return;
|
|
@@ -2082,7 +2400,9 @@ function analyzeFunctions(file) {
|
|
|
2082
2400
|
name: fn.name === null ? null : qualify(scope, fn.name),
|
|
2083
2401
|
cyclomatic: cyclomaticOf(fn.body, file.language),
|
|
2084
2402
|
cognitive: cognitiveComplexityOf(fn.body, file.language),
|
|
2085
|
-
nestingDepth: nestingDepthOf(fn.body, file.language, 0)
|
|
2403
|
+
nestingDepth: nestingDepthOf(fn.body, file.language, 0),
|
|
2404
|
+
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
|
|
2405
|
+
...functionShapeStats(fn.node, fn.body, lines)
|
|
2086
2406
|
});
|
|
2087
2407
|
});
|
|
2088
2408
|
return stats;
|
|
@@ -2090,12 +2410,12 @@ function analyzeFunctions(file) {
|
|
|
2090
2410
|
function functionAt(node, fnTypes) {
|
|
2091
2411
|
if (fnTypes.has(node.type)) {
|
|
2092
2412
|
const body = node.childForFieldName("body");
|
|
2093
|
-
return body ? { name: node.childForFieldName("name")?.text ?? null, body } : null;
|
|
2413
|
+
return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
|
|
2094
2414
|
}
|
|
2095
2415
|
if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
|
|
2096
2416
|
const body = node.childForFieldName("body");
|
|
2097
2417
|
if (!body) return null;
|
|
2098
|
-
return { name: node.parent.childForFieldName("name")?.text ?? null, body };
|
|
2418
|
+
return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
|
|
2099
2419
|
}
|
|
2100
2420
|
return null;
|
|
2101
2421
|
}
|
|
@@ -2130,6 +2450,146 @@ function cyclomaticOf(body, language) {
|
|
|
2130
2450
|
return complexity;
|
|
2131
2451
|
}
|
|
2132
2452
|
|
|
2453
|
+
// src/analysis/dead-code-python.ts
|
|
2454
|
+
var TERMINALS = /* @__PURE__ */ new Set([
|
|
2455
|
+
"return_statement",
|
|
2456
|
+
"raise_statement",
|
|
2457
|
+
"break_statement",
|
|
2458
|
+
"continue_statement"
|
|
2459
|
+
]);
|
|
2460
|
+
var SCOPE_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda", "class_definition"]);
|
|
2461
|
+
var SPLAT_TYPES = /* @__PURE__ */ new Set(["list_splat_pattern", "dictionary_splat_pattern"]);
|
|
2462
|
+
var RECEIVERS2 = /* @__PURE__ */ new Set(["self", "cls"]);
|
|
2463
|
+
function pythonDeadCode(root) {
|
|
2464
|
+
const out = { unreachable: 0, locals: 0, params: 0 };
|
|
2465
|
+
const visit = (node) => {
|
|
2466
|
+
if (node.type === "block") out.unreachable += unreachableInBlock(node);
|
|
2467
|
+
if (node.type === "function_definition") {
|
|
2468
|
+
const r = analyzeFunction(node);
|
|
2469
|
+
out.locals += r.locals;
|
|
2470
|
+
out.params += r.params;
|
|
2471
|
+
}
|
|
2472
|
+
for (const child of node.namedChildren) {
|
|
2473
|
+
if (child) visit(child);
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
visit(root);
|
|
2477
|
+
return out;
|
|
2478
|
+
}
|
|
2479
|
+
function unreachableInBlock(block) {
|
|
2480
|
+
let seenTerminal = false;
|
|
2481
|
+
let count = 0;
|
|
2482
|
+
for (const child of block.namedChildren) {
|
|
2483
|
+
if (!child) continue;
|
|
2484
|
+
if (seenTerminal && child.type !== "comment") count++;
|
|
2485
|
+
if (TERMINALS.has(child.type)) seenTerminal = true;
|
|
2486
|
+
}
|
|
2487
|
+
return count;
|
|
2488
|
+
}
|
|
2489
|
+
function analyzeFunction(fn) {
|
|
2490
|
+
const own = ownBindings(fn);
|
|
2491
|
+
const nested = nestedBindings(fn);
|
|
2492
|
+
const refs = countReferences(fn, own.declIds);
|
|
2493
|
+
const isUnused = (name) => !name.startsWith("_") && !nested.has(name) && !own.declared.has(name) && (refs.get(name) ?? 0) === 0;
|
|
2494
|
+
let params = 0;
|
|
2495
|
+
const paramNames = ownParamNames(fn);
|
|
2496
|
+
for (let i = isStub(fn) ? -1 : paramNames.length - 1; i >= 0; i--) {
|
|
2497
|
+
const name = paramNames[i] ?? null;
|
|
2498
|
+
if (name !== null && isUnused(name)) params++;
|
|
2499
|
+
else break;
|
|
2500
|
+
}
|
|
2501
|
+
const paramSet = new Set(paramNames);
|
|
2502
|
+
let locals = 0;
|
|
2503
|
+
for (const name of own.locals) {
|
|
2504
|
+
if (!paramSet.has(name) && isUnused(name)) locals++;
|
|
2505
|
+
}
|
|
2506
|
+
return { locals, params };
|
|
2507
|
+
}
|
|
2508
|
+
function isStub(fn) {
|
|
2509
|
+
const body = fn.childForFieldName("body");
|
|
2510
|
+
return (body?.namedChildren ?? []).every((stmt) => {
|
|
2511
|
+
if (!stmt || stmt.type === "comment" || stmt.type === "pass_statement") return true;
|
|
2512
|
+
if (stmt.type === "raise_statement") return /^raise NotImplementedError\b/.test(stmt.text);
|
|
2513
|
+
const expr = stmt.type === "expression_statement" ? stmt.namedChild(0) : null;
|
|
2514
|
+
return expr?.type === "ellipsis" || expr?.type === "string";
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
function ownParamNames(fn) {
|
|
2518
|
+
const params = fn.childForFieldName("parameters");
|
|
2519
|
+
if (!params) return [];
|
|
2520
|
+
const out = [];
|
|
2521
|
+
for (const p of params.namedChildren) {
|
|
2522
|
+
if (!p || p.type === "keyword_separator" || p.type === "positional_separator") continue;
|
|
2523
|
+
const id = paramIdentifier(p);
|
|
2524
|
+
out.push(id && !RECEIVERS2.has(id.text) ? id.text : null);
|
|
2525
|
+
}
|
|
2526
|
+
return out;
|
|
2527
|
+
}
|
|
2528
|
+
function paramIdentifier(p) {
|
|
2529
|
+
if (p.type === "identifier") return p;
|
|
2530
|
+
if (SPLAT_TYPES.has(p.type)) return null;
|
|
2531
|
+
const inner = p.childForFieldName("name") ?? p.namedChild(0);
|
|
2532
|
+
return inner?.type === "identifier" ? inner : null;
|
|
2533
|
+
}
|
|
2534
|
+
function assignedIdentifier(node) {
|
|
2535
|
+
if (node.type !== "assignment" || !node.childForFieldName("right")) return null;
|
|
2536
|
+
const left = node.childForFieldName("left");
|
|
2537
|
+
return left?.type === "identifier" ? left : null;
|
|
2538
|
+
}
|
|
2539
|
+
function ownBindings(fn) {
|
|
2540
|
+
const out = { locals: /* @__PURE__ */ new Set(), declared: /* @__PURE__ */ new Set(), declIds: /* @__PURE__ */ new Set() };
|
|
2541
|
+
for (const p of fn.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
2542
|
+
const id = p ? paramIdentifier(p) : null;
|
|
2543
|
+
if (id) out.declIds.add(id.id);
|
|
2544
|
+
}
|
|
2545
|
+
const walk2 = (node) => {
|
|
2546
|
+
if (SCOPE_TYPES.has(node.type)) return;
|
|
2547
|
+
const id = assignedIdentifier(node);
|
|
2548
|
+
if (id) {
|
|
2549
|
+
out.locals.add(id.text);
|
|
2550
|
+
out.declIds.add(id.id);
|
|
2551
|
+
}
|
|
2552
|
+
if (node.type === "global_statement" || node.type === "nonlocal_statement") {
|
|
2553
|
+
for (const n of node.namedChildren) if (n) out.declared.add(n.text);
|
|
2554
|
+
}
|
|
2555
|
+
for (const child of node.namedChildren) if (child) walk2(child);
|
|
2556
|
+
};
|
|
2557
|
+
const body = fn.childForFieldName("body");
|
|
2558
|
+
if (body) walk2(body);
|
|
2559
|
+
return out;
|
|
2560
|
+
}
|
|
2561
|
+
function nestedBindings(fn) {
|
|
2562
|
+
const out = /* @__PURE__ */ new Set();
|
|
2563
|
+
const walk2 = (node, nested) => {
|
|
2564
|
+
const inner = nested || node !== fn && SCOPE_TYPES.has(node.type);
|
|
2565
|
+
if (inner) {
|
|
2566
|
+
const id = assignedIdentifier(node);
|
|
2567
|
+
if (id) out.add(id.text);
|
|
2568
|
+
if (node.type === "parameters" || node.type === "lambda_parameters") {
|
|
2569
|
+
for (const p of node.namedChildren) {
|
|
2570
|
+
const pid = p ? paramIdentifier(p) : null;
|
|
2571
|
+
if (pid) out.add(pid.text);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
for (const child of node.namedChildren) if (child) walk2(child, inner);
|
|
2576
|
+
};
|
|
2577
|
+
walk2(fn, false);
|
|
2578
|
+
return out;
|
|
2579
|
+
}
|
|
2580
|
+
function countReferences(fn, declIds) {
|
|
2581
|
+
const out = /* @__PURE__ */ new Map();
|
|
2582
|
+
const walk2 = (node) => {
|
|
2583
|
+
if (node.type === "identifier" && !declIds.has(node.id)) {
|
|
2584
|
+
out.set(node.text, (out.get(node.text) ?? 0) + 1);
|
|
2585
|
+
}
|
|
2586
|
+
for (const child of node.namedChildren) if (child) walk2(child);
|
|
2587
|
+
};
|
|
2588
|
+
const body = fn.childForFieldName("body");
|
|
2589
|
+
if (body) walk2(body);
|
|
2590
|
+
return out;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2133
2593
|
// src/analysis/dead-code.ts
|
|
2134
2594
|
var DEAD_CODE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
2135
2595
|
"unreachable_statements",
|
|
@@ -2145,7 +2605,7 @@ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
2145
2605
|
"generator_function"
|
|
2146
2606
|
]);
|
|
2147
2607
|
var PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
|
|
2148
|
-
var
|
|
2608
|
+
var TERMINALS2 = /* @__PURE__ */ new Set([
|
|
2149
2609
|
"return_statement",
|
|
2150
2610
|
"throw_statement",
|
|
2151
2611
|
"break_statement",
|
|
@@ -2154,13 +2614,13 @@ var TERMINALS = /* @__PURE__ */ new Set([
|
|
|
2154
2614
|
function computeDeadCodeMetrics(files, fileIdOf) {
|
|
2155
2615
|
const out = [];
|
|
2156
2616
|
for (const file of files) {
|
|
2157
|
-
|
|
2617
|
+
const counts = deadCodeOf(file);
|
|
2618
|
+
if (!counts) continue;
|
|
2158
2619
|
const id = fileIdOf(file.filePath);
|
|
2159
|
-
const unreachable =
|
|
2620
|
+
const { unreachable, locals, params } = counts;
|
|
2160
2621
|
if (unreachable > 0) {
|
|
2161
2622
|
out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
|
|
2162
2623
|
}
|
|
2163
|
-
const { locals, params } = countUnusedBindings(file.tree.rootNode);
|
|
2164
2624
|
if (locals > 0) {
|
|
2165
2625
|
out.push({ nodeId: id, name: "unused_locals", value: locals, unit: "count" });
|
|
2166
2626
|
}
|
|
@@ -2170,10 +2630,16 @@ function computeDeadCodeMetrics(files, fileIdOf) {
|
|
|
2170
2630
|
}
|
|
2171
2631
|
return out;
|
|
2172
2632
|
}
|
|
2633
|
+
function deadCodeOf(file) {
|
|
2634
|
+
const root = file.tree.rootNode;
|
|
2635
|
+
if (file.language === "python") return pythonDeadCode(root);
|
|
2636
|
+
if (file.language !== "typescript" && file.language !== "tsx") return null;
|
|
2637
|
+
return { unreachable: countUnreachable(root), ...countUnusedBindings(root) };
|
|
2638
|
+
}
|
|
2173
2639
|
function countUnreachable(root) {
|
|
2174
2640
|
let count = 0;
|
|
2175
2641
|
const visit = (node) => {
|
|
2176
|
-
if (node.type === "statement_block") count +=
|
|
2642
|
+
if (node.type === "statement_block") count += unreachableInBlock2(node);
|
|
2177
2643
|
for (const child of node.namedChildren) {
|
|
2178
2644
|
if (child) visit(child);
|
|
2179
2645
|
}
|
|
@@ -2181,7 +2647,7 @@ function countUnreachable(root) {
|
|
|
2181
2647
|
visit(root);
|
|
2182
2648
|
return count;
|
|
2183
2649
|
}
|
|
2184
|
-
function
|
|
2650
|
+
function unreachableInBlock2(block) {
|
|
2185
2651
|
let seenTerminal = false;
|
|
2186
2652
|
let count = 0;
|
|
2187
2653
|
for (const child of block.namedChildren) {
|
|
@@ -2189,7 +2655,7 @@ function unreachableInBlock(block) {
|
|
|
2189
2655
|
if (seenTerminal && child.type !== "comment" && child.type !== "function_declaration") {
|
|
2190
2656
|
count++;
|
|
2191
2657
|
}
|
|
2192
|
-
if (
|
|
2658
|
+
if (TERMINALS2.has(child.type)) seenTerminal = true;
|
|
2193
2659
|
}
|
|
2194
2660
|
return count;
|
|
2195
2661
|
}
|
|
@@ -2198,7 +2664,7 @@ function countUnusedBindings(root) {
|
|
|
2198
2664
|
let params = 0;
|
|
2199
2665
|
const visit = (node) => {
|
|
2200
2666
|
if (FUNCTION_TYPES.has(node.type)) {
|
|
2201
|
-
const r =
|
|
2667
|
+
const r = analyzeFunction2(node);
|
|
2202
2668
|
locals += r.locals;
|
|
2203
2669
|
params += r.params;
|
|
2204
2670
|
}
|
|
@@ -2209,14 +2675,14 @@ function countUnusedBindings(root) {
|
|
|
2209
2675
|
visit(root);
|
|
2210
2676
|
return { locals, params };
|
|
2211
2677
|
}
|
|
2212
|
-
function
|
|
2678
|
+
function analyzeFunction2(fn) {
|
|
2213
2679
|
const declIds = /* @__PURE__ */ new Set();
|
|
2214
2680
|
const nameCount = /* @__PURE__ */ new Map();
|
|
2215
2681
|
collectBindingDecls(fn, declIds, nameCount);
|
|
2216
2682
|
const refs = /* @__PURE__ */ new Map();
|
|
2217
|
-
|
|
2683
|
+
countReferences2(fn, declIds, refs);
|
|
2218
2684
|
const isUnused = (name) => !name.startsWith("_") && (nameCount.get(name) ?? 0) <= 1 && (refs.get(name) ?? 0) === 0;
|
|
2219
|
-
const paramNames =
|
|
2685
|
+
const paramNames = ownParamNames2(fn);
|
|
2220
2686
|
let params = 0;
|
|
2221
2687
|
for (let i = paramNames.length - 1; i >= 0; i--) {
|
|
2222
2688
|
const name = paramNames[i] ?? null;
|
|
@@ -2229,7 +2695,7 @@ function analyzeFunction(fn) {
|
|
|
2229
2695
|
}
|
|
2230
2696
|
return { locals, params };
|
|
2231
2697
|
}
|
|
2232
|
-
function
|
|
2698
|
+
function ownParamNames2(fn) {
|
|
2233
2699
|
const params = fn.childForFieldName("parameters");
|
|
2234
2700
|
if (!params) return [];
|
|
2235
2701
|
const out = [];
|
|
@@ -2272,7 +2738,7 @@ function collectBindingDecls(fn, ids, nameCount) {
|
|
|
2272
2738
|
};
|
|
2273
2739
|
walk2(fn);
|
|
2274
2740
|
}
|
|
2275
|
-
function
|
|
2741
|
+
function countReferences2(fn, declIds, out) {
|
|
2276
2742
|
const walk2 = (node) => {
|
|
2277
2743
|
if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
|
|
2278
2744
|
out.set(node.text, (out.get(node.text) ?? 0) + 1);
|
|
@@ -2962,7 +3428,7 @@ function qualifiedByName(file) {
|
|
|
2962
3428
|
}
|
|
2963
3429
|
|
|
2964
3430
|
// src/indexer.ts
|
|
2965
|
-
var INDEX_VERSION = "0.
|
|
3431
|
+
var INDEX_VERSION = "0.17.0";
|
|
2966
3432
|
var LANGUAGES = ["typescript", "python"];
|
|
2967
3433
|
function canonicalizePath(p) {
|
|
2968
3434
|
try {
|
|
@@ -3930,6 +4396,116 @@ var SOURCE = [
|
|
|
3930
4396
|
source: "source-metrics",
|
|
3931
4397
|
description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
|
|
3932
4398
|
},
|
|
4399
|
+
{
|
|
4400
|
+
name: "symbol_loc",
|
|
4401
|
+
unit: "lines",
|
|
4402
|
+
appliesTo: SYMBOL,
|
|
4403
|
+
rollup: "max",
|
|
4404
|
+
direction: "higher-worse",
|
|
4405
|
+
absent: "exclude",
|
|
4406
|
+
source: "source-metrics",
|
|
4407
|
+
description: "Lines spanned by the function a symbol names, signature included; the max when several share the name."
|
|
4408
|
+
},
|
|
4409
|
+
{
|
|
4410
|
+
name: "symbol_max_nesting",
|
|
4411
|
+
unit: "count",
|
|
4412
|
+
appliesTo: SYMBOL,
|
|
4413
|
+
rollup: "max",
|
|
4414
|
+
direction: "higher-worse",
|
|
4415
|
+
absent: "exclude",
|
|
4416
|
+
source: "source-metrics",
|
|
4417
|
+
description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
|
|
4418
|
+
},
|
|
4419
|
+
{
|
|
4420
|
+
name: "symbol_comment_lines",
|
|
4421
|
+
unit: "lines",
|
|
4422
|
+
appliesTo: SYMBOL,
|
|
4423
|
+
rollup: "max",
|
|
4424
|
+
direction: "neutral",
|
|
4425
|
+
absent: "exclude",
|
|
4426
|
+
source: "source-metrics",
|
|
4427
|
+
description: "Rows holding a comment inside the function a symbol names, the docstring excluded; the max when several share the name."
|
|
4428
|
+
},
|
|
4429
|
+
{
|
|
4430
|
+
name: "symbol_docstring_lines",
|
|
4431
|
+
unit: "lines",
|
|
4432
|
+
appliesTo: SYMBOL,
|
|
4433
|
+
rollup: "max",
|
|
4434
|
+
direction: "neutral",
|
|
4435
|
+
absent: "exclude",
|
|
4436
|
+
source: "source-metrics",
|
|
4437
|
+
description: "Rows of the function's docstring: the Python docstring, or the JSDoc block attached to a TypeScript declaration."
|
|
4438
|
+
},
|
|
4439
|
+
{
|
|
4440
|
+
name: "symbol_body_lines",
|
|
4441
|
+
unit: "lines",
|
|
4442
|
+
appliesTo: SYMBOL,
|
|
4443
|
+
rollup: "max",
|
|
4444
|
+
direction: "higher-worse",
|
|
4445
|
+
absent: "exclude",
|
|
4446
|
+
source: "source-metrics",
|
|
4447
|
+
description: "Non-blank rows of the function's body that hold code, comments and the docstring excluded."
|
|
4448
|
+
},
|
|
4449
|
+
{
|
|
4450
|
+
name: "symbol_comment_ratio",
|
|
4451
|
+
unit: "ratio",
|
|
4452
|
+
appliesTo: SYMBOL,
|
|
4453
|
+
rollup: "max",
|
|
4454
|
+
direction: "neutral",
|
|
4455
|
+
absent: "exclude",
|
|
4456
|
+
source: "source-metrics",
|
|
4457
|
+
description: "symbol_comment_lines / max(symbol_body_lines, 1). High values suggest narration; zero is not a defect."
|
|
4458
|
+
},
|
|
4459
|
+
{
|
|
4460
|
+
name: "symbol_narrating_comments",
|
|
4461
|
+
unit: "count",
|
|
4462
|
+
appliesTo: SYMBOL,
|
|
4463
|
+
rollup: "max",
|
|
4464
|
+
direction: "higher-worse",
|
|
4465
|
+
absent: "exclude",
|
|
4466
|
+
source: "source-metrics",
|
|
4467
|
+
description: "Comments in the body whose words overlap the next statement's identifiers by 2 tokens or half the comment. A candidate signal."
|
|
4468
|
+
},
|
|
4469
|
+
{
|
|
4470
|
+
name: "symbol_pass_through",
|
|
4471
|
+
unit: "count",
|
|
4472
|
+
appliesTo: SYMBOL,
|
|
4473
|
+
rollup: "max",
|
|
4474
|
+
direction: "higher-worse",
|
|
4475
|
+
absent: "exclude",
|
|
4476
|
+
source: "source-metrics",
|
|
4477
|
+
description: "1 when the function's body is one call forwarding every parameter, in order, as a bare argument; else 0."
|
|
4478
|
+
},
|
|
4479
|
+
{
|
|
4480
|
+
name: "except_count",
|
|
4481
|
+
unit: "count",
|
|
4482
|
+
appliesTo: FILE,
|
|
4483
|
+
rollup: "sum",
|
|
4484
|
+
direction: "neutral",
|
|
4485
|
+
absent: "zero",
|
|
4486
|
+
source: "exception-handling",
|
|
4487
|
+
description: "Python except clauses and TypeScript catch clauses in the file."
|
|
4488
|
+
},
|
|
4489
|
+
{
|
|
4490
|
+
name: "except_density",
|
|
4491
|
+
unit: "per100loc",
|
|
4492
|
+
appliesTo: FILE,
|
|
4493
|
+
rollup: "none",
|
|
4494
|
+
direction: "higher-worse",
|
|
4495
|
+
absent: "zero",
|
|
4496
|
+
source: "exception-handling",
|
|
4497
|
+
description: "except_count per 100 non-blank lines. Recompute from the sums for a group."
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
name: "swallowed_except",
|
|
4501
|
+
unit: "count",
|
|
4502
|
+
appliesTo: FILE,
|
|
4503
|
+
rollup: "sum",
|
|
4504
|
+
direction: "higher-worse",
|
|
4505
|
+
absent: "zero",
|
|
4506
|
+
source: "exception-handling",
|
|
4507
|
+
description: "Handlers whose body is empty, pass, ..., continue, a bare or empty return, or a single logging call."
|
|
4508
|
+
},
|
|
3933
4509
|
{
|
|
3934
4510
|
name: "class_count",
|
|
3935
4511
|
unit: "count",
|
|
@@ -4353,6 +4929,43 @@ function locateNode(node) {
|
|
|
4353
4929
|
return location;
|
|
4354
4930
|
}
|
|
4355
4931
|
|
|
4932
|
+
// src/check/outlier-rule.ts
|
|
4933
|
+
var DEFAULT_OUTLIER_MIN_SAMPLE = 20;
|
|
4934
|
+
function runMetricOutlierRule(rule, ctx) {
|
|
4935
|
+
const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
|
|
4936
|
+
const carriers = [];
|
|
4937
|
+
for (const node of pool) {
|
|
4938
|
+
if (node.kind !== rule.kind) continue;
|
|
4939
|
+
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4940
|
+
if (value !== void 0) carriers.push({ node, value });
|
|
4941
|
+
}
|
|
4942
|
+
if (carriers.length < (rule.minSample ?? DEFAULT_OUTLIER_MIN_SAMPLE)) return [];
|
|
4943
|
+
const threshold = percentileOf(carriers.map((c) => c.value), rule.percentile);
|
|
4944
|
+
return carriers.filter((c) => c.value > threshold).map((c) => toViolation(rule, c.node, c.value, threshold));
|
|
4945
|
+
}
|
|
4946
|
+
function percentileOf(values, percentile) {
|
|
4947
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4948
|
+
const rank = percentile / 100 * (sorted.length - 1);
|
|
4949
|
+
const lower = Math.floor(rank);
|
|
4950
|
+
const upper = Math.min(lower + 1, sorted.length - 1);
|
|
4951
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (rank - lower);
|
|
4952
|
+
}
|
|
4953
|
+
function toViolation(rule, node, value, threshold) {
|
|
4954
|
+
const bound = `p${formatNumber(rule.percentile)} ${formatNumber(threshold)}`;
|
|
4955
|
+
return {
|
|
4956
|
+
ruleId: rule.id,
|
|
4957
|
+
severity: severityOf(rule),
|
|
4958
|
+
nodeId: node.id,
|
|
4959
|
+
metric: rule.metric,
|
|
4960
|
+
value,
|
|
4961
|
+
threshold,
|
|
4962
|
+
message: `${rule.metric}=${formatNumber(value)} > ${bound}`,
|
|
4963
|
+
...locateNode(node),
|
|
4964
|
+
evidence: `${rule.metric}=${formatNumber(value)} (${bound})`,
|
|
4965
|
+
tool: CODE_GRAPH_TOOL
|
|
4966
|
+
};
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4356
4969
|
// src/check/metric-rules.ts
|
|
4357
4970
|
function nodeFilter(rule) {
|
|
4358
4971
|
return {
|
|
@@ -4372,7 +4985,7 @@ function candidateNodes(rule, ctx) {
|
|
|
4372
4985
|
const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
|
|
4373
4986
|
return pool.filter((node) => passesFilter(node, filter));
|
|
4374
4987
|
}
|
|
4375
|
-
function
|
|
4988
|
+
function toViolation2(rule, breach) {
|
|
4376
4989
|
return {
|
|
4377
4990
|
ruleId: rule.id,
|
|
4378
4991
|
severity: severityOf(rule),
|
|
@@ -4402,7 +5015,7 @@ function runMetricMaxRule(rule, ctx) {
|
|
|
4402
5015
|
for (const node of candidateNodes(rule, ctx)) {
|
|
4403
5016
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4404
5017
|
if (value === void 0 || value <= rule.max) continue;
|
|
4405
|
-
out.push(
|
|
5018
|
+
out.push(toViolation2(rule, thresholdBreach(node, rule.metric, value, "max", rule.max)));
|
|
4406
5019
|
}
|
|
4407
5020
|
return out;
|
|
4408
5021
|
}
|
|
@@ -4411,7 +5024,7 @@ function runMetricMinRule(rule, ctx) {
|
|
|
4411
5024
|
for (const node of candidateNodes(rule, ctx)) {
|
|
4412
5025
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4413
5026
|
if (value === void 0 || value >= rule.min) continue;
|
|
4414
|
-
out.push(
|
|
5027
|
+
out.push(toViolation2(rule, thresholdBreach(node, rule.metric, value, "min", rule.min)));
|
|
4415
5028
|
}
|
|
4416
5029
|
return out;
|
|
4417
5030
|
}
|
|
@@ -4424,7 +5037,7 @@ function runMetricProductMaxRule(rule, ctx) {
|
|
|
4424
5037
|
if (!components) continue;
|
|
4425
5038
|
const product = components.reduce((a, b) => a * b, 1);
|
|
4426
5039
|
if (product <= rule.max) continue;
|
|
4427
|
-
out.push(
|
|
5040
|
+
out.push(toViolation2(rule, productBreach(rule, node, components, product)));
|
|
4428
5041
|
}
|
|
4429
5042
|
return out;
|
|
4430
5043
|
}
|
|
@@ -4458,6 +5071,8 @@ function runRule(rule, ctx) {
|
|
|
4458
5071
|
return runMetricMinRule(rule, ctx);
|
|
4459
5072
|
case "metric-product-max":
|
|
4460
5073
|
return runMetricProductMaxRule(rule, ctx);
|
|
5074
|
+
case "metric-outlier":
|
|
5075
|
+
return runMetricOutlierRule(rule, ctx);
|
|
4461
5076
|
case "forbid-import":
|
|
4462
5077
|
return runForbidImportRule(rule, ctx);
|
|
4463
5078
|
case "layered-deps":
|
|
@@ -4602,6 +5217,8 @@ function validateRule(raw, index, warn) {
|
|
|
4602
5217
|
return assertMetricMin(r, warn);
|
|
4603
5218
|
case "metric-product-max":
|
|
4604
5219
|
return assertMetricProductMax(r, warn);
|
|
5220
|
+
case "metric-outlier":
|
|
5221
|
+
return assertMetricOutlier(r, warn);
|
|
4605
5222
|
case "forbid-import":
|
|
4606
5223
|
return assertForbidImport(r);
|
|
4607
5224
|
case "layered-deps":
|
|
@@ -4711,6 +5328,29 @@ function assertMetricMin(r, warn) {
|
|
|
4711
5328
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
4712
5329
|
};
|
|
4713
5330
|
}
|
|
5331
|
+
var NODE_KINDS = /* @__PURE__ */ new Set(["package", "module", "file", "symbol", "external"]);
|
|
5332
|
+
function assertMetricOutlier(r, warn) {
|
|
5333
|
+
if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
|
|
5334
|
+
if (typeof r.kind !== "string" || !NODE_KINDS.has(r.kind)) {
|
|
5335
|
+
throw new Error(`${r.id}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
|
|
5336
|
+
}
|
|
5337
|
+
if (typeof r.percentile !== "number" || r.percentile < 50 || r.percentile > 100) {
|
|
5338
|
+
throw new Error(`${r.id}: percentile must be a number from 50 to 100`);
|
|
5339
|
+
}
|
|
5340
|
+
if (r.minSample !== void 0 && (!Number.isInteger(r.minSample) || r.minSample < 1)) {
|
|
5341
|
+
throw new Error(`${r.id}: minSample must be a positive integer`);
|
|
5342
|
+
}
|
|
5343
|
+
const ruleId = r.id;
|
|
5344
|
+
return {
|
|
5345
|
+
type: "metric-outlier",
|
|
5346
|
+
id: ruleId,
|
|
5347
|
+
metric: healMetricName(r.metric, ruleId, warn),
|
|
5348
|
+
kind: r.kind,
|
|
5349
|
+
percentile: r.percentile,
|
|
5350
|
+
minSample: r.minSample,
|
|
5351
|
+
severity: r.severity
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
4714
5354
|
function assertForbidImport(r) {
|
|
4715
5355
|
if (typeof r.from !== "string") throw new Error(`${r.id}: from must be a string`);
|
|
4716
5356
|
if (typeof r.to !== "string") throw new Error(`${r.id}: to must be a string`);
|
|
@@ -5411,7 +6051,9 @@ export {
|
|
|
5411
6051
|
CodeGraphStore,
|
|
5412
6052
|
DEAD_CODE_METRIC_NAMES,
|
|
5413
6053
|
DEFAULT_CHURN_WINDOWS,
|
|
6054
|
+
DEFAULT_OUTLIER_MIN_SAMPLE,
|
|
5414
6055
|
DOMAIN_DDL,
|
|
6056
|
+
EXCEPTION_METRIC_NAMES,
|
|
5415
6057
|
GROWTH_RISK_METRIC_NAMES,
|
|
5416
6058
|
INDEX_VERSION,
|
|
5417
6059
|
KIT,
|
|
@@ -5424,6 +6066,7 @@ export {
|
|
|
5424
6066
|
SOURCE_METRIC_NAMES,
|
|
5425
6067
|
SYMBOL_EMBEDDING_NAMESPACE,
|
|
5426
6068
|
SYMBOL_ID_SEP,
|
|
6069
|
+
SYMBOL_METRIC_NAMES,
|
|
5427
6070
|
TsMorphGraphExtractor,
|
|
5428
6071
|
aggregateMetrics,
|
|
5429
6072
|
aliasChain,
|
|
@@ -5506,6 +6149,7 @@ export {
|
|
|
5506
6149
|
parseFile2 as parseFile,
|
|
5507
6150
|
parseSymbolId,
|
|
5508
6151
|
patternToRegex,
|
|
6152
|
+
percentileOf,
|
|
5509
6153
|
planPrune,
|
|
5510
6154
|
priorSnapshotForRef,
|
|
5511
6155
|
pruneDanglingReferences,
|