@sarj/eslint-plugin 15.1.0 → 15.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1010,11 +1010,11 @@ function isTrivialInitializer(node) {
1010
1010
  }
1011
1011
  function restatesStatementHead(body2, statement) {
1012
1012
  if (statement === null) return false;
1013
- const words = body2.match(/[A-Za-z][\w$]*/g) ?? [];
1014
- const opener = words[0];
1015
- if (opener === void 0 || words.length > NARRATION_MAX_WORDS) return false;
1013
+ const words2 = body2.match(/[A-Za-z][\w$]*/g) ?? [];
1014
+ const opener = words2[0];
1015
+ if (opener === void 0 || words2.length > NARRATION_MAX_WORDS) return false;
1016
1016
  if (!NARRATION_VERB_RE.test(opener)) return false;
1017
- const content = words.slice(1).map(normalizeToken).filter((word) => !NARRATION_STOPWORDS.has(word));
1017
+ const content = words2.slice(1).map(normalizeToken).filter((word) => !NARRATION_STOPWORDS.has(word));
1018
1018
  if (content.length < NARRATION_MIN_CONTENT) return false;
1019
1019
  const head = statement.split("(")[0] ?? statement;
1020
1020
  const code = headTokens(head);
@@ -1282,8 +1282,8 @@ function isRedundantNarration(body2, statementBelow, standalone, isolatedEnumera
1282
1282
  if (META_COMMENTARY_RE.test(t) && !justified) return true;
1283
1283
  if (isBareDeferral(t) && !justified) return true;
1284
1284
  if (HELPER_OPENER_RE.test(t) || LETS_RE.test(t)) return true;
1285
- const words = t.split(/\s+/);
1286
- if (words.length > 1 && words.length <= 4 && DUMMY_TRANSLATION_RE.test(t) && !/[():=]/.test(t)) {
1285
+ const words2 = t.split(/\s+/);
1286
+ if (words2.length > 1 && words2.length <= 4 && DUMMY_TRANSLATION_RE.test(t) && !/[():=]/.test(t)) {
1287
1287
  const lowerT = t.toLowerCase();
1288
1288
  if (!RATIONALE_WORDS.some((word) => lowerT.includes(word)) && restatesWholeStatement(t, statementBelow)) {
1289
1289
  return true;
@@ -1365,8 +1365,8 @@ function isWeakWalkthroughComment(body2, statement) {
1365
1365
  if (normalized.length === 0 || normalized.endsWith("?") || normalized.split(/\s+/).length > WALL_MAX_WORDS || isDirective(normalized) || isProtected(normalized) || !WALL_NARRATION_RE.test(normalized)) {
1366
1366
  return false;
1367
1367
  }
1368
- const words = contentTokens(normalized);
1369
- const described = words.slice(1);
1368
+ const words2 = contentTokens(normalized);
1369
+ const described = words2.slice(1);
1370
1370
  if (described.length === 0) return false;
1371
1371
  const code = codeTokens(statement);
1372
1372
  const matched = described.filter((word) => restates([word], code)).length;
@@ -2710,7 +2710,7 @@ var no_fat_try_blocks_default = createRule({
2710
2710
  const sourceCode = context.sourceCode;
2711
2711
  return {
2712
2712
  TryStatement(node) {
2713
- if (node.finalizer !== null) {
2713
+ if (node.finalizer !== null && node.handler === null) {
2714
2714
  return;
2715
2715
  }
2716
2716
  if (handlerRethrows(node.handler)) {
@@ -2950,15 +2950,32 @@ var noHandRolledSpinnerDocumentation = {
2950
2950
  rationale: "One-off loading indicators duplicate a shared primitive and let accessibility and styling diverge.",
2951
2951
  remediation: "Render the design-system Spinner component instead.",
2952
2952
  category: "maintainability",
2953
- limitations: ["Only static className values on div and span elements are inspected."],
2953
+ limitations: ["Only static className values on div and span elements are inspected; tests, stories, generated files, and the design-system implementation are excluded."],
2954
2954
  examples: [
2955
2955
  { id: "design-system-spinner", title: "Use the shared spinner", outcome: "no-match", files: [{ path: "src/loading-state.tsx", source: '<Spinner className="size-4" />' }], focusPath: "src/loading-state.tsx", expectedCount: 0, public: true },
2956
2956
  { id: "border-ring-spinner", title: "Do not rebuild a spinner", outcome: "match", files: [{ path: "src/loading-state.tsx", source: '<div className="size-4 animate-spin rounded-full border-2 border-t-transparent" />' }], focusPath: "src/loading-state.tsx", expectedCount: 1, public: true }
2957
2957
  ]
2958
2958
  };
2959
2959
  var DESIGN_SYSTEM_PATH = /(?:^|[/\\])components[/\\]ui[/\\]/u;
2960
- var BORDER_WIDTH = /^border(?:-[0-9]+)?$/u;
2961
- var CONTRASTING_EDGE = /^border-[trbl]-(?!0$|[0-9]+$).+/u;
2960
+ var DIRECTIONAL_BORDER = /^border-([trblsexy])-(.+)$/u;
2961
+ var CSS_LENGTH = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:cap|ch|cm|dvh|dvw|em|ex|ic|in|lh|lvh|lvw|mm|pc|pt|px|q|rcap|rch|rem|rex|ric|rlh|svh|svw|vb|vh|vi|vmax|vmin|vw|%)$/u;
2962
+ var ARBITRARY_LENGTH_FUNCTION = /^(?:calc|clamp|max|min)\(.+\)$/u;
2963
+ function isBorderWidthValue(value) {
2964
+ if (/^\d+$/u.test(value)) return true;
2965
+ if (value.startsWith("[") && value.endsWith("]")) {
2966
+ const arbitrary = value.slice(1, -1);
2967
+ const length = arbitrary.startsWith("length:") ? arbitrary.slice("length:".length) : arbitrary;
2968
+ return CSS_LENGTH.test(length) || ARBITRARY_LENGTH_FUNCTION.test(length) || arbitrary.startsWith("length:") && /^var\(.+\)$/u.test(length);
2969
+ }
2970
+ return value.startsWith("(length:") && value.endsWith(")") && value.length > "(length:)".length;
2971
+ }
2972
+ function isBorderWidth(token) {
2973
+ return token === "border" || token.startsWith("border-") && isBorderWidthValue(token.slice("border-".length));
2974
+ }
2975
+ function isContrastingEdge(token) {
2976
+ const match = DIRECTIONAL_BORDER.exec(token);
2977
+ return match?.[2] !== void 0 && !isBorderWidthValue(match[2]);
2978
+ }
2962
2979
  function staticClassName(attribute) {
2963
2980
  const value = attribute.value;
2964
2981
  if (value?.type === import_utils13.AST_NODE_TYPES.Literal && typeof value.value === "string") {
@@ -2987,7 +3004,7 @@ var no_hand_rolled_spinner_default = createRule({
2987
3004
  },
2988
3005
  defaultOptions: [],
2989
3006
  create(context) {
2990
- if (DESIGN_SYSTEM_PATH.test(context.filename)) {
3007
+ if (DESIGN_SYSTEM_PATH.test(context.filename) || isTestFile(context.filename) || isStoryFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
2991
3008
  return {};
2992
3009
  }
2993
3010
  return {
@@ -3002,7 +3019,7 @@ var no_hand_rolled_spinner_default = createRule({
3002
3019
  const className = staticClassName(classNameAttribute);
3003
3020
  if (className === null) return;
3004
3021
  const classes = className.split(/\s+/u);
3005
- if (classes.includes("animate-spin") && classes.includes("rounded-full") && classes.some((token) => BORDER_WIDTH.test(token)) && classes.some((token) => CONTRASTING_EDGE.test(token))) {
3022
+ if (classes.includes("animate-spin") && classes.includes("rounded-full") && classes.some(isBorderWidth) && classes.some(isContrastingEdge)) {
3006
3023
  context.report({ node, messageId: "handRolledSpinner" });
3007
3024
  }
3008
3025
  }
@@ -3068,9 +3085,9 @@ function nameWords(name) {
3068
3085
  return name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
3069
3086
  }
3070
3087
  function isStrongSecurityName(name) {
3071
- const words = nameWords(name);
3072
- return words.some((word) => STRONG_SECURITY_WORDS.has(word)) || words.some(
3073
- (word, index) => word === "api" && words[index + 1] === "key" || word === "auth" && words[index + 1] === "id" || word === "verification" && words[index + 1] === "code"
3088
+ const words2 = nameWords(name);
3089
+ return words2.some((word) => STRONG_SECURITY_WORDS.has(word)) || words2.some(
3090
+ (word, index) => word === "api" && words2[index + 1] === "key" || word === "auth" && words2[index + 1] === "id" || word === "verification" && words2[index + 1] === "code"
3074
3091
  );
3075
3092
  }
3076
3093
  function isNonSecurityName(name) {
@@ -3247,14 +3264,12 @@ var noJsonStringifyErrorDocumentation = {
3247
3264
  rationale: "Native Error details are non-enumerable, so generic JSON serialization discards diagnostic information.",
3248
3265
  remediation: "Serialize explicit error fields or use an error-aware serializer.",
3249
3266
  category: "correctness",
3250
- limitations: ["The rule uses local syntax and naming evidence rather than type information."],
3267
+ limitations: ["The rule uses local catch-binding and constructor provenance rather than type information."],
3251
3268
  examples: [
3252
3269
  { id: "explicit-error-message", title: "Serialize an enumerable error field", outcome: "no-match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err.message }); }" }], focusPath: "src/report.ts", expectedCount: 0, public: true },
3253
3270
  { id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true }
3254
3271
  ]
3255
3272
  };
3256
- var ERROR_NAME_PATTERN = /^(e|err|error|ex|exc)$/i;
3257
- var ERROR_PROP_PATTERN = /^(cause|lastError|error|err|exception|originalError|innerError)$/i;
3258
3273
  var SAFE_STRING_PROPS = /* @__PURE__ */ new Set(["message", "stack", "name"]);
3259
3274
  var PAYLOAD_PROPS = /* @__PURE__ */ new Set([
3260
3275
  "data",
@@ -3282,6 +3297,21 @@ var BUILTIN_ERROR_CONSTRUCTORS = /* @__PURE__ */ new Set([
3282
3297
  "TypeError",
3283
3298
  "URIError"
3284
3299
  ]);
3300
+ function identifierIsProvenError(identifier, scope) {
3301
+ if (isCatchBinding(scope, identifier.name)) return true;
3302
+ let current = scope;
3303
+ while (current !== null && !current.set.has(identifier.name)) {
3304
+ current = current.upper;
3305
+ }
3306
+ const variable = current?.set.get(identifier.name);
3307
+ if (variable === void 0 || variable.defs.length !== 1) return false;
3308
+ const definition = variable.defs[0];
3309
+ if (definition?.type !== "Variable") return false;
3310
+ const initializer = definition.node.init;
3311
+ return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && variable.references.every(
3312
+ (reference) => !reference.isWrite() || reference.init === true
3313
+ );
3314
+ }
3285
3315
  function isCatchBinding(scope, name) {
3286
3316
  let current = scope;
3287
3317
  while (current) {
@@ -3297,22 +3327,6 @@ function isCatchBinding(scope, name) {
3297
3327
  }
3298
3328
  return false;
3299
3329
  }
3300
- function memberSuggestsError(member, scope) {
3301
- const propName2 = !member.computed && member.property.type === "Identifier" ? member.property.name : null;
3302
- if (propName2 !== null && ERROR_PROP_PATTERN.test(propName2)) {
3303
- return true;
3304
- }
3305
- const base = member.object;
3306
- const baseSuggestsError = base.type === "Identifier" && (ERROR_NAME_PATTERN.test(base.name) || isCatchBinding(scope, base.name));
3307
- if (baseSuggestsError) {
3308
- if (propName2 === null) {
3309
- return true;
3310
- }
3311
- const lowered = propName2.toLowerCase();
3312
- return !SAFE_STRING_PROPS.has(lowered) && !PAYLOAD_PROPS.has(lowered);
3313
- }
3314
- return false;
3315
- }
3316
3330
  function positiveErrorSubject(test) {
3317
3331
  return instanceofErrorSubject(test) ?? typeGuardSubject(test);
3318
3332
  }
@@ -3416,27 +3430,25 @@ function directLiteralValues(argument) {
3416
3430
  }
3417
3431
  function expressionSuggestsError(expression, scope) {
3418
3432
  if (expression.type === "Identifier") {
3419
- return ERROR_NAME_PATTERN.test(expression.name) || isCatchBinding(scope, expression.name);
3433
+ return identifierIsProvenError(expression, scope);
3434
+ }
3435
+ if (expression.type === "NewExpression" && expression.callee.type === "Identifier") {
3436
+ return BUILTIN_ERROR_CONSTRUCTORS.has(expression.callee.name);
3420
3437
  }
3421
3438
  return expression.type === "MemberExpression" && memberSuggestsError(expression, scope);
3422
3439
  }
3423
- function nestedExpressionSuggestsError(expression, scope) {
3424
- if (expression.type === "Identifier") {
3425
- if (isCatchBinding(scope, expression.name)) return true;
3426
- let current = scope;
3427
- while (current !== null && !current.set.has(expression.name)) {
3428
- current = current.upper;
3429
- }
3430
- const variable = current?.set.get(expression.name);
3431
- if (variable === void 0 || variable.defs.length !== 1) return false;
3432
- const definition = variable.defs[0];
3433
- if (definition?.type !== "Variable") return false;
3434
- const initializer = definition.node.init;
3435
- return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && variable.references.every(
3436
- (reference) => !reference.isWrite() || reference.init === true
3437
- );
3440
+ function memberSuggestsError(member, scope) {
3441
+ const propName2 = !member.computed && member.property.type === "Identifier" ? member.property.name : null;
3442
+ const base = member.object;
3443
+ const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, scope);
3444
+ if (baseSuggestsError) {
3445
+ if (propName2 === null) {
3446
+ return true;
3447
+ }
3448
+ const lowered = propName2.toLowerCase();
3449
+ return !SAFE_STRING_PROPS.has(lowered) && !PAYLOAD_PROPS.has(lowered);
3438
3450
  }
3439
- return expression.type === "MemberExpression" && memberSuggestsError(expression, scope);
3451
+ return false;
3440
3452
  }
3441
3453
  var no_json_stringify_error_default = createRule({
3442
3454
  name: "no-json-stringify-error",
@@ -3463,9 +3475,8 @@ var no_json_stringify_error_default = createRule({
3463
3475
  return;
3464
3476
  }
3465
3477
  const scope = context.sourceCode.getScope(firstArg);
3466
- const isNestedLiteral = firstArg.type === "ObjectExpression" || firstArg.type === "ArrayExpression";
3467
3478
  const unsafeValue = directLiteralValues(firstArg).find(
3468
- (value) => (isNestedLiteral ? nestedExpressionSuggestsError(value, scope) : expressionSuggestsError(value, scope)) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
3479
+ (value) => expressionSuggestsError(value, scope) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
3469
3480
  );
3470
3481
  if (unsafeValue === void 0) {
3471
3482
  return;
@@ -4032,7 +4043,7 @@ var VALUE_TAG_RE = /@(example|deprecated|see|remarks|throws|internal|public|alph
4032
4043
  var BOUNDARY_RE = /(?<=[.!?])["'`)\]]*\s+(?=[A-Z0-9`])/;
4033
4044
  var BULLET_RE = /^\s*(?:[-*+] |\d+[.)] )/;
4034
4045
  var HEADING_RE = /^[A-Za-z][A-Za-z ]+:$/;
4035
- var TECHNICAL_ANCHOR_RE = /https?:\/\/|`[^`\n]+`|:[a-z][a-z0-9_-]*:|(["'])[^"'\n]+\1|\d|\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|\b[\w.-]+\.(?:py|pyi|js|jsx|ts|tsx|json|ya?ml|toml|csv|parquet|md)\b|->|=>|==|!=|<=|>=|\|/mu;
4046
+ var TECHNICAL_ANCHOR_RE = /https?:\/\/|`[^`\n]+`|:[a-z][a-z0-9_-]*:|(["'])[^"'\n]+\1|\bv?\d+\.\d+(?:\.\d+)?\b|\b\d+(?:\.\d+)?\s?(?:ns|us|ms|s|sec|secs|seconds?|mins?|minutes?|hours?|days?|bytes?|kib|mib|gib|kb|mb|gb|hz|khz|mhz|px|%)\b|\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b|\b[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+\b|(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|\b[\w.-]+\.(?:py|pyi|js|jsx|ts|tsx|json|ya?ml|toml|csv|parquet|md)\b|->|=>|==|!=|<=|>=|\|/mu;
4036
4047
  function body(comment) {
4037
4048
  return comment.value.replace(/^\*/, "").split("\n").map((line) => line.replace(/^\s*\*?\s?/, "")).join("\n").trim();
4038
4049
  }
@@ -4924,6 +4935,7 @@ var ANALYTICS_SEGMENTS2 = /* @__PURE__ */ new Set([
4924
4935
  ]);
4925
4936
  var SERVER_ACTION_SKIP_FILE_RE = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
4926
4937
  var NON_REACT_FRAMEWORK_RE = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
4938
+ var NEXT_MODULE_PATH_RE = /(?:^|[/\\])(?:app|pages)[/\\]/u;
4927
4939
  function isGlobalFetchCall(node, resolvesToGlobal) {
4928
4940
  const callee = node.callee;
4929
4941
  if (callee.type === "Identifier") {
@@ -5039,6 +5051,13 @@ var no_raw_fetch_outside_clients_default = createRule({
5039
5051
  const nonReactFramework = context.sourceCode.ast.body.some(
5040
5052
  (statement) => statement.type === import_utils25.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && NON_REACT_FRAMEWORK_RE.test(statement.source.value)
5041
5053
  );
5054
+ const hasUseClientDirective = context.sourceCode.ast.body.some(
5055
+ (statement) => statement.type === import_utils25.AST_NODE_TYPES.ExpressionStatement && statement.expression.type === import_utils25.AST_NODE_TYPES.Literal && statement.expression.value === "use client"
5056
+ );
5057
+ const hasNextImport = context.sourceCode.ast.body.some(
5058
+ (statement) => statement.type === import_utils25.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "next" || statement.source.value.startsWith("next/"))
5059
+ );
5060
+ const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE.test(filename);
5042
5061
  function resolvesToGlobal(identifier) {
5043
5062
  const variable = import_utils25.ASTUtils.findVariable(
5044
5063
  context.sourceCode.getScope(identifier),
@@ -5099,7 +5118,7 @@ var no_raw_fetch_outside_clients_default = createRule({
5099
5118
  return resolved?.type === import_utils25.AST_NODE_TYPES.LogicalExpression && resolved.operator === "||" && (isMutationMethod2(resolved.left) || isMutationMethod2(resolved.right));
5100
5119
  }
5101
5120
  function serverActionOwns(node) {
5102
- if (node.callee.type !== import_utils25.AST_NODE_TYPES.Identifier || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
5121
+ if (node.callee.type !== import_utils25.AST_NODE_TYPES.Identifier || !hasNextEvidence || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
5103
5122
  return false;
5104
5123
  }
5105
5124
  const url = node.arguments[0];
@@ -6318,6 +6337,20 @@ var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
6318
6337
  function isBodyDecodeNode(node) {
6319
6338
  return node.type === import_utils32.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils32.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils32.AST_NODE_TYPES.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
6320
6339
  }
6340
+ function isSafeParseSupportCall(node) {
6341
+ const callee = node.callee;
6342
+ if (callee.type !== import_utils32.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils32.AST_NODE_TYPES.Identifier) {
6343
+ return false;
6344
+ }
6345
+ if (callee.property.name === "isArray" && callee.object.type === import_utils32.AST_NODE_TYPES.Identifier && callee.object.name === "Array") {
6346
+ return true;
6347
+ }
6348
+ if (callee.property.name !== "getItem") return false;
6349
+ if (callee.object.type === import_utils32.AST_NODE_TYPES.Identifier && (callee.object.name === "localStorage" || callee.object.name === "sessionStorage")) {
6350
+ return true;
6351
+ }
6352
+ return callee.object.type === import_utils32.AST_NODE_TYPES.MemberExpression && !callee.object.computed && callee.object.object.type === import_utils32.AST_NODE_TYPES.Identifier && (callee.object.object.name === "window" || callee.object.object.name === "globalThis") && callee.object.property.type === import_utils32.AST_NODE_TYPES.Identifier && (callee.object.property.name === "localStorage" || callee.object.property.name === "sessionStorage");
6353
+ }
6321
6354
  var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
6322
6355
  "json",
6323
6356
  "text",
@@ -6386,6 +6419,7 @@ function tryReturnsSafeParse(catchNode) {
6386
6419
  if (current.type === import_utils32.AST_NODE_TYPES.CallExpression || current.type === import_utils32.AST_NODE_TYPES.NewExpression) {
6387
6420
  if (isParseShapedNode(current) || isBodyDecodeNode(current)) {
6388
6421
  sawSafeParse = true;
6422
+ } else if (current.type === import_utils32.AST_NODE_TYPES.CallExpression && isSafeParseSupportCall(current)) {
6389
6423
  } else {
6390
6424
  sawUnsafeOperation = true;
6391
6425
  return;
@@ -6848,22 +6882,25 @@ var noStorageInStatelessModulesDocumentation = {
6848
6882
  rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
6849
6883
  remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
6850
6884
  category: "architecture",
6851
- limitations: ["The rule is disabled until module path patterns are configured and recognizes only configured storage method names."],
6885
+ limitations: ["The rule is disabled until module path patterns are configured, recognizes only configured storage method names, and requires storage-like receiver evidence for the overloaded `put` method."],
6852
6886
  examples: [
6853
6887
  { id: "system-of-record", title: "Read from the system of record", outcome: "no-match", files: [{ path: "src/engineer-digest/post.ts", source: "const issues = await linear.listIssues();" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 0, public: true },
6854
6888
  { id: "private-storage", title: "Do not write private state in a stateless module", outcome: "match", files: [{ path: "src/engineer-digest/post.ts", source: "await kv.put('digest:last', timestamp);" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 1, public: true }
6855
6889
  ]
6856
6890
  };
6857
6891
  function compile2(patterns) {
6858
- const compiled = [];
6859
- for (const pattern of patterns) {
6860
- try {
6861
- compiled.push(new RegExp(pattern));
6862
- } catch {
6863
- }
6864
- }
6865
- return compiled;
6892
+ return patterns.map((pattern) => new RegExp(pattern));
6866
6893
  }
6894
+ var STORAGE_RECEIVER_WORDS = /* @__PURE__ */ new Set([
6895
+ "bucket",
6896
+ "cache",
6897
+ "kv",
6898
+ "namespace",
6899
+ "r2",
6900
+ "redis",
6901
+ "storage",
6902
+ "store"
6903
+ ]);
6867
6904
  function storageMethodName(node, methods) {
6868
6905
  const callee = node.callee;
6869
6906
  if (callee.type !== import_utils35.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils35.AST_NODE_TYPES.Identifier) {
@@ -6876,8 +6913,31 @@ function storageMethodName(node, methods) {
6876
6913
  if (node.arguments.length < (MIN_ARGUMENTS.get(name) ?? 1)) {
6877
6914
  return null;
6878
6915
  }
6916
+ if (name === "put" && !isStorageLikeReceiver(callee.object)) {
6917
+ return null;
6918
+ }
6879
6919
  return name;
6880
6920
  }
6921
+ function isStorageLikeReceiver(node) {
6922
+ if (node.type === import_utils35.AST_NODE_TYPES.Identifier) {
6923
+ return isStorageIdentifier(node.name);
6924
+ }
6925
+ if (node.type !== import_utils35.AST_NODE_TYPES.MemberExpression) {
6926
+ return false;
6927
+ }
6928
+ if (!node.computed && node.property.type === import_utils35.AST_NODE_TYPES.Identifier && isStorageIdentifier(node.property.name)) {
6929
+ return true;
6930
+ }
6931
+ return isStorageLikeReceiver(node.object);
6932
+ }
6933
+ function isStorageIdentifier(name) {
6934
+ return identifierWords(name).some(
6935
+ (word) => STORAGE_RECEIVER_WORDS.has(word.toLowerCase())
6936
+ );
6937
+ }
6938
+ function identifierWords(name) {
6939
+ return name.match(/[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)|\d+/gu) ?? [name];
6940
+ }
6881
6941
  var no_storage_in_stateless_modules_default = createRule({
6882
6942
  name: "no-storage-in-stateless-modules",
6883
6943
  documentation: noStorageInStatelessModulesDocumentation,
@@ -7279,12 +7339,86 @@ var no_tautological_expect_default = createRule({
7279
7339
  });
7280
7340
 
7281
7341
  // src/rules/no-typed-doc-sections.ts
7342
+ var TYPED_TAG_RE2 = /^\s*@(arg|argument|param|return|returns|yield|yields)\b(.*)$/iu;
7343
+ var PARAM_TAGS2 = /* @__PURE__ */ new Set(["arg", "argument", "param"]);
7344
+ var PARAMETER_FILLER = /* @__PURE__ */ new Set([
7345
+ "a",
7346
+ "an",
7347
+ "argument",
7348
+ "given",
7349
+ "input",
7350
+ "parameter",
7351
+ "passed",
7352
+ "provided",
7353
+ "the",
7354
+ "value"
7355
+ ]);
7356
+ var RESULT_FILLER = /* @__PURE__ */ new Set([
7357
+ "a",
7358
+ "an",
7359
+ "array",
7360
+ "boolean",
7361
+ "generator",
7362
+ "number",
7363
+ "object",
7364
+ "output",
7365
+ "promise",
7366
+ "result",
7367
+ "return",
7368
+ "returned",
7369
+ "returns",
7370
+ "string",
7371
+ "the",
7372
+ "value"
7373
+ ]);
7374
+ function hasVacuousTypedTag(text) {
7375
+ const tags = typedTags(text);
7376
+ return tags.length > 0 && tags.some(isVacuousTag);
7377
+ }
7378
+ function typedTags(text) {
7379
+ const tags = [];
7380
+ for (const raw of text.split("\n")) {
7381
+ const match = TYPED_TAG_RE2.exec(raw);
7382
+ if (match !== null) {
7383
+ tags.push({ kind: (match[1] ?? "").toLowerCase(), payload: (match[2] ?? "").trim() });
7384
+ } else if (tags.length > 0 && raw.trim().length > 0 && !raw.trim().startsWith("@")) {
7385
+ const last = tags.at(-1);
7386
+ last.payload = `${last.payload} ${raw.trim()}`.trim();
7387
+ }
7388
+ }
7389
+ return tags.map(({ kind, payload }) => {
7390
+ let rest = payload.replace(/^\{[^}\n]+\}\s*/u, "").trim();
7391
+ if (!PARAM_TAGS2.has(kind)) {
7392
+ return { kind, name: null, description: rest.replace(/^-\s*/u, "").trim() };
7393
+ }
7394
+ const match = /^(\[[^\]]+\]|[A-Za-z_$][\w$.[\]-]*)(?:\s+-\s*|\s+)?(.*)$/u.exec(rest);
7395
+ if (match === null) return { kind, name: null, description: "" };
7396
+ const rawName = (match[1] ?? "").replace(/^\[/u, "").replace(/\]$/u, "").split("=")[0] ?? "";
7397
+ rest = (match[2] ?? "").trim();
7398
+ return { kind, name: rawName, description: rest };
7399
+ });
7400
+ }
7401
+ function isVacuousTag(tag) {
7402
+ const description = words(tag.description).map(canonicalWord);
7403
+ if (description.length === 0) return true;
7404
+ if (tag.name === null) return description.every((word) => RESULT_FILLER.has(word));
7405
+ const nameWords2 = new Set(words(tag.name).map(canonicalWord));
7406
+ return description.every((word) => PARAMETER_FILLER.has(word) || nameWords2.has(word));
7407
+ }
7408
+ function words(text) {
7409
+ return text.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").toLowerCase().match(/[a-z][a-z0-9]*/gu) ?? [];
7410
+ }
7411
+ function canonicalWord(word) {
7412
+ if (["identifier", "identifiers", "ids"].includes(word)) return "id";
7413
+ if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
7414
+ return word;
7415
+ }
7282
7416
  var noTypedDocSectionsDocumentation = {
7283
7417
  summary: "Reject typed-signature repetition while preserving behavior that types cannot express.",
7284
7418
  rationale: "Parameter and return tags repeat typed signatures and can drift without adding runtime behavior or constraints.",
7285
7419
  remediation: "Remove repeated parameter and return tags; retain documentation for behavior, failures, and external contracts.",
7286
7420
  category: "maintainability",
7287
- limitations: ["Parameter and return tags are reported only when the documented function has corresponding explicit TypeScript types."],
7421
+ limitations: ["Description-free or name-restating parameter and return tags are reported only when the documented function has corresponding explicit TypeScript types."],
7288
7422
  examples: [
7289
7423
  {
7290
7424
  id: "behavioral-documentation",
@@ -7322,7 +7456,7 @@ var no_typed_doc_sections_default = createRule({
7322
7456
  return {
7323
7457
  Program() {
7324
7458
  for (const group of proseGroups(context.filename, context.sourceCode, true)) {
7325
- if (group.hasTypedTags && documentsTypedFunction(context.sourceCode, group.comment)) {
7459
+ if (group.hasTypedTags && hasVacuousTypedTag(group.text) && documentsTypedFunction(context.sourceCode, group.comment)) {
7326
7460
  context.report({ node: group.comment, messageId: "typedSection" });
7327
7461
  }
7328
7462
  }
@@ -7423,28 +7557,32 @@ var STOPWORDS3 = /* @__PURE__ */ new Set([
7423
7557
  "with"
7424
7558
  ]);
7425
7559
  var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7560
+ var UNIT_NAME_SUFFIX_RE = /(?:_(?:NS|US|MS|S|SEC|SECS|SECOND|SECONDS|MIN|MINS|MINUTE|MINUTES|HOUR|HOURS|DAY|DAYS|BYTE|BYTES|KB|MB|GB|HZ|KHZ|MHZ|PX)|(?:Ns|Us|Ms|Sec|Secs|Second|Seconds|Min|Mins|Minute|Minutes|Hour|Hours|Day|Days|Byte|Bytes|Kb|Mb|Gb|Hz|Khz|Mhz|Px))$/u;
7426
7561
  function narratesValue(body2, code) {
7427
7562
  if (body2.length === 0 || DIRECTIVE_RE5.test(body2) || hasExternalReference(body2)) return false;
7428
7563
  const codeNumbers = numbersIn(code);
7429
7564
  if (codeNumbers.size === 0) return false;
7430
- const words = (body2.match(WORD_RE3) ?? []).map((word) => word.toLowerCase());
7431
- if (words.length === 0) return false;
7565
+ const words2 = (body2.match(WORD_RE3) ?? []).map((word) => word.toLowerCase());
7566
+ if (words2.length === 0) return false;
7432
7567
  const commentNumbers = numbersIn(body2);
7433
7568
  if (commentNumbers.size === 0) return false;
7434
7569
  for (const number of commentNumbers) {
7435
7570
  if (!codeNumbers.has(number)) return false;
7436
7571
  }
7437
- if (!words.some((word) => UNIT_WORDS.has(word))) return false;
7572
+ if (!words2.some((word) => UNIT_WORDS.has(word))) return false;
7438
7573
  const identifiers = codeTokens(code);
7439
7574
  const stems = /* @__PURE__ */ new Set();
7440
7575
  for (const token of identifiers) stems.add(stem(token));
7441
- return words.every(
7576
+ return words2.every(
7442
7577
  (word) => STOPWORDS3.has(word) || UNIT_WORDS.has(word) || commentNumbers.has(word) || identifiers.has(word) || stems.has(stem(word))
7443
7578
  );
7444
7579
  }
7445
7580
  function numbersIn(text) {
7446
7581
  return new Set(text.match(NUMBER_RE) ?? []);
7447
7582
  }
7583
+ function nameAlreadyCarriesUnit(code) {
7584
+ return (code.match(/[A-Za-z_$][\w$]*/gu) ?? []).some((identifier) => UNIT_NAME_SUFFIX_RE.test(identifier));
7585
+ }
7448
7586
  var no_trailing_value_narration_default = createRule({
7449
7587
  name: "no-trailing-value-narration",
7450
7588
  documentation: noTrailingValueNarrationDocumentation,
@@ -7455,6 +7593,7 @@ var no_trailing_value_narration_default = createRule({
7455
7593
  },
7456
7594
  schema: [],
7457
7595
  messages: {
7596
+ deleteNarration: "Trailing comment restates the literal and the identifier already names its unit \u2014 delete the comment so it cannot drift.",
7458
7597
  narratesValue: "Trailing comment restates the literal on this line \u2014 put the unit in the name (STALE_TIME_MS) so it cannot drift."
7459
7598
  }
7460
7599
  },
@@ -7487,7 +7626,10 @@ var no_trailing_value_narration_default = createRule({
7487
7626
  const code = line.slice(0, comment.loc.start.column);
7488
7627
  const body2 = comment.value.replace(/^\*+/, "").replace(/\*+$/, "").trim();
7489
7628
  if (narratesValue(body2, code)) {
7490
- context.report({ node: comment, messageId: "narratesValue" });
7629
+ context.report({
7630
+ node: comment,
7631
+ messageId: nameAlreadyCarriesUnit(code) ? "deleteNarration" : "narratesValue"
7632
+ });
7491
7633
  }
7492
7634
  }
7493
7635
  }
@@ -8528,15 +8670,23 @@ var no_zod_native_enum_default = createRule({
8528
8670
  } catch {
8529
8671
  services = null;
8530
8672
  }
8531
- const zodImportedNames = /* @__PURE__ */ new Map();
8532
- const zodNamespaces = /* @__PURE__ */ new Set();
8673
+ const zodImportedBindings = /* @__PURE__ */ new Map();
8674
+ const zodNamespaceBindings = /* @__PURE__ */ new Set();
8675
+ function resolvedBinding(identifier) {
8676
+ return import_utils45.ASTUtils.findVariable(
8677
+ sourceCode.getScope(identifier),
8678
+ identifier.name
8679
+ );
8680
+ }
8533
8681
  function isZodMemberCall(node, api) {
8534
8682
  const callee = node.callee;
8535
- if (callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && zodNamespaces.has(callee.object.name) && callee.property.type === import_utils45.AST_NODE_TYPES.Identifier) {
8536
- return callee.property.name === api;
8683
+ if (callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && callee.property.type === import_utils45.AST_NODE_TYPES.Identifier) {
8684
+ const binding = resolvedBinding(callee.object);
8685
+ return binding !== null && zodNamespaceBindings.has(binding) && callee.property.name === api;
8537
8686
  }
8538
8687
  if (callee.type === import_utils45.AST_NODE_TYPES.Identifier) {
8539
- return zodImportedNames.get(callee.name) === api;
8688
+ const binding = resolvedBinding(callee);
8689
+ return binding !== null && zodImportedBindings.get(binding) === api;
8540
8690
  }
8541
8691
  return false;
8542
8692
  }
@@ -8571,10 +8721,14 @@ var no_zod_native_enum_default = createRule({
8571
8721
  }
8572
8722
  for (const spec of node.specifiers) {
8573
8723
  if (spec.type === import_utils45.AST_NODE_TYPES.ImportNamespaceSpecifier || spec.type === import_utils45.AST_NODE_TYPES.ImportDefaultSpecifier || spec.type === import_utils45.AST_NODE_TYPES.ImportSpecifier && (spec.imported.type === import_utils45.AST_NODE_TYPES.Identifier ? spec.imported.name === "z" : spec.imported.value === "z")) {
8574
- zodNamespaces.add(spec.local.name);
8724
+ const binding = resolvedBinding(spec.local);
8725
+ if (binding !== null) zodNamespaceBindings.add(binding);
8575
8726
  }
8576
8727
  if (spec.type === import_utils45.AST_NODE_TYPES.ImportSpecifier && spec.imported.type === import_utils45.AST_NODE_TYPES.Identifier) {
8577
- zodImportedNames.set(spec.local.name, spec.imported.name);
8728
+ const binding = resolvedBinding(spec.local);
8729
+ if (binding !== null) {
8730
+ zodImportedBindings.set(binding, spec.imported.name);
8731
+ }
8578
8732
  }
8579
8733
  }
8580
8734
  },
@@ -10789,7 +10943,70 @@ var bindingValidationPolarity = (test, bindingName) => {
10789
10943
  }
10790
10944
  return test.type === import_utils57.AST_NODE_TYPES.CallExpression && test.arguments.length === 1 && test.arguments[0]?.type === import_utils57.AST_NODE_TYPES.Identifier && test.arguments[0].name === bindingName && test.callee.type === import_utils57.AST_NODE_TYPES.MemberExpression && !test.callee.computed && test.callee.object.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.object.name === "Array" && test.callee.property.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.property.name === "isArray" ? "valid-when-true" : null;
10791
10945
  };
10946
+ var plainMemberAccess = (node) => node.type === import_utils57.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils57.AST_NODE_TYPES.Identifier && node.property.type === import_utils57.AST_NODE_TYPES.Identifier ? { object: node.object.name, property: node.property.name } : null;
10947
+ var isSamePlainMember = (node, access) => {
10948
+ const candidate = plainMemberAccess(node);
10949
+ return candidate !== null && candidate.object === access.object && candidate.property === access.property;
10950
+ };
10792
10951
  var nodeWithin2 = (node, container) => node.range[0] >= container.range[0] && node.range[1] <= container.range[1];
10952
+ var isUseWithinValidatedBranch = (node, bindingName) => {
10953
+ for (let current = node.parent; current !== void 0 && current !== null; current = current.parent) {
10954
+ if (current.type === import_utils57.AST_NODE_TYPES.ConditionalExpression) {
10955
+ const polarity = bindingValidationPolarity(current.test, bindingName);
10956
+ if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && nodeWithin2(node, current.alternate)) {
10957
+ return true;
10958
+ }
10959
+ }
10960
+ if (current.type === import_utils57.AST_NODE_TYPES.IfStatement) {
10961
+ const polarity = bindingValidationPolarity(current.test, bindingName);
10962
+ if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && current.alternate !== null && nodeWithin2(node, current.alternate)) {
10963
+ return true;
10964
+ }
10965
+ }
10966
+ if (current.type === import_utils57.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils57.AST_NODE_TYPES.FunctionExpression || current.type === import_utils57.AST_NODE_TYPES.ArrowFunctionExpression) {
10967
+ return false;
10968
+ }
10969
+ }
10970
+ return false;
10971
+ };
10972
+ var isMemberUseWithinValidatedBranch = (node, access) => {
10973
+ for (let current = node.parent; current !== void 0 && current !== null; current = current.parent) {
10974
+ if (current.type === import_utils57.AST_NODE_TYPES.ConditionalExpression) {
10975
+ const polarity = memberValidationPolarity(current.test, access);
10976
+ if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && nodeWithin2(node, current.alternate)) {
10977
+ return true;
10978
+ }
10979
+ }
10980
+ if (current.type === import_utils57.AST_NODE_TYPES.IfStatement) {
10981
+ const polarity = memberValidationPolarity(current.test, access);
10982
+ if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && current.alternate !== null && nodeWithin2(node, current.alternate)) {
10983
+ return true;
10984
+ }
10985
+ }
10986
+ if (current.type === import_utils57.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils57.AST_NODE_TYPES.FunctionExpression || current.type === import_utils57.AST_NODE_TYPES.ArrowFunctionExpression) {
10987
+ return false;
10988
+ }
10989
+ }
10990
+ return false;
10991
+ };
10992
+ var memberValidationPolarity = (test, access) => {
10993
+ if (test.type === import_utils57.AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
10994
+ const inner = memberValidationPolarity(test.argument, access);
10995
+ return inner === "valid-when-true" ? "valid-when-false" : inner === "valid-when-false" ? "valid-when-true" : null;
10996
+ }
10997
+ if (test.type === import_utils57.AST_NODE_TYPES.BinaryExpression) {
10998
+ const isMatchingTypeof = (node) => node.type === import_utils57.AST_NODE_TYPES.UnaryExpression && node.operator === "typeof" && isSamePlainMember(node.argument, access);
10999
+ const isPrimitiveType = (node) => node.type === import_utils57.AST_NODE_TYPES.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
11000
+ if (!(isMatchingTypeof(test.left) && isPrimitiveType(test.right) || isMatchingTypeof(test.right) && isPrimitiveType(test.left))) {
11001
+ return null;
11002
+ }
11003
+ if (test.operator === "===" || test.operator === "==") {
11004
+ return "valid-when-true";
11005
+ }
11006
+ return test.operator === "!==" || test.operator === "!=" ? "valid-when-false" : null;
11007
+ }
11008
+ return test.type === import_utils57.AST_NODE_TYPES.CallExpression && test.arguments.length === 1 && test.arguments[0] !== void 0 && test.arguments[0].type !== import_utils57.AST_NODE_TYPES.SpreadElement && isSamePlainMember(test.arguments[0], access) && test.callee.type === import_utils57.AST_NODE_TYPES.MemberExpression && !test.callee.computed && test.callee.object.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.object.name === "Array" && test.callee.property.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.property.name === "isArray" ? "valid-when-true" : null;
11009
+ };
10793
11010
  var isFullyValidatedExtractedBinding = (member, source, context) => {
10794
11011
  const isValidationReference = (identifier) => {
10795
11012
  for (let current = identifier.parent; current !== void 0 && current !== null; current = current.parent) {
@@ -11059,7 +11276,14 @@ var prefer_schema_for_api_payload_default = createRule({
11059
11276
  return;
11060
11277
  }
11061
11278
  const variable = obj?.type === import_utils57.AST_NODE_TYPES.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
11062
- if (variable !== null) {
11279
+ if (variable !== null && obj?.type === import_utils57.AST_NODE_TYPES.Identifier) {
11280
+ if (isUseWithinValidatedBranch(node, obj.name)) {
11281
+ return;
11282
+ }
11283
+ const access = plainMemberAccess(node);
11284
+ if (access !== null && isMemberUseWithinValidatedBranch(node, access)) {
11285
+ return;
11286
+ }
11063
11287
  if (isFullyValidatedExtractedBinding(node, variable, context)) {
11064
11288
  return;
11065
11289
  }
@@ -11511,16 +11735,17 @@ var preferServerActionsDocumentation = {
11511
11735
  rationale: "Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.",
11512
11736
  remediation: "Move the mutation into a Server Action and invoke that action from the React client.",
11513
11737
  category: "architecture",
11514
- limitations: ["Only statically recognizable /api/ mutations in applicable React modules are reported."],
11738
+ limitations: ["Only statically recognizable /api/ mutations in modules with positive Next.js evidence are reported: an explicit next import, or an app/pages path with a top-level use-client directive."],
11515
11739
  examples: [
11516
11740
  { id: "server-action-call", title: "Call a Server Action", outcome: "no-match", files: [{ path: "app/tasks/page.tsx", source: "import { createTask } from './actions'; await createTask(input);" }], focusPath: "app/tasks/page.tsx", expectedCount: 0, public: true },
11517
- { id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
11741
+ { id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "'use client'; await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
11518
11742
  ]
11519
11743
  };
11520
11744
  var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
11521
11745
  var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
11522
11746
  var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
11523
11747
  var NON_REACT_FRAMEWORK_RE2 = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
11748
+ var NEXT_MODULE_PATH_RE2 = /(?:^|[/\\])(?:app|pages)[/\\]/u;
11524
11749
  function getScope(context, node) {
11525
11750
  return context.sourceCode.getScope(node);
11526
11751
  }
@@ -11634,6 +11859,16 @@ var prefer_server_actions_default = createRule({
11634
11859
  const isNonReactFramework = context.sourceCode.ast.body.some(
11635
11860
  (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && NON_REACT_FRAMEWORK_RE2.test(node.source.value)
11636
11861
  );
11862
+ const hasUseClientDirective = context.sourceCode.ast.body.some(
11863
+ (node) => node.type === "ExpressionStatement" && node.expression.type === "Literal" && node.expression.value === "use client"
11864
+ );
11865
+ const hasNextImport = context.sourceCode.ast.body.some(
11866
+ (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "next" || node.source.value.startsWith("next/"))
11867
+ );
11868
+ const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE2.test(filename);
11869
+ if (!hasNextEvidence) {
11870
+ return {};
11871
+ }
11637
11872
  return {
11638
11873
  CallExpression(node) {
11639
11874
  if (isNonReactFramework) return;
@@ -12356,6 +12591,9 @@ var requireAssertNeverDocumentation = {
12356
12591
  };
12357
12592
  var isRuntimeHandlingStatement = (statement) => {
12358
12593
  if (statement.type === import_utils62.AST_NODE_TYPES.EmptyStatement) return false;
12594
+ if (statement.type === import_utils62.AST_NODE_TYPES.BreakStatement) {
12595
+ return statement.label !== null;
12596
+ }
12359
12597
  if (statement.type === import_utils62.AST_NODE_TYPES.TSTypeAliasDeclaration || statement.type === import_utils62.AST_NODE_TYPES.TSInterfaceDeclaration) {
12360
12598
  return false;
12361
12599
  }
@@ -12385,7 +12623,12 @@ function isExhaustiveFiniteSwitch(node, services) {
12385
12623
  const discriminant = services.esTreeNodeToTSNodeMap.get(node.discriminant);
12386
12624
  const discriminantType = checker.getTypeAtLocation(discriminant);
12387
12625
  const constituents = discriminantType.isUnion() ? discriminantType.types : [discriminantType];
12388
- if (constituents.length === 0) return false;
12626
+ if (!discriminantType.isUnion() || constituents.length < 2) return false;
12627
+ if (constituents.every(
12628
+ (constituent) => (constituent.flags & import_typescript.default.TypeFlags.BooleanLiteral) !== 0
12629
+ )) {
12630
+ return false;
12631
+ }
12389
12632
  const expected = /* @__PURE__ */ new Set();
12390
12633
  for (const constituent of constituents) {
12391
12634
  const key = finiteTypeKey(constituent, checker);
@@ -13192,28 +13435,26 @@ var requireZodFormValidationDocumentation = {
13192
13435
  rationale: "FormData values are untrusted strings or files and need runtime validation before use.",
13193
13436
  remediation: "Read the value inside a Zod schema's `parse` or `safeParse` input.",
13194
13437
  category: "security",
13438
+ limitations: [
13439
+ "Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.",
13440
+ "Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer."
13441
+ ],
13195
13442
  examples: [
13196
13443
  { id: "validated-form-value", title: "Validate the form value", outcome: "no-match", files: [{ path: "src/action.ts", source: "const input = UserSchema.parse({ name: formData.get('name') });" }], focusPath: "src/action.ts", expectedCount: 0, public: true },
13197
13444
  { id: "raw-form-value", title: "Do not use a raw form value", outcome: "match", files: [{ path: "src/action.ts", source: "const name = formData.get('name');" }], focusPath: "src/action.ts", expectedCount: 1, public: true }
13198
13445
  ]
13199
13446
  };
13200
- var isZodParseCall = (node) => {
13201
- if (node.type !== import_utils66.AST_NODE_TYPES.CallExpression) return false;
13202
- const callee = node.callee;
13203
- if (callee.type !== import_utils66.AST_NODE_TYPES.MemberExpression) return false;
13204
- if (callee.computed) return false;
13205
- if (callee.property.type !== import_utils66.AST_NODE_TYPES.Identifier) return false;
13206
- const method = callee.property.name;
13207
- if (method !== "parse" && method !== "safeParse" && method !== "parseAsync" && method !== "safeParseAsync") {
13208
- return false;
13209
- }
13210
- return looksLikeZodSchema(callee.object);
13211
- };
13212
- var looksLikeZodSchema = (node) => {
13447
+ var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set([
13448
+ "parse",
13449
+ "safeParse",
13450
+ "parseAsync",
13451
+ "safeParseAsync"
13452
+ ]);
13453
+ var zodReceiverRoot = (node) => {
13213
13454
  let current = node;
13214
13455
  while (true) {
13215
13456
  if (current.type === import_utils66.AST_NODE_TYPES.Identifier) {
13216
- return current.name === "z" || ZOD_SCHEMA_NAME_RE.test(current.name);
13457
+ return current;
13217
13458
  }
13218
13459
  if (current.type === import_utils66.AST_NODE_TYPES.CallExpression) {
13219
13460
  current = current.callee;
@@ -13223,7 +13464,7 @@ var looksLikeZodSchema = (node) => {
13223
13464
  current = current.object;
13224
13465
  continue;
13225
13466
  }
13226
- return false;
13467
+ return null;
13227
13468
  }
13228
13469
  };
13229
13470
  var isFormDataMethodCall = (node) => {
@@ -13253,6 +13494,34 @@ var require_zod_form_validation_default = createRule({
13253
13494
  if (isTestFile(context.filename)) {
13254
13495
  return {};
13255
13496
  }
13497
+ const zodBindings = /* @__PURE__ */ new Set();
13498
+ const resolvedBinding = (identifier) => import_utils66.ASTUtils.findVariable(
13499
+ context.sourceCode.getScope(identifier),
13500
+ identifier.name
13501
+ );
13502
+ const isProvablyNonZodLocal = (identifier) => {
13503
+ const binding = resolvedBinding(identifier);
13504
+ if (binding === null || zodBindings.has(binding) || binding.defs.length !== 1) {
13505
+ return false;
13506
+ }
13507
+ const definition = binding.defs[0];
13508
+ if (definition?.type !== "Variable" || definition.node.type !== import_utils66.AST_NODE_TYPES.VariableDeclarator) {
13509
+ return false;
13510
+ }
13511
+ const init = definition.node.init;
13512
+ return init?.type === import_utils66.AST_NODE_TYPES.ObjectExpression || init?.type === import_utils66.AST_NODE_TYPES.ArrayExpression || init?.type === import_utils66.AST_NODE_TYPES.Literal || init?.type === import_utils66.AST_NODE_TYPES.ArrowFunctionExpression || init?.type === import_utils66.AST_NODE_TYPES.FunctionExpression;
13513
+ };
13514
+ const isZodParseCall = (node) => {
13515
+ if (node.type !== import_utils66.AST_NODE_TYPES.CallExpression) return false;
13516
+ const callee = node.callee;
13517
+ if (callee.type !== import_utils66.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils66.AST_NODE_TYPES.Identifier || !ZOD_PARSE_METHODS.has(callee.property.name)) {
13518
+ return false;
13519
+ }
13520
+ const root = zodReceiverRoot(callee.object);
13521
+ if (root === null) return false;
13522
+ const binding = resolvedBinding(root);
13523
+ return binding !== null && zodBindings.has(binding) || (root.name === "z" || ZOD_SCHEMA_NAME_RE.test(root.name)) && !isProvablyNonZodLocal(root);
13524
+ };
13256
13525
  const isFormSourceIdentifier = (node) => {
13257
13526
  if (node.type !== import_utils66.AST_NODE_TYPES.Identifier) return false;
13258
13527
  if (/formdata/i.test(node.name)) return true;
@@ -13278,14 +13547,15 @@ var require_zod_form_validation_default = createRule({
13278
13547
  }
13279
13548
  return isFormSourceIdentifier(callee.object);
13280
13549
  };
13281
- const hasZodParseAncestor = (node) => {
13550
+ const zodParseAncestor = (node) => {
13282
13551
  let parent = node.parent;
13283
13552
  while (parent !== null && parent !== void 0) {
13284
- if (isZodParseCall(parent)) return true;
13553
+ if (isZodParseCall(parent)) return parent;
13285
13554
  parent = parent.parent;
13286
13555
  }
13287
- return false;
13556
+ return null;
13288
13557
  };
13558
+ const hasZodParseAncestor = (node) => zodParseAncestor(node) !== null;
13289
13559
  const isInstanceofNarrowing = (node) => {
13290
13560
  const parent = node.parent;
13291
13561
  return parent !== null && parent !== void 0 && parent.type === import_utils66.AST_NODE_TYPES.BinaryExpression && parent.operator === "instanceof" && parent.left === node && parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && (parent.right.name === "File" || parent.right.name === "Blob");
@@ -13302,14 +13572,111 @@ var require_zod_form_validation_default = createRule({
13302
13572
  }
13303
13573
  return null;
13304
13574
  };
13575
+ const containingStatement = (node) => {
13576
+ let current = node;
13577
+ while (current.parent !== void 0) {
13578
+ const parent = current.parent;
13579
+ if (parent.type === import_utils66.AST_NODE_TYPES.BlockStatement || parent.type === import_utils66.AST_NODE_TYPES.Program) {
13580
+ return current;
13581
+ }
13582
+ current = parent;
13583
+ }
13584
+ return null;
13585
+ };
13586
+ const zodParseMethod = (call) => {
13587
+ const callee = call.callee;
13588
+ return callee.type === import_utils66.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils66.AST_NODE_TYPES.Identifier ? callee.property.name : null;
13589
+ };
13590
+ const hasConditionalAncestorBeforeStatement = (node, statement) => {
13591
+ let current = node.parent;
13592
+ while (current !== void 0 && current !== statement) {
13593
+ if (current.type === import_utils66.AST_NODE_TYPES.LogicalExpression || current.type === import_utils66.AST_NODE_TYPES.ConditionalExpression) {
13594
+ return true;
13595
+ }
13596
+ current = current.parent;
13597
+ }
13598
+ return false;
13599
+ };
13600
+ const isAwaitedBeforeStatement = (node, statement) => {
13601
+ let current = node.parent;
13602
+ while (current !== void 0 && current !== statement) {
13603
+ if (current.type === import_utils66.AST_NODE_TYPES.AwaitExpression) return true;
13604
+ current = current.parent;
13605
+ }
13606
+ return false;
13607
+ };
13608
+ const guaranteedValidationStatement = (declarator, reference) => {
13609
+ const parse2 = zodParseAncestor(reference);
13610
+ if (parse2 === null) return null;
13611
+ const declarationStatement = containingStatement(declarator);
13612
+ const validationStatement = containingStatement(parse2);
13613
+ if (declarationStatement === null || validationStatement === null || declarationStatement.parent !== validationStatement.parent || validationStatement.range[0] <= declarationStatement.range[1] || hasConditionalAncestorBeforeStatement(parse2, validationStatement)) {
13614
+ return null;
13615
+ }
13616
+ if (validationStatement.type !== import_utils66.AST_NODE_TYPES.VariableDeclaration && validationStatement.type !== import_utils66.AST_NODE_TYPES.ExpressionStatement) {
13617
+ return null;
13618
+ }
13619
+ const method = zodParseMethod(parse2);
13620
+ if (method === "parse") return validationStatement;
13621
+ if (method === "parseAsync" && isAwaitedBeforeStatement(parse2, validationStatement)) {
13622
+ return validationStatement;
13623
+ }
13624
+ return null;
13625
+ };
13626
+ const isSafePrevalidationInspection = (identifier) => {
13627
+ const parent = identifier.parent;
13628
+ if (parent.type === import_utils66.AST_NODE_TYPES.UnaryExpression && parent.operator === "typeof") {
13629
+ return true;
13630
+ }
13631
+ if (parent.type !== import_utils66.AST_NODE_TYPES.BinaryExpression || parent.left !== identifier) {
13632
+ return false;
13633
+ }
13634
+ if (parent.operator === "instanceof") {
13635
+ return parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && (parent.right.name === "File" || parent.right.name === "Blob");
13636
+ }
13637
+ return ["===", "!==", "==", "!="].includes(parent.operator) && (parent.right.type === import_utils66.AST_NODE_TYPES.Literal && parent.right.value === null || parent.right.type === import_utils66.AST_NODE_TYPES.Identifier && parent.right.name === "undefined");
13638
+ };
13639
+ const statementWithinBlock = (node, block) => {
13640
+ let current = node;
13641
+ while (current.parent !== void 0 && current.parent !== block) {
13642
+ current = current.parent;
13643
+ }
13644
+ return current.parent === block ? current : null;
13645
+ };
13305
13646
  const bindingIsValidated = (declarator) => {
13306
13647
  const variable = context.sourceCode.getDeclaredVariables(declarator)[0];
13307
13648
  if (variable === void 0) return false;
13308
- return variable.references.some(
13309
- (ref) => hasZodParseAncestor(ref.identifier) || isInstanceofNarrowing(ref.identifier)
13649
+ const references = variable.references.filter((reference) => !reference.isWriteOnly()).map((reference) => reference.identifier).filter(
13650
+ (identifier) => identifier.type === import_utils66.AST_NODE_TYPES.Identifier
13651
+ );
13652
+ if (references.length === 0) return false;
13653
+ if (references.some(isInstanceofNarrowing)) return true;
13654
+ const validationStatements = references.map((reference) => guaranteedValidationStatement(declarator, reference)).filter(
13655
+ (statement) => statement !== null
13310
13656
  );
13657
+ const declarationStatement = containingStatement(declarator);
13658
+ const declarationBlock = declarationStatement?.parent;
13659
+ return references.every((reference) => {
13660
+ if (zodParseAncestor(reference) !== null || isSafePrevalidationInspection(reference)) {
13661
+ return true;
13662
+ }
13663
+ if (declarationBlock === void 0) return false;
13664
+ const useStatement = statementWithinBlock(reference, declarationBlock);
13665
+ return useStatement !== null && validationStatements.some(
13666
+ (statement) => statement.range[1] < useStatement.range[0]
13667
+ );
13668
+ });
13311
13669
  };
13312
13670
  return {
13671
+ ImportDeclaration(node) {
13672
+ if (!isZodModule(node.source.value)) return;
13673
+ for (const specifier of node.specifiers) {
13674
+ if (specifier.type === import_utils66.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils66.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils66.AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === import_utils66.AST_NODE_TYPES.Identifier ? specifier.imported.name === "z" : specifier.imported.value === "z")) {
13675
+ const binding = resolvedBinding(specifier.local);
13676
+ if (binding !== null) zodBindings.add(binding);
13677
+ }
13678
+ }
13679
+ },
13313
13680
  CallExpression(node) {
13314
13681
  if (!isFormDataGetCall(node)) return;
13315
13682
  if (hasZodParseAncestor(node) || isInstanceofNarrowing(node)) return;
@@ -14069,7 +14436,7 @@ var rules = {
14069
14436
  };
14070
14437
  var meta = {
14071
14438
  name: "@sarj/eslint-plugin",
14072
- version: "15.1.0"
14439
+ version: "15.3.0"
14073
14440
  };
14074
14441
  var applicationOnlyRules = [
14075
14442
  "no-restricted-library-load",