UNPKG

@bomb.sh/tools

Version:

The internal dev, build, and lint CLI for Bombshell projects

255 lines (236 loc) 7.22 kB
/** @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;