Skip to content

Commit 1be80d8

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 e4ad9d2 commit 1be80d8

4 files changed

Lines changed: 102 additions & 4 deletions

File tree

oxlintrc.json

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

4444
"unicorn/prefer-node-protocol": "error",
4545
"typescript/consistent-type-imports": "error",
46-
"no-console": ["warn", { "allow": ["info", "warn", "error", "debug"] }],
46+
"no-console": "off",
47+
"bombshell-dev/no-console-log": "warn",
4748
"prefer-const": "error",
4849
"no-var": "error",
4950
"max-params": "off",

rules/plugin.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,48 @@ const plugin = {
7070
},
7171
},
7272

73+
/**
74+
* Ban `console.log` (and other unlisted console methods) in favor of
75+
* leveled output: `console.info`, `console.warn`, `console.error`,
76+
* `console.debug`.
77+
*
78+
* `console.log` is auto-fixable to `console.info` — in Node.js the two
79+
* are aliases for the same stdout write, so the fix is semantically
80+
* neutral.
81+
*/
82+
'no-console-log': {
83+
meta: { fixable: 'code' },
84+
create(context) {
85+
const ALLOWED = new Set(['info', 'warn', 'error', 'debug']);
86+
87+
return {
88+
CallExpression(node) {
89+
const callee = node.callee;
90+
if (
91+
callee.type !== 'MemberExpression' ||
92+
callee.object.type !== 'Identifier' ||
93+
callee.object.name !== 'console' ||
94+
callee.property.type !== 'Identifier' ||
95+
ALLOWED.has(callee.property.name)
96+
) {
97+
return;
98+
}
99+
const fixable = callee.property.name === 'log';
100+
context.report({
101+
node: callee.property,
102+
message: fixable
103+
? 'Use `console.info` instead of `console.log`.'
104+
: `Unexpected \`console.${callee.property.name}\` — use console.info/warn/error/debug.`,
105+
...(fixable ? { fix: (fixer) => fixer.replaceText(callee.property, 'info') } : {}),
106+
});
107+
},
108+
};
109+
},
110+
},
111+
};
112+
},
113+
},
114+
73115
/**
74116
* Disallow `throw new Error(...)` in favor of custom error classes.
75117
*

src/commands/lint.test.ts

Lines changed: 43 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', () => {
@@ -83,6 +83,21 @@ describe('lint command', () => {
8383
[],
8484
);
8585
});
86+
87+
it('auto-fixes console.log to console.info', async () => {
88+
fixture = await createFixture({
89+
src: {
90+
'index.ts': 'console.log("hello");\nconsole.info("ok");\n',
91+
},
92+
});
93+
process.chdir(fileURLToPath(fixture.root));
94+
95+
await runOxlint(['./src'], true);
96+
97+
expect(await fixture.text('src/index.ts')).toBe(
98+
'console.info("hello");\nconsole.info("ok");\n',
99+
);
100+
});
86101
});
87102

88103
describe('bombshell-dev/max-params', () => {
@@ -121,6 +136,33 @@ describe('lint command', () => {
121136
});
122137
});
123138

139+
describe('runKnipFix', () => {
140+
it('removes unused dependencies from package.json', async () => {
141+
fixture = await createFixture({
142+
'package.json': {
143+
name: 'test-pkg',
144+
version: '1.0.0',
145+
type: 'module',
146+
exports: './src/index.ts',
147+
dependencies: { 'left-pad': '^1.3.0' },
148+
},
149+
src: {
150+
'index.ts': 'export const value = 42;\n',
151+
},
152+
});
153+
process.chdir(fileURLToPath(fixture.root));
154+
155+
const before = await runKnip();
156+
expect(before.some((v) => v.code === 'unused-dependency')).toBe(true);
157+
158+
await runKnipFix();
159+
160+
const pkg = (await fixture.json('package.json')) as { dependencies?: unknown };
161+
expect(pkg.dependencies ?? {}).toEqual({});
162+
expect(await runKnip()).toEqual([]);
163+
});
164+
});
165+
124166
describe('runKnip', () => {
125167
const knipFixture = () =>
126168
createFixture({

src/commands/lint.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,25 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise<Viola
102102
} catch {
103103
// in some cases, failures or no-ops do not produce valid JSON
104104
// fallback to raw output rather than throwing an error
105-
console.log(result.stdout);
105+
console.info(result.stdout);
106106
return [];
107107
}
108108
});
109109
}
110110

111+
/**
112+
* Mechanically fix knip-reported issues. Dependency hygiene (removing unused
113+
* deps from package.json) always runs with `--fix`; dead-code fixes (unused
114+
* exports/types) only run in `--strict` mode, matching the report tiers.
115+
* Unused files are never deleted automatically.
116+
*/
117+
export async function runKnipFix(strict?: boolean): Promise<void> {
118+
const types = strict ? 'dependencies,exports,types' : 'dependencies';
119+
await x(local('knip'), ['--fix', '--fix-type', types, '--no-progress'], {
120+
throwOnError: false,
121+
});
122+
}
123+
111124
/**
112125
* Knip dead-code issue kinds (unused exports/types/files) fire constantly
113126
* mid-implementation — an export is "unused" until its consumer exists.
@@ -268,7 +281,6 @@ export function printViolations(violations: Violation[], options?: { warnings?:
268281
const key = v.file ?? '(project)';
269282
if (!grouped.has(key)) grouped.set(key, []);
270283
grouped.get(key)!.push(v);
271-
}
272284

273285
for (const [file, items] of grouped) {
274286
console.log(`\n${file}`);
@@ -338,6 +350,7 @@ export async function lint(ctx: CommandContext) {
338350

339351
if (args.fix) {
340352
await runOxlint(targets.length > 0 ? targets : ['.'], true);
353+
await runKnipFix(args.strict);
341354

342355
// Report remaining
343356
const remaining = await collectViolations(targets, { strict: args.strict });

0 commit comments

Comments
 (0)