{"version":3,"file":"require_validator_any_required.mjs","names":[],"sources":["../../../src/eslint/rules/require_validator_any_required.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/require_validator_any_required\n *\n * Flags every `validator.any()` schema chain that does not declare intent with `.required()`\n * or `.optional()`.\n *\n * Why: in `@nhtio/validation`, `.any()` ADMITS `null`/`undefined` unless you make it `.required()`.\n * That default is silent and easy to miss — a schema you believe rejects missing values quietly\n * accepts them, and any `.custom()` refinement is skipped for an absent value (a `.custom()` guard\n * is the usual way this surfaces, e.g. `implementsX(undefined) === true`). The fix is to make the\n * disposition an EXPLICIT declaration with no ambiguity — every `.any()` must say which it is:\n *   - `.required()`  → reject null/undefined\n *   - `.optional()`  → deliberately allow null/undefined\n *   - `.default(x)`  → allow absence, substituting a fallback value\n *   - `.forbidden()` → the value must be absent\n * This applies whether the `.any()` is top-level or nested inside `items(...)` / `alternatives(...)`:\n * the enclosing schema being `.required()` does NOT govern an inner `.any()`'s null/undefined handling.\n *\n * The sharpest illustration is `.valid(null)`: an author writing `validator.any().valid(null)`\n * means \"must be exactly null\" — but because `.any()` admits `undefined`, that schema actually\n * accepts BOTH `null` and `undefined` (and `undefined !== null`). The fix is\n * `validator.any().required().valid(null)`.\n *\n * Opt-out (e.g. a bare `.any()` used purely as a type argument like `items(validator.any())`):\n *   // eslint-disable-next-line adk/require-validator-any-required -- <reason>\n */\n\nimport { createRule } from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\n// The base identifier a member/call chain roots at, e.g. `validator` in `validator.any()` or\n// `validator.alternatives(...).any()`. Returns undefined if the chain doesn't root at a plain name.\nconst baseIdentifierName = (node: TSESTree.Node | undefined): string | undefined => {\n  let cur: TSESTree.Node | undefined = node\n  while (cur) {\n    if (cur.type === 'Identifier') return cur.name\n    if (cur.type === 'MemberExpression') {\n      cur = cur.object\n      continue\n    }\n    if (cur.type === 'CallExpression') {\n      cur = cur.callee\n      continue\n    }\n    return undefined\n  }\n  return undefined\n}\n\n// `validator.any()` (or a `validator.…().any()` chain) — NOT `expect.any()`, `_.any()`, etc.\nconst isAnyCall = (node: TSESTree.CallExpression): boolean =>\n  node.callee.type === 'MemberExpression' &&\n  node.callee.property.type === 'Identifier' &&\n  node.callee.property.name === 'any' &&\n  baseIdentifierName(node.callee.object) === 'validator'\n\n// Collect every CallExpression in the method chain `anyCall` belongs to — inward through the\n// callee object (`a.b().c()` -> `a.b()`) and outward through `.parent` links\n// (`x.any()` -> `x.any().required` -> `x.any().required()`).\nconst collectChainCalls = (anyCall: TSESTree.CallExpression): TSESTree.CallExpression[] => {\n  const calls: TSESTree.CallExpression[] = []\n\n  let cur: TSESTree.Node | null = anyCall\n  while (cur && cur.type === 'CallExpression') {\n    calls.push(cur)\n    cur = cur.callee.type === 'MemberExpression' ? cur.callee.object : null\n  }\n\n  cur = anyCall\n  while (cur) {\n    const p: TSESTree.Node | undefined = cur.parent\n    if (!p) break\n    if (p.type === 'MemberExpression' && p.object === cur) {\n      cur = p\n      continue\n    }\n    if (p.type === 'CallExpression' && p.callee === cur) {\n      calls.push(p)\n      cur = p\n      continue\n    }\n    break\n  }\n\n  return calls\n}\n\nconst chainCalls = (calls: TSESTree.CallExpression[], name: string): boolean =>\n  calls.some(\n    (c) =>\n      c.callee.type === 'MemberExpression' &&\n      c.callee.property.type === 'Identifier' &&\n      c.callee.property.name === name\n  )\n\n/** ESLint rule: flags a validator `any()` schema lacking an explicit required/optional/forbidden disposition. */\nconst requireValidatorAnyRequiredRule = createRule({\n  name: 'require-validator-any-required',\n  meta: {\n    type: 'problem',\n    docs: {\n      description:\n        '`validator.any()` admits null/undefined unless `.required()`; make the disposition explicit by ending every `.any()` in `.required()`, `.optional()`, or `.default(…)`. Opt out with a tactical disable comment + reason.',\n    },\n    schema: [],\n    messages: {\n      declareIntent:\n        '`validator.any()` admits null/undefined unless made `.required()`. Make the disposition explicit: end this `.any()` in `.required()` (reject null/undefined), `.optional()` (deliberately allow it), or `.default(…)` (allow it with a fallback) — applies even when nested in items()/alternatives(). Or add an eslint-disable-next-line adk/require-validator-any-required comment with a reason.',\n    },\n  },\n  defaultOptions: [],\n  create(context) {\n    return {\n      CallExpression(node: TSESTree.CallExpression) {\n        if (!isAnyCall(node)) return\n        const calls = collectChainCalls(node)\n        // An unambiguous disposition declaration clears the rule:\n        //   .required()  — reject null/undefined\n        //   .optional()  — deliberately allow null/undefined\n        //   .default(x)  — allow absence, substituting a fallback\n        //   .forbidden() — the value must be absent\n        if (\n          chainCalls(calls, 'required') ||\n          chainCalls(calls, 'optional') ||\n          chainCalls(calls, 'default') ||\n          chainCalls(calls, 'forbidden')\n        ) {\n          return\n        }\n        const reportNode = node.callee.type === 'MemberExpression' ? node.callee.property : node\n        context.report({ node: reportNode, messageId: 'declareIntent' })\n      },\n    }\n  },\n})\n\nexport default requireValidatorAnyRequiredRule\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,sBAAsB,SAAwD;CAClF,IAAI,MAAiC;CACrC,OAAO,KAAK;EACV,IAAI,IAAI,SAAS,cAAc,OAAO,IAAI;EAC1C,IAAI,IAAI,SAAS,oBAAoB;GACnC,MAAM,IAAI;GACV;EACF;EACA,IAAI,IAAI,SAAS,kBAAkB;GACjC,MAAM,IAAI;GACV;EACF;EACA;CACF;AAEF;AAGA,IAAM,aAAa,SACjB,KAAK,OAAO,SAAS,sBACrB,KAAK,OAAO,SAAS,SAAS,gBAC9B,KAAK,OAAO,SAAS,SAAS,SAC9B,mBAAmB,KAAK,OAAO,MAAM,MAAM;AAK7C,IAAM,qBAAqB,YAAgE;CACzF,MAAM,QAAmC,CAAC;CAE1C,IAAI,MAA4B;CAChC,OAAO,OAAO,IAAI,SAAS,kBAAkB;EAC3C,MAAM,KAAK,GAAG;EACd,MAAM,IAAI,OAAO,SAAS,qBAAqB,IAAI,OAAO,SAAS;CACrE;CAEA,MAAM;CACN,OAAO,KAAK;EACV,MAAM,IAA+B,IAAI;EACzC,IAAI,CAAC,GAAG;EACR,IAAI,EAAE,SAAS,sBAAsB,EAAE,WAAW,KAAK;GACrD,MAAM;GACN;EACF;EACA,IAAI,EAAE,SAAS,oBAAoB,EAAE,WAAW,KAAK;GACnD,MAAM,KAAK,CAAC;GACZ,MAAM;GACN;EACF;EACA;CACF;CAEA,OAAO;AACT;AAEA,IAAM,cAAc,OAAkC,SACpD,MAAM,MACH,MACC,EAAE,OAAO,SAAS,sBAClB,EAAE,OAAO,SAAS,SAAS,gBAC3B,EAAE,OAAO,SAAS,SAAS,IAC/B;;AAGF,IAAM,kCAAkC,WAAW;CACjD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,4NACJ;EACA,QAAQ,CAAC;EACT,UAAU,EACR,eACE,sYACJ;CACF;CACA,gBAAgB,CAAC;CACjB,OAAO,SAAS;EACd,OAAO,EACL,eAAe,MAA+B;GAC5C,IAAI,CAAC,UAAU,IAAI,GAAG;GACtB,MAAM,QAAQ,kBAAkB,IAAI;GAMpC,IACE,WAAW,OAAO,UAAU,KAC5B,WAAW,OAAO,UAAU,KAC5B,WAAW,OAAO,SAAS,KAC3B,WAAW,OAAO,WAAW,GAE7B;GAEF,MAAM,aAAa,KAAK,OAAO,SAAS,qBAAqB,KAAK,OAAO,WAAW;GACpF,QAAQ,OAAO;IAAE,MAAM;IAAY,WAAW;GAAgB,CAAC;EACjE,EACF;CACF;AACF,CAAC"}