import type { Category, Severity, Tier } from "../types"; export interface GrepExtendedRuleDefinition { id: string; pattern: RegExp; category: Category; severity: Severity; tier: Tier; message: string; desc: string; fix?: string; fileFilter?: RegExp; } export const GREP_EXTENDED_RULES: GrepExtendedRuleDefinition[] = [ { id: "EMPTY_TEST", pattern: /^\s*(it|test)\s*\(\s*['"][^'"]+['"]\s*,\s*(\(\)|async\s*\(\))\s*=>\s*\{\s*\}\s*\)/, category: "test-quality", severity: "HIGH", tier: 0, message: "Empty test body — test passes but checks nothing", desc: "Empty test body — passes but checks nothing", fileFilter: /\.(test|spec)\.(ts|tsx|js|jsx)$/, }, { id: "WEAK_ASSERTION", pattern: /expect\([^)]+\)\.(toBeTruthy|toBeDefined|not\.toBeNull|not\.toBeUndefined)\(\)/, category: "test-quality", severity: "MEDIUM", tier: 0, message: "Weak assertion — tests existence not correctness. Assert on the actual value.", desc: "Weak assertion — tests existence not value", fileFilter: /\.(test|spec)\.(ts|tsx|js|jsx)$/, }, { id: "ASSERT_TRUE", pattern: /\b(assert\s+True|assertTrue\(true|expect\(true\)\.toBe\(true\))/, category: "test-quality", severity: "HIGH", tier: 0, message: "Tautological assertion — always passes, tests nothing", desc: "Tautological assert True — always passes", fileFilter: /\.(test|spec)\.(ts|tsx|js|jsx|py)$/, }, { id: "SLEEPY_TEST", pattern: /\b(setTimeout|sleep|await\s+delay|await\s+new\s+Promise.*setTimeout)\s*\(/, category: "test-quality", severity: "MEDIUM", tier: 0, message: "Sleep in test — use waitFor/polling instead of arbitrary delays", desc: "Sleep in test — use waitFor instead", fileFilter: /\.(test|spec)\.(ts|tsx|js|jsx)$/, }, { id: "SKIPPED_TEST", pattern: /\b(it\.skip|test\.skip|xit|xdescribe|xtest|@pytest\.mark\.skip|@unittest\.skip)/, category: "test-quality", severity: "LOW", tier: 0, message: "Skipped test — resolve or remove, don't leave disabled tests", desc: "Skipped test — resolve or remove", fileFilter: /\.(test|spec)\.(ts|tsx|js|jsx|py)$/, }, { id: "SNAPSHOT_OVERUSE", pattern: /toMatchSnapshot\(\)|toMatchInlineSnapshot\(/, category: "test-quality", severity: "LOW", tier: 0, message: "Snapshot test — brittle, breaks on any change. Assert on specific values.", desc: "Snapshot test — brittle, assert on values", fileFilter: /\.(test|spec)\.(ts|tsx|js|jsx)$/, }, { id: "FOREACH_ASYNC", pattern: /\.forEach\(\s*async\s/, category: "async-correctness", severity: "CRITICAL", tier: 0, message: "async callback in forEach — promises are fire-and-forgotten. Use for...of or Promise.all(map()).", desc: "async in forEach — promises fire-and-forget", }, { id: "ASYNC_PROMISE_EXECUTOR", pattern: /new\s+Promise\s*\(\s*async\s*\(/, category: "async-correctness", severity: "HIGH", tier: 0, message: "async Promise executor — use a plain async function or non-async executor with explicit resolve/reject", desc: "async Promise executor — avoid async inside new Promise", }, { id: "USEEFFECT_ASYNC", pattern: /useEffect\(\s*async\s*\(/, category: "async-correctness", severity: "HIGH", tier: 0, message: "async useEffect callback — wrap the async work in an inner function instead", desc: "async useEffect callback — wrap async work inside", fileFilter: /\.(tsx|jsx)$/, }, { id: "REDUNDANT_RETURN_AWAIT", pattern: /return\s+await\s+\w+\(/, category: "async-correctness", severity: "LOW", tier: 0, message: "return await on a direct promise — return the promise unless you need try/catch semantics", desc: "return await on direct promise", }, { id: "BARE_ASYNC_MAP", pattern: /\.map\(\s*async\s/, category: "async-correctness", severity: "HIGH", tier: 0, message: "async map result not obviously awaited — collect with Promise.all(...) or use for...of", desc: "async map result not obviously awaited", }, { id: "REQUESTS_IN_ASYNC", pattern: /^\s*(response|r|res)\s*=\s*(requests\.(get|post|put|patch|delete))/, category: "async-correctness", severity: "CRITICAL", tier: 0, message: "Blocking requests.get/post in async context — use httpx.AsyncClient instead", desc: "Blocking requests in async — use httpx", fileFilter: /\.py$/, }, { id: "SEQUENTIAL_AWAIT", pattern: /await\s+\w+\([^)]*\)\s*;\s*\n\s*(?:const|let|var)\s+\w+\s*=\s*await\s+\w+\(/, category: "async-correctness", severity: "MEDIUM", tier: 0, message: "Sequential awaits — if independent, use Promise.all() for parallel execution", desc: "Sequential awaits — use Promise.all()", }, { id: "CALLBACK_PROMISE_MIX", pattern: /(?:^|[^/'"`])\.then\(\s*(?:async\b|(?:\([^)]*\)|\w+)\s*=>.*await\b)/, category: "async-correctness", severity: "MEDIUM", tier: 0, message: "Mixing .then() with async/await — pick one style", desc: "Mixing .then() with async/await", }, { id: "JSON_DEEP_CLONE", pattern: /JSON\.parse\(\s*JSON\.stringify\(/, category: "ai-slop", severity: "MEDIUM", tier: 0, message: "JSON stringify/parse deep clone — lossy and slow. Use structuredClone when available.", desc: "JSON stringify/parse deep clone", }, { id: "HANDWAVY_COMMENT", pattern: /^\s*(\/\/|#)\s*(quick fix|temporary workaround|works for now|good enough|safe enough|hacky|dirty)\b/i, category: "ai-slop", severity: "LOW", tier: 1, message: "Handwavy comment — vague workaround language leaked into code", desc: "Handwavy workaround comment", fileFilter: /\.(ts|tsx|js|jsx)$/, }, { id: "NOT_IMPLEMENTED_STUB", pattern: /throw\s+new\s+Error\s*\(\s*["'`](todo|not implemented|unimplemented|stub|implement me|placeholder)["'`]\s*\)/i, category: "ai-slop", severity: "MEDIUM", tier: 1, message: "Not-implemented stub shipped in JS/TS code", desc: "Not-implemented JS/TS stub", fix: "Implement the function or replace the placeholder stub", fileFilter: /\.(ts|tsx|js|jsx)$/, }, { id: "DEAD_FEATURE_FLAG", pattern: /^\s*(?:export\s+)?(?:const|let|var)\s+[A-Za-z_$][\w$]*(?:flag|feature|enabled|toggle|experiment|gate)[A-Za-z_$\d]*\s*=\s*(true|false)\s*;?\s*$/i, category: "legacy-code", severity: "MEDIUM", tier: 0, message: "Feature flag always on/off — remove the dead branch or make it real configuration", desc: "Feature flag always on/off", fileFilter: /\.(ts|tsx|js|jsx)$/, }, { id: "THROW_NON_ERROR", pattern: /^\s*throw\s+(?:["'`]|\{|\[|\d|true\b|false\b|null\b|undefined\b)/, category: "defensive-programming", severity: "HIGH", tier: 0, message: "Thrown value is not an Error instance — throw Error objects for stack/cause semantics", desc: "Thrown value is not an Error instance", fileFilter: /\.(ts|tsx|js|jsx)$/, }, { id: "CATCH_WRAP_NO_CAUSE", pattern: /^\s*throw\s+new\s+(?:Error|TypeError|RangeError|ReferenceError|SyntaxError|URIError|EvalError|AggregateError)\s*\(/, category: "defensive-programming", severity: "MEDIUM", tier: 0, message: "Catch block wraps an error without preserving the original cause", desc: "Catch wraps error without preserving cause", fileFilter: /\.(ts|tsx|js|jsx)$/, }, { id: "UNVALIDATED_BODY", pattern: /(?:req|request)\.body\s*(as\s+\w|:\s*\w)/, category: "runtime-validation", severity: "CRITICAL", tier: 0, message: "req.body type-cast without runtime validation — TypeScript types are erased at runtime. Use Zod/Yup/Joi.", desc: "req.body cast without runtime validation", }, { id: "JSON_PARSE_CAST", pattern: /JSON\.parse\([^)]+\)\s*as\s+\w/, category: "runtime-validation", severity: "HIGH", tier: 0, message: "JSON.parse cast to type without validation — parse result is unknown at runtime", desc: "JSON.parse cast without validation", }, { id: "FETCH_RESPONSE_CAST", pattern: /\.json\(\)\s*(as\s+\w|<\w)/, category: "runtime-validation", severity: "HIGH", tier: 0, message: "Fetch response cast without validation — API responses need runtime schema checks", desc: "Fetch response cast without validation", }, { id: "LOCALSTORAGE_CAST", pattern: /localStorage\.getItem\([^)]+\)\s*(as\s+\w|!)/, category: "runtime-validation", severity: "MEDIUM", tier: 0, message: "localStorage value cast without validation — user-controlled data needs parsing", desc: "localStorage cast without validation", }, { id: "DANGEROUSLY_SET_INNER_HTML", pattern: /dangerouslySetInnerHTML\s*=\s*\{\{/, category: "security-slop", severity: "HIGH", tier: 0, message: "dangerouslySetInnerHTML — sanitize untrusted HTML or avoid raw injection entirely", desc: "dangerouslySetInnerHTML — sanitize or avoid raw HTML", fileFilter: /\.(tsx|jsx)$/, }, { id: "INTERACTIVE_DIV", pattern: /