[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
0

Configure Feed

Select the types of activity you want to include in your feed.

ref(lint): relax rules, add custom

Nate Moore (May 1, 2026, 10:00 AM EDT) 0ba78156 ef6e4b6e

+253 -41
+4 -1
oxlintrc.json
··· 48 48 ], 49 49 "prefer-const": "error", 50 50 "no-var": "error", 51 - "typescript/explicit-function-return-type": ["warn", { "allowExpressions": true }], 52 51 53 52 "import/no-default-export": "error", 54 53 "bombshell-dev/no-generic-error": "error", 55 54 "bombshell-dev/require-export-jsdoc": "warn", 55 + "bombshell-dev/jsdoc-has-params": "warn", 56 56 "bombshell-dev/exported-function-async": "warn", 57 + "bombshell-dev/exported-needs-return-type": "warn", 58 + "bombshell-dev/exported-options-bag": "warn", 59 + "bombshell-dev/no-let-at-module-scope": "warn", 57 60 "bombshell-dev/max-params": ["error", { "max": 2 }] 58 61 }, 59 62 "overrides": [
+249 -40
rules/plugin.js
··· 1 + // Shared helpers — hoisted so multiple rules share one implementation. 2 + 3 + function isExportedDeclaration(node) { 4 + const parent = node.parent; 5 + if (!parent) return false; 6 + return ( 7 + parent.type === 'ExportNamedDeclaration' || 8 + parent.type === 'ExportDefaultDeclaration' 9 + ); 10 + } 11 + 12 + function isExportedConstInitializer(node) { 13 + const parent = node.parent; 14 + if (!parent || parent.type !== 'VariableDeclarator') return false; 15 + const decl = parent.parent; 16 + if (!decl || decl.type !== 'VariableDeclaration') return false; 17 + const exp = decl.parent; 18 + return ( 19 + !!exp && 20 + (exp.type === 'ExportNamedDeclaration' || exp.type === 'ExportDefaultDeclaration') 21 + ); 22 + } 23 + 24 + function isExportedFunction(node) { 25 + return isExportedDeclaration(node) || isExportedConstInitializer(node); 26 + } 27 + 28 + function functionName(node) { 29 + if (node.id?.name) return node.id.name; 30 + if (node.parent?.type === 'VariableDeclarator' && node.parent.id?.name) { 31 + return node.parent.id.name; 32 + } 33 + return 'anonymous'; 34 + } 35 + 36 + function getJSDocText(node, sourceText) { 37 + let idx = node.start - 1; 38 + while (idx >= 0 && (sourceText[idx] === ' ' || sourceText[idx] === '\t' || sourceText[idx] === '\n' || sourceText[idx] === '\r')) { 39 + idx--; 40 + } 41 + if (idx >= 1 && sourceText[idx] === '/' && sourceText[idx - 1] === '*') { 42 + const closeIdx = idx; 43 + const openIdx = sourceText.lastIndexOf('/**', closeIdx); 44 + if (openIdx !== -1 && openIdx < closeIdx) { 45 + return sourceText.substring(openIdx, closeIdx + 1); 46 + } 47 + } 48 + return null; 49 + } 50 + 51 + function paramName(param) { 52 + if (!param) return null; 53 + if (param.type === 'Identifier') return param.name; 54 + if (param.type === 'AssignmentPattern') return paramName(param.left); 55 + if (param.type === 'RestElement') return paramName(param.argument); 56 + if (param.type === 'TSParameterProperty') return paramName(param.parameter); 57 + return null; 58 + } 59 + 60 + function isOptionsBagParam(param) { 61 + if (!param) return false; 62 + if (param.type === 'ObjectPattern') return true; 63 + if (param.type === 'AssignmentPattern' && param.left?.type === 'ObjectPattern') return true; 64 + if (param.type === 'RestElement') return true; 65 + const name = paramName(param); 66 + if (!name) return false; 67 + return /^(options?|opts|config|settings)$/i.test(name); 68 + } 69 + 1 70 /** @type {import("oxlint").Plugin} */ 2 71 const plugin = { 3 72 meta: { ··· 59 128 */ 60 129 'require-export-jsdoc': { 61 130 create(context) { 62 - function hasJSDoc(node) { 63 - const src = context.sourceCode.text; 64 - let idx = node.start - 1; 65 - // Walk backwards past whitespace 66 - while (idx >= 0 && (src[idx] === ' ' || src[idx] === '\t' || src[idx] === '\n' || src[idx] === '\r')) { 67 - idx--; 68 - } 69 - // Check if preceding non-whitespace ends with */ 70 - if (idx >= 1 && src[idx] === '/' && src[idx - 1] === '*') { 71 - // Find the opening /** 72 - const closeIdx = idx; 73 - const openIdx = src.lastIndexOf('/**', closeIdx); 74 - if (openIdx !== -1 && openIdx < closeIdx) { 75 - return true; 76 - } 131 + function check(node) { 132 + const target = isExportedDeclaration(node) ? node.parent : null; 133 + if (!target) return; 134 + if (!getJSDocText(target, context.sourceCode.text)) { 135 + context.report({ 136 + node, 137 + message: 'Exported functions and classes should have a JSDoc comment.', 138 + }); 77 139 } 78 - return false; 79 140 } 80 141 81 - function isExported(node) { 82 - const parent = node.parent; 83 - if (!parent) return false; 84 - return ( 85 - parent.type === 'ExportNamedDeclaration' || 86 - parent.type === 'ExportDefaultDeclaration' 87 - ); 88 - } 142 + return { 143 + FunctionDeclaration: check, 144 + ClassDeclaration: check, 145 + }; 146 + }, 147 + }, 148 + 149 + /** 150 + * Require each parameter on an exported function to have a `@param` tag, 151 + * and require a `@returns` tag when the function declares a non-void 152 + * return type. Catches drift between signature and docs. 153 + * 154 + * Only fires when JSDoc is present; missing JSDoc is `require-export-jsdoc`'s 155 + * concern. 156 + */ 157 + 'jsdoc-has-params': { 158 + create(context) { 159 + const PARAM_TAG_RE = /@param\s+(?:\{[^}]*\}\s+)?(?:\[)?([A-Za-z_$][\w$]*)/g; 89 160 90 161 function check(node) { 91 - const target = isExported(node) ? node.parent : null; 162 + const target = isExportedDeclaration(node) ? node.parent : null; 92 163 if (!target) return; 93 - if (!hasJSDoc(target)) { 164 + const text = getJSDocText(target, context.sourceCode.text); 165 + if (!text) return; 166 + 167 + const documented = new Set(); 168 + PARAM_TAG_RE.lastIndex = 0; 169 + let m; 170 + while ((m = PARAM_TAG_RE.exec(text))) documented.add(m[1]); 171 + 172 + for (const param of node.params) { 173 + const name = paramName(param); 174 + if (!name) continue; 175 + if (name.startsWith('_')) continue; 176 + if (!documented.has(name)) { 177 + context.report({ 178 + node: param, 179 + message: `Parameter \`${name}\` is missing from JSDoc \`@param\` tags.`, 180 + }); 181 + } 182 + } 183 + 184 + const returnType = node.returnType?.typeAnnotation; 185 + if (!returnType) return; 186 + const isVoidLike = 187 + returnType.type === 'TSVoidKeyword' || 188 + returnType.type === 'TSUndefinedKeyword' || 189 + returnType.type === 'TSNeverKeyword'; 190 + if (!isVoidLike && !text.includes('@returns') && !text.includes('@return ')) { 94 191 context.report({ 95 192 node, 96 - message: 'Exported functions and classes should have a JSDoc comment.', 193 + message: 'Function with a non-void return type should document `@returns`.', 97 194 }); 98 195 } 99 196 } 100 197 101 198 return { 102 199 FunctionDeclaration: check, 103 - ClassDeclaration: check, 104 200 }; 105 201 }, 106 202 }, ··· 184 280 */ 185 281 'exported-function-async': { 186 282 create(context) { 187 - function isExported(node) { 188 - const parent = node.parent; 189 - if (!parent) return false; 190 - return ( 191 - parent.type === 'ExportNamedDeclaration' || 192 - parent.type === 'ExportDefaultDeclaration' 193 - ); 194 - } 195 - 196 283 return { 197 284 FunctionDeclaration(node) { 198 - if (!isExported(node)) return; 285 + if (!isExportedDeclaration(node)) return; 199 286 if (node.async) return; 200 287 if (node.returnType) return; 201 - const name = node.id?.name ?? 'anonymous'; 202 288 context.report({ 203 289 node, 204 - message: `Exported function \`${name}\` should be \`async\`, or annotate an explicit return type to opt out. Public APIs default to async to avoid breaking changes.`, 290 + message: `Exported function \`${functionName(node)}\` should be \`async\`, or annotate an explicit return type to opt out. Public APIs default to async to avoid breaking changes.`, 291 + }); 292 + }, 293 + }; 294 + }, 295 + }, 296 + 297 + /** 298 + * Require exported functions and public class methods to declare an 299 + * explicit return type. 300 + * 301 + * Replaces the broader `typescript/explicit-function-return-type`, 302 + * which fires on private/protected methods where TS inference is fine. 303 + * Drift on the public surface is what costs consumers — internal 304 + * inference is a feature, not a bug. 305 + * 306 + * Checks: 307 + * - `export function foo() {}` 308 + * - `export const foo = () => {}` / `export const foo = function () {}` 309 + * - Public methods (no modifier or explicit `public`) on exported classes 310 + * 311 + * Skips: private/protected/constructor methods, internals. 312 + */ 313 + 'exported-needs-return-type': { 314 + create(context) { 315 + function reportIfMissing(node, name, kind) { 316 + if (node.returnType) return; 317 + context.report({ 318 + node, 319 + message: `Exported ${kind} \`${name}\` must declare an explicit return type. Stable signatures keep accidental drift from leaking into consumers.`, 320 + }); 321 + } 322 + 323 + return { 324 + FunctionDeclaration(node) { 325 + if (!isExportedDeclaration(node)) return; 326 + reportIfMissing(node, functionName(node), 'function'); 327 + }, 328 + ArrowFunctionExpression(node) { 329 + if (!isExportedConstInitializer(node)) return; 330 + reportIfMissing(node, functionName(node), 'function'); 331 + }, 332 + FunctionExpression(node) { 333 + if (!isExportedConstInitializer(node)) return; 334 + reportIfMissing(node, functionName(node), 'function'); 335 + }, 336 + MethodDefinition(node) { 337 + if (node.kind === 'constructor') return; 338 + if (node.accessibility === 'private' || node.accessibility === 'protected') return; 339 + const classBody = node.parent; 340 + if (!classBody || classBody.type !== 'ClassBody') return; 341 + const cls = classBody.parent; 342 + if (!cls || cls.type !== 'ClassDeclaration') return; 343 + if (!isExportedDeclaration(cls)) return; 344 + const fn = node.value; 345 + if (!fn) return; 346 + const keyName = node.key?.name ?? node.key?.value ?? 'method'; 347 + reportIfMissing(fn, keyName, 'method'); 348 + }, 349 + }; 350 + }, 351 + }, 352 + 353 + /** 354 + * Steer exported functions toward the options-bag pattern. 355 + * 356 + * Multi-positional parameters force callers to remember order and make 357 + * later additions a breaking change. Prefer `fn({ foo, bar })` — 358 + * additive, self-documenting, and tooling-friendly. 359 + * 360 + * Allows: 361 + * - 0 or 1 param of any shape 362 + * - `fn(value, options)` where the trailing param is an 363 + * `ObjectPattern` or named `options` / `opts` / `config` / `settings` 364 + * - Rest parameters (`...args`) 365 + * 366 + * Flags: any other shape, e.g. `fn(a, b)` or `fn(a, b, c)`. 367 + */ 368 + 'exported-options-bag': { 369 + create(context) { 370 + function check(node) { 371 + if (!isExportedFunction(node)) return; 372 + if (node.params.length <= 1) return; 373 + for (let i = 1; i < node.params.length; i++) { 374 + const p = node.params[i]; 375 + if (!isOptionsBagParam(p)) { 376 + const name = paramName(p) ?? 'param'; 377 + context.report({ 378 + node: p, 379 + message: `Exported APIs should pass multiple values as a single options object instead of positional parameters. Move \`${name}\` into an options bag (e.g. \`fn({ ${name} })\`) for forward-compatible signatures.`, 380 + }); 381 + return; 382 + } 383 + } 384 + } 385 + 386 + return { 387 + FunctionDeclaration: check, 388 + ArrowFunctionExpression: check, 389 + FunctionExpression: check, 390 + }; 391 + }, 392 + }, 393 + 394 + /** 395 + * Forbid module-scoped `let` and `var`. 396 + * 397 + * Module-level mutable state is hidden global state — it bakes into 398 + * snapshots, leaks between callers, and makes testing harder. Use 399 + * `const` for true constants, or move mutable state inside a factory 400 + * function (`createX()` returns a fresh state object per call). 401 + * 402 + * Especially important for SEA snapshots: module-init values get 403 + * frozen at build time, which is rarely what authors of `let` intend. 404 + */ 405 + 'no-let-at-module-scope': { 406 + create(context) { 407 + return { 408 + VariableDeclaration(node) { 409 + if (node.kind === 'const') return; 410 + if (node.parent?.type !== 'Program') return; 411 + context.report({ 412 + node, 413 + message: `Module-scoped \`${node.kind}\` is hidden state — bakes into snapshots, leaks across callers, and breaks tests. Use \`const\`, or move into a factory function.`, 205 414 }); 206 415 }, 207 416 };