Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v6 — Conservative Rule Expansion

- Added 5 new conservative analysis rules: `no-console`, `no-empty-function`, `duplicate-imports`, `no-unreachable-after-return`, `no-throw-literal`
- All new rules registered in config defaults, all 4 presets, and rule registry
- Updated strict preset to elevate `no-console`, `no-unreachable-after-return`, and `no-throw-literal` to error severity
- Updated export workflow Known Limitations and ROADMAP for new rules
- Updated README with new rules table, configuration examples, and roadmap
- Added 20 new test cases covering all 5 new rules (194 total tests)

## v5 — Summary Polish and Config Coverage

- Improved human summary generator with complete sentence enforcement and deploy-readiness detection
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Manual code review is time-consuming and inconsistent. InspectoRepo provides det
| `no-empty-catch` | warn | Flags empty catch blocks that silently hide errors — report only |
| `no-useless-return` | info | Detects redundant `return;` at the end of functions — auto-fixable |
| `ts-diagnostics` | error | Reports high-confidence TypeScript compiler diagnostics (unreachable code, duplicate identifiers, missing names, type mismatches) — report only |
| `no-console` | warn | Detects `console.log`, `console.warn`, `console.error` and similar calls left in production code |
| `no-empty-function` | info | Flags empty function bodies that may indicate incomplete implementation |
| `duplicate-imports` | info | Detects multiple import declarations from the same module and suggests combining them |
| `no-unreachable-after-return` | warn | Flags unreachable code after `return`, `throw`, `break`, or `continue` statements |
| `no-throw-literal` | warn | Detects throwing literal values instead of Error objects, which lose stack trace information |

## Tech Stack

Expand Down Expand Up @@ -146,7 +151,7 @@ Apply safe code fixes interactively:
inspectorepo fix ./my-project
```

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.
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.

### Fix Preview Mode

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

## Configuration

Expand All @@ -229,7 +239,12 @@ Create `.inspectorepo.json` in your project root to configure which rules run an
"no-debugger": "warn",
"no-empty-catch": "warn",
"no-useless-return": "warn",
"ts-diagnostics": "off"
"ts-diagnostics": "off",
"no-console": "warn",
"no-empty-function": "warn",
"duplicate-imports": "warn",
"no-unreachable-after-return": "warn",
"no-throw-literal": "warn"
}
}
```
Expand All @@ -249,7 +264,7 @@ Available presets:
| Preset | Description |
|--------|-------------|
| `recommended` | Balanced defaults — all rules at `warn` |
| `strict` | Stricter — `unused-imports` and `complexity-hotspot` at `error` |
| `strict` | Stricter — `unused-imports`, `complexity-hotspot`, `no-console`, `no-unreachable-after-return`, and `no-throw-literal` at `error` |
| `cleanup` | Style-focused — disables `complexity-hotspot`, keeps simplification rules |
| `react` | React/TS projects — `unused-imports` at `error`, all others `warn` |

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

### V4 — Planned

