Skip to content

Commit 51a91c2

Browse files
authored
Merge pull request #39 from wai-coding/dev
feat: add 5 conservative analysis rules
2 parents 8b28b95 + dcefee2 commit 51a91c2

14 files changed

Lines changed: 568 additions & 3 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## v6 — Conservative Rule Expansion
4+
5+
- Added 5 new conservative analysis rules: `no-console`, `no-empty-function`, `duplicate-imports`, `no-unreachable-after-return`, `no-throw-literal`
6+
- All new rules registered in config defaults, all 4 presets, and rule registry
7+
- Updated strict preset to elevate `no-console`, `no-unreachable-after-return`, and `no-throw-literal` to error severity
8+
- Updated export workflow Known Limitations and ROADMAP for new rules
9+
- Updated README with new rules table, configuration examples, and roadmap
10+
- Added 20 new test cases covering all 5 new rules (194 total tests)
11+
312
## v5 — Summary Polish and Config Coverage
413

514
- Improved human summary generator with complete sentence enforcement and deploy-readiness detection

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ Manual code review is time-consuming and inconsistent. InspectoRepo provides det
3232
| `no-empty-catch` | warn | Flags empty catch blocks that silently hide errors — report only |
3333
| `no-useless-return` | info | Detects redundant `return;` at the end of functions — auto-fixable |
3434
| `ts-diagnostics` | error | Reports high-confidence TypeScript compiler diagnostics (unreachable code, duplicate identifiers, missing names, type mismatches) — report only |
35+
| `no-console` | warn | Detects `console.log`, `console.warn`, `console.error` and similar calls left in production code |
36+
| `no-empty-function` | info | Flags empty function bodies that may indicate incomplete implementation |
37+
| `duplicate-imports` | info | Detects multiple import declarations from the same module and suggests combining them |
38+
| `no-unreachable-after-return` | warn | Flags unreachable code after `return`, `throw`, `break`, or `continue` statements |
39+
| `no-throw-literal` | warn | Detects throwing literal values instead of Error objects, which lose stack trace information |
3540

3641
## Tech Stack
3742

@@ -146,7 +151,7 @@ Apply safe code fixes interactively:
146151
inspectorepo fix ./my-project
147152
```
148153

149-
The fix command runs analysis, finds issues with safe auto-fix suggestions, shows a preview of each proposed change, and asks for confirmation before applying. Rules with auto-fix support: `optional-chaining`, `boolean-simplification`, `unused-imports`, `early-return`, `no-debugger`, and `no-useless-return`. Advisory rules like `complexity-hotspot`, `no-empty-catch`, and `ts-diagnostics` are never auto-applied.
154+
The fix command runs analysis, finds issues with safe auto-fix suggestions, shows a preview of each proposed change, and asks for confirmation before applying. Rules with auto-fix support: `optional-chaining`, `boolean-simplification`, `unused-imports`, `early-return`, `no-debugger`, and `no-useless-return`. Advisory rules like `complexity-hotspot`, `no-empty-catch`, `ts-diagnostics`, `no-console`, `no-empty-function`, `duplicate-imports`, `no-unreachable-after-return`, and `no-throw-literal` are never auto-applied.
150155

151156
### Fix Preview Mode
152157

@@ -213,6 +218,11 @@ The CLI uses the same analysis engine as the web UI. Output is deterministic —
213218
| `no-empty-catch` | warn || Flags empty catch blocks that silently hide errors |
214219
| `no-useless-return` | info || Detects redundant `return;` at the end of functions |
215220
| `ts-diagnostics` | error || Reports high-confidence TypeScript compiler diagnostics |
221+
| `no-console` | warn || Detects console calls left in production code |
222+
| `no-empty-function` | info || Flags empty function bodies that may indicate incomplete implementation |
223+
| `duplicate-imports` | info || Detects multiple imports from the same module |
224+
| `no-unreachable-after-return` | warn || Flags unreachable code after return/throw/break/continue |
225+
| `no-throw-literal` | warn || Detects throwing literals instead of Error objects |
216226

217227
## Configuration
218228

@@ -229,7 +239,12 @@ Create `.inspectorepo.json` in your project root to configure which rules run an
229239
"no-debugger": "warn",
230240
"no-empty-catch": "warn",
231241
"no-useless-return": "warn",
232-
"ts-diagnostics": "off"
242+
"ts-diagnostics": "off",
243+
"no-console": "warn",
244+
"no-empty-function": "warn",
245+
"duplicate-imports": "warn",
246+
"no-unreachable-after-return": "warn",
247+
"no-throw-literal": "warn"
233248
}
234249
}
235250
```
@@ -249,7 +264,7 @@ Available presets:
249264
| Preset | Description |
250265
|--------|-------------|
251266
| `recommended` | Balanced defaults — all rules at `warn` |
252-
| `strict` | Stricter — `unused-imports` and `complexity-hotspot` at `error` |
267+
| `strict` | Stricter — `unused-imports`, `complexity-hotspot`, `no-console`, `no-unreachable-after-return`, and `no-throw-literal` at `error` |
253268
| `cleanup` | Style-focused — disables `complexity-hotspot`, keeps simplification rules |
254269
| `react` | React/TS projects — `unused-imports` at `error`, all others `warn` |
255270

