roast-my-codebase 1.0.0 → 1.1.2

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.
Files changed (3) hide show
  1. package/README.md +20 -13
  2. package/dist/index.js +1572 -176
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  // src/cli/index.ts
10
10
  import { Command } from "commander";
11
11
  import path15 from "path";
12
- import fs17 from "fs";
12
+ import fs21 from "fs";
13
13
  import { fileURLToPath } from "url";
14
14
  import ora from "ora";
15
15
  import chalk8 from "chalk";
@@ -161,7 +161,7 @@ var FileScanner = class {
161
161
  let devDependencies = 0;
162
162
  try {
163
163
  const pkg = await import("fs").then(
164
- (fs18) => JSON.parse(fs18.readFileSync(pkgPath, "utf-8"))
164
+ (fs22) => JSON.parse(fs22.readFileSync(pkgPath, "utf-8"))
165
165
  );
166
166
  dependencies = Object.keys(pkg.dependencies || {}).length;
167
167
  devDependencies = Object.keys(pkg.devDependencies || {}).length;
@@ -1728,86 +1728,1237 @@ var FrameworkScanner = class {
1728
1728
  }
1729
1729
  }
1730
1730
  }
1731
- if (isReact) {
1732
- const rootComponents = await fg12(
1733
- [
1734
- "app/layout.{ts,tsx}",
1735
- "app/error.{ts,tsx}",
1736
- "pages/_app.{ts,tsx,js,jsx}",
1737
- "src/App.{ts,tsx,js,jsx}"
1738
- ],
1739
- {
1740
- cwd: rootDir,
1741
- ignore: IGNORE_PATTERNS,
1742
- absolute: true
1743
- }
1744
- );
1745
- if (rootComponents.length > 0) {
1746
- for (const compPath of rootComponents) {
1747
- try {
1748
- const content = fs9.readFileSync(compPath, "utf-8");
1749
- const hasErrorBoundary = /ErrorBoundary|componentDidCatch|static getDerivedStateFromError/.test(
1750
- content
1751
- );
1752
- if (!hasErrorBoundary) {
1753
- const rel = path8.relative(rootDir, compPath).replace(/\\/g, "/");
1754
- findings.push({
1755
- id: `no-error-boundary-${rel}`,
1756
- severity: "info",
1757
- category: "react-error-boundary",
1758
- message: `${rel} could benefit from an error boundary`,
1759
- file: rel
1760
- });
1761
- }
1762
- } catch (error) {
1763
- continue;
1731
+ if (isReact) {
1732
+ const rootComponents = await fg12(
1733
+ [
1734
+ "app/layout.{ts,tsx}",
1735
+ "app/error.{ts,tsx}",
1736
+ "pages/_app.{ts,tsx,js,jsx}",
1737
+ "src/App.{ts,tsx,js,jsx}"
1738
+ ],
1739
+ {
1740
+ cwd: rootDir,
1741
+ ignore: IGNORE_PATTERNS,
1742
+ absolute: true
1743
+ }
1744
+ );
1745
+ if (rootComponents.length > 0) {
1746
+ for (const compPath of rootComponents) {
1747
+ try {
1748
+ const content = fs9.readFileSync(compPath, "utf-8");
1749
+ const hasErrorBoundary = /ErrorBoundary|componentDidCatch|static getDerivedStateFromError/.test(
1750
+ content
1751
+ );
1752
+ if (!hasErrorBoundary) {
1753
+ const rel = path8.relative(rootDir, compPath).replace(/\\/g, "/");
1754
+ findings.push({
1755
+ id: `no-error-boundary-${rel}`,
1756
+ severity: "info",
1757
+ category: "react-error-boundary",
1758
+ message: `${rel} could benefit from an error boundary`,
1759
+ file: rel
1760
+ });
1761
+ }
1762
+ } catch (error) {
1763
+ continue;
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ return {
1769
+ findings,
1770
+ stats: { isNextJS, isReact }
1771
+ };
1772
+ }
1773
+ };
1774
+
1775
+ // src/scanners/python.ts
1776
+ import fg13 from "fast-glob";
1777
+ import fs10 from "fs";
1778
+ var PythonComplexityScanner = class {
1779
+ constructor() {
1780
+ this.name = "python-complexity";
1781
+ }
1782
+ async scan(rootDir) {
1783
+ const findings = [];
1784
+ const files = await fg13(["**/*.py"], {
1785
+ cwd: rootDir,
1786
+ ignore: IGNORE_PATTERNS,
1787
+ absolute: true
1788
+ });
1789
+ for (const file of files) {
1790
+ try {
1791
+ const content = fs10.readFileSync(file, "utf-8");
1792
+ const rel = relativePath(rootDir, file);
1793
+ const functions = this.extractFunctions(content);
1794
+ for (const func of functions) {
1795
+ const complexity = this.calculateComplexity(func.body);
1796
+ if (complexity >= 20) {
1797
+ findings.push({
1798
+ id: `py-complexity-${rel}-${func.name}`,
1799
+ severity: "critical",
1800
+ category: "complexity",
1801
+ message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
1802
+ file: rel,
1803
+ detail: `${complexity} decision points`
1804
+ });
1805
+ } else if (complexity >= 10) {
1806
+ findings.push({
1807
+ id: `py-complexity-${rel}-${func.name}`,
1808
+ severity: "warning",
1809
+ category: "complexity",
1810
+ message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
1811
+ file: rel,
1812
+ detail: `${complexity} decision points`
1813
+ });
1814
+ }
1815
+ }
1816
+ } catch {
1817
+ }
1818
+ }
1819
+ return { findings };
1820
+ }
1821
+ extractFunctions(content) {
1822
+ const functions = [];
1823
+ const lines = content.split("\n");
1824
+ for (let i = 0; i < lines.length; i++) {
1825
+ const line = lines[i];
1826
+ const funcMatch = line.match(/^\s*def\s+(\w+)\s*\(/);
1827
+ if (funcMatch) {
1828
+ const name = funcMatch[1];
1829
+ const indent = line.match(/^\s*/)?.[0].length || 0;
1830
+ let body = line + "\n";
1831
+ for (let j = i + 1; j < lines.length; j++) {
1832
+ const nextLine = lines[j];
1833
+ const nextIndent = nextLine.match(/^\s*/)?.[0].length || 0;
1834
+ if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
1835
+ body += nextLine + "\n";
1836
+ continue;
1837
+ }
1838
+ if (nextIndent <= indent) {
1839
+ break;
1840
+ }
1841
+ body += nextLine + "\n";
1842
+ }
1843
+ functions.push({ name, body });
1844
+ }
1845
+ }
1846
+ return functions;
1847
+ }
1848
+ calculateComplexity(code) {
1849
+ let complexity = 1;
1850
+ const patterns = [
1851
+ /\bif\b/g,
1852
+ /\belif\b/g,
1853
+ /\bfor\b/g,
1854
+ /\bwhile\b/g,
1855
+ /\band\b/g,
1856
+ /\bor\b/g,
1857
+ /\bexcept\b/g,
1858
+ /\bwith\b/g,
1859
+ /\?/g
1860
+ // Ternary
1861
+ ];
1862
+ for (const pattern of patterns) {
1863
+ const matches = code.match(pattern);
1864
+ if (matches) {
1865
+ complexity += matches.length;
1866
+ }
1867
+ }
1868
+ return complexity;
1869
+ }
1870
+ };
1871
+ var PythonTypeHintsScanner = class {
1872
+ constructor() {
1873
+ this.name = "python-type-hints";
1874
+ }
1875
+ async scan(rootDir) {
1876
+ const findings = [];
1877
+ const files = await fg13(["**/*.py"], {
1878
+ cwd: rootDir,
1879
+ ignore: IGNORE_PATTERNS,
1880
+ absolute: true
1881
+ });
1882
+ let totalFunctions = 0;
1883
+ let untypedFunctions = 0;
1884
+ for (const file of files) {
1885
+ try {
1886
+ const content = fs10.readFileSync(file, "utf-8");
1887
+ const rel = relativePath(rootDir, file);
1888
+ const funcPattern = /def\s+(\w+)\s*\([^)]{0,500}\)(?:\s*->\s*\w+)?:/g;
1889
+ const functions = Array.from(content.matchAll(funcPattern));
1890
+ for (const match of functions) {
1891
+ totalFunctions++;
1892
+ const fullDef = match[0];
1893
+ const hasParamTypes = /:\s*\w+/.test(fullDef);
1894
+ const hasReturnType = /->/.test(fullDef);
1895
+ if (!hasParamTypes && !hasReturnType) {
1896
+ untypedFunctions++;
1897
+ }
1898
+ }
1899
+ } catch {
1900
+ }
1901
+ }
1902
+ if (totalFunctions > 0) {
1903
+ const untypedPercent = Math.round(
1904
+ untypedFunctions / totalFunctions * 100
1905
+ );
1906
+ if (untypedPercent > 70) {
1907
+ findings.push({
1908
+ id: "py-type-hints-low",
1909
+ severity: "warning",
1910
+ category: "type-safety",
1911
+ message: `${untypedPercent}% of Python functions lack type hints`,
1912
+ detail: `${untypedFunctions} of ${totalFunctions} functions`
1913
+ });
1914
+ }
1915
+ }
1916
+ return {
1917
+ findings,
1918
+ stats: {
1919
+ totalFunctions,
1920
+ untypedFunctions,
1921
+ untypedPercent: totalFunctions > 0 ? Math.round(untypedFunctions / totalFunctions * 100) : 0
1922
+ }
1923
+ };
1924
+ }
1925
+ };
1926
+ var PythonImportsScanner = class {
1927
+ constructor() {
1928
+ this.name = "python-imports";
1929
+ }
1930
+ async scan(rootDir) {
1931
+ const findings = [];
1932
+ const files = await fg13(["**/*.py"], {
1933
+ cwd: rootDir,
1934
+ ignore: IGNORE_PATTERNS,
1935
+ absolute: true
1936
+ });
1937
+ for (const file of files) {
1938
+ try {
1939
+ const content = fs10.readFileSync(file, "utf-8");
1940
+ const rel = relativePath(rootDir, file);
1941
+ const wildcardImports = content.match(/from\s+[\w.]+\s+import\s+\*/g);
1942
+ if (wildcardImports && wildcardImports.length > 0) {
1943
+ findings.push({
1944
+ id: `py-wildcard-import-${rel}`,
1945
+ severity: "warning",
1946
+ category: "python-imports",
1947
+ message: `${rel} uses wildcard imports (${wildcardImports.length} found)`,
1948
+ file: rel,
1949
+ detail: "Wildcard imports pollute namespace"
1950
+ });
1951
+ }
1952
+ const deepRelative = content.match(/from\s+\.{3,}/g);
1953
+ if (deepRelative && deepRelative.length > 0) {
1954
+ findings.push({
1955
+ id: `py-deep-relative-${rel}`,
1956
+ severity: "info",
1957
+ category: "python-imports",
1958
+ message: `${rel} uses deep relative imports`,
1959
+ file: rel,
1960
+ detail: "Consider absolute imports"
1961
+ });
1962
+ }
1963
+ } catch {
1964
+ }
1965
+ }
1966
+ return { findings };
1967
+ }
1968
+ };
1969
+ var PythonDocstringScanner = class {
1970
+ constructor() {
1971
+ this.name = "python-docstrings";
1972
+ }
1973
+ async scan(rootDir) {
1974
+ const findings = [];
1975
+ const files = await fg13(["**/*.py"], {
1976
+ cwd: rootDir,
1977
+ ignore: [...IGNORE_PATTERNS, "**/*test*/**", "**/tests/**", "**/setup.py"],
1978
+ absolute: true
1979
+ });
1980
+ let totalPublicFunctions = 0;
1981
+ let undocumentedFunctions = 0;
1982
+ for (const file of files) {
1983
+ try {
1984
+ const content = fs10.readFileSync(file, "utf-8");
1985
+ const lines = content.split("\n");
1986
+ for (let i = 0; i < lines.length; i++) {
1987
+ const line = lines[i];
1988
+ const funcMatch = line.match(/^\s*def\s+([a-zA-Z]\w*)\s*\(/);
1989
+ if (funcMatch && !funcMatch[1].startsWith("_")) {
1990
+ totalPublicFunctions++;
1991
+ const nextNonEmpty = this.getNextNonEmptyLine(lines, i + 1);
1992
+ if (!nextNonEmpty || !nextNonEmpty.includes('"""') && !nextNonEmpty.includes("'''")) {
1993
+ undocumentedFunctions++;
1994
+ }
1995
+ }
1996
+ }
1997
+ } catch {
1998
+ }
1999
+ }
2000
+ if (totalPublicFunctions > 0) {
2001
+ const undocPercent = Math.round(undocumentedFunctions / totalPublicFunctions * 100);
2002
+ if (undocPercent > 70) {
2003
+ findings.push({
2004
+ id: "py-docstrings-low",
2005
+ severity: "warning",
2006
+ category: "python-docstrings",
2007
+ message: `${undocPercent}% of public functions lack docstrings`,
2008
+ detail: `${undocumentedFunctions} of ${totalPublicFunctions} public functions undocumented`
2009
+ });
2010
+ } else if (undocPercent > 40) {
2011
+ findings.push({
2012
+ id: "py-docstrings-moderate",
2013
+ severity: "info",
2014
+ category: "python-docstrings",
2015
+ message: `${undocPercent}% of public functions lack docstrings`,
2016
+ detail: `${undocumentedFunctions} of ${totalPublicFunctions} public functions undocumented`
2017
+ });
2018
+ }
2019
+ }
2020
+ return {
2021
+ findings,
2022
+ stats: { totalPublicFunctions, undocumentedFunctions }
2023
+ };
2024
+ }
2025
+ getNextNonEmptyLine(lines, startIdx) {
2026
+ for (let i = startIdx; i < Math.min(startIdx + 3, lines.length); i++) {
2027
+ const trimmed = lines[i].trim();
2028
+ if (trimmed.length > 0) return trimmed;
2029
+ }
2030
+ return null;
2031
+ }
2032
+ };
2033
+ var PythonCodeSmellScanner = class {
2034
+ constructor() {
2035
+ this.name = "python-code-smells";
2036
+ }
2037
+ async scan(rootDir) {
2038
+ const findings = [];
2039
+ const files = await fg13(["**/*.py"], {
2040
+ cwd: rootDir,
2041
+ ignore: [...IGNORE_PATTERNS, "**/*test*/**", "**/tests/**"],
2042
+ absolute: true
2043
+ });
2044
+ for (const file of files) {
2045
+ try {
2046
+ const content = fs10.readFileSync(file, "utf-8");
2047
+ const rel = relativePath(rootDir, file);
2048
+ const bareExcepts = content.match(/^\s*except\s*:/gm);
2049
+ if (bareExcepts && bareExcepts.length > 0) {
2050
+ findings.push({
2051
+ id: `py-bare-except-${rel}`,
2052
+ severity: "warning",
2053
+ category: "python-smells",
2054
+ message: `${rel} uses bare except: ${bareExcepts.length} time(s)`,
2055
+ file: rel,
2056
+ detail: "Bare except catches SystemExit, KeyboardInterrupt \u2014 use except Exception:"
2057
+ });
2058
+ }
2059
+ const mutableDefaults = content.match(
2060
+ /def\s+\w+\s*\([^)]*(?:=\s*(?:\[\]|\{\}|\bset\(\)))[^)]*\)/g
2061
+ );
2062
+ if (mutableDefaults && mutableDefaults.length > 0) {
2063
+ findings.push({
2064
+ id: `py-mutable-default-${rel}`,
2065
+ severity: "warning",
2066
+ category: "python-smells",
2067
+ message: `${rel} has ${mutableDefaults.length} mutable default argument(s)`,
2068
+ file: rel,
2069
+ detail: "Mutable defaults are shared across calls \u2014 use None and create inside function"
2070
+ });
2071
+ }
2072
+ const globals = content.match(/^\s*global\s+\w+/gm);
2073
+ if (globals && globals.length >= 3) {
2074
+ findings.push({
2075
+ id: `py-globals-${rel}`,
2076
+ severity: "warning",
2077
+ category: "python-smells",
2078
+ message: `${rel} uses global statement ${globals.length} times`,
2079
+ file: rel,
2080
+ detail: "Excessive globals indicate poor encapsulation"
2081
+ });
2082
+ }
2083
+ const lines = content.split("\n");
2084
+ let longLineCount = 0;
2085
+ for (const line of lines) {
2086
+ if (line.length > 120 && !line.trim().startsWith("#") && !line.includes("http")) {
2087
+ longLineCount++;
2088
+ }
2089
+ }
2090
+ if (longLineCount >= 20) {
2091
+ findings.push({
2092
+ id: `py-long-lines-${rel}`,
2093
+ severity: "info",
2094
+ category: "python-smells",
2095
+ message: `${rel} has ${longLineCount} lines exceeding 120 characters`,
2096
+ file: rel,
2097
+ detail: "Consider reformatting with black or ruff"
2098
+ });
2099
+ }
2100
+ let maxDepth = 0;
2101
+ let currentDepth = 0;
2102
+ for (const line of lines) {
2103
+ if (line.match(/^\s*def\s+/)) {
2104
+ const indent = (line.match(/^\s*/)?.[0].length || 0) / 4;
2105
+ currentDepth = indent;
2106
+ if (currentDepth > maxDepth) maxDepth = currentDepth;
2107
+ }
2108
+ }
2109
+ if (maxDepth >= 3) {
2110
+ findings.push({
2111
+ id: `py-nested-functions-${rel}`,
2112
+ severity: "info",
2113
+ category: "python-smells",
2114
+ message: `${rel} has functions nested ${maxDepth} levels deep`,
2115
+ file: rel,
2116
+ detail: "Deeply nested functions are hard to test and reason about"
2117
+ });
2118
+ }
2119
+ } catch {
2120
+ }
2121
+ }
2122
+ return { findings };
2123
+ }
2124
+ };
2125
+ var PythonSecurityScanner = class {
2126
+ constructor() {
2127
+ this.name = "python-security";
2128
+ }
2129
+ async scan(rootDir) {
2130
+ const findings = [];
2131
+ const files = await fg13(["**/*.py"], {
2132
+ cwd: rootDir,
2133
+ ignore: [...IGNORE_PATTERNS, "**/*test*/**", "**/tests/**"],
2134
+ absolute: true
2135
+ });
2136
+ for (const file of files) {
2137
+ try {
2138
+ const content = fs10.readFileSync(file, "utf-8");
2139
+ const rel = relativePath(rootDir, file);
2140
+ const evalExec = content.match(/\b(?:eval|exec)\s*\(/g);
2141
+ if (evalExec && evalExec.length > 0) {
2142
+ findings.push({
2143
+ id: `py-eval-${rel}`,
2144
+ severity: "critical",
2145
+ category: "python-security",
2146
+ message: `${rel} uses eval()/exec() ${evalExec.length} time(s)`,
2147
+ file: rel,
2148
+ detail: "eval/exec can execute arbitrary code \u2014 use ast.literal_eval for data"
2149
+ });
2150
+ }
2151
+ if (/\bpickle\.(?:load|loads)\s*\(/.test(content)) {
2152
+ findings.push({
2153
+ id: `py-pickle-${rel}`,
2154
+ severity: "warning",
2155
+ category: "python-security",
2156
+ message: `${rel} uses pickle deserialization`,
2157
+ file: rel,
2158
+ detail: "Pickle can execute arbitrary code during deserialization \u2014 use json or msgpack"
2159
+ });
2160
+ }
2161
+ if (/subprocess\.\w+\([^)]*shell\s*=\s*True/s.test(content)) {
2162
+ findings.push({
2163
+ id: `py-shell-true-${rel}`,
2164
+ severity: "warning",
2165
+ category: "python-security",
2166
+ message: `${rel} uses subprocess with shell=True`,
2167
+ file: rel,
2168
+ detail: "shell=True enables shell injection \u2014 pass args as a list instead"
2169
+ });
2170
+ }
2171
+ const secretPatterns = content.match(
2172
+ /(?:password|passwd|secret|api_key|token)\s*=\s*['"][^'"]{8,}['"]/gi
2173
+ );
2174
+ if (secretPatterns && secretPatterns.length > 0) {
2175
+ findings.push({
2176
+ id: `py-hardcoded-secret-${rel}`,
2177
+ severity: "critical",
2178
+ category: "python-security",
2179
+ message: `${rel} may have hardcoded secrets (${secretPatterns.length} found)`,
2180
+ file: rel,
2181
+ detail: "Use environment variables or a secrets manager"
2182
+ });
2183
+ }
2184
+ const sqlInjection = content.match(
2185
+ /(?:execute|cursor\.execute)\s*\(\s*(?:f['"]|['"].*%|['"].*\.format)/g
2186
+ );
2187
+ if (sqlInjection && sqlInjection.length > 0) {
2188
+ findings.push({
2189
+ id: `py-sql-injection-${rel}`,
2190
+ severity: "critical",
2191
+ category: "python-security",
2192
+ message: `${rel} has potential SQL injection (${sqlInjection.length} found)`,
2193
+ file: rel,
2194
+ detail: "Use parameterized queries instead of string formatting"
2195
+ });
2196
+ }
2197
+ } catch {
2198
+ }
2199
+ }
2200
+ return { findings };
2201
+ }
2202
+ };
2203
+ var PythonClassDesignScanner = class {
2204
+ constructor() {
2205
+ this.name = "python-class-design";
2206
+ }
2207
+ async scan(rootDir) {
2208
+ const findings = [];
2209
+ const files = await fg13(["**/*.py"], {
2210
+ cwd: rootDir,
2211
+ ignore: [...IGNORE_PATTERNS, "**/*test*/**", "**/tests/**"],
2212
+ absolute: true
2213
+ });
2214
+ for (const file of files) {
2215
+ try {
2216
+ const content = fs10.readFileSync(file, "utf-8");
2217
+ const rel = relativePath(rootDir, file);
2218
+ const lines = content.split("\n");
2219
+ const classes = this.extractClasses(lines);
2220
+ for (const cls of classes) {
2221
+ if (cls.methodCount >= 20) {
2222
+ findings.push({
2223
+ id: `py-god-class-${rel}-${cls.name}`,
2224
+ severity: "warning",
2225
+ category: "python-design",
2226
+ message: `Class ${cls.name} in ${rel} has ${cls.methodCount} methods`,
2227
+ file: rel,
2228
+ detail: "Consider splitting into smaller, focused classes"
2229
+ });
2230
+ }
2231
+ if (cls.methodCount === 0 && cls.lineCount > 3) {
2232
+ findings.push({
2233
+ id: `py-dataclass-candidate-${rel}-${cls.name}`,
2234
+ severity: "info",
2235
+ category: "python-design",
2236
+ message: `Class ${cls.name} in ${rel} has no methods \u2014 consider @dataclass`,
2237
+ file: rel
2238
+ });
2239
+ }
2240
+ }
2241
+ const deepInheritance = content.match(
2242
+ // eslint-disable-next-line security/detect-unsafe-regex
2243
+ /class\s+\w+\s*\(\s*\w+(?:\.\w+)*(?:\s*,\s*\w+(?:\.\w+)*){3,}\s*\)/g
2244
+ );
2245
+ if (deepInheritance) {
2246
+ findings.push({
2247
+ id: `py-multi-inherit-${rel}`,
2248
+ severity: "info",
2249
+ category: "python-design",
2250
+ message: `${rel} has classes with 4+ parent classes`,
2251
+ file: rel,
2252
+ detail: "Deep multiple inheritance creates complex MRO \u2014 prefer composition"
2253
+ });
2254
+ }
2255
+ } catch {
2256
+ }
2257
+ }
2258
+ return { findings };
2259
+ }
2260
+ extractClasses(lines) {
2261
+ const classes = [];
2262
+ for (let i = 0; i < lines.length; i++) {
2263
+ const classMatch = lines[i].match(/^class\s+(\w+)/);
2264
+ if (classMatch) {
2265
+ const name = classMatch[1];
2266
+ const classIndent = lines[i].match(/^\s*/)?.[0].length || 0;
2267
+ let methodCount = 0;
2268
+ let lineCount = 0;
2269
+ for (let j = i + 1; j < lines.length; j++) {
2270
+ const line = lines[j];
2271
+ if (line.trim() === "") {
2272
+ lineCount++;
2273
+ continue;
2274
+ }
2275
+ const indent = line.match(/^\s*/)?.[0].length || 0;
2276
+ if (indent <= classIndent && line.trim().length > 0) break;
2277
+ lineCount++;
2278
+ if (line.match(/^\s+def\s+/)) methodCount++;
2279
+ }
2280
+ classes.push({ name, methodCount, lineCount });
2281
+ }
2282
+ }
2283
+ return classes;
2284
+ }
2285
+ };
2286
+
2287
+ // src/scanners/go.ts
2288
+ import fg14 from "fast-glob";
2289
+ import fs11 from "fs";
2290
+ var GoComplexityScanner = class {
2291
+ constructor() {
2292
+ this.name = "go-complexity";
2293
+ }
2294
+ async scan(rootDir) {
2295
+ const findings = [];
2296
+ const files = await fg14(["**/*.go"], {
2297
+ cwd: rootDir,
2298
+ ignore: [...IGNORE_PATTERNS, "**/vendor/**"],
2299
+ absolute: true
2300
+ });
2301
+ for (const file of files) {
2302
+ try {
2303
+ const content = fs11.readFileSync(file, "utf-8");
2304
+ const rel = relativePath(rootDir, file);
2305
+ const functions = this.extractFunctions(content);
2306
+ for (const func of functions) {
2307
+ const complexity = this.calculateComplexity(func.body);
2308
+ if (complexity >= 20) {
2309
+ findings.push({
2310
+ id: `go-complexity-${rel}-${func.name}`,
2311
+ severity: "critical",
2312
+ category: "complexity",
2313
+ message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
2314
+ file: rel,
2315
+ detail: `${complexity} decision points`
2316
+ });
2317
+ } else if (complexity >= 10) {
2318
+ findings.push({
2319
+ id: `go-complexity-${rel}-${func.name}`,
2320
+ severity: "warning",
2321
+ category: "complexity",
2322
+ message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
2323
+ file: rel,
2324
+ detail: `${complexity} decision points`
2325
+ });
2326
+ }
2327
+ }
2328
+ } catch {
2329
+ }
2330
+ }
2331
+ return { findings };
2332
+ }
2333
+ extractFunctions(content) {
2334
+ const functions = [];
2335
+ const lines = content.split("\n");
2336
+ for (let i = 0; i < lines.length; i++) {
2337
+ const line = lines[i];
2338
+ const funcMatch = line.match(/^func\s+(?:\([^)]{0,200}\)\s+)?(\w+)\s*\(/);
2339
+ if (funcMatch) {
2340
+ const name = funcMatch[1];
2341
+ let braceCount = 0;
2342
+ let body = "";
2343
+ let started = false;
2344
+ for (let j = i; j < lines.length; j++) {
2345
+ body += lines[j] + "\n";
2346
+ for (const ch of lines[j]) {
2347
+ if (ch === "{") {
2348
+ braceCount++;
2349
+ started = true;
2350
+ }
2351
+ if (ch === "}") {
2352
+ braceCount--;
2353
+ }
2354
+ }
2355
+ if (started && braceCount <= 0) break;
2356
+ }
2357
+ functions.push({ name, body });
2358
+ }
2359
+ }
2360
+ return functions;
2361
+ }
2362
+ calculateComplexity(code) {
2363
+ let complexity = 1;
2364
+ const patterns = [
2365
+ /\bif\b/g,
2366
+ /\belse if\b/g,
2367
+ /\bfor\b/g,
2368
+ /\bcase\b/g,
2369
+ /\b&&\b/g,
2370
+ /\b\|\|\b/g,
2371
+ /\bselect\b/g
2372
+ ];
2373
+ for (const pattern of patterns) {
2374
+ const matches = code.match(pattern);
2375
+ if (matches) complexity += matches.length;
2376
+ }
2377
+ return complexity;
2378
+ }
2379
+ };
2380
+ var GoErrorHandlingScanner = class {
2381
+ constructor() {
2382
+ this.name = "go-error-handling";
2383
+ }
2384
+ async scan(rootDir) {
2385
+ const findings = [];
2386
+ const files = await fg14(["**/*.go"], {
2387
+ cwd: rootDir,
2388
+ ignore: [...IGNORE_PATTERNS, "**/vendor/**", "**/*_test.go"],
2389
+ absolute: true
2390
+ });
2391
+ for (const file of files) {
2392
+ try {
2393
+ const content = fs11.readFileSync(file, "utf-8");
2394
+ const rel = relativePath(rootDir, file);
2395
+ const ignoredErrors = content.match(/\b_\s*=\s*\w+\([^)]*\)/g);
2396
+ const blankErrCount = ignoredErrors ? ignoredErrors.length : 0;
2397
+ if (blankErrCount >= 5) {
2398
+ findings.push({
2399
+ id: `go-ignored-errors-${rel}`,
2400
+ severity: "warning",
2401
+ category: "go-error-handling",
2402
+ message: `${rel} ignores ${blankErrCount} error returns with blank identifier`,
2403
+ file: rel,
2404
+ detail: "Ignored errors can mask bugs in production"
2405
+ });
2406
+ } else if (blankErrCount >= 3) {
2407
+ findings.push({
2408
+ id: `go-ignored-errors-${rel}`,
2409
+ severity: "info",
2410
+ category: "go-error-handling",
2411
+ message: `${rel} ignores ${blankErrCount} error returns`,
2412
+ file: rel
2413
+ });
2414
+ }
2415
+ const panicCalls = content.match(/\bpanic\s*\(/g);
2416
+ if (panicCalls && panicCalls.length > 0) {
2417
+ findings.push({
2418
+ id: `go-panic-${rel}`,
2419
+ severity: "warning",
2420
+ category: "go-error-handling",
2421
+ message: `${rel} uses panic() ${panicCalls.length} time(s)`,
2422
+ file: rel,
2423
+ detail: "Prefer returning errors over panic in library code"
2424
+ });
2425
+ }
2426
+ } catch {
2427
+ }
2428
+ }
2429
+ return { findings };
2430
+ }
2431
+ };
2432
+ var GoLintScanner = class {
2433
+ constructor() {
2434
+ this.name = "go-lint";
2435
+ }
2436
+ async scan(rootDir) {
2437
+ const findings = [];
2438
+ const files = await fg14(["**/*.go"], {
2439
+ cwd: rootDir,
2440
+ ignore: [...IGNORE_PATTERNS, "**/vendor/**"],
2441
+ absolute: true
2442
+ });
2443
+ let totalExported = 0;
2444
+ let undocumentedExported = 0;
2445
+ for (const file of files) {
2446
+ try {
2447
+ const content = fs11.readFileSync(file, "utf-8");
2448
+ const rel = relativePath(rootDir, file);
2449
+ const lines = content.split("\n");
2450
+ for (let i = 0; i < lines.length; i++) {
2451
+ const line = lines[i];
2452
+ const exportedMatch = line.match(
2453
+ /^(?:func|type|var|const)\s+([A-Z]\w*)/
2454
+ );
2455
+ if (exportedMatch) {
2456
+ totalExported++;
2457
+ const prevLine = i > 0 ? lines[i - 1].trim() : "";
2458
+ if (!prevLine.startsWith("//")) {
2459
+ undocumentedExported++;
2460
+ }
2461
+ }
2462
+ }
2463
+ const initFuncs = content.match(/^func\s+init\s*\(\s*\)/gm);
2464
+ if (initFuncs && initFuncs.length > 1) {
2465
+ findings.push({
2466
+ id: `go-multi-init-${rel}`,
2467
+ severity: "info",
2468
+ category: "go-lint",
2469
+ message: `${rel} has ${initFuncs.length} init() functions`,
2470
+ file: rel,
2471
+ detail: "Multiple init() functions can make initialization order unclear"
2472
+ });
2473
+ }
2474
+ } catch {
2475
+ }
2476
+ }
2477
+ if (totalExported > 0) {
2478
+ const undocPercent = Math.round(undocumentedExported / totalExported * 100);
2479
+ if (undocPercent > 50) {
2480
+ findings.push({
2481
+ id: "go-undocumented-exports",
2482
+ severity: "warning",
2483
+ category: "go-lint",
2484
+ message: `${undocPercent}% of exported symbols lack documentation comments`,
2485
+ detail: `${undocumentedExported} of ${totalExported} exported symbols`
2486
+ });
2487
+ }
2488
+ }
2489
+ return { findings };
2490
+ }
2491
+ };
2492
+
2493
+ // src/scanners/rust.ts
2494
+ import fg15 from "fast-glob";
2495
+ import fs12 from "fs";
2496
+ var RustComplexityScanner = class {
2497
+ constructor() {
2498
+ this.name = "rust-complexity";
2499
+ }
2500
+ async scan(rootDir) {
2501
+ const findings = [];
2502
+ const files = await fg15(["**/*.rs"], {
2503
+ cwd: rootDir,
2504
+ ignore: [...IGNORE_PATTERNS, "**/target/**"],
2505
+ absolute: true
2506
+ });
2507
+ for (const file of files) {
2508
+ try {
2509
+ const content = fs12.readFileSync(file, "utf-8");
2510
+ const rel = relativePath(rootDir, file);
2511
+ const functions = this.extractFunctions(content);
2512
+ for (const func of functions) {
2513
+ const complexity = this.calculateComplexity(func.body);
2514
+ if (complexity >= 20) {
2515
+ findings.push({
2516
+ id: `rust-complexity-${rel}-${func.name}`,
2517
+ severity: "critical",
2518
+ category: "complexity",
2519
+ message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
2520
+ file: rel,
2521
+ detail: `${complexity} decision points`
2522
+ });
2523
+ } else if (complexity >= 10) {
2524
+ findings.push({
2525
+ id: `rust-complexity-${rel}-${func.name}`,
2526
+ severity: "warning",
2527
+ category: "complexity",
2528
+ message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
2529
+ file: rel,
2530
+ detail: `${complexity} decision points`
2531
+ });
2532
+ }
2533
+ }
2534
+ } catch {
2535
+ }
2536
+ }
2537
+ return { findings };
2538
+ }
2539
+ extractFunctions(content) {
2540
+ const functions = [];
2541
+ const lines = content.split("\n");
2542
+ for (let i = 0; i < lines.length; i++) {
2543
+ const line = lines[i];
2544
+ const funcMatch = line.match(/^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
2545
+ if (funcMatch) {
2546
+ const name = funcMatch[1];
2547
+ let braceCount = 0;
2548
+ let body = "";
2549
+ let started = false;
2550
+ for (let j = i; j < lines.length; j++) {
2551
+ body += lines[j] + "\n";
2552
+ for (const ch of lines[j]) {
2553
+ if (ch === "{") {
2554
+ braceCount++;
2555
+ started = true;
2556
+ }
2557
+ if (ch === "}") {
2558
+ braceCount--;
2559
+ }
2560
+ }
2561
+ if (started && braceCount <= 0) break;
2562
+ }
2563
+ functions.push({ name, body });
2564
+ }
2565
+ }
2566
+ return functions;
2567
+ }
2568
+ calculateComplexity(code) {
2569
+ let complexity = 1;
2570
+ const patterns = [
2571
+ /\bif\b/g,
2572
+ /\belse if\b/g,
2573
+ /\bfor\b/g,
2574
+ /\bwhile\b/g,
2575
+ /\bloop\b/g,
2576
+ /\bmatch\b/g,
2577
+ /=>/g,
2578
+ /&&/g,
2579
+ /\|\|/g,
2580
+ /\?/g
2581
+ ];
2582
+ for (const pattern of patterns) {
2583
+ const matches = code.match(pattern);
2584
+ if (matches) complexity += matches.length;
2585
+ }
2586
+ return complexity;
2587
+ }
2588
+ };
2589
+ var RustUnsafeScanner = class {
2590
+ constructor() {
2591
+ this.name = "rust-unsafe";
2592
+ }
2593
+ async scan(rootDir) {
2594
+ const findings = [];
2595
+ const files = await fg15(["**/*.rs"], {
2596
+ cwd: rootDir,
2597
+ ignore: [...IGNORE_PATTERNS, "**/target/**"],
2598
+ absolute: true
2599
+ });
2600
+ let totalUnsafeBlocks = 0;
2601
+ for (const file of files) {
2602
+ try {
2603
+ const content = fs12.readFileSync(file, "utf-8");
2604
+ const rel = relativePath(rootDir, file);
2605
+ const unsafeBlocks = content.match(/\bunsafe\s*\{/g);
2606
+ const unsafeFns = content.match(/\bunsafe\s+fn\b/g);
2607
+ const fileUnsafe = (unsafeBlocks?.length || 0) + (unsafeFns?.length || 0);
2608
+ totalUnsafeBlocks += fileUnsafe;
2609
+ if (fileUnsafe >= 5) {
2610
+ findings.push({
2611
+ id: `rust-unsafe-heavy-${rel}`,
2612
+ severity: "warning",
2613
+ category: "rust-unsafe",
2614
+ message: `${rel} has ${fileUnsafe} unsafe blocks/functions`,
2615
+ file: rel,
2616
+ detail: "Heavy unsafe usage increases risk of memory safety issues"
2617
+ });
2618
+ }
2619
+ } catch {
2620
+ }
2621
+ }
2622
+ if (totalUnsafeBlocks >= 20) {
2623
+ findings.push({
2624
+ id: "rust-unsafe-total",
2625
+ severity: "warning",
2626
+ category: "rust-unsafe",
2627
+ message: `Project has ${totalUnsafeBlocks} total unsafe blocks/functions`,
2628
+ detail: "Consider wrapping unsafe code in safe abstractions"
2629
+ });
2630
+ }
2631
+ return { findings };
2632
+ }
2633
+ };
2634
+ var RustClippyHintsScanner = class {
2635
+ constructor() {
2636
+ this.name = "rust-clippy-hints";
2637
+ }
2638
+ async scan(rootDir) {
2639
+ const findings = [];
2640
+ const files = await fg15(["**/*.rs"], {
2641
+ cwd: rootDir,
2642
+ ignore: [...IGNORE_PATTERNS, "**/target/**"],
2643
+ absolute: true
2644
+ });
2645
+ for (const file of files) {
2646
+ try {
2647
+ const content = fs12.readFileSync(file, "utf-8");
2648
+ const rel = relativePath(rootDir, file);
2649
+ const unwraps = content.match(/\.unwrap\(\)/g);
2650
+ if (unwraps && unwraps.length >= 10) {
2651
+ findings.push({
2652
+ id: `rust-unwrap-heavy-${rel}`,
2653
+ severity: "warning",
2654
+ category: "rust-clippy",
2655
+ message: `${rel} uses .unwrap() ${unwraps.length} times`,
2656
+ file: rel,
2657
+ detail: "Prefer .expect(), ?, or proper error handling over .unwrap()"
2658
+ });
2659
+ }
2660
+ const clones = content.match(/\.clone\(\)/g);
2661
+ if (clones && clones.length >= 15) {
2662
+ findings.push({
2663
+ id: `rust-clone-heavy-${rel}`,
2664
+ severity: "info",
2665
+ category: "rust-clippy",
2666
+ message: `${rel} uses .clone() ${clones.length} times`,
2667
+ file: rel,
2668
+ detail: "Excessive cloning may indicate ownership issues"
2669
+ });
2670
+ }
2671
+ const deadCodeAllows = content.match(/#\[allow\(dead_code\)]/g);
2672
+ if (deadCodeAllows && deadCodeAllows.length >= 3) {
2673
+ findings.push({
2674
+ id: `rust-dead-code-${rel}`,
2675
+ severity: "info",
2676
+ category: "rust-clippy",
2677
+ message: `${rel} suppresses dead_code warnings ${deadCodeAllows.length} times`,
2678
+ file: rel,
2679
+ detail: "Consider removing unused code instead of suppressing warnings"
2680
+ });
2681
+ }
2682
+ const todos = content.match(/\b(?:todo|unimplemented)!\s*\(/g);
2683
+ if (todos && todos.length > 0) {
2684
+ findings.push({
2685
+ id: `rust-todo-macro-${rel}`,
2686
+ severity: "info",
2687
+ category: "rust-clippy",
2688
+ message: `${rel} has ${todos.length} todo!()/unimplemented!() macro(s)`,
2689
+ file: rel,
2690
+ detail: "These will panic at runtime if reached"
2691
+ });
2692
+ }
2693
+ } catch {
2694
+ }
2695
+ }
2696
+ return { findings };
2697
+ }
2698
+ };
2699
+
2700
+ // src/scanners/java.ts
2701
+ import fg16 from "fast-glob";
2702
+ import fs13 from "fs";
2703
+ var JavaComplexityScanner = class {
2704
+ constructor() {
2705
+ this.name = "java-complexity";
2706
+ }
2707
+ async scan(rootDir) {
2708
+ const findings = [];
2709
+ const files = await fg16(["**/*.java"], {
2710
+ cwd: rootDir,
2711
+ ignore: [...IGNORE_PATTERNS, "**/target/**", "**/build/**"],
2712
+ absolute: true
2713
+ });
2714
+ for (const file of files) {
2715
+ try {
2716
+ const content = fs13.readFileSync(file, "utf-8");
2717
+ const rel = relativePath(rootDir, file);
2718
+ const methods = this.extractMethods(content);
2719
+ for (const method of methods) {
2720
+ const complexity = this.calculateComplexity(method.body);
2721
+ if (complexity >= 20) {
2722
+ findings.push({
2723
+ id: `java-complexity-${rel}-${method.name}`,
2724
+ severity: "critical",
2725
+ category: "complexity",
2726
+ message: `Method ${method.name} has cyclomatic complexity of ${complexity}`,
2727
+ file: rel,
2728
+ detail: `${complexity} decision points`
2729
+ });
2730
+ } else if (complexity >= 10) {
2731
+ findings.push({
2732
+ id: `java-complexity-${rel}-${method.name}`,
2733
+ severity: "warning",
2734
+ category: "complexity",
2735
+ message: `Method ${method.name} has cyclomatic complexity of ${complexity}`,
2736
+ file: rel,
2737
+ detail: `${complexity} decision points`
2738
+ });
2739
+ }
2740
+ }
2741
+ } catch {
2742
+ }
2743
+ }
2744
+ return { findings };
2745
+ }
2746
+ extractMethods(content) {
2747
+ const methods = [];
2748
+ const lines = content.split("\n");
2749
+ for (let i = 0; i < lines.length; i++) {
2750
+ const line = lines[i];
2751
+ const methodMatch = line.match(
2752
+ // eslint-disable-next-line security/detect-unsafe-regex
2753
+ /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:\w+(?:<[^>]{0,100}>)?)\s+(\w+)\s*\(/
2754
+ );
2755
+ if (methodMatch && !line.includes("class ") && !line.includes("interface ")) {
2756
+ const name = methodMatch[1];
2757
+ let braceCount = 0;
2758
+ let body = "";
2759
+ let started = false;
2760
+ for (let j = i; j < lines.length; j++) {
2761
+ body += lines[j] + "\n";
2762
+ for (const ch of lines[j]) {
2763
+ if (ch === "{") {
2764
+ braceCount++;
2765
+ started = true;
2766
+ }
2767
+ if (ch === "}") {
2768
+ braceCount--;
2769
+ }
2770
+ }
2771
+ if (started && braceCount <= 0) break;
2772
+ }
2773
+ methods.push({ name, body });
2774
+ }
2775
+ }
2776
+ return methods;
2777
+ }
2778
+ calculateComplexity(code) {
2779
+ let complexity = 1;
2780
+ const patterns = [
2781
+ /\bif\s*\(/g,
2782
+ /\belse if\s*\(/g,
2783
+ /\bfor\s*\(/g,
2784
+ /\bwhile\s*\(/g,
2785
+ /\bcase\s+/g,
2786
+ /\bcatch\s*\(/g,
2787
+ /&&/g,
2788
+ /\|\|/g,
2789
+ /\?[^?]/g
2790
+ ];
2791
+ for (const pattern of patterns) {
2792
+ const matches = code.match(pattern);
2793
+ if (matches) complexity += matches.length;
2794
+ }
2795
+ return complexity;
2796
+ }
2797
+ };
2798
+ var JavaCodeSmellScanner = class {
2799
+ constructor() {
2800
+ this.name = "java-code-smells";
2801
+ }
2802
+ async scan(rootDir) {
2803
+ const findings = [];
2804
+ const files = await fg16(["**/*.java"], {
2805
+ cwd: rootDir,
2806
+ ignore: [...IGNORE_PATTERNS, "**/target/**", "**/build/**", "**/*Test.java", "**/*Tests.java"],
2807
+ absolute: true
2808
+ });
2809
+ for (const file of files) {
2810
+ try {
2811
+ const content = fs13.readFileSync(file, "utf-8");
2812
+ const rel = relativePath(rootDir, file);
2813
+ const methodCount = (content.match(
2814
+ // eslint-disable-next-line security/detect-unsafe-regex
2815
+ /^\s*(?:public|private|protected)\s+(?:static\s+)?(?:\w+)\s+\w+\s*\(/gm
2816
+ ) || []).length;
2817
+ if (methodCount >= 30) {
2818
+ findings.push({
2819
+ id: `java-god-class-${rel}`,
2820
+ severity: "warning",
2821
+ category: "java-smells",
2822
+ message: `${rel} has ${methodCount} methods \u2014 possible God class`,
2823
+ file: rel,
2824
+ detail: "Consider splitting into smaller, focused classes"
2825
+ });
2826
+ }
2827
+ const rawTypes = content.match(/\b(?:List|Map|Set|Collection|Iterator)\s+\w+/g);
2828
+ if (rawTypes && rawTypes.length >= 3) {
2829
+ findings.push({
2830
+ id: `java-raw-types-${rel}`,
2831
+ severity: "info",
2832
+ category: "java-smells",
2833
+ message: `${rel} uses raw types ${rawTypes.length} times`,
2834
+ file: rel,
2835
+ detail: "Use parameterized types for type safety (e.g., List<String>)"
2836
+ });
2837
+ }
2838
+ const suppressions = content.match(/@SuppressWarnings/g);
2839
+ if (suppressions && suppressions.length >= 3) {
2840
+ findings.push({
2841
+ id: `java-suppressions-${rel}`,
2842
+ severity: "info",
2843
+ category: "java-smells",
2844
+ message: `${rel} suppresses ${suppressions.length} warnings`,
2845
+ file: rel,
2846
+ detail: "Fix warnings instead of suppressing them"
2847
+ });
2848
+ }
2849
+ const sysout = content.match(/System\.(out|err)\.(println|print|printf)\s*\(/g);
2850
+ if (sysout && sysout.length >= 3) {
2851
+ findings.push({
2852
+ id: `java-sysout-${rel}`,
2853
+ severity: "info",
2854
+ category: "java-smells",
2855
+ message: `${rel} uses System.out/err ${sysout.length} times`,
2856
+ file: rel,
2857
+ detail: "Use a logging framework (SLF4J, Log4j) instead"
2858
+ });
2859
+ }
2860
+ const emptyCatch = content.match(/catch\s*\([^)]*\)\s*\{\s*\}/g);
2861
+ if (emptyCatch && emptyCatch.length > 0) {
2862
+ findings.push({
2863
+ id: `java-empty-catch-${rel}`,
2864
+ severity: "warning",
2865
+ category: "java-smells",
2866
+ message: `${rel} has ${emptyCatch.length} empty catch block(s)`,
2867
+ file: rel,
2868
+ detail: "Empty catch blocks silently swallow exceptions"
2869
+ });
2870
+ }
2871
+ } catch {
2872
+ }
2873
+ }
2874
+ return { findings };
2875
+ }
2876
+ };
2877
+ var JavaNamingScanner = class {
2878
+ constructor() {
2879
+ this.name = "java-naming";
2880
+ }
2881
+ async scan(rootDir) {
2882
+ const findings = [];
2883
+ const files = await fg16(["**/*.java"], {
2884
+ cwd: rootDir,
2885
+ ignore: [...IGNORE_PATTERNS, "**/target/**", "**/build/**"],
2886
+ absolute: true
2887
+ });
2888
+ for (const file of files) {
2889
+ try {
2890
+ const content = fs13.readFileSync(file, "utf-8");
2891
+ const rel = relativePath(rootDir, file);
2892
+ const fileName = rel.split("/").pop() || "";
2893
+ const classMatch = content.match(/public\s+(?:class|interface|enum)\s+(\w+)/);
2894
+ if (classMatch) {
2895
+ const className = classMatch[1];
2896
+ const expectedFile = `${className}.java`;
2897
+ if (fileName !== expectedFile) {
2898
+ findings.push({
2899
+ id: `java-naming-mismatch-${rel}`,
2900
+ severity: "warning",
2901
+ category: "java-naming",
2902
+ message: `Class ${className} doesn't match file name ${fileName}`,
2903
+ file: rel
2904
+ });
1764
2905
  }
1765
2906
  }
2907
+ const badConstants = content.match(
2908
+ /\bstatic\s+final\s+\w+\s+([a-z]\w*)\s*=/g
2909
+ );
2910
+ if (badConstants && badConstants.length >= 3) {
2911
+ findings.push({
2912
+ id: `java-constant-naming-${rel}`,
2913
+ severity: "info",
2914
+ category: "java-naming",
2915
+ message: `${rel} has ${badConstants.length} constants not in UPPER_SNAKE_CASE`,
2916
+ file: rel
2917
+ });
2918
+ }
2919
+ } catch {
1766
2920
  }
1767
2921
  }
1768
- return {
1769
- findings,
1770
- stats: { isNextJS, isReact }
1771
- };
2922
+ return { findings };
1772
2923
  }
1773
2924
  };
1774
2925
 
1775
- // src/scanners/python.ts
1776
- import fg13 from "fast-glob";
1777
- import fs10 from "fs";
1778
- var PythonComplexityScanner = class {
2926
+ // src/scanners/csharp.ts
2927
+ import fg17 from "fast-glob";
2928
+ import fs14 from "fs";
2929
+ var CSharpComplexityScanner = class {
1779
2930
  constructor() {
1780
- this.name = "python-complexity";
2931
+ this.name = "csharp-complexity";
1781
2932
  }
1782
2933
  async scan(rootDir) {
1783
2934
  const findings = [];
1784
- const files = await fg13(["**/*.py"], {
2935
+ const files = await fg17(["**/*.cs"], {
1785
2936
  cwd: rootDir,
1786
- ignore: IGNORE_PATTERNS,
2937
+ ignore: [...IGNORE_PATTERNS, "**/bin/**", "**/obj/**"],
1787
2938
  absolute: true
1788
2939
  });
1789
2940
  for (const file of files) {
1790
2941
  try {
1791
- const content = fs10.readFileSync(file, "utf-8");
2942
+ const content = fs14.readFileSync(file, "utf-8");
1792
2943
  const rel = relativePath(rootDir, file);
1793
- const functions = this.extractFunctions(content);
1794
- for (const func of functions) {
1795
- const complexity = this.calculateComplexity(func.body);
2944
+ const methods = this.extractMethods(content);
2945
+ for (const method of methods) {
2946
+ const complexity = this.calculateComplexity(method.body);
1796
2947
  if (complexity >= 20) {
1797
2948
  findings.push({
1798
- id: `py-complexity-${rel}-${func.name}`,
2949
+ id: `csharp-complexity-${rel}-${method.name}`,
1799
2950
  severity: "critical",
1800
2951
  category: "complexity",
1801
- message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
2952
+ message: `Method ${method.name} has cyclomatic complexity of ${complexity}`,
1802
2953
  file: rel,
1803
2954
  detail: `${complexity} decision points`
1804
2955
  });
1805
2956
  } else if (complexity >= 10) {
1806
2957
  findings.push({
1807
- id: `py-complexity-${rel}-${func.name}`,
2958
+ id: `csharp-complexity-${rel}-${method.name}`,
1808
2959
  severity: "warning",
1809
2960
  category: "complexity",
1810
- message: `Function ${func.name} has cyclomatic complexity of ${complexity}`,
2961
+ message: `Method ${method.name} has cyclomatic complexity of ${complexity}`,
1811
2962
  file: rel,
1812
2963
  detail: `${complexity} decision points`
1813
2964
  });
@@ -1818,146 +2969,174 @@ var PythonComplexityScanner = class {
1818
2969
  }
1819
2970
  return { findings };
1820
2971
  }
1821
- extractFunctions(content) {
1822
- const functions = [];
2972
+ extractMethods(content) {
2973
+ const methods = [];
1823
2974
  const lines = content.split("\n");
1824
2975
  for (let i = 0; i < lines.length; i++) {
1825
2976
  const line = lines[i];
1826
- const funcMatch = line.match(/^\s*def\s+(\w+)\s*\(/);
1827
- if (funcMatch) {
1828
- const name = funcMatch[1];
1829
- const indent = line.match(/^\s*/)?.[0].length || 0;
1830
- let body = line + "\n";
1831
- for (let j = i + 1; j < lines.length; j++) {
1832
- const nextLine = lines[j];
1833
- const nextIndent = nextLine.match(/^\s*/)?.[0].length || 0;
1834
- if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
1835
- body += nextLine + "\n";
1836
- continue;
1837
- }
1838
- if (nextIndent <= indent) {
1839
- break;
2977
+ const methodMatch = line.match(
2978
+ // eslint-disable-next-line security/detect-unsafe-regex
2979
+ /^\s*(?:public|private|protected|internal)?\s*(?:static\s+)?(?:async\s+)?(?:virtual\s+)?(?:override\s+)?(?:\w+(?:<[^>]{0,100}>)?)\s+(\w+)\s*\(/
2980
+ );
2981
+ if (methodMatch && !line.includes("class ") && !line.includes("interface ") && !line.includes("namespace ")) {
2982
+ const name = methodMatch[1];
2983
+ let braceCount = 0;
2984
+ let body = "";
2985
+ let started = false;
2986
+ for (let j = i; j < lines.length; j++) {
2987
+ body += lines[j] + "\n";
2988
+ for (const ch of lines[j]) {
2989
+ if (ch === "{") {
2990
+ braceCount++;
2991
+ started = true;
2992
+ }
2993
+ if (ch === "}") {
2994
+ braceCount--;
2995
+ }
1840
2996
  }
1841
- body += nextLine + "\n";
2997
+ if (started && braceCount <= 0) break;
1842
2998
  }
1843
- functions.push({ name, body });
2999
+ methods.push({ name, body });
1844
3000
  }
1845
3001
  }
1846
- return functions;
3002
+ return methods;
1847
3003
  }
1848
3004
  calculateComplexity(code) {
1849
3005
  let complexity = 1;
1850
3006
  const patterns = [
1851
- /\bif\b/g,
1852
- /\belif\b/g,
1853
- /\bfor\b/g,
1854
- /\bwhile\b/g,
1855
- /\band\b/g,
1856
- /\bor\b/g,
1857
- /\bexcept\b/g,
1858
- /\bwith\b/g,
1859
- /\?/g
1860
- // Ternary
3007
+ /\bif\s*\(/g,
3008
+ /\belse if\s*\(/g,
3009
+ /\bfor\s*\(/g,
3010
+ /\bforeach\s*\(/g,
3011
+ /\bwhile\s*\(/g,
3012
+ /\bcase\s+/g,
3013
+ /\bcatch\s*\(/g,
3014
+ /&&/g,
3015
+ /\|\|/g,
3016
+ /\?\?/g,
3017
+ /\?[^?.\s]/g
1861
3018
  ];
1862
3019
  for (const pattern of patterns) {
1863
3020
  const matches = code.match(pattern);
1864
- if (matches) {
1865
- complexity += matches.length;
1866
- }
3021
+ if (matches) complexity += matches.length;
1867
3022
  }
1868
3023
  return complexity;
1869
3024
  }
1870
3025
  };
1871
- var PythonTypeHintsScanner = class {
3026
+ var CSharpCodeSmellScanner = class {
1872
3027
  constructor() {
1873
- this.name = "python-type-hints";
3028
+ this.name = "csharp-code-smells";
1874
3029
  }
1875
3030
  async scan(rootDir) {
1876
3031
  const findings = [];
1877
- const files = await fg13(["**/*.py"], {
3032
+ const files = await fg17(["**/*.cs"], {
1878
3033
  cwd: rootDir,
1879
- ignore: IGNORE_PATTERNS,
3034
+ ignore: [...IGNORE_PATTERNS, "**/bin/**", "**/obj/**", "**/*Test*.cs", "**/Migrations/**"],
1880
3035
  absolute: true
1881
3036
  });
1882
- let totalFunctions = 0;
1883
- let untypedFunctions = 0;
1884
3037
  for (const file of files) {
1885
3038
  try {
1886
- const content = fs10.readFileSync(file, "utf-8");
3039
+ const content = fs14.readFileSync(file, "utf-8");
1887
3040
  const rel = relativePath(rootDir, file);
1888
- const funcPattern = /def\s+(\w+)\s*\([^)]*\)(?:\s*->\s*\w+)?:/g;
1889
- const functions = Array.from(content.matchAll(funcPattern));
1890
- for (const match of functions) {
1891
- totalFunctions++;
1892
- const fullDef = match[0];
1893
- const hasParamTypes = /:\s*\w+/.test(fullDef);
1894
- const hasReturnType = /->/.test(fullDef);
1895
- if (!hasParamTypes && !hasReturnType) {
1896
- untypedFunctions++;
1897
- }
3041
+ const methodCount = (content.match(
3042
+ // eslint-disable-next-line security/detect-unsafe-regex
3043
+ /^\s*(?:public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:\w+)\s+\w+\s*\(/gm
3044
+ ) || []).length;
3045
+ if (methodCount >= 30) {
3046
+ findings.push({
3047
+ id: `csharp-god-class-${rel}`,
3048
+ severity: "warning",
3049
+ category: "csharp-smells",
3050
+ message: `${rel} has ${methodCount} methods \u2014 possible God class`,
3051
+ file: rel,
3052
+ detail: "Consider applying Single Responsibility Principle"
3053
+ });
3054
+ }
3055
+ const regions = content.match(/#region/g);
3056
+ if (regions && regions.length >= 5) {
3057
+ findings.push({
3058
+ id: `csharp-regions-${rel}`,
3059
+ severity: "info",
3060
+ category: "csharp-smells",
3061
+ message: `${rel} has ${regions.length} #region blocks`,
3062
+ file: rel,
3063
+ detail: "Excessive regions often hide classes that are too large"
3064
+ });
3065
+ }
3066
+ const emptyCatch = content.match(/catch\s*(?:\([^)]{0,100}\))?\s*\{\s*\}/g);
3067
+ if (emptyCatch && emptyCatch.length > 0) {
3068
+ findings.push({
3069
+ id: `csharp-empty-catch-${rel}`,
3070
+ severity: "warning",
3071
+ category: "csharp-smells",
3072
+ message: `${rel} has ${emptyCatch.length} empty catch block(s)`,
3073
+ file: rel,
3074
+ detail: "Empty catch blocks silently swallow exceptions"
3075
+ });
3076
+ }
3077
+ const consoleWrites = content.match(/Console\.(Write|WriteLine)\s*\(/g);
3078
+ if (consoleWrites && consoleWrites.length >= 3) {
3079
+ findings.push({
3080
+ id: `csharp-console-${rel}`,
3081
+ severity: "info",
3082
+ category: "csharp-smells",
3083
+ message: `${rel} uses Console.Write* ${consoleWrites.length} times`,
3084
+ file: rel,
3085
+ detail: "Use ILogger/logging framework instead of Console output"
3086
+ });
3087
+ }
3088
+ const pragmas = content.match(/#pragma\s+warning\s+disable/g);
3089
+ if (pragmas && pragmas.length >= 3) {
3090
+ findings.push({
3091
+ id: `csharp-pragma-${rel}`,
3092
+ severity: "info",
3093
+ category: "csharp-smells",
3094
+ message: `${rel} disables ${pragmas.length} compiler warnings`,
3095
+ file: rel,
3096
+ detail: "Fix warnings instead of suppressing them"
3097
+ });
1898
3098
  }
1899
3099
  } catch {
1900
3100
  }
1901
3101
  }
1902
- if (totalFunctions > 0) {
1903
- const untypedPercent = Math.round(
1904
- untypedFunctions / totalFunctions * 100
1905
- );
1906
- if (untypedPercent > 70) {
1907
- findings.push({
1908
- id: "py-type-hints-low",
1909
- severity: "warning",
1910
- category: "type-safety",
1911
- message: `${untypedPercent}% of Python functions lack type hints`,
1912
- detail: `${untypedFunctions} of ${totalFunctions} functions`
1913
- });
1914
- }
1915
- }
1916
- return {
1917
- findings,
1918
- stats: {
1919
- totalFunctions,
1920
- untypedFunctions,
1921
- untypedPercent: totalFunctions > 0 ? Math.round(untypedFunctions / totalFunctions * 100) : 0
1922
- }
1923
- };
3102
+ return { findings };
1924
3103
  }
1925
3104
  };
1926
- var PythonImportsScanner = class {
3105
+ var CSharpAsyncScanner = class {
1927
3106
  constructor() {
1928
- this.name = "python-imports";
3107
+ this.name = "csharp-async";
1929
3108
  }
1930
3109
  async scan(rootDir) {
1931
3110
  const findings = [];
1932
- const files = await fg13(["**/*.py"], {
3111
+ const files = await fg17(["**/*.cs"], {
1933
3112
  cwd: rootDir,
1934
- ignore: IGNORE_PATTERNS,
3113
+ ignore: [...IGNORE_PATTERNS, "**/bin/**", "**/obj/**"],
1935
3114
  absolute: true
1936
3115
  });
1937
3116
  for (const file of files) {
1938
3117
  try {
1939
- const content = fs10.readFileSync(file, "utf-8");
3118
+ const content = fs14.readFileSync(file, "utf-8");
1940
3119
  const rel = relativePath(rootDir, file);
1941
- const wildcardImports = content.match(/from\s+[\w.]+\s+import\s+\*/g);
1942
- if (wildcardImports && wildcardImports.length > 0) {
3120
+ const asyncVoid = content.match(/\basync\s+void\s+\w+/g);
3121
+ if (asyncVoid && asyncVoid.length > 0) {
1943
3122
  findings.push({
1944
- id: `py-wildcard-import-${rel}`,
3123
+ id: `csharp-async-void-${rel}`,
1945
3124
  severity: "warning",
1946
- category: "python-imports",
1947
- message: `${rel} uses wildcard imports (${wildcardImports.length} found)`,
3125
+ category: "csharp-async",
3126
+ message: `${rel} has ${asyncVoid.length} async void method(s)`,
1948
3127
  file: rel,
1949
- detail: "Wildcard imports pollute namespace"
3128
+ detail: "async void cannot be awaited and exceptions are unobservable. Use async Task instead."
1950
3129
  });
1951
3130
  }
1952
- const deepRelative = content.match(/from\s+\.{3,}/g);
1953
- if (deepRelative && deepRelative.length > 0) {
3131
+ const syncOverAsync = content.match(/\.(?:Result|Wait\(\))/g);
3132
+ if (syncOverAsync && syncOverAsync.length >= 3) {
1954
3133
  findings.push({
1955
- id: `py-deep-relative-${rel}`,
1956
- severity: "info",
1957
- category: "python-imports",
1958
- message: `${rel} uses deep relative imports`,
3134
+ id: `csharp-sync-over-async-${rel}`,
3135
+ severity: "warning",
3136
+ category: "csharp-async",
3137
+ message: `${rel} has ${syncOverAsync.length} sync-over-async calls (.Result/.Wait())`,
1959
3138
  file: rel,
1960
- detail: "Consider absolute imports"
3139
+ detail: "Use await instead of .Result/.Wait() to avoid deadlocks"
1961
3140
  });
1962
3141
  }
1963
3142
  } catch {
@@ -2025,12 +3204,12 @@ var LANGUAGE_CONFIGS = {
2025
3204
  sourcePatterns: ["**/*.cs"]
2026
3205
  }
2027
3206
  };
2028
- function detectProjectLanguage(rootDir, fs18) {
3207
+ function detectProjectLanguage(rootDir, fs22) {
2029
3208
  const detectedLanguages = [];
2030
3209
  for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
2031
3210
  for (const packageFile of config.packageFiles) {
2032
3211
  const fullPath = __require("path").join(rootDir, packageFile);
2033
- if (fs18.existsSync(fullPath)) {
3212
+ if (fs22.existsSync(fullPath)) {
2034
3213
  detectedLanguages.push(lang);
2035
3214
  break;
2036
3215
  }
@@ -2147,7 +3326,7 @@ function getLabel(score) {
2147
3326
 
2148
3327
  // src/ai/index.ts
2149
3328
  import Anthropic from "@anthropic-ai/sdk";
2150
- import fs11 from "fs";
3329
+ import fs15 from "fs";
2151
3330
  import path9 from "path";
2152
3331
  import crypto from "crypto";
2153
3332
  var DEFAULT_MODEL = "claude-3-5-sonnet-20241022";
@@ -2253,11 +3432,11 @@ function getFindingHash(finding) {
2253
3432
  }
2254
3433
  function getCachedRoast(finding, rootDir, cachePath) {
2255
3434
  const cacheFile = path9.join(rootDir, cachePath || CACHE_FILE);
2256
- if (!fs11.existsSync(cacheFile)) {
3435
+ if (!fs15.existsSync(cacheFile)) {
2257
3436
  return null;
2258
3437
  }
2259
3438
  try {
2260
- const content = fs11.readFileSync(cacheFile, "utf-8");
3439
+ const content = fs15.readFileSync(cacheFile, "utf-8");
2261
3440
  const cache = JSON.parse(content);
2262
3441
  const findingHash = getFindingHash(finding);
2263
3442
  const now = Date.now();
@@ -2273,8 +3452,8 @@ function cacheRoast(finding, roast, rootDir, cachePath) {
2273
3452
  const cacheFile = path9.join(rootDir, cachePath || CACHE_FILE);
2274
3453
  try {
2275
3454
  let cache = [];
2276
- if (fs11.existsSync(cacheFile)) {
2277
- const content = fs11.readFileSync(cacheFile, "utf-8");
3455
+ if (fs15.existsSync(cacheFile)) {
3456
+ const content = fs15.readFileSync(cacheFile, "utf-8");
2278
3457
  cache = JSON.parse(content);
2279
3458
  }
2280
3459
  const findingHash = getFindingHash(finding);
@@ -2289,7 +3468,7 @@ function cacheRoast(finding, roast, rootDir, cachePath) {
2289
3468
  if (cache.length > 100) {
2290
3469
  cache = cache.slice(-100);
2291
3470
  }
2292
- fs11.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
3471
+ fs15.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
2293
3472
  } catch (error) {
2294
3473
  }
2295
3474
  }
@@ -2424,6 +3603,71 @@ var pythonImportRoasts = [
2424
3603
  "Deep relative imports: your codebase is spaghetti that imports other spaghetti.",
2425
3604
  "from module import * \u2014 the programming equivalent of 'throw everything in and hope.'"
2426
3605
  ];
3606
+ var pythonDocstringRoasts = [
3607
+ "Docstrings are optional. So is understanding your code in 6 months.",
3608
+ "Your functions are mysteries wrapped in enigmas. Add a docstring.",
3609
+ "Self-documenting code is a myth. Your future self will thank you for docstrings."
3610
+ ];
3611
+ var pythonSmellRoasts = [
3612
+ "Bare except: catching everything including your dignity.",
3613
+ "Mutable default arguments: the gift that keeps on mutating.",
3614
+ "This much global state makes singletons look elegant.",
3615
+ "Your functions are nested deeper than your tech debt."
3616
+ ];
3617
+ var pythonSecurityRoasts = [
3618
+ "eval() in Python: because you want hackers to feel welcome.",
3619
+ "pickle.load() from untrusted data is just exec() with extra steps.",
3620
+ "shell=True with user input: RCE as a feature, not a bug.",
3621
+ "SQL string formatting: Little Bobby Tables approves.",
3622
+ "Hardcoded secrets in Python: because .env files are too mainstream."
3623
+ ];
3624
+ var pythonDesignRoasts = [
3625
+ "This class has more methods than a Swiss Army knife has tools.",
3626
+ "A class with no methods is just a dict wearing a trench coat. Use @dataclass.",
3627
+ "4+ parent classes: your MRO looks like a family tree from the Habsburgs."
3628
+ ];
3629
+ var goErrorHandlingRoasts = [
3630
+ "Ignoring errors in Go is like ignoring check engine lights.",
3631
+ "_ = dangerousOperation() \u2014 the Go equivalent of 'it's fine.'",
3632
+ "Your error handling strategy appears to be 'hope.'",
3633
+ "panic() in production: because graceful degradation is overrated."
3634
+ ];
3635
+ var goLintRoasts = [
3636
+ "Unexported symbols don't need docs. Exported ones do. Guess which you forgot.",
3637
+ "Multiple init() functions: because one confusing startup sequence wasn't enough.",
3638
+ "Go proverbs say 'a little copying is better than a little dependency.' You took that personally."
3639
+ ];
3640
+ var rustUnsafeRoasts = [
3641
+ "unsafe {} \u2014 Rust's way of saying 'I know what I'm doing.' Do you, though?",
3642
+ "This much unsafe code defeats the purpose of choosing Rust.",
3643
+ "The borrow checker can't save you if you keep bypassing it."
3644
+ ];
3645
+ var rustClippyRoasts = [
3646
+ ".unwrap() everywhere: living dangerously, one None at a time.",
3647
+ "This much .clone() suggests a fundamental misunderstanding of ownership.",
3648
+ "todo!() in production: the Rust equivalent of 'I'll fix it later.'"
3649
+ ];
3650
+ var javaSmellRoasts = [
3651
+ "This class has more methods than a phone book has entries.",
3652
+ "System.out.println in production \u2014 logging frameworks exist, you know.",
3653
+ "Empty catch blocks: because exceptions are just suggestions.",
3654
+ "God classes: when Single Responsibility Principle is just a suggestion."
3655
+ ];
3656
+ var javaNamingRoasts = [
3657
+ "AbstractSingletonProxyFactoryBean called. It wants its naming convention back.",
3658
+ "Java naming conventions aren't optional. Even Java thinks so.",
3659
+ "Your constant naming is more chaotic than your class hierarchy."
3660
+ ];
3661
+ var csharpSmellRoasts = [
3662
+ "#region is not architecture. It's a rug to sweep complexity under.",
3663
+ "Console.WriteLine in production? ILogger is right there.",
3664
+ "This class is so large it needs its own table of contents."
3665
+ ];
3666
+ var csharpAsyncRoasts = [
3667
+ "async void: the fire-and-forget-and-pray pattern.",
3668
+ ".Result and .Wait() \u2014 deadlocks as a service.",
3669
+ "Sync-over-async: because who needs scalability anyway."
3670
+ ];
2427
3671
  function pick(arr) {
2428
3672
  return arr[Math.floor(Math.random() * arr.length)];
2429
3673
  }
@@ -2580,6 +3824,102 @@ async function generateRoasts(findings, aiConfig, rootDir) {
2580
3824
  category: "python-imports"
2581
3825
  });
2582
3826
  }
3827
+ const pythonDocstrings = findings.filter((f) => f.category === "python-docstrings");
3828
+ if (pythonDocstrings.length > 0) {
3829
+ roasts.push({
3830
+ target: "Python documentation",
3831
+ message: pick(pythonDocstringRoasts),
3832
+ category: "python-docstrings"
3833
+ });
3834
+ }
3835
+ const pythonSmells = findings.filter((f) => f.category === "python-smells");
3836
+ if (pythonSmells.length > 0) {
3837
+ roasts.push({
3838
+ target: pythonSmells[0].file || "Python code",
3839
+ message: pick(pythonSmellRoasts),
3840
+ category: "python-smells"
3841
+ });
3842
+ }
3843
+ const pythonSecurity = findings.filter((f) => f.category === "python-security");
3844
+ if (pythonSecurity.length > 0) {
3845
+ roasts.push({
3846
+ target: pythonSecurity[0].file || "Python security",
3847
+ message: pick(pythonSecurityRoasts),
3848
+ category: "python-security"
3849
+ });
3850
+ }
3851
+ const pythonDesign = findings.filter((f) => f.category === "python-design");
3852
+ if (pythonDesign.length > 0) {
3853
+ roasts.push({
3854
+ target: pythonDesign[0].file || "Python classes",
3855
+ message: pick(pythonDesignRoasts),
3856
+ category: "python-design"
3857
+ });
3858
+ }
3859
+ const goErrors = findings.filter((f) => f.category === "go-error-handling");
3860
+ if (goErrors.length > 0) {
3861
+ roasts.push({
3862
+ target: goErrors[0].file || "Go code",
3863
+ message: pick(goErrorHandlingRoasts),
3864
+ category: "go-error-handling"
3865
+ });
3866
+ }
3867
+ const goLint = findings.filter((f) => f.category === "go-lint");
3868
+ if (goLint.length > 0) {
3869
+ roasts.push({
3870
+ target: "Go conventions",
3871
+ message: pick(goLintRoasts),
3872
+ category: "go-lint"
3873
+ });
3874
+ }
3875
+ const rustUnsafe = findings.filter((f) => f.category === "rust-unsafe");
3876
+ if (rustUnsafe.length > 0) {
3877
+ roasts.push({
3878
+ target: rustUnsafe[0].file || "Rust code",
3879
+ message: pick(rustUnsafeRoasts),
3880
+ category: "rust-unsafe"
3881
+ });
3882
+ }
3883
+ const rustClippy = findings.filter((f) => f.category === "rust-clippy");
3884
+ if (rustClippy.length > 0) {
3885
+ roasts.push({
3886
+ target: rustClippy[0].file || "Rust code",
3887
+ message: pick(rustClippyRoasts),
3888
+ category: "rust-clippy"
3889
+ });
3890
+ }
3891
+ const javaSmells = findings.filter((f) => f.category === "java-smells");
3892
+ if (javaSmells.length > 0) {
3893
+ roasts.push({
3894
+ target: javaSmells[0].file || "Java code",
3895
+ message: pick(javaSmellRoasts),
3896
+ category: "java-smells"
3897
+ });
3898
+ }
3899
+ const javaNaming = findings.filter((f) => f.category === "java-naming");
3900
+ if (javaNaming.length > 0) {
3901
+ roasts.push({
3902
+ target: javaNaming[0].file || "Java code",
3903
+ message: pick(javaNamingRoasts),
3904
+ category: "java-naming"
3905
+ });
3906
+ }
3907
+ const csharpSmells = findings.filter((f) => f.category === "csharp-smells");
3908
+ if (csharpSmells.length > 0) {
3909
+ roasts.push({
3910
+ target: csharpSmells[0].file || "C# code",
3911
+ message: pick(csharpSmellRoasts),
3912
+ category: "csharp-smells"
3913
+ });
3914
+ }
3915
+ const csharpAsync = findings.filter((f) => f.category === "csharp-async");
3916
+ if (csharpAsync.length > 0) {
3917
+ roasts.push({
3918
+ target: csharpAsync[0].file || "C# code",
3919
+ message: pick(csharpAsyncRoasts),
3920
+ category: "csharp-async"
3921
+ });
3922
+ }
2583
3923
  return roasts;
2584
3924
  }
2585
3925
  function generateVerdict(health) {
@@ -2650,7 +3990,7 @@ function renderJsonReport(report) {
2650
3990
  }
2651
3991
 
2652
3992
  // src/report/badge.ts
2653
- import fs12 from "fs";
3993
+ import fs16 from "fs";
2654
3994
  import chalk2 from "chalk";
2655
3995
 
2656
3996
  // src/utils/security.ts
@@ -2722,7 +4062,7 @@ function getBadgeColor(score) {
2722
4062
  function saveBadge(svgContent, rootDir) {
2723
4063
  try {
2724
4064
  const badgePath = validateOutputPath(rootDir, ".roast-badge.svg");
2725
- fs12.writeFileSync(badgePath, svgContent, "utf-8");
4065
+ fs16.writeFileSync(badgePath, svgContent, "utf-8");
2726
4066
  console.log(chalk2.green("\u2713") + " Badge saved to .roast-badge.svg");
2727
4067
  } catch (error) {
2728
4068
  console.error(chalk2.red("Error saving badge:"), error instanceof Error ? error.message : String(error));
@@ -3097,7 +4437,7 @@ function renderWatchSummary(health, delta, findingCounts) {
3097
4437
 
3098
4438
  // src/compare/index.ts
3099
4439
  import path11 from "path";
3100
- import fs13 from "fs";
4440
+ import fs17 from "fs";
3101
4441
  import { spawnSync as spawnSync3 } from "child_process";
3102
4442
  import { randomBytes } from "crypto";
3103
4443
  import { tmpdir } from "os";
@@ -3139,9 +4479,9 @@ async function compareWithBranch(rootDir, branchName, scanFunc) {
3139
4479
  if (result.status === 0) return;
3140
4480
  await new Promise((resolve) => setTimeout(resolve, 100 * (i + 1)));
3141
4481
  }
3142
- if (fs13.existsSync(worktreePath)) {
4482
+ if (fs17.existsSync(worktreePath)) {
3143
4483
  try {
3144
- fs13.rmSync(worktreePath, { recursive: true, force: true });
4484
+ fs17.rmSync(worktreePath, { recursive: true, force: true });
3145
4485
  } catch {
3146
4486
  }
3147
4487
  }
@@ -3217,7 +4557,7 @@ function renderComparison(comparison, branchName) {
3217
4557
  }
3218
4558
 
3219
4559
  // src/config/index.ts
3220
- import fs14 from "fs";
4560
+ import fs18 from "fs";
3221
4561
  import path12 from "path";
3222
4562
 
3223
4563
  // src/config/validation.ts
@@ -3340,11 +4680,11 @@ var DEFAULT_CONFIG = {
3340
4680
  };
3341
4681
  function loadConfig(rootDir) {
3342
4682
  const configPath = path12.join(rootDir, ".roastrc.json");
3343
- if (!fs14.existsSync(configPath)) {
4683
+ if (!fs18.existsSync(configPath)) {
3344
4684
  return DEFAULT_CONFIG;
3345
4685
  }
3346
4686
  try {
3347
- const content = fs14.readFileSync(configPath, "utf-8");
4687
+ const content = fs18.readFileSync(configPath, "utf-8");
3348
4688
  const parsed = safeJsonParse(content);
3349
4689
  if (!parsed) {
3350
4690
  console.warn("Warning: Failed to parse .roastrc.json");
@@ -3386,7 +4726,7 @@ import { confirm, select } from "@inquirer/prompts";
3386
4726
  import chalk6 from "chalk";
3387
4727
 
3388
4728
  // src/interactive/fixes.ts
3389
- import fs15 from "fs";
4729
+ import fs19 from "fs";
3390
4730
  import path13 from "path";
3391
4731
  import { spawnSync as spawnSync4 } from "child_process";
3392
4732
  async function applyAutoFix(finding, fix, rootDir, dryRun = false) {
@@ -3453,7 +4793,7 @@ function fixTodoComment(finding, rootDir, dryRun) {
3453
4793
  }
3454
4794
  const filePath = path13.join(rootDir, finding.file);
3455
4795
  try {
3456
- const content = fs15.readFileSync(filePath, "utf-8");
4796
+ const content = fs19.readFileSync(filePath, "utf-8");
3457
4797
  const lines = content.split("\n");
3458
4798
  const todoPattern = /\/\/\s*(TODO|FIXME|HACK|XXX):\s*(.+)/i;
3459
4799
  let modified = false;
@@ -3481,7 +4821,7 @@ function fixTodoComment(finding, rootDir, dryRun) {
3481
4821
  message: `Would add issue references to ${changedLines} TODO comment(s)`
3482
4822
  };
3483
4823
  }
3484
- fs15.writeFileSync(filePath, newLines.join("\n"), "utf-8");
4824
+ fs19.writeFileSync(filePath, newLines.join("\n"), "utf-8");
3485
4825
  return {
3486
4826
  success: true,
3487
4827
  message: `Added issue references to TODO comments in ${finding.file}`
@@ -3510,7 +4850,7 @@ function fixDeadExport(finding, rootDir, dryRun) {
3510
4850
  const exportName = match[1];
3511
4851
  const filePath = path13.join(rootDir, finding.file);
3512
4852
  try {
3513
- const content = fs15.readFileSync(filePath, "utf-8");
4853
+ const content = fs19.readFileSync(filePath, "utf-8");
3514
4854
  const lines = content.split("\n");
3515
4855
  const exportPattern = new RegExp(
3516
4856
  `export\\s+(const|let|var|function|class|type|interface)\\s+${exportName}\\b`,
@@ -3536,7 +4876,7 @@ function fixDeadExport(finding, rootDir, dryRun) {
3536
4876
  message: `Would remove dead export: ${exportName} from ${finding.file}`
3537
4877
  };
3538
4878
  }
3539
- fs15.writeFileSync(filePath, newLines.join("\n"), "utf-8");
4879
+ fs19.writeFileSync(filePath, newLines.join("\n"), "utf-8");
3540
4880
  return {
3541
4881
  success: true,
3542
4882
  message: `Removed dead export: ${exportName} from ${finding.file}`
@@ -3825,17 +5165,17 @@ function getSeverityBadge(severity) {
3825
5165
  }
3826
5166
 
3827
5167
  // src/history/index.ts
3828
- import fs16 from "fs";
5168
+ import fs20 from "fs";
3829
5169
  import path14 from "path";
3830
5170
  import { spawnSync as spawnSync5 } from "child_process";
3831
5171
  var HISTORY_FILE = ".roast-history.json";
3832
5172
  function loadHistory(rootDir) {
3833
5173
  const historyPath = path14.join(rootDir, HISTORY_FILE);
3834
- if (!fs16.existsSync(historyPath)) {
5174
+ if (!fs20.existsSync(historyPath)) {
3835
5175
  return null;
3836
5176
  }
3837
5177
  try {
3838
- const content = fs16.readFileSync(historyPath, "utf-8");
5178
+ const content = fs20.readFileSync(historyPath, "utf-8");
3839
5179
  return JSON.parse(content);
3840
5180
  } catch (error) {
3841
5181
  console.warn(`Warning: Failed to load health history: ${error}`);
@@ -3845,7 +5185,7 @@ function loadHistory(rootDir) {
3845
5185
  function saveHistory(rootDir, history) {
3846
5186
  const historyPath = path14.join(rootDir, HISTORY_FILE);
3847
5187
  try {
3848
- fs16.writeFileSync(historyPath, JSON.stringify(history, null, 2), "utf-8");
5188
+ fs20.writeFileSync(historyPath, JSON.stringify(history, null, 2), "utf-8");
3849
5189
  } catch (error) {
3850
5190
  console.warn(`Warning: Failed to save health history: ${error}`);
3851
5191
  }
@@ -4164,8 +5504,8 @@ function loadPackageVersion() {
4164
5504
  let dir = path15.dirname(__filename);
4165
5505
  while (dir !== path15.dirname(dir)) {
4166
5506
  const pkgPath = path15.join(dir, "package.json");
4167
- if (fs17.existsSync(pkgPath)) {
4168
- const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf-8"));
5507
+ if (fs21.existsSync(pkgPath)) {
5508
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
4169
5509
  if (pkg.name === "roast-my-codebase") return pkg.version;
4170
5510
  }
4171
5511
  dir = path15.dirname(dir);
@@ -4180,11 +5520,11 @@ function createCli() {
4180
5520
  parseInt
4181
5521
  ).action(async (targetPath, options) => {
4182
5522
  const rootDir = path15.resolve(targetPath);
4183
- if (!fs17.existsSync(rootDir)) {
5523
+ if (!fs21.existsSync(rootDir)) {
4184
5524
  console.error(`Error: "${rootDir}" does not exist.`);
4185
5525
  process.exit(1);
4186
5526
  }
4187
- if (!fs17.statSync(rootDir).isDirectory()) {
5527
+ if (!fs21.statSync(rootDir).isDirectory()) {
4188
5528
  console.error(`Error: "${rootDir}" is not a directory.`);
4189
5529
  process.exit(1);
4190
5530
  }
@@ -4373,7 +5713,7 @@ Comparison failed: ${error instanceof Error ? error.message : String(error)}`);
4373
5713
  const frameworkScanner = new FrameworkScanner();
4374
5714
  const frameworkResult = await frameworkScanner.scan(rootDir);
4375
5715
  allFindings.push(...frameworkResult.findings);
4376
- const detectedLanguages = detectProjectLanguage(rootDir, fs17);
5716
+ const detectedLanguages = detectProjectLanguage(rootDir, fs21);
4377
5717
  if (detectedLanguages.includes("python")) {
4378
5718
  spinner.text = "Analyzing Python complexity...";
4379
5719
  const pyComplexityScanner = new PythonComplexityScanner();
@@ -4387,6 +5727,62 @@ Comparison failed: ${error instanceof Error ? error.message : String(error)}`);
4387
5727
  const pyImportsScanner = new PythonImportsScanner();
4388
5728
  const pyImportsResult = await pyImportsScanner.scan(rootDir);
4389
5729
  allFindings.push(...pyImportsResult.findings);
5730
+ spinner.text = "Checking Python docstrings...";
5731
+ const pyDocstrings = new PythonDocstringScanner();
5732
+ allFindings.push(...(await pyDocstrings.scan(rootDir)).findings);
5733
+ spinner.text = "Detecting Python code smells...";
5734
+ const pySmells = new PythonCodeSmellScanner();
5735
+ allFindings.push(...(await pySmells.scan(rootDir)).findings);
5736
+ spinner.text = "Scanning Python security...";
5737
+ const pySecurity = new PythonSecurityScanner();
5738
+ allFindings.push(...(await pySecurity.scan(rootDir)).findings);
5739
+ spinner.text = "Analyzing Python class design...";
5740
+ const pyDesign = new PythonClassDesignScanner();
5741
+ allFindings.push(...(await pyDesign.scan(rootDir)).findings);
5742
+ }
5743
+ if (detectedLanguages.includes("go")) {
5744
+ spinner.text = "Analyzing Go complexity...";
5745
+ const goComplexity = new GoComplexityScanner();
5746
+ allFindings.push(...(await goComplexity.scan(rootDir)).findings);
5747
+ spinner.text = "Checking Go error handling...";
5748
+ const goErrors = new GoErrorHandlingScanner();
5749
+ allFindings.push(...(await goErrors.scan(rootDir)).findings);
5750
+ spinner.text = "Checking Go conventions...";
5751
+ const goLint = new GoLintScanner();
5752
+ allFindings.push(...(await goLint.scan(rootDir)).findings);
5753
+ }
5754
+ if (detectedLanguages.includes("rust")) {
5755
+ spinner.text = "Analyzing Rust complexity...";
5756
+ const rustComplexity = new RustComplexityScanner();
5757
+ allFindings.push(...(await rustComplexity.scan(rootDir)).findings);
5758
+ spinner.text = "Checking Rust unsafe usage...";
5759
+ const rustUnsafe = new RustUnsafeScanner();
5760
+ allFindings.push(...(await rustUnsafe.scan(rootDir)).findings);
5761
+ spinner.text = "Running Rust clippy hints...";
5762
+ const rustClippy = new RustClippyHintsScanner();
5763
+ allFindings.push(...(await rustClippy.scan(rootDir)).findings);
5764
+ }
5765
+ if (detectedLanguages.includes("java")) {
5766
+ spinner.text = "Analyzing Java complexity...";
5767
+ const javaComplexity = new JavaComplexityScanner();
5768
+ allFindings.push(...(await javaComplexity.scan(rootDir)).findings);
5769
+ spinner.text = "Checking Java code smells...";
5770
+ const javaSmells = new JavaCodeSmellScanner();
5771
+ allFindings.push(...(await javaSmells.scan(rootDir)).findings);
5772
+ spinner.text = "Checking Java naming conventions...";
5773
+ const javaNaming = new JavaNamingScanner();
5774
+ allFindings.push(...(await javaNaming.scan(rootDir)).findings);
5775
+ }
5776
+ if (detectedLanguages.includes("csharp")) {
5777
+ spinner.text = "Analyzing C# complexity...";
5778
+ const csharpComplexity = new CSharpComplexityScanner();
5779
+ allFindings.push(...(await csharpComplexity.scan(rootDir)).findings);
5780
+ spinner.text = "Checking C# code smells...";
5781
+ const csharpSmells = new CSharpCodeSmellScanner();
5782
+ allFindings.push(...(await csharpSmells.scan(rootDir)).findings);
5783
+ spinner.text = "Checking C# async patterns...";
5784
+ const csharpAsync = new CSharpAsyncScanner();
5785
+ allFindings.push(...(await csharpAsync.scan(rootDir)).findings);
4390
5786
  }
4391
5787
  spinner.stop();
4392
5788
  const health = calculateHealth(allFindings);
@@ -4428,7 +5824,7 @@ Comparison failed: ${error instanceof Error ? error.message : String(error)}`);
4428
5824
  if (options.markdownFile) {
4429
5825
  try {
4430
5826
  const outputPath = validateOutputPath(rootDir, ".roast-report.md");
4431
- fs17.writeFileSync(outputPath, markdownOutput, "utf-8");
5827
+ fs21.writeFileSync(outputPath, markdownOutput, "utf-8");
4432
5828
  console.log(`
4433
5829
  \u2713 Markdown report saved to ${outputPath}
4434
5830
  `);
@@ -4454,7 +5850,7 @@ Comparison failed: ${error instanceof Error ? error.message : String(error)}`);
4454
5850
  const debugPath = path15.join(rootDir, ".roast-debug.log");
4455
5851
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4456
5852
  const errorStr = error instanceof Error ? error.stack : String(error);
4457
- fs17.appendFileSync(debugPath, `${timestamp}: ${errorStr}
5853
+ fs21.appendFileSync(debugPath, `${timestamp}: ${errorStr}
4458
5854
  `);
4459
5855
  console.log(`Debug info saved to ${debugPath}`);
4460
5856
  } catch {
@@ -4468,7 +5864,7 @@ Comparison failed: ${error instanceof Error ? error.message : String(error)}`);
4468
5864
  function getProjectName(rootDir) {
4469
5865
  const pkgPath = path15.join(rootDir, "package.json");
4470
5866
  try {
4471
- const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf-8"));
5867
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
4472
5868
  return pkg.name || path15.basename(rootDir);
4473
5869
  } catch {
4474
5870
  return path15.basename(rootDir);