[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255/** @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;