@bomb.sh/tools
Version:
The internal dev, build, and lint CLI for Bombshell projects
255 lines (236 loc) • 7.22 kB
JavaScript
/** @type {import("oxlint").Plugin} */
const plugin = {
meta: {
name: 'bombshell-dev',
},
rules: {
/**
* Limit functions to 2 parameters in APIs we author.
*
* Beyond that, use an options bag. Functions that conform to an
* interface we don't control are exempt — the signature is imposed,
* not designed:
*
* - `override` methods
* - members of classes that `extends` or `implements`
* - inline callbacks (arguments, object-literal properties)
*/
'max-params': {
meta: {
schema: [
{
type: 'object',
properties: { max: { type: 'number' } },
additionalProperties: false,
},
],
},
create(context) {
const max = context.options?.[0]?.max ?? 2;
function isExempt(node) {
const parent = node.parent;
if (!parent) return false;
// Class members: exempt when conforming to a base class or
// interface; standalone class members are authored API.
if (parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition') {
if (parent.override) return true;
const classNode = parent.parent?.parent;
return Boolean(classNode?.superClass || classNode?.implements?.length);
}
// Named functions assigned to variables are authored API.
if (parent.type === 'VariableDeclarator') return false;
// Function declarations are always authored API.
if (node.type === 'FunctionDeclaration') return false;
// Everything else is an inline callback (call arguments,
// object-literal properties, array elements, ...) conforming
// to someone else's signature.
return true;
}
function check(node) {
if (node.params.length <= max) return;
if (isExempt(node)) return;
const name = node.id?.name ?? node.parent?.key?.name ?? 'anonymous';
context.report({
node,
message: `Function \`${name}\` has too many parameters (${node.params.length}). Maximum allowed is ${max} — use an options bag.`,
});
}
return {
FunctionDeclaration: check,
FunctionExpression: check,
ArrowFunctionExpression: check,
};
},
},
/**
* 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.
*
* Generic `Error` objects lack structured metadata (error codes, hints, etc.)
* and make it harder to provide actionable diagnostics. Use a project-specific
* error class instead.
*
* Catches:
* - `throw new Error(...)`
* - `throw new TypeError(...)` / `throw new RangeError(...)` etc.
*
* Allows:
* - `throw new MyCustomError(...)` (any non-builtin name)
* - Re-throwing: `throw err`
*/
'no-generic-error': {
create(context) {
const BUILTIN_ERRORS = new Set([
'Error',
'TypeError',
'RangeError',
'ReferenceError',
'SyntaxError',
'URIError',
'EvalError',
'AggregateError',
]);
return {
ThrowStatement(node) {
const arg = node.argument;
if (
arg &&
arg.type === 'NewExpression' &&
arg.callee.type === 'Identifier' &&
BUILTIN_ERRORS.has(arg.callee.name)
) {
context.report({
node: arg,
message: `Do not throw generic \`${arg.callee.name}\`. Use a project-specific error class with structured metadata instead.`,
});
}
},
};
},
},
/**
* Require JSDoc comments on exported functions and classes.
*
* Public APIs should have `/** ... */` documentation. Internal/unexported
* functions are not flagged.
*/
'require-export-jsdoc': {
create(context) {
function hasJSDoc(node) {
const src = context.sourceCode.text;
let idx = node.start - 1;
// Walk backwards past whitespace
while (
idx >= 0 &&
(src[idx] === ' ' || src[idx] === '\t' || src[idx] === '\n' || src[idx] === '\r')
) {
idx--;
}
// Check if preceding non-whitespace ends with */
if (idx >= 1 && src[idx] === '/' && src[idx - 1] === '*') {
// Find the opening /**
const closeIdx = idx;
const openIdx = src.lastIndexOf('/**', closeIdx);
if (openIdx !== -1 && openIdx < closeIdx) {
return true;
}
}
return false;
}
function isExported(node) {
const parent = node.parent;
if (!parent) return false;
return (
parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration'
);
}
function check(node) {
const target = isExported(node) ? node.parent : null;
if (!target) return;
if (!hasJSDoc(target)) {
context.report({
node,
message: 'Exported functions and classes should have a JSDoc comment.',
});
}
}
return {
FunctionDeclaration: check,
ClassDeclaration: check,
};
},
},
/**
* Require exported functions to be `async`.
*
* Public-facing functions should default to `async` to future-proof
* the API — adding async later is a breaking change for callers that
* don't `await`.
*
* Ignores:
* - Non-exported functions
* - Class methods (checked separately if needed)
* - Functions that return explicit non-Promise types (type-level;
* this rule only checks the `async` keyword at the syntax level)
*/
'exported-function-async': {
create(context) {
function isExported(node) {
const parent = node.parent;
if (!parent) return false;
return (
parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration'
);
}
return {
FunctionDeclaration(node) {
if (!isExported(node)) return;
if (node.async) return;
const name = node.id?.name ?? 'anonymous';
context.report({
node,
message: `Exported function \`${name}\` should be \`async\`. Public APIs default to async to avoid breaking changes.`,
});
},
};
},
},
},
};
export default plugin;