Skip to content

Commit 48cf3fc

Browse files
Merge pull request #86 from CodeDeficient/claude/project-thread-36cl1x-unknown-options
Reject unknown options and warn on unknown severityOverrides keys
2 parents b86b8b6 + 70a1a37 commit 48cf3fc

4 files changed

Lines changed: 52 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes will be documented in this file.
88
- `swallowed_error` (medium): a `catch` block whose only statements are `console` calls. The error is logged and the code carries on as if the operation succeeded. A comment in the block explaining why that is safe, a rethrow, or any other statement (such as `setError(e)`) means it is not reported, and so does a fallback after the `try`, as for `empty_catch`.
99

1010
### Changed
11+
- An unknown command-line option stops the run with exit 1 and names the option. `--stirct` used to run without strict mode. A `severityOverrides` key that is not a rule id or custom pattern id prints a warning: `type_asertion: "off"` used to do nothing, silently.
1112
- A `customPatterns` entry whose `id` is a built-in rule id (`empty_catch`) or the id of another custom pattern is an invalid config. A shared id made `severityOverrides` and the exit code treat two rules as one.
1213
- A run fails (exit 1) on high and critical findings only. Medium findings such as `type_assertion` and `any_type_usage` are still reported but no longer fail a build; every clean repository tested (ky, express, hono, zod, date-fns, TanStack Query) exited 1 before. `--fail-on=<critical|high|medium|low>` sets the threshold.
1314
- A bundler config marks an import as an alias only when it quotes the whole name (`'@components'`, `'~/lib'`, webpack's `'@acme/ui$'`). A scoped name covers that package only: `'@acme/ui'` in a config or tsconfig `paths` does not cover `@acme/other`. A longer name that starts the same way (`'vite-plugin-pwa'` for `vite-plugin`) used to hide an undeclared or nonexistent package.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Create `.karpesloprc.json` in your project root:
9797
}
9898
```
9999

100+
An unknown option on the command line stops the run with exit 1, and a `severityOverrides` key that is not a rule id prints a warning, so a typo never silently changes what is checked.
101+
100102
There are no inline suppression comments. A comment that silences the linter is easy for anyone, or any tool, to add next to the problem it hides. Changing a rule's severity, or turning it off, goes through `severityOverrides`, where it shows up in review.
101103

102104
`customPatterns` are plain regexes matched line by line. Each needs an `id` of its own: a built-in rule id or one used by another pattern is rejected.

‎ai-slop-detector.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,13 @@ export class ConfigError extends Error {}
248248
/** A command-line flag with a value KarpeSlop does not accept. */
249249
export class UsageError extends Error {}
250250

251+
const KNOWN_FLAGS = new Set(['--help', '-h', '/?', '--version', '-v', '--quiet', '-q', '--strict', '-s']);
252+
253+
/** Flags KarpeSlop does not know. A typo such as `--stirct` would otherwise run without the option. */
254+
export function unknownFlags(flagArgs: readonly string[]): string[] {
255+
return flagArgs.filter(arg => !KNOWN_FLAGS.has(arg) && !arg.startsWith('--fail-on='));
256+
}
257+
251258
/** The severity named by `--fail-on=<severity>`, high when absent. Throws on an unknown value. */
252259
export function failOnFrom(flagArgs: readonly string[]): Severity {
253260
const flag = flagArgs.filter(arg => arg.startsWith('--fail-on=')).at(-1);
@@ -414,7 +421,11 @@ class AISlopDetector {
414421
});
415422
}
416423

424+
const knownIds = new Set([...BUILT_IN_RULE_IDS, ...this.customPatterns.map(pattern => pattern.id)]);
417425
for (const [ruleId, severity] of Object.entries(this.config.severityOverrides ?? {})) {
426+
if (!knownIds.has(ruleId)) {
427+
console.warn(`${path.basename(configPath)}: severityOverrides.${ruleId} does not name a rule and has no effect. Run karpeslop --help for the rule ids.`);
428+
}
418429
this.severityOverrides.set(ruleId, severity);
419430
}
420431

@@ -1076,6 +1087,13 @@ async function runIfMain() {
10761087
process.exit(0);
10771088
}
10781089

1090+
const unknown = unknownFlags(flagArgs);
1091+
if (unknown.length > 0) {
1092+
console.error(`KarpeSlop: unknown option${unknown.length === 1 ? '' : 's'} ${unknown.map(flag => `'${flag}'`).join(', ')}. ` +
1093+
'Run karpeslop --help for the list, or put paths that start with "-" after --.');
1094+
process.exit(1);
1095+
}
1096+
10791097
let detector: AISlopDetector;
10801098
try {
10811099
detector = new AISlopDetector(rootDir, targetPaths.length > 0 ? targetPaths : undefined);

‎tests/ai-slop-detector.behavior.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
shouldAnalyzePathInQuietMode,
1919
splitCliArgs,
2020
uniqueRealFiles,
21+
unknownFlags,
2122
} from '../ai-slop-detector.ts';
2223
import { npmRegistryLookup } from '../imports.ts';
2324

@@ -593,6 +594,36 @@ test('exit code: high and critical fail by default, --fail-on moves the threshol
593594
assert.throws(() => failOnFrom(['--fail-on=warning']), /--fail-on must be one of: critical, high, medium, low/);
594595
});
595596

597+
test('unknown flags are rejected instead of silently ignored', () => {
598+
assert.deepEqual(unknownFlags(['--quiet', '-s', '--fail-on=low']), []);
599+
assert.deepEqual(unknownFlags(['--stirct', '--json', '-dash-file.ts']), ['--stirct', '--json', '-dash-file.ts']);
600+
const result = spawnSync(
601+
process.execPath,
602+
['--import', import.meta.resolve('tsx'), path.resolve(process.cwd(), 'ai-slop-detector.ts'), '--stirct'],
603+
{ encoding: 'utf-8', cwd: os.tmpdir(), env: { ...process.env, KARPESLOP_OFFLINE: '1' } }
604+
);
605+
assert.equal(result.status, 1);
606+
assert.match(result.stderr, /unknown option '--stirct'/);
607+
assert.equal(result.stdout.includes('Scanning'), false);
608+
});
609+
610+
test('a severityOverrides key that is not a rule id is reported as a warning', () => {
611+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'karpeslop-override-'));
612+
fs.writeFileSync(path.join(projectDir, '.karpesloprc.json'), JSON.stringify({ severityOverrides: { type_asertion: 'off', type_assertion: 'low' } }), 'utf-8');
613+
fs.writeFileSync(path.join(projectDir, 'index.ts'), 'export const n = (v: unknown) => v as number;\n', 'utf-8');
614+
const result = spawnSync(
615+
process.execPath,
616+
['--import', import.meta.resolve('tsx'), path.resolve(process.cwd(), 'ai-slop-detector.ts')],
617+
{ encoding: 'utf-8', cwd: projectDir, env: { ...process.env, KARPESLOP_OFFLINE: '1' } }
618+
);
619+
try {
620+
assert.match(result.stderr, /severityOverrides\.type_asertion does not name a rule/);
621+
assert.equal(result.stderr.includes('severityOverrides.type_assertion '), false);
622+
} finally {
623+
fs.rmSync(projectDir, { recursive: true, force: true });
624+
}
625+
});
626+
596627
test('a cast alone does not fail the run unless --fail-on=medium is given', () => {
597628
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'karpeslop-failon-'));
598629
fs.writeFileSync(path.join(projectDir, 'index.ts'), 'export const n = (v: unknown) => v as number;\n', 'utf-8');

0 commit comments

Comments
 (0)