Skip to content

Commit f66e1df

Browse files
authored
feat(lint): make --fix actually fix things (#57)
* 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) * chore: apply no-console-log autofix console.log → console.info across the CLI, applied by the new fixer. * chore: add changeset * fix: resolve rebase fallout Missing brace in printViolations grouping loop, stray rule-object closers in plugin.js, TS18048 on optional v.line in sort comparator.
1 parent e4ad9d2 commit f66e1df

10 files changed

Lines changed: 119 additions & 15 deletions

File tree

.changeset/lint-fix-pipeline.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@bomb.sh/tools': minor
3+
---
4+
5+
Makes `bsh lint --fix` fix far more than it used to
6+
7+
- `--fix` now chains `knip --fix` after oxlint: unused dependencies and devDependencies are removed from `package.json` automatically. Dead-code fixes (unused exports/types) additionally run with `--fix --strict`; unused files are never deleted automatically. knip fixes respect your knip config — keep `ignoreDependencies` accurate, since untraceable deps (e.g. binaries referenced by path) will otherwise be removed.
8+
- The stock `no-console` rule is replaced by `bombshell-dev/no-console-log`, which auto-fixes `console.log``console.info` (semantically neutral in Node — they're the same stdout write). Other unlisted console methods are still flagged but not auto-fixed.

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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,45 @@ 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+
73112
/**
74113
* Disallow `throw new Error(...)` in favor of custom error classes.
75114
*

src/bin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ async function main() {
1515
const [command, ...args] = argv.slice(2);
1616

1717
if (!command) {
18-
console.log(`No command provided. Available commands: ${Object.keys(commands).join(', ')}\n`);
18+
console.info(`No command provided. Available commands: ${Object.keys(commands).join(', ')}\n`);
1919
return;
2020
}
2121

2222
const run = commands[command as keyof typeof commands];
2323
if (!run) {
24-
console.log(
24+
console.info(
2525
`Unknown command: ${command}. Available commands: ${Object.keys(commands).join(', ')}`,
2626
);
2727
return;

src/commands/dev.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export async function dev(ctx: CommandContext) {
66
const { args } = ctx;
77
const [file = './src/index.ts', ...rest] = args;
88
// console.clear();
9-
console.log(
9+
console.info(
1010
`node --experimental-transform-types --disable-warning=ExperimentalWarning ${args.join(' ')}`,
1111
);
1212
const stdio = x('node', [
@@ -16,18 +16,18 @@ export async function dev(ctx: CommandContext) {
1616
file,
1717
...rest,
1818
]);
19-
console.log('Starting dev server...');
20-
console.log('Press Ctrl+C to stop the server.');
19+
console.info('Starting dev server...');
20+
console.info('Press Ctrl+C to stop the server.');
2121

2222
for await (const line of stdio) {
2323
if (line.startsWith('Restarting')) {
24-
console.log(line);
24+
console.info(line);
2525
continue;
2626
}
2727
if (line.startsWith('Completed')) {
28-
console.log();
28+
console.info();
2929
continue;
3030
}
31-
console.log(line);
31+
console.info(line);
3232
}
3333
}

src/commands/format.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ export async function format(ctx: CommandContext) {
99
const stdio = x(local('oxfmt'), ['-c', config, ...ctx.args]);
1010

1111
for await (const line of stdio) {
12-
console.log(line);
12+
console.info(line);
1313
}
1414
}

src/commands/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function init(ctx: CommandContext) {
1818
// `--force` lets the template extract into an existing (non-empty) directory.
1919
if (inPlace) gigetArgs.push('--force');
2020
for await (const line of x('pnpx', gigetArgs)) {
21-
console.log(line);
21+
console.info(line);
2222
}
2323

2424
const promises: Promise<void>[] = [];

src/commands/lint.test.ts

Lines changed: 44 additions & 2 deletions
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', () => {
@@ -117,7 +132,34 @@ describe('lint command', () => {
117132
const violations = await runOxlint(['./src']);
118133
const maxParams = violations.filter((v) => v.code === 'bombshell-dev(max-params)');
119134

120-
expect(maxParams.map((v) => v.line).sort((a, b) => a - b)).toEqual([2, 3, 4, 11]);
135+
expect(maxParams.map((v) => v.line!).sort((a, b) => a - b)).toEqual([2, 3, 4, 11]);
136+
});
137+
});
138+
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([]);
121163
});
122164
});
123165

src/commands/lint.ts

Lines changed: 15 additions & 1 deletion
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.
@@ -338,6 +351,7 @@ export async function lint(ctx: CommandContext) {
338351

339352
if (args.fix) {
340353
await runOxlint(targets.length > 0 ? targets : ['.'], true);
354+
await runKnipFix(args.strict);
341355

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

src/commands/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export async function test(ctx: CommandContext) {
1818
const stdio = x(local('vitest'), ['run', '--config', resolveConfig(), ...ctx.args]);
1919

2020
for await (const line of stdio) {
21-
console.log(line);
21+
console.info(line);
2222
}
2323
}

0 commit comments

Comments
 (0)