aislop 0.9.3 → 0.9.4
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 +8 -2
- package/dist/cli.js +100 -3
- package/dist/index.js +101 -4
- package/dist/{json-BhO1Ufj3.js → json-CXiEvR_M.js} +1 -1
- package/dist/mcp.js +86 -1
- package/dist/{version-BNO_Lw7E.js → version-C45P3Q1N.js} +1 -1
- package/package.json +92 -91
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# aislop
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Catch the slop AI coding agents leave in your code.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/aislop)
|
|
6
6
|
[](https://www.npmjs.com/package/aislop)
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The patterns Claude Code, Cursor, Codex, and OpenCode leave behind: narrative comments above self-explanatory code, swallowed exceptions, `as any` casts, hallucinated imports, duplicated helpers, dead code, todo stubs, oversized functions. Tests pass. Lint passes. The code rots anyway.
|
|
13
|
+
|
|
14
|
+
aislop catches them. 40+ rules across 7 languages (TS/JS, Python, Go, Rust, Ruby, PHP, Java). Scores every change 0–100. Sub-second. Deterministic — no LLM in the runtime path, same code in, same score out. MIT-licensed, free CLI.
|
|
13
15
|
|
|
14
16
|
## Quick start
|
|
15
17
|
|
|
@@ -265,6 +267,10 @@ See the full [rules reference](docs/rules.md).
|
|
|
265
267
|
|
|
266
268
|
[Installation](docs/installation.md) · [Commands](docs/commands.md) · [Rules](docs/rules.md) · [Config](docs/configuration.md) · [Scoring](docs/scoring.md) · [CI/CD](docs/ci.md) · [Telemetry](docs/telemetry.md)
|
|
267
269
|
|
|
270
|
+
## Community
|
|
271
|
+
|
|
272
|
+
[Discussions](https://github.com/scanaislop/aislop/discussions) for questions, rule requests, and false-positive triage · [Issues](https://github.com/scanaislop/aislop/issues) for bugs
|
|
273
|
+
|
|
268
274
|
## Contributing
|
|
269
275
|
|
|
270
276
|
See [CONTRIBUTING.md](CONTRIBUTING.md). AI assistants: [AGENTS.md](AGENTS.md).
|
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/version.ts
|
|
37
|
-
const APP_VERSION = "0.9.
|
|
37
|
+
const APP_VERSION = "0.9.4";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -2763,6 +2763,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2763
2763
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2764
2764
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2765
2765
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2766
|
+
const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
|
|
2767
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2768
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2769
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2770
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2766
2771
|
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2767
2772
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2768
2773
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2815,6 +2820,13 @@ const pushFinding = (out, a) => {
|
|
|
2815
2820
|
fixable: false
|
|
2816
2821
|
});
|
|
2817
2822
|
};
|
|
2823
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2824
|
+
pushFinding(out, {
|
|
2825
|
+
relPath,
|
|
2826
|
+
line,
|
|
2827
|
+
...finding
|
|
2828
|
+
});
|
|
2829
|
+
};
|
|
2818
2830
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2819
2831
|
for (let i = 0; i < lines.length; i++) {
|
|
2820
2832
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2896,6 +2908,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2896
2908
|
});
|
|
2897
2909
|
}
|
|
2898
2910
|
};
|
|
2911
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
2912
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2913
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
2914
|
+
if (!match) continue;
|
|
2915
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2916
|
+
rule: "ai-slop/python-range-len-loop",
|
|
2917
|
+
severity: "info",
|
|
2918
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
2919
|
+
help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
2924
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2925
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
2926
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2927
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
2928
|
+
severity: "warning",
|
|
2929
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
2930
|
+
help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
2935
|
+
let count = 1;
|
|
2936
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
2937
|
+
const line = lines[i];
|
|
2938
|
+
const trimmed = line.trim();
|
|
2939
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2940
|
+
const match = pattern.exec(line);
|
|
2941
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
2942
|
+
count++;
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
2946
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
2947
|
+
}
|
|
2948
|
+
return count;
|
|
2949
|
+
};
|
|
2950
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
2951
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2952
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2953
|
+
if (reported.has(i)) continue;
|
|
2954
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
2955
|
+
if (valueMatch) {
|
|
2956
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
2957
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
2958
|
+
reported.add(i);
|
|
2959
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2960
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
2961
|
+
severity: "warning",
|
|
2962
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
2963
|
+
help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
2969
|
+
if (!instanceMatch) continue;
|
|
2970
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
2971
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
2972
|
+
reported.add(i);
|
|
2973
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2974
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
2975
|
+
severity: "warning",
|
|
2976
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
2977
|
+
help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
};
|
|
2899
2981
|
const detectPythonPatterns = async (context) => {
|
|
2900
2982
|
const diagnostics = [];
|
|
2901
2983
|
const files = getSourceFiles(context);
|
|
@@ -2915,6 +2997,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2915
2997
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2916
2998
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2917
2999
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
3000
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
3001
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
3002
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2918
3003
|
}
|
|
2919
3004
|
return diagnostics;
|
|
2920
3005
|
};
|
|
@@ -8717,6 +8802,10 @@ const RULE_LABELS = {
|
|
|
8717
8802
|
"ai-slop/python-broad-except": "Broad except",
|
|
8718
8803
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
8719
8804
|
"ai-slop/python-print-debug": "print() left in code",
|
|
8805
|
+
"ai-slop/python-range-len-loop": "range(len(...)) loop",
|
|
8806
|
+
"ai-slop/python-chained-dict-get": "Chained dict get",
|
|
8807
|
+
"ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
|
|
8808
|
+
"ai-slop/python-isinstance-ladder": "isinstance ladder",
|
|
8720
8809
|
"ai-slop/go-library-panic": "panic() in Go library code",
|
|
8721
8810
|
"ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
|
|
8722
8811
|
"ai-slop/rust-todo-stub": "Rust todo!() stub",
|
|
@@ -8820,6 +8909,9 @@ const renderSummary = (input, deps = {}) => {
|
|
|
8820
8909
|
}
|
|
8821
8910
|
return lines.join("\n");
|
|
8822
8911
|
};
|
|
8912
|
+
const renderStarCta = (deps = {}) => {
|
|
8913
|
+
return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
|
|
8914
|
+
};
|
|
8823
8915
|
const renderCleanRun = (input, deps = {}) => {
|
|
8824
8916
|
const t = deps.theme ?? theme;
|
|
8825
8917
|
const s = deps.symbols ?? symbols;
|
|
@@ -8889,11 +8981,12 @@ const buildScanRender = (input) => {
|
|
|
8889
8981
|
const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
|
|
8890
8982
|
const fixable = input.diagnostics.filter((d) => d.fixable).length;
|
|
8891
8983
|
const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
|
|
8984
|
+
const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
|
|
8892
8985
|
if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
|
|
8893
8986
|
score: input.score.score,
|
|
8894
8987
|
label: input.score.label,
|
|
8895
8988
|
elapsedMs: input.elapsedMs
|
|
8896
|
-
}, deps)}`;
|
|
8989
|
+
}, deps)}${starCta}`;
|
|
8897
8990
|
const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
|
|
8898
8991
|
const nextSteps = [];
|
|
8899
8992
|
if (fixable > 0) nextSteps.push({
|
|
@@ -8920,7 +9013,7 @@ const buildScanRender = (input) => {
|
|
|
8920
9013
|
nextSteps,
|
|
8921
9014
|
breakdown: computeBreakdown(input.diagnostics),
|
|
8922
9015
|
thresholds: input.thresholds
|
|
8923
|
-
}, deps)}`;
|
|
9016
|
+
}, deps)}${starCta}`;
|
|
8924
9017
|
};
|
|
8925
9018
|
const scanCommand = async (directory, config, options) => {
|
|
8926
9019
|
const resolvedDir = path.resolve(directory);
|
|
@@ -11459,6 +11552,10 @@ const BUILTIN_RULES = [
|
|
|
11459
11552
|
"ai-slop/python-broad-except",
|
|
11460
11553
|
"ai-slop/python-mutable-default",
|
|
11461
11554
|
"ai-slop/python-print-debug",
|
|
11555
|
+
"ai-slop/python-range-len-loop",
|
|
11556
|
+
"ai-slop/python-chained-dict-get",
|
|
11557
|
+
"ai-slop/python-repetitive-dispatch",
|
|
11558
|
+
"ai-slop/python-isinstance-ladder",
|
|
11462
11559
|
"ai-slop/go-library-panic",
|
|
11463
11560
|
"ai-slop/rust-non-test-unwrap",
|
|
11464
11561
|
"ai-slop/rust-todo-stub",
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-
|
|
1
|
+
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-C45P3Q1N.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
3
|
import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
|
|
4
4
|
import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
|
|
@@ -2931,6 +2931,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2931
2931
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2932
2932
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2933
2933
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2934
|
+
const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
|
|
2935
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2936
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2937
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2938
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2934
2939
|
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2935
2940
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2936
2941
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2983,6 +2988,13 @@ const pushFinding = (out, a) => {
|
|
|
2983
2988
|
fixable: false
|
|
2984
2989
|
});
|
|
2985
2990
|
};
|
|
2991
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2992
|
+
pushFinding(out, {
|
|
2993
|
+
relPath,
|
|
2994
|
+
line,
|
|
2995
|
+
...finding
|
|
2996
|
+
});
|
|
2997
|
+
};
|
|
2986
2998
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2987
2999
|
for (let i = 0; i < lines.length; i++) {
|
|
2988
3000
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -3064,6 +3076,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
3064
3076
|
});
|
|
3065
3077
|
}
|
|
3066
3078
|
};
|
|
3079
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
3080
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3081
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
3082
|
+
if (!match) continue;
|
|
3083
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3084
|
+
rule: "ai-slop/python-range-len-loop",
|
|
3085
|
+
severity: "info",
|
|
3086
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
3087
|
+
help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
3092
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3093
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
3094
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3095
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
3096
|
+
severity: "warning",
|
|
3097
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
3098
|
+
help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
3103
|
+
let count = 1;
|
|
3104
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
3105
|
+
const line = lines[i];
|
|
3106
|
+
const trimmed = line.trim();
|
|
3107
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
3108
|
+
const match = pattern.exec(line);
|
|
3109
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
3110
|
+
count++;
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
3113
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
3114
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
3115
|
+
}
|
|
3116
|
+
return count;
|
|
3117
|
+
};
|
|
3118
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
3119
|
+
const reported = /* @__PURE__ */ new Set();
|
|
3120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3121
|
+
if (reported.has(i)) continue;
|
|
3122
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
3123
|
+
if (valueMatch) {
|
|
3124
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
3125
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
3126
|
+
reported.add(i);
|
|
3127
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3128
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
3129
|
+
severity: "warning",
|
|
3130
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
3131
|
+
help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
3137
|
+
if (!instanceMatch) continue;
|
|
3138
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
3139
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
3140
|
+
reported.add(i);
|
|
3141
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3142
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
3143
|
+
severity: "warning",
|
|
3144
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
3145
|
+
help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
};
|
|
3067
3149
|
const detectPythonPatterns = async (context) => {
|
|
3068
3150
|
const diagnostics = [];
|
|
3069
3151
|
const files = getSourceFiles(context);
|
|
@@ -3083,6 +3165,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
3083
3165
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
3084
3166
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
3085
3167
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
3168
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
3169
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
3170
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
3086
3171
|
}
|
|
3087
3172
|
return diagnostics;
|
|
3088
3173
|
};
|
|
@@ -7170,6 +7255,10 @@ const RULE_LABELS = {
|
|
|
7170
7255
|
"ai-slop/python-broad-except": "Broad except",
|
|
7171
7256
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
7172
7257
|
"ai-slop/python-print-debug": "print() left in code",
|
|
7258
|
+
"ai-slop/python-range-len-loop": "range(len(...)) loop",
|
|
7259
|
+
"ai-slop/python-chained-dict-get": "Chained dict get",
|
|
7260
|
+
"ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
|
|
7261
|
+
"ai-slop/python-isinstance-ladder": "isinstance ladder",
|
|
7173
7262
|
"ai-slop/go-library-panic": "panic() in Go library code",
|
|
7174
7263
|
"ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
|
|
7175
7264
|
"ai-slop/rust-todo-stub": "Rust todo!() stub",
|
|
@@ -7273,6 +7362,9 @@ const renderSummary = (input, deps = {}) => {
|
|
|
7273
7362
|
}
|
|
7274
7363
|
return lines.join("\n");
|
|
7275
7364
|
};
|
|
7365
|
+
const renderStarCta = (deps = {}) => {
|
|
7366
|
+
return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
|
|
7367
|
+
};
|
|
7276
7368
|
const renderCleanRun = (input, deps = {}) => {
|
|
7277
7369
|
const t = deps.theme ?? theme;
|
|
7278
7370
|
const s = deps.symbols ?? symbols;
|
|
@@ -7388,11 +7480,12 @@ const buildScanRender = (input) => {
|
|
|
7388
7480
|
const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
|
|
7389
7481
|
const fixable = input.diagnostics.filter((d) => d.fixable).length;
|
|
7390
7482
|
const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
|
|
7483
|
+
const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
|
|
7391
7484
|
if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
|
|
7392
7485
|
score: input.score.score,
|
|
7393
7486
|
label: input.score.label,
|
|
7394
7487
|
elapsedMs: input.elapsedMs
|
|
7395
|
-
}, deps)}`;
|
|
7488
|
+
}, deps)}${starCta}`;
|
|
7396
7489
|
const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
|
|
7397
7490
|
const nextSteps = [];
|
|
7398
7491
|
if (fixable > 0) nextSteps.push({
|
|
@@ -7419,7 +7512,7 @@ const buildScanRender = (input) => {
|
|
|
7419
7512
|
nextSteps,
|
|
7420
7513
|
breakdown: computeBreakdown(input.diagnostics),
|
|
7421
7514
|
thresholds: input.thresholds
|
|
7422
|
-
}, deps)}`;
|
|
7515
|
+
}, deps)}${starCta}`;
|
|
7423
7516
|
};
|
|
7424
7517
|
const scanCommand = async (directory, config, options) => {
|
|
7425
7518
|
const resolvedDir = path.resolve(directory);
|
|
@@ -7530,7 +7623,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
7530
7623
|
engineTimings
|
|
7531
7624
|
};
|
|
7532
7625
|
if (options.json) {
|
|
7533
|
-
const { buildJsonOutput } = await import("./json-
|
|
7626
|
+
const { buildJsonOutput } = await import("./json-CXiEvR_M.js");
|
|
7534
7627
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
7535
7628
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
7536
7629
|
return completion;
|
|
@@ -9355,6 +9448,10 @@ const BUILTIN_RULES = [
|
|
|
9355
9448
|
"ai-slop/python-broad-except",
|
|
9356
9449
|
"ai-slop/python-mutable-default",
|
|
9357
9450
|
"ai-slop/python-print-debug",
|
|
9451
|
+
"ai-slop/python-range-len-loop",
|
|
9452
|
+
"ai-slop/python-chained-dict-get",
|
|
9453
|
+
"ai-slop/python-repetitive-dispatch",
|
|
9454
|
+
"ai-slop/python-isinstance-ladder",
|
|
9358
9455
|
"ai-slop/go-library-panic",
|
|
9359
9456
|
"ai-slop/rust-non-test-unwrap",
|
|
9360
9457
|
"ai-slop/rust-todo-stub",
|
package/dist/mcp.js
CHANGED
|
@@ -2190,6 +2190,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2190
2190
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2191
2191
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2192
2192
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2193
|
+
const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
|
|
2194
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2195
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2196
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2197
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2193
2198
|
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2194
2199
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2195
2200
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2242,6 +2247,13 @@ const pushFinding = (out, a) => {
|
|
|
2242
2247
|
fixable: false
|
|
2243
2248
|
});
|
|
2244
2249
|
};
|
|
2250
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2251
|
+
pushFinding(out, {
|
|
2252
|
+
relPath,
|
|
2253
|
+
line,
|
|
2254
|
+
...finding
|
|
2255
|
+
});
|
|
2256
|
+
};
|
|
2245
2257
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2246
2258
|
for (let i = 0; i < lines.length; i++) {
|
|
2247
2259
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2323,6 +2335,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2323
2335
|
});
|
|
2324
2336
|
}
|
|
2325
2337
|
};
|
|
2338
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
2339
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2340
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
2341
|
+
if (!match) continue;
|
|
2342
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2343
|
+
rule: "ai-slop/python-range-len-loop",
|
|
2344
|
+
severity: "info",
|
|
2345
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
2346
|
+
help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
2351
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2352
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
2353
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2354
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
2355
|
+
severity: "warning",
|
|
2356
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
2357
|
+
help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
2362
|
+
let count = 1;
|
|
2363
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
2364
|
+
const line = lines[i];
|
|
2365
|
+
const trimmed = line.trim();
|
|
2366
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2367
|
+
const match = pattern.exec(line);
|
|
2368
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
2369
|
+
count++;
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
2373
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
2374
|
+
}
|
|
2375
|
+
return count;
|
|
2376
|
+
};
|
|
2377
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
2378
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2379
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2380
|
+
if (reported.has(i)) continue;
|
|
2381
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
2382
|
+
if (valueMatch) {
|
|
2383
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
2384
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
2385
|
+
reported.add(i);
|
|
2386
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2387
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
2388
|
+
severity: "warning",
|
|
2389
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
2390
|
+
help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
2396
|
+
if (!instanceMatch) continue;
|
|
2397
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
2398
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
2399
|
+
reported.add(i);
|
|
2400
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2401
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
2402
|
+
severity: "warning",
|
|
2403
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
2404
|
+
help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2326
2408
|
const detectPythonPatterns = async (context) => {
|
|
2327
2409
|
const diagnostics = [];
|
|
2328
2410
|
const files = getSourceFiles(context);
|
|
@@ -2342,6 +2424,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2342
2424
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2343
2425
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2344
2426
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
2427
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
2428
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
2429
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2345
2430
|
}
|
|
2346
2431
|
return diagnostics;
|
|
2347
2432
|
};
|
|
@@ -5757,7 +5842,7 @@ const handleAislopBaseline = (input) => {
|
|
|
5757
5842
|
|
|
5758
5843
|
//#endregion
|
|
5759
5844
|
//#region src/version.ts
|
|
5760
|
-
const APP_VERSION = "0.9.
|
|
5845
|
+
const APP_VERSION = "0.9.4";
|
|
5761
5846
|
|
|
5762
5847
|
//#endregion
|
|
5763
5848
|
//#region src/telemetry/env.ts
|
|
@@ -29,7 +29,7 @@ const getEngineLabel = (engine) => ENGINE_INFO[engine].label;
|
|
|
29
29
|
|
|
30
30
|
//#endregion
|
|
31
31
|
//#region src/version.ts
|
|
32
|
-
const APP_VERSION = "0.9.
|
|
32
|
+
const APP_VERSION = "0.9.4";
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
export { ENGINE_INFO as n, getEngineLabel as r, APP_VERSION as t };
|
package/package.json
CHANGED
|
@@ -1,93 +1,94 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
2
|
+
"name": "aislop",
|
|
3
|
+
"version": "0.9.4",
|
|
4
|
+
"description": "The engineering standards layer and quality gate for AI-written code. Define your standard once. Every agent — Claude Code, Cursor, Codex — is held to it automatically, on every edit and every PR. Catches the slop they leave behind, enforces the rules your team sets. 8+ languages. Deterministic.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aislop": "./dist/cli.js",
|
|
8
|
+
"aislop-mcp": "./dist/mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"scripts"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "tsdown --watch",
|
|
22
|
+
"build": "rm -rf dist && NODE_ENV=production tsdown",
|
|
23
|
+
"postinstall": "node scripts/postinstall-tools.mjs",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "pnpm build && vitest run",
|
|
26
|
+
"scan": "pnpm build && node dist/cli.js scan .",
|
|
27
|
+
"scan:exclude": "pnpm build && node dist/cli.js scan . --exclude .idea --exclude .gitnore --exclude node_modules",
|
|
28
|
+
"scan:include": "pnpm build && node dist/cli.js scan . --include src --include tests",
|
|
29
|
+
"scan:json": "pnpm build && node dist/cli.js scan . --json",
|
|
30
|
+
"quality": "pnpm typecheck && pnpm test && node dist/cli.js scan . --json"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"aislop",
|
|
34
|
+
"ai-slop",
|
|
35
|
+
"code-quality",
|
|
36
|
+
"linter",
|
|
37
|
+
"formatter",
|
|
38
|
+
"cli",
|
|
39
|
+
"ai",
|
|
40
|
+
"copilot",
|
|
41
|
+
"code-review",
|
|
42
|
+
"static-analysis",
|
|
43
|
+
"typescript",
|
|
44
|
+
"javascript",
|
|
45
|
+
"python",
|
|
46
|
+
"go",
|
|
47
|
+
"rust",
|
|
48
|
+
"ruby",
|
|
49
|
+
"php"
|
|
50
|
+
],
|
|
51
|
+
"author": "heavykenny",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"homepage": "https://github.com/scanaislop/aislop#readme",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/scanaislop/aislop.git"
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/scanaislop/aislop/issues"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
},
|
|
64
|
+
"packageManager": "pnpm@10.28.0",
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@biomejs/biome": "^2.4.5",
|
|
67
|
+
"@clack/prompts": "^1.2.0",
|
|
68
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
69
|
+
"adm-zip": "^0.5.16",
|
|
70
|
+
"commander": "^14.0.3",
|
|
71
|
+
"expo-doctor": "^1.18.10",
|
|
72
|
+
"knip": "^5.85.0",
|
|
73
|
+
"micromatch": "^4.0.8",
|
|
74
|
+
"oxlint": "^1.51.0",
|
|
75
|
+
"picocolors": "^1.1.1",
|
|
76
|
+
"tar": "^7.5.11",
|
|
77
|
+
"typescript": "^5.9.3",
|
|
78
|
+
"wcwidth": "^1.0.1",
|
|
79
|
+
"yaml": "^2.8.2",
|
|
80
|
+
"zod": "^4.3.6"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@types/micromatch": "^4.0.10",
|
|
84
|
+
"@types/node": "^25.6.0",
|
|
85
|
+
"tsdown": "^0.20.3",
|
|
86
|
+
"vitest": "^4.0.18"
|
|
87
|
+
},
|
|
88
|
+
"pnpm": {
|
|
89
|
+
"overrides": {
|
|
90
|
+
"postcss@<8.5.10": "^8.5.10",
|
|
91
|
+
"qs@>=6.11.1 <=6.15.1": "^6.15.2"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
93
94
|
}
|