{"version":3,"file":"require_string_empty_disposition.cjs","names":[],"sources":["../../../src/eslint/rules/require_string_empty_disposition.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/require_string_empty_disposition\n *\n * Flags a `validator.string()` chain — written literally inside an `inputSchema` value passed to\n * `new Tool({...})` or `new ArtifactTool({...})` — that is `.optional()`/`.default(…)`-shaped but has\n * no explicit empty-string disposition.\n *\n * Why: confirmed empirically against the actual installed `@nhtio/validation` package (Joi under the\n * hood), `validator.string()` rejects `\"\"` with `\"{{#label}} is not allowed to be empty\"` regardless\n * of whether the chain is bare, `.optional()`, `.default(x)`, or even `.required()` — including the\n * absurd case where `\"\"` is the schema's own configured default (`validator.string().default('')`\n * still rejects an explicit `''`). A model filling in a tool call will often send `\"\"` instead of\n * omitting an unwanted optional parameter; without an explicit disposition, that fails schema\n * validation instead of degrading gracefully. This is the same category of footgun\n * `adk/require-validator-any-required` polices for `.any()` — a silent, non-obvious default that only\n * bites at the worst time.\n *\n * Scope, deliberately narrow (read this before assuming the rule catches more than it does): this\n * rule only flags a `.string()` chain that is *(1)* rooted directly in `validator.string()`, *(2)*\n * `.optional()`/`.default(…)`-shaped (a bare `.required()`-only or fully bare chain is out of scope —\n * demanding `''`-handling there would mostly add noise around ids/expressions/JSON payloads that\n * should keep rejecting empty input), and *(3)* written literally inside the `inputSchema` value of a\n * `new Tool({...})`/`new ArtifactTool({...})` call — including nested inside a `validator.object(\n * {...})` that is itself the `inputSchema` value. It does **not** trace a schema assembled in a\n * helper function and handed in via a variable, it does **not** track cross-branch/conditional\n * reassignment of a schema-holding variable, and it does **not** recognize any project-specific\n * \"param spec\" object-literal pattern. A `validator.string()` chain anywhere else — a battery's own\n * construction-options `validation.ts`, an embeddings/generation/TTS config schema, etc. — is out of\n * scope entirely, on purpose: a plugin shipped to arbitrary consumers cannot assume any particular\n * file layout or authoring convention, so it reasons only from the `new Tool(...)`/`new\n * ArtifactTool(...)` call shape itself, the one thing it can see without executing code. A\n * file-glob-scoped, pattern-aware sibling rule with a broader detection surface exists for this\n * repository's own internal use (not part of what ships to external consumers).\n *\n * Clearing methods (an unambiguous empty-string disposition — any one of these clears the rule):\n *   - `.allow('')`  — only when a `''` string literal is among the call's arguments; `.allow(null)`\n *     alone does NOT clear it (confirmed empirically: `.allow(null)` still rejects `''`).\n *   - `.empty('')`  — same argument-literal check.\n *   - `.valid(...)` — ANY `.valid(...)` call clears the rule, regardless of whether `''` is among its\n *     arguments. An explicit closed enum is sufficient, intentional disposition on its own — a model\n *     sending `''` against a `.valid('a', 'b')` enum gets Joi's normal enum-rejection message, which\n *     is exactly the tool author's intent by writing a closed enum.\n *   - `.forbidden()` — the value must be absent entirely; trivially clears.\n *\n * Opt-out (e.g. a deliberately strict optional/default string that should keep rejecting `''`):\n *   // eslint-disable-next-line adk/require-string-empty-disposition -- <reason>\n */\n\nimport { createRule } from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\nconst literalKey = (prop: TSESTree.Property): string | undefined => {\n  if (prop.computed) return undefined\n  if (prop.key.type === 'Identifier') return prop.key.name\n  if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') return prop.key.value\n  return undefined\n}\n\n// The base identifier a member/call chain roots at, e.g. `validator` in `validator.string()`.\n// 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.string()` — the root call a tracked chain must start from, directly (not several\n// calls deep — `foo.bar().string()` does not qualify, only a direct `validator.string()`).\nconst isValidatorStringRootCall = (node: TSESTree.CallExpression): boolean =>\n  node.callee.type === 'MemberExpression' &&\n  node.callee.property.type === 'Identifier' &&\n  node.callee.property.name === 'string' &&\n  baseIdentifierName(node.callee.object) === 'validator'\n\n// Collect every CallExpression in the method chain `rootCall` belongs to — inward through the\n// callee object (unused here since `rootCall` is already the innermost call) and outward through\n// `.parent` links (`v.string()` -> `v.string().optional` -> `v.string().optional()`).\nconst collectChainCalls = (rootCall: TSESTree.CallExpression): TSESTree.CallExpression[] => {\n  const calls: TSESTree.CallExpression[] = [rootCall]\n\n  let cur: TSESTree.Node = rootCall\n  for (;;) {\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 callMethodName = (call: TSESTree.CallExpression): string | undefined =>\n  call.callee.type === 'MemberExpression' && call.callee.property.type === 'Identifier'\n    ? call.callee.property.name\n    : undefined\n\nconst chainHasMethod = (calls: TSESTree.CallExpression[], name: string): boolean =>\n  calls.some((c) => callMethodName(c) === name)\n\n// `.allow('')` / `.empty('')` only clear the rule when a bare `''` literal is among the arguments —\n// `.allow(null)` alone does not (confirmed empirically it still rejects `''`).\nconst isEmptyLiteralArg = (node: TSESTree.CallExpressionArgument): boolean =>\n  node.type === 'Literal' && node.value === ''\n\nconst chainHasEmptyStringClear = (calls: TSESTree.CallExpression[]): boolean =>\n  calls.some((c) => {\n    const name = callMethodName(c)\n    return (name === 'allow' || name === 'empty') && c.arguments.some(isEmptyLiteralArg)\n  })\n\n// Generic AST walk over a schema literal's subtree — does not skip nested function scopes, since a\n// schema literal itself is not expected to contain unrelated closures worth excluding, and the\n// simpler, uniform walk keeps this rule's detection surface exactly what it claims: \"anything written\n// literally inside this value.\"\nconst walkTree = (node: TSESTree.Node, visit: (node: TSESTree.Node) => void): void => {\n  visit(node)\n  for (const key of Object.keys(node)) {\n    if (key === 'parent') continue\n    const value = (node as unknown as Record<string, unknown>)[key]\n    const children = Array.isArray(value) ? value : [value]\n    for (const child of children) {\n      if (!child || typeof child !== 'object') continue\n      const childNode = child as TSESTree.Node\n      if (typeof childNode.type !== 'string') continue\n      walkTree(childNode, visit)\n    }\n  }\n}\n\n/**\n * ESLint rule: flags a `validator.string()` chain inside a `new Tool`/`new ArtifactTool`\n * `inputSchema` value that is `.optional()`/`.default(…)`-shaped with no empty-string disposition.\n */\nconst requireStringEmptyDispositionRule = createRule({\n  name: 'require-string-empty-disposition',\n  meta: {\n    type: 'problem',\n    docs: {\n      description:\n        \"A `validator.string()` chain inside a Tool/ArtifactTool `inputSchema` that is `.optional()`/`.default(…)`-shaped still rejects an empty string unless `.allow('')`, `.empty('')`, or a `.valid(...)` enum is also present.\",\n    },\n    schema: [],\n    messages: {\n      requireEmptyDisposition:\n        \"`validator.string()` combined with `.optional()`/`.default(...)` still rejects an empty string unless you also add `.allow('')` (or `.empty('')`, or a `.valid(...)` enum). A model filling in a tool call will often send `\\\"\\\"` instead of omitting an unwanted optional parameter — without an explicit disposition, that fails schema validation instead of degrading gracefully. If empty input should genuinely be rejected here, add `// eslint-disable-next-line adk/require-string-empty-disposition -- <reason>`.\",\n    },\n  },\n  defaultOptions: [],\n  create(context) {\n    const handleInputSchemaValue = (value: TSESTree.Node): void => {\n      walkTree(value, (node) => {\n        if (node.type !== 'CallExpression' || !isValidatorStringRootCall(node)) return\n        const calls = collectChainCalls(node)\n\n        // Out of scope entirely unless the chain is .optional()/.default(...)-shaped — a bare or\n        // .required()-only string is a different contract (\"you must give me a real value\") that\n        // this rule does not police.\n        if (!chainHasMethod(calls, 'optional') && !chainHasMethod(calls, 'default')) return\n\n        // Clearing methods, in the order documented above.\n        if (chainHasMethod(calls, 'forbidden')) return\n        if (chainHasMethod(calls, 'valid')) return // Policy A: any .valid(...) is sufficient on its own\n        if (chainHasEmptyStringClear(calls)) return\n\n        const reportNode = node.callee.type === 'MemberExpression' ? node.callee.property : node\n        context.report({ node: reportNode, messageId: 'requireEmptyDisposition' })\n      })\n    }\n\n    return {\n      NewExpression(node: TSESTree.NewExpression) {\n        if (node.callee.type !== 'Identifier') return\n        if (node.callee.name !== 'Tool' && node.callee.name !== 'ArtifactTool') return\n        const arg = node.arguments[0]\n        if (!arg || arg.type !== 'ObjectExpression') return\n\n        for (const p of arg.properties) {\n          if (p.type === 'SpreadElement') continue\n          if (literalKey(p) !== 'inputSchema') continue\n          handleInputSchemaValue(p.value)\n        }\n      },\n    }\n  },\n})\n\nexport default requireStringEmptyDispositionRule\n"],"mappings":"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"}