@@ -376,6 +391,7 @@ Download the `inspectorepo-report` artifact from the Actions tab to see the full
376391
- [x] VS Code extension for in-editor analysis
377392
- [x] Summary-only CLI mode (`--summary-only`)
378393
- [x] Improved PR comment summaries with top rules and package highlights
394+
- [x] Conservative rules: `no-console`, `no-empty-function`, `duplicate-imports`, `no-unreachable-after-return`, `no-throw-literal`
379395

380396
### V4 — Planned
381397

ai/scripts/generate-repomix-exports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ const ROADMAP: RoadmapItem[] = [
566566
{ label: 'Web app onboarding About section and empty state', implemented: true },
567567
{ label: 'Richer complexity warnings with contributor breakdown', implemented: true },
568568
{ label: 'Conservative analysis rules (no-debugger, no-empty-catch, no-useless-return, ts-diagnostics)', implemented: true },
569+
{ label: 'Additional conservative rules (no-console, no-empty-function, duplicate-imports, no-unreachable-after-return, no-throw-literal)', implemented: true },
569570
{ label: 'Deploy web app as a hosted service', implemented: false },
570571
{ label: 'Rule dependency graph and cascade analysis', implemented: false },
571572
{ label: 'Performance profiling for large codebases', implemented: false },
@@ -716,6 +717,7 @@ ${formatGroupedFiles(milestoneFiles)}
716717
717718
## Known Limitations
718719
- Auto-fix supports 6 rules (optional-chaining, boolean-simplification, unused-imports, early-return, no-debugger, no-useless-return) — more planned
720+
- no-console, no-empty-function, duplicate-imports, no-unreachable-after-return, and no-throw-literal are advisory only — no auto-fix support
719721
- Complexity-hotspot, no-empty-catch, and ts-diagnostics are advisory only — no auto-fix support
720722
- Browser folder picker requires Chrome/Edge (File System Access API)
721723

packages/core/src/config.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ const ALL_RULE_IDS = [
1818
'no-empty-catch',
1919
'no-useless-return',
2020
'ts-diagnostics',
21+
'no-console',
22+
'no-empty-function',
23+
'duplicate-imports',
24+
'no-unreachable-after-return',
25+
'no-throw-literal',
2126
];
2227

2328
describe('parseConfig', () => {

packages/core/src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const DEFAULT_CONFIG: RuleConfig = {
2222
'no-empty-catch': 'warn',
2323
'no-useless-return': 'warn',
2424
'ts-diagnostics': 'off',
25+
'no-console': 'warn',
26+
'no-empty-function': 'warn',
27+
'duplicate-imports': 'warn',
28+
'no-unreachable-after-return': 'warn',
29+
'no-throw-literal': 'warn',
2530
};
2631

2732
/**

packages/core/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export {
1313
noEmptyCatchRule,
1414
noUselessReturnRule,
1515
tsDiagnosticsRule,
16+
noConsoleRule,
17+
noEmptyFunctionRule,
18+
duplicateImportsRule,
19+
noUnreachableAfterReturnRule,
20+
noThrowLiteralRule,
1621
} from './rules/index.js';
1722
export {
1823
isExcludedDir,

packages/core/src/presets.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
1313
'no-empty-catch': 'warn',
1414
'no-useless-return': 'warn',
1515
'ts-diagnostics': 'off',
16+
'no-console': 'warn',
17+
'no-empty-function': 'warn',
18+
'duplicate-imports': 'warn',
19+
'no-unreachable-after-return': 'warn',
20+
'no-throw-literal': 'warn',
1621
},
1722
strict: {
1823
'unused-imports': 'error',
@@ -24,6 +29,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
2429
'no-empty-catch': 'error',
2530
'no-useless-return': 'warn',
2631
'ts-diagnostics': 'error',
32+
'no-console': 'error',
33+
'no-empty-function': 'warn',
34+
'duplicate-imports': 'warn',
35+
'no-unreachable-after-return': 'error',
36+
'no-throw-literal': 'error',
2737
},
2838
cleanup: {
2939
'unused-imports': 'warn',
@@ -35,6 +45,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
3545
'no-empty-catch': 'warn',
3646
'no-useless-return': 'warn',
3747
'ts-diagnostics': 'off',
48+
'no-console': 'warn',
49+
'no-empty-function': 'warn',
50+
'duplicate-imports': 'warn',
51+
'no-unreachable-after-return': 'warn',
52+
'no-throw-literal': 'warn',
3853
},
3954
react: {
4055
'unused-imports': 'error',
@@ -46,6 +61,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
4661
'no-empty-catch': 'warn',
4762
'no-useless-return': 'warn',
4863
'ts-diagnostics': 'off',
64+
'no-console': 'warn',
65+
'no-empty-function': 'warn',
66+
'duplicate-imports': 'warn',
67+
'no-unreachable-after-return': 'warn',
68+
'no-throw-literal': 'warn',
4969
},
5070
};
5171

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { analyzeCodebase } from '../analyzer.js';
3+
import { noConsoleRule } from './no-console.js';
4+
import { noEmptyFunctionRule } from './no-empty-function.js';
5+
import { duplicateImportsRule } from './duplicate-imports.js';
6+
import { noUnreachableAfterReturnRule } from './no-unreachable-after-return.js';
7+
import { noThrowLiteralRule } from './no-throw-literal.js';
8+
9+
function analyze(code: string, rules: NonNullable<Parameters<typeof analyzeCodebase>[0]['options']>['rules']) {
10+
return analyzeCodebase({
11+
files: [{ path: 'src/test.ts', content: code }],
12+
selectedDirectories: ['src'],
13+
options: { rules },
14+
});
15+
}
16+
17+
describe('no-console rule', () => {
18+
it('detects console.log', () => {
19+
const code = 'function foo() { console.log("hi"); }';
20+
const report = analyze(code, [noConsoleRule]);
21+
expect(report.issues.length).toBe(1);
22+
expect(report.issues[0].ruleId).toBe('no-console');
23+
expect(report.issues[0].message).toContain('console.log');
24+
});
25+
26+
it('detects console.warn and console.error', () => {
27+
const code = [
28+
'function a() { console.warn("w"); }',
29+
'function b() { console.error("e"); }',
30+
].join('\n');
31+
const report = analyze(code, [noConsoleRule]);
32+
expect(report.issues.length).toBe(2);
33+
});
34+
35+
it('does not flag non-console calls', () => {
36+
const code = 'function foo() { logger.log("hi"); }';
37+
const report = analyze(code, [noConsoleRule]);
38+
expect(report.issues.length).toBe(0);
39+
});
40+
41+
it('does not flag console without method call', () => {
42+
const code = 'const c = console;';
43+
const report = analyze(code, [noConsoleRule]);
44+
expect(report.issues.length).toBe(0);
45+
});
46+
});
47+
48+
describe('no-empty-function rule', () => {
49+
it('detects empty function declaration', () => {
50+
const code = 'function foo() {}';
51+
const report = analyze(code, [noEmptyFunctionRule]);
52+
expect(report.issues.length).toBe(1);
53+
expect(report.issues[0].ruleId).toBe('no-empty-function');
54+
});
55+
56+
it('detects empty arrow function', () => {
57+
const code = 'const fn = () => {};';
58+
const report = analyze(code, [noEmptyFunctionRule]);
59+
expect(report.issues.length).toBe(1);
60+
});
61+
62+
it('does not flag function with body', () => {
63+
const code = 'function foo() { return 1; }';
64+
const report = analyze(code, [noEmptyFunctionRule]);
65+
expect(report.issues.length).toBe(0);
66+
});
67+
68+
it('does not flag function with comment (intentional stub)', () => {
69+
const code = [
70+
'function foo() {',
71+
' // intentionally empty',
72+
'}',
73+
].join('\n');
74+
const report = analyze(code, [noEmptyFunctionRule]);
75+
expect(report.issues.length).toBe(0);
76+
});
77+
});
78+
79+
describe('duplicate-imports rule', () => {
80+
it('detects duplicate imports from same module', () => {
81+
const code = [
82+
"import { a } from 'mod';",
83+
"import { b } from 'mod';",
84+
].join('\n');
85+
const report = analyze(code, [duplicateImportsRule]);
86+
expect(report.issues.length).toBe(1);
87+
expect(report.issues[0].ruleId).toBe('duplicate-imports');
88+
expect(report.issues[0].message).toContain('mod');
89+
});
90+
91+
it('does not flag imports from different modules', () => {
92+
const code = [
93+
"import { a } from 'mod-a';",
94+
"import { b } from 'mod-b';",
95+
].join('\n');
96+
const report = analyze(code, [duplicateImportsRule]);
97+
expect(report.issues.length).toBe(0);
98+
});
99+
100+
it('does not flag single import', () => {
101+
const code = "import { a } from 'mod';";
102+
const report = analyze(code, [duplicateImportsRule]);
103+
expect(report.issues.length).toBe(0);
104+
});
105+
});
106+
107+
describe('no-unreachable-after-return rule', () => {
108+
it('detects unreachable code after return', () => {
109+
const code = [
110+
'function foo() {',
111+
' return 1;',
112+
' const x = 2;',
113+
'}',
114+
].join('\n');
115+
const report = analyze(code, [noUnreachableAfterReturnRule]);
116+
expect(report.issues.length).toBe(1);
117+
expect(report.issues[0].ruleId).toBe('no-unreachable-after-return');
118+
expect(report.issues[0].message).toContain('Unreachable');
119+
});
120+
121+
it('detects unreachable code after throw', () => {
122+
const code = [
123+
'function foo() {',
124+
' throw new Error("fail");',
125+
' console.log("never");',
126+
'}',
127+
].join('\n');
128+
const report = analyze(code, [noUnreachableAfterReturnRule]);
129+
expect(report.issues.length).toBe(1);
130+
});
131+
132+
it('does not flag reachable code', () => {
133+
const code = [
134+
'function foo(x: boolean) {',
135+
' if (x) return 1;',
136+
' return 2;',
137+
'}',
138+
].join('\n');
139+
const report = analyze(code, [noUnreachableAfterReturnRule]);
140+
expect(report.issues.length).toBe(0);
141+
});
142+
143+
it('does not flag type declarations after return', () => {
144+
const code = [
145+
'function foo() {',
146+
' return 1;',
147+
' type MyType = string;',
148+
'}',
149+
].join('\n');
150+
const report = analyze(code, [noUnreachableAfterReturnRule]);
151+
expect(report.issues.length).toBe(0);
152+
});
153+
});
154+
155+
describe('no-throw-literal rule', () => {
156+
it('detects throw string literal', () => {
157+
const code = 'function foo() { throw "error"; }';
158+
const report = analyze(code, [noThrowLiteralRule]);
159+
expect(report.issues.length).toBe(1);
160+
expect(report.issues[0].ruleId).toBe('no-throw-literal');
161+
expect(report.issues[0].message).toContain('literal');
162+
});
163+
164+
it('detects throw number literal', () => {
165+
const code = 'function foo() { throw 42; }';
166+
const report = analyze(code, [noThrowLiteralRule]);
167+
expect(report.issues.length).toBe(1);
168+
});
169+
170+
it('does not flag throw new Error()', () => {
171+
const code = 'function foo() { throw new Error("fail"); }';
172+
const report = analyze(code, [noThrowLiteralRule]);
173+
expect(report.issues.length).toBe(0);
174+
});
175+
176+
it('does not flag throw variable', () => {
177+
const code = [
178+
'function foo() {',
179+
' const err = new Error("fail");',
180+
' throw err;',
181+
'}',
182+
].join('\n');
183+
const report = analyze(code, [noThrowLiteralRule]);
184+
expect(report.issues.length).toBe(0);
185+
});
186+
187+
it('does not flag throw function call result', () => {
188+
const code = 'function foo() { throw createError(); }';
189+
const report = analyze(code, [noThrowLiteralRule]);
190+
expect(report.issues.length).toBe(0);
191+
});
192+
});

0 commit comments

Comments
 (0)