Skip to content

Commit 83c20f7

Browse files
authored
feat(lint): replace stock max-params with conformance-aware rule (#53)
bombshell-dev/max-params keeps the 2-param limit for signatures we author, but exempts functions conforming to external APIs: override methods, members of classes that extends/implements, and inline callbacks.
1 parent 23997e5 commit 83c20f7

4 files changed

Lines changed: 111 additions & 1 deletion

File tree

.changeset/max-params-rule.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@bomb.sh/tools': minor
3+
---
4+
5+
Replaces the stock `max-params` lint rule with a Bombshell-aware version
6+
7+
The 2-parameter limit (use an options bag beyond that) now only applies to signatures we author. Functions conforming to APIs we don't control are exempt: `override` methods, members of classes that `extends` or `implements` (e.g. Node streams, platform-shaped interfaces), and inline callbacks passed to other functions.

oxlintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"no-console": ["warn", { "allow": ["info", "warn", "error", "debug"] }],
3939
"prefer-const": "error",
4040
"no-var": "error",
41-
"max-params": ["error", { "max": 2 }],
41+
"max-params": "off",
42+
"bombshell-dev/max-params": ["error", { "max": 2 }],
4243
"typescript/explicit-function-return-type": ["warn", { "allowExpressions": true }],
4344

4445
"import/no-default-export": "error",

rules/plugin.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,72 @@ const plugin = {
44
name: 'bombshell-dev',
55
},
66
rules: {
7+
/**
8+
* Limit functions to 2 parameters in APIs we author.
9+
*
10+
* Beyond that, use an options bag. Functions that conform to an
11+
* interface we don't control are exempt — the signature is imposed,
12+
* not designed:
13+
*
14+
* - `override` methods
15+
* - members of classes that `extends` or `implements`
16+
* - inline callbacks (arguments, object-literal properties)
17+
*/
18+
'max-params': {
19+
meta: {
20+
schema: [
21+
{
22+
type: 'object',
23+
properties: { max: { type: 'number' } },
24+
additionalProperties: false,
25+
},
26+
],
27+
},
28+
create(context) {
29+
const max = context.options?.[0]?.max ?? 2;
30+
31+
function isExempt(node) {
32+
const parent = node.parent;
33+
if (!parent) return false;
34+
35+
// Class members: exempt when conforming to a base class or
36+
// interface; standalone class members are authored API.
37+
if (parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition') {
38+
if (parent.override) return true;
39+
const classNode = parent.parent?.parent;
40+
return Boolean(classNode?.superClass || classNode?.implements?.length);
41+
}
42+
43+
// Named functions assigned to variables are authored API.
44+
if (parent.type === 'VariableDeclarator') return false;
45+
46+
// Function declarations are always authored API.
47+
if (node.type === 'FunctionDeclaration') return false;
48+
49+
// Everything else is an inline callback (call arguments,
50+
// object-literal properties, array elements, ...) conforming
51+
// to someone else's signature.
52+
return true;
53+
}
54+
55+
function check(node) {
56+
if (node.params.length <= max) return;
57+
if (isExempt(node)) return;
58+
const name = node.id?.name ?? node.parent?.key?.name ?? 'anonymous';
59+
context.report({
60+
node,
61+
message: `Function \`${name}\` has too many parameters (${node.params.length}). Maximum allowed is ${max} — use an options bag.`,
62+
});
63+
}
64+
65+
return {
66+
FunctionDeclaration: check,
67+
FunctionExpression: check,
68+
ArrowFunctionExpression: check,
69+
};
70+
},
71+
},
72+
773
/**
874
* Disallow `throw new Error(...)` in favor of custom error classes.
975
*

src/commands/lint.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,42 @@ describe('lint command', () => {
8585
});
8686
});
8787

88+
describe('bombshell-dev/max-params', () => {
89+
it('flags authored signatures but not conformance to external APIs', async () => {
90+
fixture = await createFixture({
91+
src: {
92+
'index.ts': `
93+
export function fourParams(a: number, b: number, c: number, d: number): number { return a + b + c + d; }
94+
export const arrow = (a: number, b: number, c: number): number => a + b + c;
95+
class Point { constructor(x: number, y: number, z: number) { void x; void y; void z; } }
96+
97+
interface Listener { on(event: string, data: unknown, ctx: unknown): void }
98+
class MyListener implements Listener {
99+
on(event: string, data: unknown, ctx: unknown): void { void event; void data; void ctx; }
100+
}
101+
102+
class Base { render(a: string, b: string, c: string): void { void a; void b; void c; } }
103+
class Sub extends Base {
104+
override render(a: string, b: string, c: string): void { void a; void b; void c; }
105+
}
106+
107+
const handlers = {
108+
handle(a: string, b: string, c: string): void { void a; void b; void c; },
109+
};
110+
[1].forEach((a, b, c) => { void a; void b; void c; });
111+
void handlers; void Point; void MyListener; void Sub;
112+
`,
113+
},
114+
});
115+
process.chdir(fileURLToPath(fixture.root));
116+
117+
const violations = await runOxlint(['./src']);
118+
const maxParams = violations.filter((v) => v.code === 'bombshell-dev(max-params)');
119+
120+
expect(maxParams.map((v) => v.line).sort((a, b) => a - b)).toEqual([2, 3, 4, 11]);
121+
});
122+
});
123+
88124
describe('runKnip', () => {
89125
const knipFixture = () =>
90126
createFixture({

0 commit comments

Comments
 (0)