Skip to content

Commit 08c75a6

Browse files
committed
feat(lint): make --fix actually fix things
- oxlint --fix now chains into knip --fix: unused dependencies are removed from package.json (dead-code fix types require --strict; files are never deleted automatically) - new bombshell-dev/no-console-log rule replaces stock no-console: flags console.log with an auto-fixer to console.info (semantically neutral in Node — they are the same stdout write)
1 parent 0c4a2c4 commit 08c75a6

4 files changed

Lines changed: 104 additions & 9 deletions

File tree

oxlintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535

3636
"unicorn/prefer-node-protocol": "error",
3737
"typescript/consistent-type-imports": "error",
38-
"no-console": ["warn", { "allow": ["info", "warn", "error", "debug"] }],
38+
"no-console": "off",
39+
"bombshell-dev/no-console-log": "warn",
3940
"prefer-const": "error",
4041
"no-var": "error",
4142
"max-params": ["error", { "max": 2 }],

rules/plugin.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,45 @@ const plugin = {
44
name: 'bombshell-dev',
55
},
66
rules: {
7+
/**
8+
* Ban `console.log` (and other unlisted console methods) in favor of
9+
* leveled output: `console.info`, `console.warn`, `console.error`,
10+
* `console.debug`.
11+
*
12+
* `console.log` is auto-fixable to `console.info` — in Node.js the two
13+
* are aliases for the same stdout write, so the fix is semantically
14+
* neutral.
15+
*/
16+
'no-console-log': {
17+
meta: { fixable: 'code' },
18+
create(context) {
19+
const ALLOWED = new Set(['info', 'warn', 'error', 'debug']);
20+
21+
return {
22+
CallExpression(node) {
23+
const callee = node.callee;
24+
if (
25+
callee.type !== 'MemberExpression' ||
26+
callee.object.type !== 'Identifier' ||
27+
callee.object.name !== 'console' ||
28+
callee.property.type !== 'Identifier' ||
29+
ALLOWED.has(callee.property.name)
30+
) {
31+
return;
32+
}
33+
const fixable = callee.property.name === 'log';
34+
context.report({
35+
node: callee.property,
36+
message: fixable
37+
? 'Use `console.info` instead of `console.log`.'
38+
: `Unexpected \`console.${callee.property.name}\` — use console.info/warn/error/debug.`,
39+
...(fixable ? { fix: (fixer) => fixer.replaceText(callee.property, 'info') } : {}),
40+
});
41+
},
42+
};
43+
},
44+
},
45+
746
/**
847
* Disallow `throw new Error(...)` in favor of custom error classes.
948
*

src/commands/lint.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
import { createFixture } from '../test-utils/index.ts';
3-
import { runOxlint, runKnip } from './lint.ts';
3+
import { runOxlint, runKnip, runKnipFix } from './lint.ts';
44
import { fileURLToPath } from 'node:url';
55

66
describe('lint command', () => {
@@ -45,6 +45,47 @@ describe('lint command', () => {
4545

4646
expect(violations).toEqual([]);
4747
});
48+
it('auto-fixes console.log to console.info', async () => {
49+
fixture = await createFixture({
50+
src: {
51+
'index.ts': 'console.log("hello");\nconsole.info("ok");\n',
52+
},
53+
});
54+
process.chdir(fileURLToPath(fixture.root));
55+
56+
await runOxlint(['./src'], true);
57+
58+
expect(await fixture.text('src/index.ts')).toBe(
59+
'console.info("hello");\nconsole.info("ok");\n',
60+
);
61+
});
62+
});
63+
64+
describe('runKnipFix', () => {
65+
it('removes unused dependencies from package.json', async () => {
66+
fixture = await createFixture({
67+
'package.json': {
68+
name: 'test-pkg',
69+
version: '1.0.0',
70+
type: 'module',
71+
exports: './src/index.ts',
72+
dependencies: { 'left-pad': '^1.3.0' },
73+
},
74+
src: {
75+
'index.ts': 'export const value = 42;\n',
76+
},
77+
});
78+
process.chdir(fileURLToPath(fixture.root));
79+
80+
const before = await runKnip();
81+
expect(before.some((v) => v.code === 'unused-dependency')).toBe(true);
82+
83+
await runKnipFix();
84+
85+
const pkg = (await fixture.json('package.json')) as { dependencies?: unknown };
86+
expect(pkg.dependencies ?? {}).toEqual({});
87+
expect(await runKnip()).toEqual([]);
88+
});
4889
});
4990

5091
describe('runKnip', () => {

src/commands/lint.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,24 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise<Viola
4747
} catch {
4848
// in some cases, failures or no-ops do not produce valid JSON
4949
// fallback to raw output rather than throwing an error
50-
console.log(result.stdout);
50+
console.info(result.stdout);
5151
return [];
5252
}
5353
}
5454

55+
/**
56+
* Mechanically fix knip-reported issues. Dependency hygiene (removing unused
57+
* deps from package.json) always runs with `--fix`; dead-code fixes (unused
58+
* exports/types) only run in `--strict` mode, matching the report tiers.
59+
* Unused files are never deleted automatically.
60+
*/
61+
export async function runKnipFix(strict?: boolean): Promise<void> {
62+
const types = strict ? 'dependencies,exports,types' : 'dependencies';
63+
await x(local('knip'), ['--fix', '--fix-type', types, '--no-progress'], {
64+
throwOnError: false,
65+
});
66+
}
67+
5568
export async function runKnip(): Promise<Violation[]> {
5669
const args = ['--no-progress', '--reporter', 'json'];
5770
const result = await x(local('knip'), args, { throwOnError: false });
@@ -166,12 +179,12 @@ export function printViolations(violations: Violation[]) {
166179
};
167180

168181
for (const [file, items] of grouped) {
169-
console.log(`\n${file}`);
182+
console.info(`\n${file}`);
170183
for (const v of items) {
171184
const loc = v.line != null ? ` ${v.line}:${v.column ?? 0}` : ' -';
172185
const color = colors[v.level];
173186
const tag = `${v.tool}/${v.code}`;
174-
console.log(
187+
console.info(
175188
`${colors.dim}${loc.padEnd(10)}${colors.reset}${color}${v.level.padEnd(12)}${colors.reset}${v.message} ${colors.dim}${tag}${colors.reset}`,
176189
);
177190
}
@@ -191,9 +204,9 @@ export function printViolations(violations: Violation[]) {
191204
`${colors.suggestion}${counts.suggestion} suggestion${counts.suggestion > 1 ? 's' : ''}${colors.reset}`,
192205
);
193206
if (parts.length > 0) {
194-
console.log(`\n${parts.join(', ')}`);
207+
console.info(`\n${parts.join(', ')}`);
195208
} else {
196-
console.log('\nNo issues found.');
209+
console.info('\nNo issues found.');
197210
}
198211
}
199212

@@ -215,20 +228,21 @@ async function collectViolations(targets: string[]): Promise<Violation[]> {
215228

216229
export async function lint(ctx: CommandContext) {
217230
const args = parse(ctx.args, {
218-
boolean: ['fix'],
231+
boolean: ['fix', 'strict'],
219232
});
220233
const targets = args._.length > 0 ? args._.map(String) : ['./src'];
221234

222235
if (args.fix) {
223236
await runOxlint(targets, true);
237+
await runKnipFix(args.strict);
224238

225239
// Report remaining
226240
const remaining = await collectViolations(targets);
227241
if (remaining.length > 0) {
228242
printViolations(remaining);
229243
process.exit(1);
230244
}
231-
console.log('No issues found.');
245+
console.info('No issues found.');
232246
return;
233247
}
234248

0 commit comments

Comments
 (0)