Expand Down
2 changes: 2 additions & 0 deletions ai/scripts/generate-repomix-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ const ROADMAP: RoadmapItem[] = [
{ label: 'Web app onboarding About section and empty state', implemented: true },
{ label: 'Richer complexity warnings with contributor breakdown', implemented: true },
{ label: 'Conservative analysis rules (no-debugger, no-empty-catch, no-useless-return, ts-diagnostics)', implemented: true },
{ label: 'Additional conservative rules (no-console, no-empty-function, duplicate-imports, no-unreachable-after-return, no-throw-literal)', implemented: true },
{ label: 'Deploy web app as a hosted service', implemented: false },
{ label: 'Rule dependency graph and cascade analysis', implemented: false },
{ label: 'Performance profiling for large codebases', implemented: false },
Expand Down Expand Up @@ -716,6 +717,7 @@ ${formatGroupedFiles(milestoneFiles)}

## Known Limitations
- Auto-fix supports 6 rules (optional-chaining, boolean-simplification, unused-imports, early-return, no-debugger, no-useless-return) — more planned
- no-console, no-empty-function, duplicate-imports, no-unreachable-after-return, and no-throw-literal are advisory only — no auto-fix support
- Complexity-hotspot, no-empty-catch, and ts-diagnostics are advisory only — no auto-fix support
- Browser folder picker requires Chrome/Edge (File System Access API)

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const ALL_RULE_IDS = [
'no-empty-catch',
'no-useless-return',
'ts-diagnostics',
'no-console',
'no-empty-function',
'duplicate-imports',
'no-unreachable-after-return',
'no-throw-literal',
];

describe('parseConfig', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const DEFAULT_CONFIG: RuleConfig = {
'no-empty-catch': 'warn',
'no-useless-return': 'warn',
'ts-diagnostics': 'off',
'no-console': 'warn',
'no-empty-function': 'warn',
'duplicate-imports': 'warn',
'no-unreachable-after-return': 'warn',
'no-throw-literal': 'warn',
};

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export {
noEmptyCatchRule,
noUselessReturnRule,
tsDiagnosticsRule,
noConsoleRule,
noEmptyFunctionRule,
duplicateImportsRule,
noUnreachableAfterReturnRule,
noThrowLiteralRule,
} from './rules/index.js';
export {
isExcludedDir,
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
'no-empty-catch': 'warn',
'no-useless-return': 'warn',
'ts-diagnostics': 'off',
'no-console': 'warn',
'no-empty-function': 'warn',
'duplicate-imports': 'warn',
'no-unreachable-after-return': 'warn',
'no-throw-literal': 'warn',
},
strict: {
'unused-imports': 'error',
Expand All @@ -24,6 +29,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
'no-empty-catch': 'error',
'no-useless-return': 'warn',
'ts-diagnostics': 'error',
'no-console': 'error',
'no-empty-function': 'warn',
'duplicate-imports': 'warn',
'no-unreachable-after-return': 'error',
'no-throw-literal': 'error',
},
cleanup: {
'unused-imports': 'warn',
Expand All @@ -35,6 +45,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
'no-empty-catch': 'warn',
'no-useless-return': 'warn',
'ts-diagnostics': 'off',
'no-console': 'warn',
'no-empty-function': 'warn',
'duplicate-imports': 'warn',
'no-unreachable-after-return': 'warn',
'no-throw-literal': 'warn',
},
react: {
'unused-imports': 'error',
Expand All @@ -46,6 +61,11 @@ const PRESETS: Record<PresetName, RuleConfig> = {
'no-empty-catch': 'warn',
'no-useless-return': 'warn',
'ts-diagnostics': 'off',
'no-console': 'warn',
'no-empty-function': 'warn',
'duplicate-imports': 'warn',
'no-unreachable-after-return': 'warn',
'no-throw-literal': 'warn',
},
};

Expand Down
192 changes: 192 additions & 0 deletions packages/core/src/rules/conservative-rules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { describe, it, expect } from 'vitest';
import { analyzeCodebase } from '../analyzer.js';
import { noConsoleRule } from './no-console.js';
import { noEmptyFunctionRule } from './no-empty-function.js';
import { duplicateImportsRule } from './duplicate-imports.js';
import { noUnreachableAfterReturnRule } from './no-unreachable-after-return.js';
import { noThrowLiteralRule } from './no-throw-literal.js';

function analyze(code: string, rules: NonNullable<Parameters<typeof analyzeCodebase>[0]['options']>['rules']) {
return analyzeCodebase({
files: [{ path: 'src/test.ts', content: code }],
selectedDirectories: ['src'],
options: { rules },
});
}

describe('no-console rule', () => {
it('detects console.log', () => {
const code = 'function foo() { console.log("hi"); }';
const report = analyze(code, [noConsoleRule]);
expect(report.issues.length).toBe(1);
expect(report.issues[0].ruleId).toBe('no-console');
expect(report.issues[0].message).toContain('console.log');
});

it('detects console.warn and console.error', () => {
const code = [
'function a() { console.warn("w"); }',
'function b() { console.error("e"); }',
].join('\n');
const report = analyze(code, [noConsoleRule]);
expect(report.issues.length).toBe(2);
});

it('does not flag non-console calls', () => {
const code = 'function foo() { logger.log("hi"); }';
const report = analyze(code, [noConsoleRule]);
expect(report.issues.length).toBe(0);
});

it('does not flag console without method call', () => {
const code = 'const c = console;';
const report = analyze(code, [noConsoleRule]);
expect(report.issues.length).toBe(0);
});
});

describe('no-empty-function rule', () => {
it('detects empty function declaration', () => {
const code = 'function foo() {}';
const report = analyze(code, [noEmptyFunctionRule]);
expect(report.issues.length).toBe(1);
expect(report.issues[0].ruleId).toBe('no-empty-function');
});

it('detects empty arrow function', () => {
const code = 'const fn = () => {};';
const report = analyze(code, [noEmptyFunctionRule]);
expect(report.issues.length).toBe(1);
});

it('does not flag function with body', () => {
const code = 'function foo() { return 1; }';
const report = analyze(code, [noEmptyFunctionRule]);
expect(report.issues.length).toBe(0);
});

it('does not flag function with comment (intentional stub)', () => {
const code = [
'function foo() {',
' // intentionally empty',
'}',
].join('\n');
const report = analyze(code, [noEmptyFunctionRule]);
expect(report.issues.length).toBe(0);
});
});

describe('duplicate-imports rule', () => {
it('detects duplicate imports from same module', () => {
const code = [
"import { a } from 'mod';",
"import { b } from 'mod';",
].join('\n');
const report = analyze(code, [duplicateImportsRule]);
expect(report.issues.length).toBe(1);
expect(report.issues[0].ruleId).toBe('duplicate-imports');
expect(report.issues[0].message).toContain('mod');
});

it('does not flag imports from different modules', () => {
const code = [
"import { a } from 'mod-a';",
"import { b } from 'mod-b';",
].join('\n');
const report = analyze(code, [duplicateImportsRule]);
expect(report.issues.length).toBe(0);
});

it('does not flag single import', () => {
const code = "import { a } from 'mod';";
const report = analyze(code, [duplicateImportsRule]);
expect(report.issues.length).toBe(0);
});
});

describe('no-unreachable-after-return rule', () => {
it('detects unreachable code after return', () => {
const code = [
'function foo() {',
' return 1;',
' const x = 2;',
'}',
].join('\n');
const report = analyze(code, [noUnreachableAfterReturnRule]);
expect(report.issues.length).toBe(1);
expect(report.issues[0].ruleId).toBe('no-unreachable-after-return');
expect(report.issues[0].message).toContain('Unreachable');
});

it('detects unreachable code after throw', () => {
const code = [
'function foo() {',
' throw new Error("fail");',
' console.log("never");',
'}',
].join('\n');
const report = analyze(code, [noUnreachableAfterReturnRule]);
expect(report.issues.length).toBe(1);
});

it('does not flag reachable code', () => {
const code = [
'function foo(x: boolean) {',
' if (x) return 1;',
' return 2;',
'}',
].join('\n');
const report = analyze(code, [noUnreachableAfterReturnRule]);
expect(report.issues.length).toBe(0);
});

it('does not flag type declarations after return', () => {
const code = [
'function foo() {',
' return 1;',
' type MyType = string;',
'}',
].join('\n');
const report = analyze(code, [noUnreachableAfterReturnRule]);
expect(report.issues.length).toBe(0);
});
});

describe('no-throw-literal rule', () => {
it('detects throw string literal', () => {
const code = 'function foo() { throw "error"; }';
const report = analyze(code, [noThrowLiteralRule]);
expect(report.issues.length).toBe(1);
expect(report.issues[0].ruleId).toBe('no-throw-literal');
expect(report.issues[0].message).toContain('literal');
});

it('detects throw number literal', () => {
const code = 'function foo() { throw 42; }';
const report = analyze(code, [noThrowLiteralRule]);
expect(report.issues.length).toBe(1);
});

it('does not flag throw new Error()', () => {
const code = 'function foo() { throw new Error("fail"); }';
const report = analyze(code, [noThrowLiteralRule]);
expect(report.issues.length).toBe(0);
});

it('does not flag throw variable', () => {
const code = [
'function foo() {',
' const err = new Error("fail");',
' throw err;',
'}',
].join('\n');
const report = analyze(code, [noThrowLiteralRule]);
expect(report.issues.length).toBe(0);
});

it('does not flag throw function call result', () => {
const code = 'function foo() { throw createError(); }';
const report = analyze(code, [noThrowLiteralRule]);
expect(report.issues.length).toBe(0);
});
});
Loading
Loading