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.
- package/README.md +20 -13
- package/dist/index.js +1572 -176
- 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
|
|
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
|
-
(
|
|
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/
|
|
1776
|
-
import
|
|
1777
|
-
import
|
|
1778
|
-
var
|
|
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 = "
|
|
2931
|
+
this.name = "csharp-complexity";
|
|
1781
2932
|
}
|
|
1782
2933
|
async scan(rootDir) {
|
|
1783
2934
|
const findings = [];
|
|
1784
|
-
const files = await
|
|
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 =
|
|
2942
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
1792
2943
|
const rel = relativePath(rootDir, file);
|
|
1793
|
-
const
|
|
1794
|
-
for (const
|
|
1795
|
-
const complexity = this.calculateComplexity(
|
|
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: `
|
|
2949
|
+
id: `csharp-complexity-${rel}-${method.name}`,
|
|
1799
2950
|
severity: "critical",
|
|
1800
2951
|
category: "complexity",
|
|
1801
|
-
message: `
|
|
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: `
|
|
2958
|
+
id: `csharp-complexity-${rel}-${method.name}`,
|
|
1808
2959
|
severity: "warning",
|
|
1809
2960
|
category: "complexity",
|
|
1810
|
-
message: `
|
|
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
|
-
|
|
1822
|
-
const
|
|
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
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
2997
|
+
if (started && braceCount <= 0) break;
|
|
1842
2998
|
}
|
|
1843
|
-
|
|
2999
|
+
methods.push({ name, body });
|
|
1844
3000
|
}
|
|
1845
3001
|
}
|
|
1846
|
-
return
|
|
3002
|
+
return methods;
|
|
1847
3003
|
}
|
|
1848
3004
|
calculateComplexity(code) {
|
|
1849
3005
|
let complexity = 1;
|
|
1850
3006
|
const patterns = [
|
|
1851
|
-
/\bif\
|
|
1852
|
-
/\
|
|
1853
|
-
/\bfor\
|
|
1854
|
-
/\
|
|
1855
|
-
/\
|
|
1856
|
-
/\
|
|
1857
|
-
/\
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
|
3026
|
+
var CSharpCodeSmellScanner = class {
|
|
1872
3027
|
constructor() {
|
|
1873
|
-
this.name = "
|
|
3028
|
+
this.name = "csharp-code-smells";
|
|
1874
3029
|
}
|
|
1875
3030
|
async scan(rootDir) {
|
|
1876
3031
|
const findings = [];
|
|
1877
|
-
const files = await
|
|
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 =
|
|
3039
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
1887
3040
|
const rel = relativePath(rootDir, file);
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
-
|
|
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
|
|
3105
|
+
var CSharpAsyncScanner = class {
|
|
1927
3106
|
constructor() {
|
|
1928
|
-
this.name = "
|
|
3107
|
+
this.name = "csharp-async";
|
|
1929
3108
|
}
|
|
1930
3109
|
async scan(rootDir) {
|
|
1931
3110
|
const findings = [];
|
|
1932
|
-
const files = await
|
|
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 =
|
|
3118
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
1940
3119
|
const rel = relativePath(rootDir, file);
|
|
1941
|
-
const
|
|
1942
|
-
if (
|
|
3120
|
+
const asyncVoid = content.match(/\basync\s+void\s+\w+/g);
|
|
3121
|
+
if (asyncVoid && asyncVoid.length > 0) {
|
|
1943
3122
|
findings.push({
|
|
1944
|
-
id: `
|
|
3123
|
+
id: `csharp-async-void-${rel}`,
|
|
1945
3124
|
severity: "warning",
|
|
1946
|
-
category: "
|
|
1947
|
-
message: `${rel}
|
|
3125
|
+
category: "csharp-async",
|
|
3126
|
+
message: `${rel} has ${asyncVoid.length} async void method(s)`,
|
|
1948
3127
|
file: rel,
|
|
1949
|
-
detail: "
|
|
3128
|
+
detail: "async void cannot be awaited and exceptions are unobservable. Use async Task instead."
|
|
1950
3129
|
});
|
|
1951
3130
|
}
|
|
1952
|
-
const
|
|
1953
|
-
if (
|
|
3131
|
+
const syncOverAsync = content.match(/\.(?:Result|Wait\(\))/g);
|
|
3132
|
+
if (syncOverAsync && syncOverAsync.length >= 3) {
|
|
1954
3133
|
findings.push({
|
|
1955
|
-
id: `
|
|
1956
|
-
severity: "
|
|
1957
|
-
category: "
|
|
1958
|
-
message: `${rel}
|
|
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: "
|
|
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,
|
|
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 (
|
|
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
|
|
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 (!
|
|
3435
|
+
if (!fs15.existsSync(cacheFile)) {
|
|
2257
3436
|
return null;
|
|
2258
3437
|
}
|
|
2259
3438
|
try {
|
|
2260
|
-
const content =
|
|
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 (
|
|
2277
|
-
const content =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
4482
|
+
if (fs17.existsSync(worktreePath)) {
|
|
3143
4483
|
try {
|
|
3144
|
-
|
|
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
|
|
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 (!
|
|
4683
|
+
if (!fs18.existsSync(configPath)) {
|
|
3344
4684
|
return DEFAULT_CONFIG;
|
|
3345
4685
|
}
|
|
3346
4686
|
try {
|
|
3347
|
-
const content =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
5174
|
+
if (!fs20.existsSync(historyPath)) {
|
|
3835
5175
|
return null;
|
|
3836
5176
|
}
|
|
3837
5177
|
try {
|
|
3838
|
-
const content =
|
|
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
|
-
|
|
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 (
|
|
4168
|
-
const pkg = JSON.parse(
|
|
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 (!
|
|
5523
|
+
if (!fs21.existsSync(rootDir)) {
|
|
4184
5524
|
console.error(`Error: "${rootDir}" does not exist.`);
|
|
4185
5525
|
process.exit(1);
|
|
4186
5526
|
}
|
|
4187
|
-
if (!
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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);
|