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
8 changes: 8 additions & 0 deletions .changeset/lint-fix-pipeline.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 39 additions & 0 deletions rules/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,45 @@ 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.
*
Expand Down
4 changes: 2 additions & 2 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/commands/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>[] = [];
Expand Down
46 changes: 44 additions & 2 deletions src/commands/lint.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -117,7 +132,34 @@ 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]);
});
});

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([]);
});
});

Expand Down
16 changes: 15 additions & 1 deletion src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,25 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise<Viola
} catch {
// in some cases, failures or no-ops do not produce valid JSON
// fallback to raw output rather than throwing an error
console.log(result.stdout);
console.info(result.stdout);
return [];
}
});
}

/**
* Mechanically fix knip-reported issues. Dependency hygiene (removing unused
* deps from package.json) always runs with `--fix`; dead-code fixes (unused
* exports/types) only run in `--strict` mode, matching the report tiers.
* Unused files are never deleted automatically.
*/
export async function runKnipFix(strict?: boolean): Promise<void> {
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.
Expand Down Expand Up @@ -338,6 +351,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 });
Expand Down
2 changes: 1 addition & 1 deletion src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading