slopfighter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — initial release
4
+
5
+ First public release. 18 rules across 4 categories, scored 0–100 (A–F).
6
+
7
+ ### Rules
8
+ - **Bloat**: `padding-comments`, `excessive-jsdoc`, `commented-out-code`
9
+ - **Wasted runtime**: `unnecessary-async`, `redundant-await`, `useless-try-catch`, `redundant-error-rethrow`, `redundant-boolean-cast`, `trivial-arrow-wrapper`, `return-undefined`, `const-string-concat`
10
+ - **Premature abstraction**: `future-proof-naming`, `single-method-class`
11
+ - **Type laziness / dead code**: `explicit-any`, `dead-imports`, `over-defensive-null-check`, `always-true-conditional`, `else-after-return`
12
+
13
+ ### CLI
14
+ - `slopfighter scan [path]` — human or JSON report
15
+ - `slopfighter score [path]` — number only, CI-friendly
16
+ - `slopfighter fix [path] --safe` — auto-fix unambiguous findings
17
+ - `slopfighter install` — drop a skill into Claude Code / Cursor / AGENTS.md
18
+
19
+ ### Tested
20
+ - 103 unit + integration tests
21
+ - Validated against 30 trending TypeScript repos created Feb–May 2026
22
+ - Cross-platform: Linux, macOS, Windows; Node 18/20/22
23
+
24
+ ### Known limitations
25
+ - JS/TS only (Python, Go, Rust on the roadmap via tree-sitter)
26
+ - No type-aware mode — `--strict` flag is reserved for future opt-in type-checker path
27
+ - Some rules use heuristics, not full semantic analysis; FP-rate ~5–15% per rule
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 slopfighter contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # slopfighter
2
+
3
+ [![ci](https://github.com/wemdio2/slopfighter/actions/workflows/ci.yml/badge.svg)](https://github.com/wemdio2/slopfighter/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/slopfighter.svg)](https://www.npmjs.com/package/slopfighter)
5
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **Anti-AI-slop refactor tool.** Detects the 18 structural bloat patterns LLMs love to generate. JS/TS, zero config, no API keys, no telemetry, runs locally in seconds.
8
+
9
+ ```
10
+ npx slopfighter scan .
11
+ ```
12
+
13
+ ```
14
+ src/foo.ts
15
+ 42:10 warn Comment "Get user name" just restates "getUserName" (padding-comments) [fixable]
16
+ 56:1 warn import "format" is never used in this file (dead-imports)
17
+ 88:14 info drop the explicit `undefined` (return-undefined) [fixable]
18
+ ...
19
+
20
+ slop score: 47/100 (D) [█████████░░░░░░░░░░░]
21
+ errors: 0 warns: 9 infos: 4
22
+ ```
23
+
24
+ ## Why this exists
25
+
26
+ We scanned **29 trending TypeScript repos** created between February and May 2026 — 32,473 files, 79,733 findings, 15 minutes.
27
+
28
+ - **Average score: 39/100 (F).** 17 of 29 repos failed.
29
+ - **78% of all slop is three patterns:** commented-out code blocks (31.8%), `async` functions with no `await` (25.5%), explicit `any` (21.0%).
30
+ - Worst offender: a 54k-star Claude orchestration repo at **4.93 findings per file**.
31
+ - Cleanest: a markdown-based skill repo with **0**.
32
+
33
+ Full report → [`bench/report.md`](bench/report.md) · Interactive visualization → [`bench/index.html`](bench/index.html) (open locally or enable GitHub Pages → `https://wemdio2.github.io/slopfighter/bench/`). Reproducible: `node bench/run.mjs && node bench/visualize.mjs`.
34
+
35
+ ## Install
36
+
37
+ ### As a CLI
38
+
39
+ ```bash
40
+ npx slopfighter scan . # scan current dir
41
+ npx slopfighter scan src/foo.ts # scan one file
42
+ npx slopfighter score . --json # CI-friendly score only
43
+ npx slopfighter fix . --safe # apply unambiguous auto-fixes
44
+ ```
45
+
46
+ Exit code 0 if `score >= 60`, else 1 — so `npx slopfighter score .` works as a CI gate.
47
+
48
+ ### As an AI agent skill
49
+
50
+ ```bash
51
+ npx slopfighter install # auto-detect .claude/.cursor/AGENTS.md, install where present
52
+ npx slopfighter install --force # install into all targets
53
+ npx slopfighter install --only claude # only Claude Code skill
54
+ ```
55
+
56
+ This drops a `SKILL.md` into `.claude/skills/slopfighter/`, a slash command into `.cursor/commands/slopfighter.md`, and a guarded section into `AGENTS.md`. After install, your agent runs slopfighter automatically after each edit batch and surfaces top findings.
57
+
58
+ ### As a GitHub Action
59
+
60
+ Copy [`.github/workflows/slopfighter.yml`](.github/workflows/slopfighter.yml) into your repo. On every PR, a bot will comment with the score and top issues. Free for public repos.
61
+
62
+ ## The rules
63
+
64
+ | Rule | Severity | Catches |
65
+ |---|---|---|
66
+ | `padding-comments` | warn | `// Get user name` over `getUserName()` |
67
+ | `useless-try-catch` | warn | `catch (e) { throw e }` or log-then-rethrow |
68
+ | `dead-imports` | warn | Imported names never referenced |
69
+ | `explicit-any` | warn | `: any` annotations |
70
+ | `future-proof-naming` | warn | `XManager`/`XHelper`/`XProvider` class with one method |
71
+ | `commented-out-code` | warn | 2+ consecutive lines of commented code |
72
+ | `unnecessary-async` | info | `async` function with no `await` |
73
+ | `trivial-arrow-wrapper` | info | `(x) => fn(x)` instead of `fn` |
74
+ | `redundant-await` | info | `return await x` outside try/catch |
75
+ | `return-undefined` | info | `return undefined` (= `return`) |
76
+ | `redundant-error-rethrow` | warn | `throw new Error(err.message)` (strips stack) |
77
+ | `excessive-jsdoc` | info | 6+ line JSDoc over a 1–3 line body |
78
+ | `over-defensive-null-check` | info | `x !== null && x !== undefined && x` |
79
+ | `always-true-conditional` | warn | `if (true)` / `if (x === x)` |
80
+ | `single-method-class` | info | Class with one `this`-free method |
81
+ | `else-after-return` | info | `else` after a returning `if` |
82
+ | `redundant-boolean-cast` | info | `!!x` / `Boolean(x)` in boolean context |
83
+ | `const-string-concat` | info | `"a" + "b"` (merge or use a template) |
84
+
85
+ Customize: `--rules a,b,c` (run only listed), `--ignore <substr>` (skip matching paths, repeatable).
86
+
87
+ ## What this is NOT
88
+
89
+ - Not a security scanner. Use Semgrep/Snyk for that.
90
+ - Not a type checker. Use `tsc --noEmit`.
91
+ - Not a formatter. Use Prettier/Biome.
92
+ - Not a semantic analyzer. It won't tell you your code is wrong, only that it's bloated.
93
+
94
+ The rules are heuristic. False-positive rate is ~5–15% per rule depending on pattern. The point isn't perfection — it's reducing the structural noise that AI-generated code accumulates so reviewers can focus on logic.
95
+
96
+ ## How it works (under the hood)
97
+
98
+ We parse files with TypeScript's compiler API into an AST, then walk the tree applying 18 rule functions. A few rules (e.g. `commented-out-code`) are line-based; the rest are structural. No type checker is loaded, which keeps it ~10–100× faster than type-aware linters at the cost of some precision.
99
+
100
+ Score: `100 * exp(-perFilePenalty / 3)`, where penalty weights `error: 5, warn: 2, info: 1`. Per-file normalization means a clean 2000-file repo isn't punished by volume.
101
+
102
+ ## Bench
103
+
104
+ ```bash
105
+ git clone <this-repo> && cd slopfighter && npm install
106
+ node bench/run.mjs
107
+ cat bench/report.md
108
+ ```
109
+
110
+ Takes 15–30 min cold (clones 30 repos), 30 sec cached. Skip individual repos by editing `bench/repos.txt`.
111
+
112
+ ## Contributing
113
+
114
+ PRs welcome — especially new rules. Each rule lives in `src/rules/<id>.mjs` and exports `{ id, severity, description, check(ctx) }`. Add 2+ positive and 2+ negative test cases to `test/unit-rules.mjs`. The full test suite is `npm test`.
115
+
116
+ Rule ideas we'd love but haven't built yet: `bloated-tests` (mocks without assertions), `magic-numbers`, `nested-ternary-pyramid`, Python/Go via tree-sitter, type-aware mode (`--strict`).
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../src/cli.mjs';
3
+ runCli(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "slopfighter",
3
+ "version": "0.1.0",
4
+ "description": "Anti-AI-slop refactor tool. Detects the structural bloat patterns LLMs love to generate — padding comments, useless try/catch, dead imports, explicit any, commented-out code, unnecessary async, and 12 more.",
5
+ "type": "module",
6
+ "bin": {
7
+ "slopfighter": "./bin/slopfighter.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "templates",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "test": "node test/run.mjs",
19
+ "scan": "node bin/slopfighter.mjs scan",
20
+ "bench": "node bench/run.mjs"
21
+ },
22
+ "keywords": [
23
+ "ai",
24
+ "slop",
25
+ "linter",
26
+ "claude-code",
27
+ "cursor",
28
+ "code-quality",
29
+ "typescript",
30
+ "javascript",
31
+ "vibe-coding",
32
+ "ai-coding",
33
+ "agent-skill",
34
+ "refactor"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/wemdio2/slopfighter.git"
39
+ },
40
+ "homepage": "https://github.com/wemdio2/slopfighter",
41
+ "bugs": {
42
+ "url": "https://github.com/wemdio2/slopfighter/issues"
43
+ },
44
+ "author": "",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "typescript": "^5.4.0",
48
+ "picocolors": "^1.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,139 @@
1
+ import path from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { scanPath } from './scanner.mjs';
4
+ import { renderReport, renderScore } from './reporter.mjs';
5
+ import { computeScore } from './score.mjs';
6
+ import { applyFixes } from './fixer.mjs';
7
+ import { runInstall } from './installer.mjs';
8
+
9
+ const VERSION = '0.1.0';
10
+
11
+ const HELP = `slopfighter v${VERSION} — anti-AI-slop refactor tool
12
+
13
+ Usage:
14
+ slopfighter scan [path] Scan files and report findings
15
+ slopfighter score [path] Print only the slop score (0-100)
16
+ slopfighter fix [path] --safe Apply safe auto-fixes
17
+ slopfighter install [root] Install the skill into .claude / .cursor / AGENTS.md
18
+ slopfighter --help Show this help
19
+ slopfighter --version Show version
20
+
21
+ Options:
22
+ --json Output JSON instead of human report
23
+ --strict Enable noisier rules
24
+ --rules a,b,c Run only these rules
25
+ --ignore pattern Skip files matching glob (repeatable)
26
+ --only a,b,c install only into these targets (claude,cursor,agents)
27
+ --skip a,b,c install skips these targets
28
+ --force install into all targets even if not auto-detected
29
+
30
+ Examples:
31
+ slopfighter scan ./src
32
+ slopfighter score . --json
33
+ slopfighter fix ./src --safe
34
+ slopfighter install # detects .claude/.cursor/AGENTS.md and writes
35
+ slopfighter install --force # writes to every target
36
+ `;
37
+
38
+ export async function runCli(argv) {
39
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
40
+ process.stdout.write(HELP);
41
+ return;
42
+ }
43
+ if (argv.includes('--version') || argv.includes('-v')) {
44
+ process.stdout.write(`slopfighter ${VERSION}\n`);
45
+ return;
46
+ }
47
+
48
+ const command = argv[0];
49
+ const rest = argv.slice(1);
50
+ const positional = rest.filter((a) => !a.startsWith('--'));
51
+ const target = path.resolve(positional[0] || '.');
52
+ const flags = parseFlags(rest);
53
+
54
+ if (command === 'scan') {
55
+ await cmdScan(target, flags);
56
+ } else if (command === 'score') {
57
+ await cmdScore(target, flags);
58
+ } else if (command === 'fix') {
59
+ await cmdFix(target, flags);
60
+ } else if (command === 'install') {
61
+ await cmdInstall(target, flags);
62
+ } else {
63
+ process.stderr.write(pc.red(`Unknown command: ${command}\n\n`));
64
+ process.stdout.write(HELP);
65
+ process.exitCode = 1;
66
+ }
67
+ }
68
+
69
+ function parseFlags(rest) {
70
+ const flags = {
71
+ json: false, strict: false, safe: false, rules: null, ignore: [],
72
+ only: null, skip: null, force: false,
73
+ };
74
+ for (let i = 0; i < rest.length; i++) {
75
+ const a = rest[i];
76
+ if (a === '--json') flags.json = true;
77
+ else if (a === '--strict') flags.strict = true;
78
+ else if (a === '--safe') flags.safe = true;
79
+ else if (a === '--force') flags.force = true;
80
+ else if (a === '--rules') flags.rules = (rest[++i] || '').split(',').filter(Boolean);
81
+ else if (a === '--ignore') flags.ignore.push(rest[++i] || '');
82
+ else if (a === '--only') flags.only = rest[++i] || null;
83
+ else if (a === '--skip') flags.skip = rest[++i] || null;
84
+ }
85
+ return flags;
86
+ }
87
+
88
+ async function cmdScan(target, flags) {
89
+ const findings = await scanPath(target, flags);
90
+ const score = computeScore(findings);
91
+ if (flags.json) {
92
+ process.stdout.write(JSON.stringify({ score, findings }, null, 2) + '\n');
93
+ } else {
94
+ process.stdout.write(renderReport(findings, score, target));
95
+ }
96
+ process.exitCode = score.value < 60 ? 1 : 0;
97
+ }
98
+
99
+ async function cmdScore(target, flags) {
100
+ const findings = await scanPath(target, flags);
101
+ const score = computeScore(findings);
102
+ if (flags.json) {
103
+ process.stdout.write(JSON.stringify(score, null, 2) + '\n');
104
+ } else {
105
+ process.stdout.write(renderScore(score) + '\n');
106
+ }
107
+ }
108
+
109
+ async function cmdInstall(root, flags) {
110
+ const result = await runInstall(root, {
111
+ only: flags.only,
112
+ skip: flags.skip,
113
+ force: flags.force,
114
+ });
115
+ if (result.message) {
116
+ process.stdout.write(pc.yellow(result.message) + '\n');
117
+ if (!result.installed?.length) process.exitCode = 1;
118
+ return;
119
+ }
120
+ process.stdout.write(`${pc.green('Installed slopfighter skill')} into:\n`);
121
+ for (const i of result.installed) {
122
+ const rel = path.relative(process.cwd(), i.file) || i.file;
123
+ process.stdout.write(` ${pc.dim('•')} ${i.label.padEnd(12)} ${pc.cyan(rel)}\n`);
124
+ }
125
+ }
126
+
127
+ async function cmdFix(target, flags) {
128
+ if (!flags.safe) {
129
+ process.stderr.write(pc.yellow('Refusing to fix without --safe (auto-fix is opt-in).\n'));
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ const findings = await scanPath(target, flags);
134
+ const result = await applyFixes(findings);
135
+ process.stdout.write(
136
+ `${pc.green('Fixed')} ${result.fixed} issue(s) in ${result.files} file(s). ` +
137
+ `${pc.dim(`${result.skipped} unsafe issue(s) left for manual review.`)}\n`
138
+ );
139
+ }
package/src/fixer.mjs ADDED
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+
3
+ export async function applyFixes(findings) {
4
+ const byFile = new Map();
5
+ let skipped = 0;
6
+ for (const f of findings) {
7
+ if (!f.fixable || !f.fix) { skipped++; continue; }
8
+ if (!byFile.has(f.file)) byFile.set(f.file, []);
9
+ byFile.get(f.file).push(f);
10
+ }
11
+
12
+ let fixed = 0;
13
+ for (const [file, items] of byFile) {
14
+ const source = fs.readFileSync(file, 'utf8');
15
+ // apply from bottom to top to keep offsets stable
16
+ const sorted = items
17
+ .filter((i) => i.fix && Number.isInteger(i.fix.start) && Number.isInteger(i.fix.end))
18
+ .sort((a, b) => b.fix.start - a.fix.start);
19
+ let out = source;
20
+ for (const item of sorted) {
21
+ out = out.slice(0, item.fix.start) + (item.fix.replacement || '') + out.slice(item.fix.end);
22
+ fixed++;
23
+ }
24
+ if (out !== source) fs.writeFileSync(file, out, 'utf8');
25
+ }
26
+
27
+ return { fixed, files: byFile.size, skipped };
28
+ }
@@ -0,0 +1,101 @@
1
+ // Installs the slopfighter skill into the project's AI-tool surfaces.
2
+ // Defaults to all known targets present in the project; --only / --skip filter.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
9
+
10
+ const TARGETS = {
11
+ claude: {
12
+ label: 'Claude Code',
13
+ detect: (root) => fs.existsSync(path.join(root, '.claude')),
14
+ install(root) {
15
+ const dest = path.join(root, '.claude', 'skills', 'slopfighter', 'SKILL.md');
16
+ writeFile(dest, readTemplate('SKILL.md'));
17
+ return dest;
18
+ },
19
+ },
20
+ cursor: {
21
+ label: 'Cursor',
22
+ detect: (root) => fs.existsSync(path.join(root, '.cursor')),
23
+ install(root) {
24
+ const dest = path.join(root, '.cursor', 'commands', 'slopfighter.md');
25
+ writeFile(dest, readTemplate('cursor-command.md'));
26
+ return dest;
27
+ },
28
+ },
29
+ agents: {
30
+ label: 'AGENTS.md',
31
+ detect: (root) => fs.existsSync(path.join(root, 'AGENTS.md')) ||
32
+ fs.existsSync(path.join(root, 'CLAUDE.md')) ||
33
+ fs.existsSync(path.join(root, '.cursorrules')),
34
+ install(root) {
35
+ const dest = path.join(root, 'AGENTS.md');
36
+ const snippet = readTemplate('agents-section.md');
37
+ upsertSection(dest, snippet);
38
+ return dest;
39
+ },
40
+ },
41
+ };
42
+
43
+ export async function runInstall(root, opts = {}) {
44
+ const only = opts.only ? new Set(opts.only.split(',')) : null;
45
+ const skip = opts.skip ? new Set(opts.skip.split(',')) : new Set();
46
+ const force = !!opts.force;
47
+
48
+ const planned = [];
49
+ for (const [key, t] of Object.entries(TARGETS)) {
50
+ if (only && !only.has(key)) continue;
51
+ if (skip.has(key)) continue;
52
+ const present = t.detect(root);
53
+ if (!present && !force && !only) continue;
54
+ planned.push({ key, target: t, present });
55
+ }
56
+
57
+ if (planned.length === 0) {
58
+ return {
59
+ installed: [],
60
+ message:
61
+ 'No AI-tool config found in this project. Pass `--force` to install into all targets, or `--only claude,cursor,agents`.',
62
+ };
63
+ }
64
+
65
+ const installed = [];
66
+ for (const p of planned) {
67
+ const file = p.target.install(root);
68
+ installed.push({ key: p.key, label: p.target.label, file });
69
+ }
70
+ return { installed };
71
+ }
72
+
73
+ function readTemplate(name) {
74
+ return fs.readFileSync(path.join(TEMPLATE_DIR, name), 'utf8');
75
+ }
76
+
77
+ function writeFile(dest, content) {
78
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
79
+ fs.writeFileSync(dest, content, 'utf8');
80
+ }
81
+
82
+ const SECTION_START = '<!-- slopfighter:start';
83
+ const SECTION_END = '<!-- slopfighter:end -->';
84
+ function upsertSection(file, snippet) {
85
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
86
+ if (existing.includes(SECTION_START)) {
87
+ const startIdx = existing.indexOf(SECTION_START);
88
+ const endIdx = existing.indexOf(SECTION_END, startIdx);
89
+ if (endIdx === -1) {
90
+ // malformed — append
91
+ fs.writeFileSync(file, existing.trimEnd() + '\n\n' + snippet.trim() + '\n', 'utf8');
92
+ return;
93
+ }
94
+ const before = existing.slice(0, startIdx);
95
+ const after = existing.slice(endIdx + SECTION_END.length);
96
+ fs.writeFileSync(file, (before + snippet.trim() + after).replace(/\n{3,}/g, '\n\n'), 'utf8');
97
+ } else {
98
+ const sep = existing.length === 0 || existing.endsWith('\n\n') ? '' : '\n\n';
99
+ fs.writeFileSync(file, existing + sep + snippet.trim() + '\n', 'utf8');
100
+ }
101
+ }
@@ -0,0 +1,66 @@
1
+ import path from 'node:path';
2
+ import pc from 'picocolors';
3
+
4
+ const SEVERITY_COLOR = {
5
+ error: pc.red,
6
+ warn: pc.yellow,
7
+ info: pc.cyan,
8
+ };
9
+
10
+ const GRADE_COLOR = {
11
+ A: pc.green,
12
+ B: pc.green,
13
+ C: pc.yellow,
14
+ D: pc.yellow,
15
+ F: pc.red,
16
+ };
17
+
18
+ export function renderReport(findings, score, target) {
19
+ if (findings.length === 0) {
20
+ return `${pc.green('✓')} No slop detected in ${pc.dim(target)}\n` + renderScore(score) + '\n';
21
+ }
22
+
23
+ const byFile = groupByFile(findings);
24
+ let out = '';
25
+ for (const [file, items] of byFile) {
26
+ const rel = path.relative(process.cwd(), file) || file;
27
+ out += `\n${pc.bold(rel)}\n`;
28
+ for (const f of items) {
29
+ const sev = (SEVERITY_COLOR[f.severity] || pc.white)(f.severity.padEnd(5));
30
+ const loc = pc.dim(`${f.line}:${f.column}`);
31
+ const ruleId = pc.dim(`(${f.ruleId})`);
32
+ const fixable = f.fixable ? pc.green(' [fixable]') : '';
33
+ out += ` ${loc} ${sev} ${f.message} ${ruleId}${fixable}\n`;
34
+ }
35
+ }
36
+ out += '\n' + renderScore(score) + '\n';
37
+ return out;
38
+ }
39
+
40
+ export function renderScore(score) {
41
+ const color = GRADE_COLOR[score.grade] || pc.white;
42
+ const bar = renderBar(score.value);
43
+ const breakdown =
44
+ `errors: ${pc.red(score.counts.error || 0)} ` +
45
+ `warns: ${pc.yellow(score.counts.warn || 0)} ` +
46
+ `infos: ${pc.cyan(score.counts.info || 0)}`;
47
+ return `${pc.bold('slop score:')} ${color(`${score.value}/100 (${score.grade})`)} ${bar}\n${breakdown}`;
48
+ }
49
+
50
+ function renderBar(value) {
51
+ const width = 20;
52
+ const filled = Math.round((value / 100) * width);
53
+ return pc.dim('[') + pc.green('█'.repeat(filled)) + pc.dim('░'.repeat(width - filled) + ']');
54
+ }
55
+
56
+ function groupByFile(findings) {
57
+ const m = new Map();
58
+ for (const f of findings) {
59
+ if (!m.has(f.file)) m.set(f.file, []);
60
+ m.get(f.file).push(f);
61
+ }
62
+ for (const arr of m.values()) {
63
+ arr.sort((a, b) => a.line - b.line || a.column - b.column);
64
+ }
65
+ return m;
66
+ }
@@ -0,0 +1,31 @@
1
+ export function walk(node, visit) {
2
+ visit(node);
3
+ node.forEachChild((c) => walk(c, visit));
4
+ }
5
+
6
+ export function lineCol(sourceFile, pos) {
7
+ const lc = sourceFile.getLineAndCharacterOfPosition(pos);
8
+ return { line: lc.line + 1, column: lc.character + 1 };
9
+ }
10
+
11
+ export function nodeRange(sourceFile, node) {
12
+ const start = lineCol(sourceFile, node.getStart(sourceFile));
13
+ const end = lineCol(sourceFile, node.getEnd());
14
+ return { line: start.line, column: start.column, endLine: end.line, endColumn: end.column };
15
+ }
16
+
17
+ export function rangeFromOffsets(sourceFile, startPos, endPos) {
18
+ const start = lineCol(sourceFile, startPos);
19
+ const end = lineCol(sourceFile, endPos);
20
+ return { line: start.line, column: start.column, endLine: end.line, endColumn: end.column };
21
+ }
22
+
23
+ // turn a CamelCaseName into a "camel case name" lower-case phrase
24
+ export function deCamel(name) {
25
+ return name
26
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
27
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
28
+ .replace(/[_-]+/g, ' ')
29
+ .toLowerCase()
30
+ .trim();
31
+ }
@@ -0,0 +1,45 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // if (true) {...} — pure padding.
4
+ // if (x == x) — same.
5
+ // Skipped: while (true) / for(;;) since they are legitimate event loops.
6
+ export default {
7
+ id: 'always-true-conditional',
8
+ severity: 'warn',
9
+ description: 'if-conditions that are statically true (or trivially redundant)',
10
+ check(ctx) {
11
+ const { sourceFile, ts } = ctx;
12
+ const findings = [];
13
+
14
+ walk(sourceFile, (node) => {
15
+ if (node.kind !== ts.SyntaxKind.IfStatement) return;
16
+ const cond = node.expression;
17
+ if (isAlwaysTrue(cond, ts)) {
18
+ const range = nodeRange(sourceFile, cond);
19
+ findings.push({
20
+ message: 'if-condition is always true — drop the wrapper',
21
+ line: range.line,
22
+ column: range.column,
23
+ });
24
+ }
25
+ });
26
+ return findings;
27
+ },
28
+ };
29
+
30
+ function isAlwaysTrue(node, ts) {
31
+ if (!node) return false;
32
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
33
+ if (node.kind === ts.SyntaxKind.NumericLiteral && node.text !== '0') return true;
34
+ if (
35
+ node.kind === ts.SyntaxKind.BinaryExpression &&
36
+ (node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
37
+ node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken) &&
38
+ node.left.kind === ts.SyntaxKind.Identifier &&
39
+ node.right.kind === ts.SyntaxKind.Identifier &&
40
+ node.left.text === node.right.text
41
+ ) {
42
+ return true;
43
+ }
44
+ return false;
45
+ }