@titan-design/code-graph 0.6.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 +29 -4
- package/dist/index.d.ts +31 -5
- package/dist/index.js +478 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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,12 +2151,97 @@ 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
|
+
|
|
1940
2233
|
// src/symbol-metrics.ts
|
|
1941
2234
|
var SYMBOL_METRICS = [
|
|
1942
2235
|
{ name: "symbol_cognitive", stat: "cognitive", unit: "count" },
|
|
1943
2236
|
{ name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
|
|
1944
2237
|
{ name: "symbol_loc", stat: "loc", unit: "lines" },
|
|
1945
|
-
{ name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" }
|
|
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" }
|
|
1946
2245
|
];
|
|
1947
2246
|
var SYMBOL_METRIC_NAMES = SYMBOL_METRICS.map((m) => m.name);
|
|
1948
2247
|
function symbolMetrics(fileId2, stats, symbolNames) {
|
|
@@ -1966,6 +2265,13 @@ function symbolMetrics(fileId2, stats, symbolNames) {
|
|
|
1966
2265
|
}
|
|
1967
2266
|
return out;
|
|
1968
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
|
+
}
|
|
1969
2275
|
|
|
1970
2276
|
// src/source-metrics.ts
|
|
1971
2277
|
var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -2016,7 +2322,8 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
2016
2322
|
"max_nesting_depth",
|
|
2017
2323
|
"class_count",
|
|
2018
2324
|
"lcom4_max",
|
|
2019
|
-
|
|
2325
|
+
...EXCEPTION_METRIC_NAMES,
|
|
2326
|
+
// Per-symbol complexity, size, comments and shape (C-58, C-64, TP-317, TP-322), keyed
|
|
2020
2327
|
// to `symbol` node ids. Source-local like the file-level metrics above, so an
|
|
2021
2328
|
// unchanged file carries them forward — but their nodeId is `<fileId>#<name>`,
|
|
2022
2329
|
// so the reuse basis buckets them under the symbol's parent file (incremental.ts).
|
|
@@ -2076,6 +2383,7 @@ function metricsForFile(nodeId, file, symbolNames) {
|
|
|
2076
2383
|
}
|
|
2077
2384
|
out.push(...symbolMetrics(nodeId, stats, symbolNames));
|
|
2078
2385
|
out.push(...computeLcomMetrics(file, nodeId));
|
|
2386
|
+
out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
|
|
2079
2387
|
return out;
|
|
2080
2388
|
}
|
|
2081
2389
|
function countLoc(content) {
|
|
@@ -2084,6 +2392,7 @@ function countLoc(content) {
|
|
|
2084
2392
|
function analyzeFunctions(file) {
|
|
2085
2393
|
const stats = [];
|
|
2086
2394
|
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
|
|
2395
|
+
const lines = file.content.split("\n");
|
|
2087
2396
|
walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
|
|
2088
2397
|
const fn = functionAt(node, fnTypes);
|
|
2089
2398
|
if (!fn) return;
|
|
@@ -2092,7 +2401,8 @@ function analyzeFunctions(file) {
|
|
|
2092
2401
|
cyclomatic: cyclomaticOf(fn.body, file.language),
|
|
2093
2402
|
cognitive: cognitiveComplexityOf(fn.body, file.language),
|
|
2094
2403
|
nestingDepth: nestingDepthOf(fn.body, file.language, 0),
|
|
2095
|
-
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1
|
|
2404
|
+
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
|
|
2405
|
+
...functionShapeStats(fn.node, fn.body, lines)
|
|
2096
2406
|
});
|
|
2097
2407
|
});
|
|
2098
2408
|
return stats;
|
|
@@ -2149,7 +2459,7 @@ var TERMINALS = /* @__PURE__ */ new Set([
|
|
|
2149
2459
|
]);
|
|
2150
2460
|
var SCOPE_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda", "class_definition"]);
|
|
2151
2461
|
var SPLAT_TYPES = /* @__PURE__ */ new Set(["list_splat_pattern", "dictionary_splat_pattern"]);
|
|
2152
|
-
var
|
|
2462
|
+
var RECEIVERS2 = /* @__PURE__ */ new Set(["self", "cls"]);
|
|
2153
2463
|
function pythonDeadCode(root) {
|
|
2154
2464
|
const out = { unreachable: 0, locals: 0, params: 0 };
|
|
2155
2465
|
const visit = (node) => {
|
|
@@ -2211,7 +2521,7 @@ function ownParamNames(fn) {
|
|
|
2211
2521
|
for (const p of params.namedChildren) {
|
|
2212
2522
|
if (!p || p.type === "keyword_separator" || p.type === "positional_separator") continue;
|
|
2213
2523
|
const id = paramIdentifier(p);
|
|
2214
|
-
out.push(id && !
|
|
2524
|
+
out.push(id && !RECEIVERS2.has(id.text) ? id.text : null);
|
|
2215
2525
|
}
|
|
2216
2526
|
return out;
|
|
2217
2527
|
}
|
|
@@ -3118,7 +3428,7 @@ function qualifiedByName(file) {
|
|
|
3118
3428
|
}
|
|
3119
3429
|
|
|
3120
3430
|
// src/indexer.ts
|
|
3121
|
-
var INDEX_VERSION = "0.
|
|
3431
|
+
var INDEX_VERSION = "0.17.0";
|
|
3122
3432
|
var LANGUAGES = ["typescript", "python"];
|
|
3123
3433
|
function canonicalizePath(p) {
|
|
3124
3434
|
try {
|
|
@@ -4106,6 +4416,96 @@ var SOURCE = [
|
|
|
4106
4416
|
source: "source-metrics",
|
|
4107
4417
|
description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
|
|
4108
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
|
+
},
|
|
4109
4509
|
{
|
|
4110
4510
|
name: "class_count",
|
|
4111
4511
|
unit: "count",
|
|
@@ -4529,6 +4929,43 @@ function locateNode(node) {
|
|
|
4529
4929
|
return location;
|
|
4530
4930
|
}
|
|
4531
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
|
+
|
|
4532
4969
|
// src/check/metric-rules.ts
|
|
4533
4970
|
function nodeFilter(rule) {
|
|
4534
4971
|
return {
|
|
@@ -4548,7 +4985,7 @@ function candidateNodes(rule, ctx) {
|
|
|
4548
4985
|
const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
|
|
4549
4986
|
return pool.filter((node) => passesFilter(node, filter));
|
|
4550
4987
|
}
|
|
4551
|
-
function
|
|
4988
|
+
function toViolation2(rule, breach) {
|
|
4552
4989
|
return {
|
|
4553
4990
|
ruleId: rule.id,
|
|
4554
4991
|
severity: severityOf(rule),
|
|
@@ -4578,7 +5015,7 @@ function runMetricMaxRule(rule, ctx) {
|
|
|
4578
5015
|
for (const node of candidateNodes(rule, ctx)) {
|
|
4579
5016
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4580
5017
|
if (value === void 0 || value <= rule.max) continue;
|
|
4581
|
-
out.push(
|
|
5018
|
+
out.push(toViolation2(rule, thresholdBreach(node, rule.metric, value, "max", rule.max)));
|
|
4582
5019
|
}
|
|
4583
5020
|
return out;
|
|
4584
5021
|
}
|
|
@@ -4587,7 +5024,7 @@ function runMetricMinRule(rule, ctx) {
|
|
|
4587
5024
|
for (const node of candidateNodes(rule, ctx)) {
|
|
4588
5025
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4589
5026
|
if (value === void 0 || value >= rule.min) continue;
|
|
4590
|
-
out.push(
|
|
5027
|
+
out.push(toViolation2(rule, thresholdBreach(node, rule.metric, value, "min", rule.min)));
|
|
4591
5028
|
}
|
|
4592
5029
|
return out;
|
|
4593
5030
|
}
|
|
@@ -4600,7 +5037,7 @@ function runMetricProductMaxRule(rule, ctx) {
|
|
|
4600
5037
|
if (!components) continue;
|
|
4601
5038
|
const product = components.reduce((a, b) => a * b, 1);
|
|
4602
5039
|
if (product <= rule.max) continue;
|
|
4603
|
-
out.push(
|
|
5040
|
+
out.push(toViolation2(rule, productBreach(rule, node, components, product)));
|
|
4604
5041
|
}
|
|
4605
5042
|
return out;
|
|
4606
5043
|
}
|
|
@@ -4634,6 +5071,8 @@ function runRule(rule, ctx) {
|
|
|
4634
5071
|
return runMetricMinRule(rule, ctx);
|
|
4635
5072
|
case "metric-product-max":
|
|
4636
5073
|
return runMetricProductMaxRule(rule, ctx);
|
|
5074
|
+
case "metric-outlier":
|
|
5075
|
+
return runMetricOutlierRule(rule, ctx);
|
|
4637
5076
|
case "forbid-import":
|
|
4638
5077
|
return runForbidImportRule(rule, ctx);
|
|
4639
5078
|
case "layered-deps":
|
|
@@ -4778,6 +5217,8 @@ function validateRule(raw, index, warn) {
|
|
|
4778
5217
|
return assertMetricMin(r, warn);
|
|
4779
5218
|
case "metric-product-max":
|
|
4780
5219
|
return assertMetricProductMax(r, warn);
|
|
5220
|
+
case "metric-outlier":
|
|
5221
|
+
return assertMetricOutlier(r, warn);
|
|
4781
5222
|
case "forbid-import":
|
|
4782
5223
|
return assertForbidImport(r);
|
|
4783
5224
|
case "layered-deps":
|
|
@@ -4887,6 +5328,29 @@ function assertMetricMin(r, warn) {
|
|
|
4887
5328
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
4888
5329
|
};
|
|
4889
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
|
+
}
|
|
4890
5354
|
function assertForbidImport(r) {
|
|
4891
5355
|
if (typeof r.from !== "string") throw new Error(`${r.id}: from must be a string`);
|
|
4892
5356
|
if (typeof r.to !== "string") throw new Error(`${r.id}: to must be a string`);
|
|
@@ -5587,7 +6051,9 @@ export {
|
|
|
5587
6051
|
CodeGraphStore,
|
|
5588
6052
|
DEAD_CODE_METRIC_NAMES,
|
|
5589
6053
|
DEFAULT_CHURN_WINDOWS,
|
|
6054
|
+
DEFAULT_OUTLIER_MIN_SAMPLE,
|
|
5590
6055
|
DOMAIN_DDL,
|
|
6056
|
+
EXCEPTION_METRIC_NAMES,
|
|
5591
6057
|
GROWTH_RISK_METRIC_NAMES,
|
|
5592
6058
|
INDEX_VERSION,
|
|
5593
6059
|
KIT,
|
|
@@ -5600,6 +6066,7 @@ export {
|
|
|
5600
6066
|
SOURCE_METRIC_NAMES,
|
|
5601
6067
|
SYMBOL_EMBEDDING_NAMESPACE,
|
|
5602
6068
|
SYMBOL_ID_SEP,
|
|
6069
|
+
SYMBOL_METRIC_NAMES,
|
|
5603
6070
|
TsMorphGraphExtractor,
|
|
5604
6071
|
aggregateMetrics,
|
|
5605
6072
|
aliasChain,
|
|
@@ -5682,6 +6149,7 @@ export {
|
|
|
5682
6149
|
parseFile2 as parseFile,
|
|
5683
6150
|
parseSymbolId,
|
|
5684
6151
|
patternToRegex,
|
|
6152
|
+
percentileOf,
|
|
5685
6153
|
planPrune,
|
|
5686
6154
|
priorSnapshotForRef,
|
|
5687
6155
|
pruneDanglingReferences,
|