From 1be80d8a1c83de25e01bf5dbcfe40dc6eba3cd8b Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 23 Jul 2026 00:41:12 -0400 Subject: [PATCH 1/4] feat(lint): make --fix actually fix things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- oxlintrc.json | 3 ++- rules/plugin.js | 42 +++++++++++++++++++++++++++++++++++++ src/commands/lint.test.ts | 44 ++++++++++++++++++++++++++++++++++++++- src/commands/lint.ts | 17 +++++++++++++-- 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/oxlintrc.json b/oxlintrc.json index 8fb6a0d..4d52026 100644 --- a/oxlintrc.json +++ b/oxlintrc.json @@ -43,7 +43,8 @@ "unicorn/prefer-node-protocol": "error", "typescript/consistent-type-imports": "error", - "no-console": ["warn", { "allow": ["info", "warn", "error", "debug"] }], + "no-console": "off", + "bombshell-dev/no-console-log": "warn", "prefer-const": "error", "no-var": "error", "max-params": "off", diff --git a/rules/plugin.js b/rules/plugin.js index 64049ce..567ac20 100644 --- a/rules/plugin.js +++ b/rules/plugin.js @@ -70,6 +70,48 @@ const plugin = { }, }, + /** + * Ban `console.log` (and other unlisted console methods) in favor of + * leveled output: `console.info`, `console.warn`, `console.error`, + * `console.debug`. + * + * `console.log` is auto-fixable to `console.info` — in Node.js the two + * are aliases for the same stdout write, so the fix is semantically + * neutral. + */ + 'no-console-log': { + meta: { fixable: 'code' }, + create(context) { + const ALLOWED = new Set(['info', 'warn', 'error', 'debug']); + + return { + CallExpression(node) { + const callee = node.callee; + if ( + callee.type !== 'MemberExpression' || + callee.object.type !== 'Identifier' || + callee.object.name !== 'console' || + callee.property.type !== 'Identifier' || + ALLOWED.has(callee.property.name) + ) { + return; + } + const fixable = callee.property.name === 'log'; + context.report({ + node: callee.property, + message: fixable + ? 'Use `console.info` instead of `console.log`.' + : `Unexpected \`console.${callee.property.name}\` — use console.info/warn/error/debug.`, + ...(fixable ? { fix: (fixer) => fixer.replaceText(callee.property, 'info') } : {}), + }); + }, + }; + }, + }, + }; + }, + }, + /** * Disallow `throw new Error(...)` in favor of custom error classes. * diff --git a/src/commands/lint.test.ts b/src/commands/lint.test.ts index d5d3d18..57a2f5f 100644 --- a/src/commands/lint.test.ts +++ b/src/commands/lint.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createFixture } from '../test-utils/index.ts'; -import { runOxlint, runKnip } from './lint.ts'; +import { runOxlint, runKnip, runKnipFix } from './lint.ts'; import { fileURLToPath } from 'node:url'; describe('lint command', () => { @@ -83,6 +83,21 @@ describe('lint command', () => { [], ); }); + + it('auto-fixes console.log to console.info', async () => { + fixture = await createFixture({ + src: { + 'index.ts': 'console.log("hello");\nconsole.info("ok");\n', + }, + }); + process.chdir(fileURLToPath(fixture.root)); + + await runOxlint(['./src'], true); + + expect(await fixture.text('src/index.ts')).toBe( + 'console.info("hello");\nconsole.info("ok");\n', + ); + }); }); describe('bombshell-dev/max-params', () => { @@ -121,6 +136,33 @@ describe('lint command', () => { }); }); + describe('runKnipFix', () => { + it('removes unused dependencies from package.json', async () => { + fixture = await createFixture({ + 'package.json': { + name: 'test-pkg', + version: '1.0.0', + type: 'module', + exports: './src/index.ts', + dependencies: { 'left-pad': '^1.3.0' }, + }, + src: { + 'index.ts': 'export const value = 42;\n', + }, + }); + process.chdir(fileURLToPath(fixture.root)); + + const before = await runKnip(); + expect(before.some((v) => v.code === 'unused-dependency')).toBe(true); + + await runKnipFix(); + + const pkg = (await fixture.json('package.json')) as { dependencies?: unknown }; + expect(pkg.dependencies ?? {}).toEqual({}); + expect(await runKnip()).toEqual([]); + }); + }); + describe('runKnip', () => { const knipFixture = () => createFixture({ diff --git a/src/commands/lint.ts b/src/commands/lint.ts index 34704f8..b42b0bb 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -102,12 +102,25 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise { + const types = strict ? 'dependencies,exports,types' : 'dependencies'; + await x(local('knip'), ['--fix', '--fix-type', types, '--no-progress'], { + throwOnError: false, + }); +} + /** * Knip dead-code issue kinds (unused exports/types/files) fire constantly * mid-implementation — an export is "unused" until its consumer exists. @@ -268,7 +281,6 @@ export function printViolations(violations: Violation[], options?: { warnings?: const key = v.file ?? '(project)'; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key)!.push(v); - } for (const [file, items] of grouped) { console.log(`\n${file}`); @@ -338,6 +350,7 @@ export async function lint(ctx: CommandContext) { if (args.fix) { await runOxlint(targets.length > 0 ? targets : ['.'], true); + await runKnipFix(args.strict); // Report remaining const remaining = await collectViolations(targets, { strict: args.strict }); From a0b625567c6f2ff24d6e3c2e81692a228a380b96 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 23 Jul 2026 00:41:23 -0400 Subject: [PATCH 2/4] chore: apply no-console-log autofix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit console.log → console.info across the CLI, applied by the new fixer. --- src/bin.ts | 4 ++-- src/commands/dev.ts | 12 ++++++------ src/commands/format.ts | 2 +- src/commands/init.ts | 2 +- src/commands/test.ts | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index ccc3e10..7215e72 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -15,13 +15,13 @@ async function main() { const [command, ...args] = argv.slice(2); if (!command) { - console.log(`No command provided. Available commands: ${Object.keys(commands).join(', ')}\n`); + console.info(`No command provided. Available commands: ${Object.keys(commands).join(', ')}\n`); return; } const run = commands[command as keyof typeof commands]; if (!run) { - console.log( + console.info( `Unknown command: ${command}. Available commands: ${Object.keys(commands).join(', ')}`, ); return; diff --git a/src/commands/dev.ts b/src/commands/dev.ts index d137e0c..f6f08af 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -6,7 +6,7 @@ export async function dev(ctx: CommandContext) { const { args } = ctx; const [file = './src/index.ts', ...rest] = args; // console.clear(); - console.log( + console.info( `node --experimental-transform-types --disable-warning=ExperimentalWarning ${args.join(' ')}`, ); const stdio = x('node', [ @@ -16,18 +16,18 @@ export async function dev(ctx: CommandContext) { file, ...rest, ]); - console.log('Starting dev server...'); - console.log('Press Ctrl+C to stop the server.'); + console.info('Starting dev server...'); + console.info('Press Ctrl+C to stop the server.'); for await (const line of stdio) { if (line.startsWith('Restarting')) { - console.log(line); + console.info(line); continue; } if (line.startsWith('Completed')) { - console.log(); + console.info(); continue; } - console.log(line); + console.info(line); } } diff --git a/src/commands/format.ts b/src/commands/format.ts index d1be452..4e015d5 100644 --- a/src/commands/format.ts +++ b/src/commands/format.ts @@ -9,6 +9,6 @@ export async function format(ctx: CommandContext) { const stdio = x(local('oxfmt'), ['-c', config, ...ctx.args]); for await (const line of stdio) { - console.log(line); + console.info(line); } } diff --git a/src/commands/init.ts b/src/commands/init.ts index 75111c0..83bb12d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -18,7 +18,7 @@ export async function init(ctx: CommandContext) { // `--force` lets the template extract into an existing (non-empty) directory. if (inPlace) gigetArgs.push('--force'); for await (const line of x('pnpx', gigetArgs)) { - console.log(line); + console.info(line); } const promises: Promise[] = []; diff --git a/src/commands/test.ts b/src/commands/test.ts index 4f7b820..9a71fe3 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -18,6 +18,6 @@ export async function test(ctx: CommandContext) { const stdio = x(local('vitest'), ['run', '--config', resolveConfig(), ...ctx.args]); for await (const line of stdio) { - console.log(line); + console.info(line); } } From 9a0322631973b6f840fa01eaf3fc84de894cf36a Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 23 Jul 2026 00:41:39 -0400 Subject: [PATCH 3/4] chore: add changeset --- .changeset/lint-fix-pipeline.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/lint-fix-pipeline.md diff --git a/.changeset/lint-fix-pipeline.md b/.changeset/lint-fix-pipeline.md new file mode 100644 index 0000000..e0e9f9d --- /dev/null +++ b/.changeset/lint-fix-pipeline.md @@ -0,0 +1,8 @@ +--- +'@bomb.sh/tools': minor +--- + +Makes `bsh lint --fix` fix far more than it used to + +- `--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. +- 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. From e59f4650d11c15b5d789e6c8f98e07fbdb29ffc8 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Sat, 25 Jul 2026 20:08:10 -0400 Subject: [PATCH 4/4] 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. --- rules/plugin.js | 3 --- src/commands/lint.test.ts | 2 +- src/commands/lint.ts | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rules/plugin.js b/rules/plugin.js index 567ac20..a3dcf4a 100644 --- a/rules/plugin.js +++ b/rules/plugin.js @@ -108,9 +108,6 @@ const plugin = { }; }, }, - }; - }, - }, /** * Disallow `throw new Error(...)` in favor of custom error classes. diff --git a/src/commands/lint.test.ts b/src/commands/lint.test.ts index 57a2f5f..d88a270 100644 --- a/src/commands/lint.test.ts +++ b/src/commands/lint.test.ts @@ -132,7 +132,7 @@ describe('lint command', () => { const violations = await runOxlint(['./src']); const maxParams = violations.filter((v) => v.code === 'bombshell-dev(max-params)'); - expect(maxParams.map((v) => v.line).sort((a, b) => a - b)).toEqual([2, 3, 4, 11]); + expect(maxParams.map((v) => v.line!).sort((a, b) => a - b)).toEqual([2, 3, 4, 11]); }); }); diff --git a/src/commands/lint.ts b/src/commands/lint.ts index b42b0bb..374af78 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -281,6 +281,7 @@ export function printViolations(violations: Violation[], options?: { warnings?: const key = v.file ?? '(project)'; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key)!.push(v); + } for (const [file, items] of grouped) { console.log(`\n${file}`);