@sarj/eslint-plugin 16.9.0 → 16.11.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
@@ -52,8 +52,8 @@ module.exports = __toCommonJS(src_exports);
52
52
  // package.json
53
53
  var package_default = {
54
54
  name: "@sarj/eslint-plugin",
55
- version: "16.9.0",
56
- packageManager: "npm@12.1.0",
55
+ version: "16.11.0",
56
+ packageManager: "npm@12.2.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
59
59
  main: "./dist/index.cjs",
@@ -106,48 +106,49 @@ var package_default = {
106
106
  author: "sarj-ai",
107
107
  license: "MIT",
108
108
  dependencies: {
109
- "@typescript-eslint/typescript-estree": "^8.70.1",
110
- "@typescript-eslint/utils": "^8.70.1",
109
+ "@typescript-eslint/typescript-estree": "^8.71.2",
110
+ "@typescript-eslint/utils": "^8.71.2",
111
111
  "jsonc-parser": "^3.3.1"
112
112
  },
113
113
  devDependencies: {
114
114
  "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
115
115
  "@eslint/compat": "^2.1.1",
116
116
  "@eslint/js": "10.0.1",
117
- "@shadcn/lint": "0.2.0",
118
- "@types/node": "26.6.2",
119
- "@typescript-eslint/parser": "8.70.1",
120
- "@typescript-eslint/rule-tester": "8.70.1",
121
- "@typescript-eslint/utils": "8.70.1",
117
+ "@shadcn/lint": "0.2.1",
118
+ "@types/node": "26.6.5",
119
+ "@typescript-eslint/parser": "8.71.2",
120
+ "@typescript-eslint/rule-tester": "8.71.2",
121
+ "@typescript-eslint/utils": "8.71.2",
122
122
  "@typescript/native": "npm:typescript@7.0.2",
123
123
  "@vitest/eslint-plugin": "1.6.27",
124
- eslint: "10.11.0",
125
- "eslint-node-test": "0.4.0",
126
- "eslint-plugin-better-tailwindcss": "^4.7.0",
127
- "eslint-plugin-jest": "29.16.6",
128
- "eslint-plugin-jsdoc": "64.5.4",
129
- "eslint-plugin-n": "^18.3.0",
124
+ eslint: "10.12.0",
125
+ "eslint-node-test": "1.0.1",
126
+ "eslint-plugin-better-tailwindcss": "^4.9.0",
127
+ "eslint-plugin-jest": "29.17.0",
128
+ "eslint-plugin-jsdoc": "65.3.1",
129
+ "eslint-plugin-n": "^18.4.1",
130
130
  "eslint-plugin-perfectionist": "5.12.1",
131
- "eslint-plugin-playwright": "2.12.0",
131
+ "eslint-plugin-playwright": "2.12.1",
132
132
  "eslint-plugin-promise": "^7.3.0",
133
133
  "eslint-plugin-react": "^7.37.5",
134
134
  "eslint-plugin-react-hooks": "^7.1.1",
135
135
  "eslint-plugin-simple-import-sort": "^14.0.0",
136
136
  "eslint-plugin-testing-library": "7.16.2",
137
- "eslint-plugin-unicorn": "^76.0.0",
138
- "eslint-plugin-zod": "^4.14.2",
139
- globals: "15.15.0",
140
- tsup: "latest",
137
+ "eslint-plugin-unicorn": "^78.0.0",
138
+ "eslint-plugin-zod": "^5.0.1",
139
+ globals: "17.13.0",
140
+ tsup: "8.5.1",
141
141
  typescript: "npm:@typescript/typescript6@6.0.2",
142
- "typescript-eslint": "^8.70.1",
143
- vitest: "5.0.1"
142
+ "typescript-eslint": "^8.71.2",
143
+ vitest: "5.0.4",
144
+ zod: "4.6.7"
144
145
  },
145
146
  peerDependencies: {
146
147
  eslint: ">=9.39.4",
147
148
  typescript: ">=5.0.0"
148
149
  },
149
150
  engines: {
150
- node: ">=22.22.2"
151
+ node: "^22.22.2 || >=24.15.0"
151
152
  },
152
153
  allowScripts: {
153
154
  "esbuild@0.28.2": true,
@@ -159,12 +160,12 @@ var package_default = {
159
160
  }
160
161
  },
161
162
  overrides: {
162
- "brace-expansion@^5.0.0": "5.0.9",
163
+ "brace-expansion@^5.0.0": "5.0.12",
163
164
  esbuild: "0.28.2",
164
- "nanoid@<3.3.18": "3.3.18",
165
165
  "eslint-plugin-react": {
166
166
  eslint: "$eslint"
167
- }
167
+ },
168
+ "nanoid@<3.3.20": "3.3.20"
168
169
  }
169
170
  };
170
171
 
@@ -7705,7 +7706,8 @@ var NO_IMPOSSIBLE_ZOD_LITERAL_BOUNDS_DOCUMENTATION = {
7705
7706
  remediation: "Choose compatible lower and upper bounds, or remove the constraint that does not express the intended domain.",
7706
7707
  category: "correctness",
7707
7708
  limitations: [
7708
- "Only finite numeric literals in a single number, string, or array schema chain are compared; explicit int() chains also require an admitted integer.",
7709
+ "Only finite numeric literals in a single schema chain are compared; number().int(), int(), int32(), uint32(), strings and arrays require an admitted integer value or length.",
7710
+ "int32() and uint32() include their intrinsic ranges. The managed preset delegates explicit real-bound conflicts to upstream zod/no-conflicting-checks to avoid duplicate findings.",
7709
7711
  "Integer-domain comparisons abstain near the JavaScript safe-integer boundary.",
7710
7712
  "Chains with dynamic bounds, non-bound validators, transforms, pipes, or preprocessors are skipped.",
7711
7713
  "Test and generated files are excluded."
@@ -7741,7 +7743,14 @@ var NO_IMPOSSIBLE_ZOD_LITERAL_BOUNDS_DOCUMENTATION = {
7741
7743
  }
7742
7744
  ]
7743
7745
  };
7744
- var KINDS = /* @__PURE__ */ new Set(["array", "number", "string"]);
7746
+ var KINDS = /* @__PURE__ */ new Map([
7747
+ ["array", "array"],
7748
+ ["number", "number"],
7749
+ ["string", "string"],
7750
+ ["int", "integer"],
7751
+ ["int32", "int32"],
7752
+ ["uint32", "uint32"]
7753
+ ]);
7745
7754
  var NUMBER_METHODS = /* @__PURE__ */ new Set([
7746
7755
  "gt",
7747
7756
  "gte",
@@ -7787,11 +7796,14 @@ function strongerUpper(current, candidate2) {
7787
7796
  }
7788
7797
  return current;
7789
7798
  }
7799
+ function isRealEmpty(lower, upper) {
7800
+ if (lower === null || upper === null) return false;
7801
+ return lower.value > upper.value || lower.value === upper.value && (lower.exclusive || upper.exclusive);
7802
+ }
7790
7803
  function isEmpty(lower, upper, integer) {
7791
7804
  if (lower === null || upper === null) return false;
7792
- const realEmpty = lower.value > upper.value || lower.value === upper.value && (lower.exclusive || upper.exclusive);
7805
+ if (isRealEmpty(lower, upper)) return true;
7793
7806
  if (integer) {
7794
- if (realEmpty) return false;
7795
7807
  const safeMargin = Number.MAX_SAFE_INTEGER - 1;
7796
7808
  if (Math.abs(lower.value) >= safeMargin || Math.abs(upper.value) >= safeMargin) {
7797
7809
  return false;
@@ -7800,11 +7812,12 @@ function isEmpty(lower, upper, integer) {
7800
7812
  const maximum = upper.exclusive ? Math.ceil(upper.value) - 1 : Math.floor(upper.value);
7801
7813
  return minimum > maximum;
7802
7814
  }
7803
- return realEmpty;
7815
+ return false;
7804
7816
  }
7805
7817
  function isOutermostCall(node) {
7806
- const parent = node.parent;
7807
- return !(parent?.type === import_utils43.AST_NODE_TYPES.MemberExpression && parent.object === node && parent.parent?.type === import_utils43.AST_NODE_TYPES.CallExpression && parent.parent.callee === parent);
7818
+ const expression = outerExpression(node);
7819
+ const parent = expression.parent;
7820
+ return !(parent?.type === import_utils43.AST_NODE_TYPES.MemberExpression && parent.object === expression && parent.parent?.type === import_utils43.AST_NODE_TYPES.CallExpression && parent.parent.callee === parent);
7808
7821
  }
7809
7822
  var no_impossible_zod_literal_bounds_default = createRule({
7810
7823
  name: "no-impossible-zod-literal-bounds",
@@ -7814,13 +7827,24 @@ var no_impossible_zod_literal_bounds_default = createRule({
7814
7827
  docs: {
7815
7828
  description: "Disallow same-chain literal Zod bounds whose accepted set is mathematically empty."
7816
7829
  },
7817
- schema: [],
7830
+ schema: [
7831
+ {
7832
+ type: "object",
7833
+ additionalProperties: false,
7834
+ properties: {
7835
+ delegateRealContradictions: {
7836
+ type: "boolean",
7837
+ description: "Let enabled upstream zod/no-conflicting-checks own explicit real-bound contradictions."
7838
+ }
7839
+ }
7840
+ }
7841
+ ],
7818
7842
  messages: {
7819
7843
  impossibleBounds: "This Zod {{kind}} schema accepts no values: {{lower}} conflicts with {{upper}}."
7820
7844
  }
7821
7845
  },
7822
- defaultOptions: [],
7823
- create(context) {
7846
+ defaultOptions: [{}],
7847
+ create(context, [options]) {
7824
7848
  const sourceCode = context.sourceCode;
7825
7849
  if (isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
7826
7850
  return {};
@@ -7834,9 +7858,13 @@ var no_impossible_zod_literal_bounds_default = createRule({
7834
7858
  if (binding === void 0) return false;
7835
7859
  let scope = sourceCode.getScope(node);
7836
7860
  while (scope !== null) {
7837
- const variable = scope.variables.find((candidate2) => candidate2.name === node.name);
7861
+ const variable = scope.variables.find(
7862
+ (candidate2) => candidate2.name === node.name
7863
+ );
7838
7864
  if (variable !== void 0) {
7839
- return variable.defs.some((definition) => definition.name === binding);
7865
+ return variable.defs.some(
7866
+ (definition) => definition.name === binding
7867
+ );
7840
7868
  }
7841
7869
  scope = scope.upper;
7842
7870
  }
@@ -7869,7 +7897,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7869
7897
  return null;
7870
7898
  }
7871
7899
  const name = import_utils43.ASTUtils.getPropertyName(callee);
7872
- return name !== null && KINDS.has(name) ? name : null;
7900
+ return name === null ? null : KINDS.get(name) ?? null;
7873
7901
  }
7874
7902
  function isInsideReshapingCall(node) {
7875
7903
  let child = node;
@@ -7891,10 +7919,33 @@ var no_impossible_zod_literal_bounds_default = createRule({
7891
7919
  return false;
7892
7920
  }
7893
7921
  function contradiction(chain) {
7894
- const allowed = chain.kind === "number" ? NUMBER_METHODS : LENGTH_METHODS;
7922
+ const bounds = readConstraints(chain);
7923
+ if (bounds === null) return null;
7924
+ let { lower, upper } = bounds;
7925
+ const { integer, numeric } = bounds;
7926
+ if (isRealEmpty(lower, upper) && (numeric && integer || options?.delegateRealContradictions === true)) {
7927
+ return null;
7928
+ }
7929
+ if (chain.kind === "int32" || chain.kind === "uint32") {
7930
+ lower = strongerLower(lower, {
7931
+ exclusive: false,
7932
+ label: `${chain.kind}() minimum`,
7933
+ value: chain.kind === "int32" ? -(2 ** 31) : 0
7934
+ });
7935
+ upper = strongerUpper(upper, {
7936
+ exclusive: false,
7937
+ label: `${chain.kind}() maximum`,
7938
+ value: chain.kind === "int32" ? 2 ** 31 - 1 : 2 ** 32 - 1
7939
+ });
7940
+ }
7941
+ return isEmpty(lower, upper, integer) && lower !== null && upper !== null ? { lower, upper } : null;
7942
+ }
7943
+ function readConstraints(chain) {
7944
+ const numeric = chain.kind !== "string" && chain.kind !== "array";
7945
+ const allowed = numeric ? NUMBER_METHODS : LENGTH_METHODS;
7895
7946
  let lower = null;
7896
7947
  let upper = null;
7897
- let integer = false;
7948
+ let integer = chain.kind !== "number";
7898
7949
  function addBound(method, semantic, value) {
7899
7950
  const label = `${method}(${String(value)})`;
7900
7951
  if (semantic !== null) {
@@ -7938,13 +7989,13 @@ var no_impossible_zod_literal_bounds_default = createRule({
7938
7989
  const semantic = zeroBoundSemantic(method);
7939
7990
  const value = semantic?.value ?? finiteNumber(node.arguments[0]);
7940
7991
  if (value === null) return null;
7941
- if (chain.kind !== "number" && (!Number.isInteger(value) || value < 0)) {
7992
+ if (!numeric && value < 0) {
7942
7993
  return null;
7943
7994
  }
7944
7995
  if (semantic !== null && node.arguments.length !== 0) return null;
7945
7996
  addBound(method, semantic, value);
7946
7997
  }
7947
- return isEmpty(lower, upper, integer) && lower !== null && upper !== null ? { lower, upper } : null;
7998
+ return { integer, lower, numeric, upper };
7948
7999
  }
7949
8000
  return {
7950
8001
  ImportDeclaration(node) {
@@ -7960,7 +8011,9 @@ var no_impossible_zod_literal_bounds_default = createRule({
7960
8011
  namespaces.add(specifier.local.name);
7961
8012
  importBindings.set(specifier.local.name, specifier.local);
7962
8013
  } else if (imported !== null && KINDS.has(imported)) {
7963
- constructors.set(specifier.local.name, imported);
8014
+ const kind = KINDS.get(imported);
8015
+ if (kind !== void 0)
8016
+ constructors.set(specifier.local.name, kind);
7964
8017
  importBindings.set(specifier.local.name, specifier.local);
7965
8018
  } else if (imported === "preprocess") {
7966
8019
  preprocessors.add(specifier.local.name);
@@ -21664,7 +21717,7 @@ var PREFER_ZOD_PARSE_OUTPUT_TYPE_DOCUMENTATION = {
21664
21717
  focusPath: "src/row.ts",
21665
21718
  expectedCount: 0,
21666
21719
  public: true,
21667
- installedDependencies: [{ module: "zod", version: "4.6.5" }]
21720
+ installedDependencies: [{ module: "zod", version: "4.6.7" }]
21668
21721
  },
21669
21722
  {
21670
21723
  id: "hand-written-parsed-return",
@@ -21683,7 +21736,7 @@ var PREFER_ZOD_PARSE_OUTPUT_TYPE_DOCUMENTATION = {
21683
21736
  focusPath: "src/row.ts",
21684
21737
  expectedCount: 1,
21685
21738
  public: true,
21686
- installedDependencies: [{ module: "zod", version: "4.6.5" }]
21739
+ installedDependencies: [{ module: "zod", version: "4.6.7" }]
21687
21740
  }
21688
21741
  ]
21689
21742
  };
@@ -22144,10 +22197,39 @@ var REQUIRE_FETCH_TIMEOUT_DOCUMENTATION = {
22144
22197
  rationale: "An unbounded request can occupy work indefinitely when an upstream stalls.",
22145
22198
  remediation: "Pass an abort signal, such as `AbortSignal.timeout(ms)`, in the fetch init.",
22146
22199
  category: "correctness",
22147
- limitations: ["Signal presence establishes an explicit cancellation path, not a guaranteed timeout. Literal null, global undefined, and void 0 do not provide a signal. Unknown spreads and dynamic values are not inferred. Forwarded Request objects can carry an existing signal."],
22200
+ limitations: [
22201
+ "Signal presence establishes an explicit cancellation path, not a guaranteed timeout. Literal null, global undefined, and void 0 do not provide a signal. Erased TypeScript wrappers preserve the same analysis. Unknown spreads and dynamic values are not inferred. Forwarded Request objects retain their signal unless an init explicitly resets it to null.",
22202
+ "Accessors and unproved prototypes are excluded because native option conversion can attach or replace a signal before it is read."
22203
+ ],
22148
22204
  examples: [
22149
- { id: "bounded-fetch", title: "Bound the request", outcome: "no-match", files: [{ path: "src/client.ts", source: "await fetch(url, { signal: AbortSignal.timeout(5000) });" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
22150
- { id: "unbounded-fetch", title: "Do not leave fetch unbounded", outcome: "match", files: [{ path: "src/client.ts", source: "await fetch('https://api.example.com/items');" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
22205
+ {
22206
+ id: "bounded-fetch",
22207
+ title: "Bound the request",
22208
+ outcome: "no-match",
22209
+ files: [
22210
+ {
22211
+ path: "src/client.ts",
22212
+ source: "await fetch(url, { signal: AbortSignal.timeout(5000) });"
22213
+ }
22214
+ ],
22215
+ focusPath: "src/client.ts",
22216
+ expectedCount: 0,
22217
+ public: true
22218
+ },
22219
+ {
22220
+ id: "unbounded-fetch",
22221
+ title: "Do not leave fetch unbounded",
22222
+ outcome: "match",
22223
+ files: [
22224
+ {
22225
+ path: "src/client.ts",
22226
+ source: "await fetch('https://api.example.com/items');"
22227
+ }
22228
+ ],
22229
+ focusPath: "src/client.ts",
22230
+ expectedCount: 1,
22231
+ public: true
22232
+ }
22151
22233
  ]
22152
22234
  };
22153
22235
  var GLOBAL_OBJECTS2 = /* @__PURE__ */ new Set([
@@ -22164,23 +22246,50 @@ function matchesAnyPattern3(filename, patterns) {
22164
22246
  }
22165
22247
  return false;
22166
22248
  }
22167
- function initProvablyLacksSignal(init, resolvesToGlobal) {
22168
- if (init.type !== import_utils119.AST_NODE_TYPES.ObjectExpression) return false;
22169
- let lacksSignal = true;
22249
+ function emptySignal(expression, resolvesToGlobal) {
22250
+ const value = unwrapExpression(expression);
22251
+ if (value.type === import_utils119.AST_NODE_TYPES.Literal && value.value === null)
22252
+ return "null";
22253
+ if (value.type === import_utils119.AST_NODE_TYPES.Identifier && value.name === "undefined" && resolvesToGlobal(value))
22254
+ return "undefined";
22255
+ if (value.type === import_utils119.AST_NODE_TYPES.UnaryExpression && value.operator === "void") {
22256
+ const argument = unwrapExpression(value.argument);
22257
+ if (argument.type === import_utils119.AST_NODE_TYPES.Literal && argument.value === 0)
22258
+ return "undefined";
22259
+ }
22260
+ return null;
22261
+ }
22262
+ function initSignalState(init, resolvesToGlobal) {
22263
+ init = unwrapExpression(init);
22264
+ if (init.type !== import_utils119.AST_NODE_TYPES.ObjectExpression) return "unknown";
22265
+ let state = "absent";
22170
22266
  for (const prop of init.properties) {
22171
22267
  if (prop.type === import_utils119.AST_NODE_TYPES.SpreadElement) {
22172
- lacksSignal = false;
22268
+ state = "unknown";
22173
22269
  continue;
22174
22270
  }
22271
+ if (prop.kind !== "init" || hasUnprovedPrototype(prop)) return "unknown";
22175
22272
  const isSignal = !prop.computed && prop.key.type === import_utils119.AST_NODE_TYPES.Identifier && prop.key.name === "signal" || prop.key.type === import_utils119.AST_NODE_TYPES.Literal && prop.key.value === "signal";
22176
22273
  if (isSignal) {
22177
- const value = prop.value;
22178
- lacksSignal = prop.kind === "init" && !prop.method && (value.type === import_utils119.AST_NODE_TYPES.Literal && value.value === null || value.type === import_utils119.AST_NODE_TYPES.Identifier && value.name === "undefined" && resolvesToGlobal(value) || value.type === import_utils119.AST_NODE_TYPES.UnaryExpression && value.operator === "void" && value.argument.type === import_utils119.AST_NODE_TYPES.Literal && value.argument.value === 0);
22274
+ state = !prop.method ? emptySignal(prop.value, resolvesToGlobal) ?? "unknown" : "unknown";
22275
+ } else if (!prop.computed && import_utils119.ASTUtils.getPropertyName(prop) === "__proto__" && state === "absent") {
22276
+ state = "unknown";
22179
22277
  } else if (prop.computed && prop.key.type !== import_utils119.AST_NODE_TYPES.Literal) {
22180
- lacksSignal = false;
22278
+ state = "unknown";
22181
22279
  }
22182
22280
  }
22183
- return lacksSignal;
22281
+ return state;
22282
+ }
22283
+ function hasUnprovedPrototype(property) {
22284
+ if (property.computed || property.method || property.shorthand || import_utils119.ASTUtils.getPropertyName(property) !== "__proto__")
22285
+ return false;
22286
+ const prototype = unwrapExpression(property.value);
22287
+ if (prototype.type === import_utils119.AST_NODE_TYPES.Literal && prototype.value === null)
22288
+ return false;
22289
+ if (prototype.type !== import_utils119.AST_NODE_TYPES.ObjectExpression) return true;
22290
+ return prototype.properties.some(
22291
+ (member) => member.type === import_utils119.AST_NODE_TYPES.SpreadElement || member.kind !== "init" || !member.computed && import_utils119.ASTUtils.getPropertyName(member) === "__proto__" || member.computed && member.key.type !== import_utils119.AST_NODE_TYPES.Literal
22292
+ );
22184
22293
  }
22185
22294
  function isInlineUrl(node, resolvesToGlobal) {
22186
22295
  node = unwrapExpression(node);
@@ -22226,40 +22335,50 @@ var require_fetch_timeout_default = createRule({
22226
22335
  return variable === null || variable.defs.length === 0;
22227
22336
  }
22228
22337
  function isGlobalFetchCall2(callee) {
22338
+ callee = unwrapExpression(callee);
22229
22339
  if (callee.type === import_utils119.AST_NODE_TYPES.Identifier) {
22230
22340
  return callee.name === "fetch" && resolvesToGlobal(callee);
22231
22341
  }
22232
- return callee.type === import_utils119.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils119.AST_NODE_TYPES.Identifier && callee.property.name === "fetch" && callee.object.type === import_utils119.AST_NODE_TYPES.Identifier && GLOBAL_OBJECTS2.has(callee.object.name) && resolvesToGlobal(callee.object);
22342
+ const receiver = callee.type === import_utils119.AST_NODE_TYPES.MemberExpression ? unwrapExpression(callee.object) : null;
22343
+ return callee.type === import_utils119.AST_NODE_TYPES.MemberExpression && import_utils119.ASTUtils.getPropertyName(callee) === "fetch" && receiver?.type === import_utils119.AST_NODE_TYPES.Identifier && GLOBAL_OBJECTS2.has(receiver.name) && resolvesToGlobal(receiver);
22233
22344
  }
22234
- function localConstInitProvablyLacksSignal(identifier) {
22345
+ function localConstInitSignalState(identifier) {
22235
22346
  const variable = import_utils119.ASTUtils.findVariable(
22236
22347
  context.sourceCode.getScope(identifier),
22237
22348
  identifier.name
22238
22349
  );
22239
- if (variable?.defs.length !== 1) return false;
22350
+ if (variable?.defs.length !== 1) return "unknown";
22240
22351
  const definition = variable.defs[0];
22241
- if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init?.type !== import_utils119.AST_NODE_TYPES.ObjectExpression || !initProvablyLacksSignal(definition.node.init, resolvesToGlobal)) {
22242
- return false;
22352
+ if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || unwrapExpression(definition.node.init).type !== import_utils119.AST_NODE_TYPES.ObjectExpression) {
22353
+ return "unknown";
22243
22354
  }
22244
22355
  for (const reference of variable.references) {
22245
22356
  const ref = reference.identifier;
22246
22357
  if (ref === identifier || ref === definition.name) continue;
22247
22358
  const member = ref.parent;
22248
- if (member.type !== import_utils119.AST_NODE_TYPES.MemberExpression || member.object !== ref || member.computed || member.property.type !== import_utils119.AST_NODE_TYPES.Identifier || member.property.name === "signal" || member.parent.type !== import_utils119.AST_NODE_TYPES.AssignmentExpression || member.parent.left !== member) {
22249
- return false;
22359
+ if (member.type !== import_utils119.AST_NODE_TYPES.MemberExpression || member.object !== ref || member.computed || member.property.type !== import_utils119.AST_NODE_TYPES.Identifier || member.property.name === "signal" || member.property.name === "__proto__" || member.parent.type !== import_utils119.AST_NODE_TYPES.AssignmentExpression || member.parent.left !== member) {
22360
+ return "unknown";
22250
22361
  }
22251
22362
  }
22252
- return true;
22363
+ return initSignalState(definition.node.init, resolvesToGlobal);
22253
22364
  }
22254
22365
  function isForwardedRequest(argument) {
22255
- let value = argument;
22366
+ let value = unwrapExpression(argument);
22256
22367
  if (value.type === import_utils119.AST_NODE_TYPES.Identifier) {
22257
- const binding = import_utils119.ASTUtils.findVariable(context.sourceCode.getScope(value), value.name);
22368
+ const binding = import_utils119.ASTUtils.findVariable(
22369
+ context.sourceCode.getScope(value),
22370
+ value.name
22371
+ );
22258
22372
  const definition = binding?.defs.length === 1 ? binding.defs[0] : void 0;
22259
- if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || binding?.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
22260
- value = definition.node.init;
22373
+ if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || binding?.references.some(
22374
+ (reference) => reference.isWrite() && reference.init !== true
22375
+ ))
22376
+ return false;
22377
+ value = unwrapExpression(definition.node.init);
22261
22378
  }
22262
- return value.type === import_utils119.AST_NODE_TYPES.NewExpression && value.callee.type === import_utils119.AST_NODE_TYPES.Identifier && value.callee.name === "Request" && resolvesToGlobal(value.callee);
22379
+ if (value.type !== import_utils119.AST_NODE_TYPES.NewExpression) return false;
22380
+ const callee = unwrapExpression(value.callee);
22381
+ return callee.type === import_utils119.AST_NODE_TYPES.Identifier && callee.name === "Request" && resolvesToGlobal(callee);
22263
22382
  }
22264
22383
  return {
22265
22384
  CallExpression(node) {
@@ -22267,11 +22386,13 @@ var require_fetch_timeout_default = createRule({
22267
22386
  if (!isGlobalFetchCall2(unwrappedNodeCallee)) {
22268
22387
  return;
22269
22388
  }
22270
- const [first, init] = node.arguments;
22271
- if (first !== void 0 && (node.arguments.length === 1 && !isInlineUrl(first, resolvesToGlobal) || isForwardedRequest(first))) {
22389
+ const [first, rawInit] = node.arguments;
22390
+ const init = rawInit === void 0 ? void 0 : unwrapExpression(rawInit);
22391
+ const state = init === void 0 ? "unknown" : init.type === import_utils119.AST_NODE_TYPES.Identifier ? localConstInitSignalState(init) : initSignalState(init, resolvesToGlobal);
22392
+ if (first !== void 0 && (node.arguments.length === 1 && !isInlineUrl(first, resolvesToGlobal) || isForwardedRequest(first) && state !== "null")) {
22272
22393
  return;
22273
22394
  }
22274
- if (init === void 0 || initProvablyLacksSignal(init, resolvesToGlobal) || init.type === import_utils119.AST_NODE_TYPES.Identifier && localConstInitProvablyLacksSignal(init)) {
22395
+ if (init === void 0 || state !== "unknown" || first !== void 0 && isInlineUrl(first, resolvesToGlobal) && emptySignal(init, resolvesToGlobal) !== null) {
22275
22396
  context.report({ node, messageId: "missingSignal" });
22276
22397
  }
22277
22398
  }
package/dist/index.d.cts CHANGED
@@ -176,7 +176,9 @@ declare const RULES: {
176
176
  readonly "no-detached-global-fetch": DocumentedRule<readonly [], "detachedGlobalFetch">;
177
177
  readonly "no-json-stringify-error": DocumentedRule<readonly [], "noJsonStringifyError">;
178
178
  readonly "no-json-stringify-object-equality": DocumentedRule<readonly [], "serializedObjectEquality">;
179
- readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
179
+ readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [{
180
+ readonly delegateRealContradictions?: boolean;
181
+ }?], "impossibleBounds">;
180
182
  readonly "no-in-operator-on-built-in-collections": DocumentedRule<readonly [], "ambiguousCollectionIn">;
181
183
  readonly "no-redundant-optional-array-default": DocumentedRule<readonly [], "redundantOptionalArrayDefault">;
182
184
  readonly "interface-contract-members-private": DocumentedRule<readonly [], "nonContractMemberMustBePrivate">;
@@ -983,7 +985,9 @@ declare const PLUGIN: {
983
985
  readonly "no-detached-global-fetch": DocumentedRule<readonly [], "detachedGlobalFetch">;
984
986
  readonly "no-json-stringify-error": DocumentedRule<readonly [], "noJsonStringifyError">;
985
987
  readonly "no-json-stringify-object-equality": DocumentedRule<readonly [], "serializedObjectEquality">;
986
- readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
988
+ readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [{
989
+ readonly delegateRealContradictions?: boolean;
990
+ }?], "impossibleBounds">;
987
991
  readonly "no-in-operator-on-built-in-collections": DocumentedRule<readonly [], "ambiguousCollectionIn">;
988
992
  readonly "no-redundant-optional-array-default": DocumentedRule<readonly [], "redundantOptionalArrayDefault">;
989
993
  readonly "interface-contract-members-private": DocumentedRule<readonly [], "nonContractMemberMustBePrivate">;
package/dist/index.d.ts CHANGED
@@ -176,7 +176,9 @@ declare const RULES: {
176
176
  readonly "no-detached-global-fetch": DocumentedRule<readonly [], "detachedGlobalFetch">;
177
177
  readonly "no-json-stringify-error": DocumentedRule<readonly [], "noJsonStringifyError">;
178
178
  readonly "no-json-stringify-object-equality": DocumentedRule<readonly [], "serializedObjectEquality">;
179
- readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
179
+ readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [{
180
+ readonly delegateRealContradictions?: boolean;
181
+ }?], "impossibleBounds">;
180
182
  readonly "no-in-operator-on-built-in-collections": DocumentedRule<readonly [], "ambiguousCollectionIn">;
181
183
  readonly "no-redundant-optional-array-default": DocumentedRule<readonly [], "redundantOptionalArrayDefault">;
182
184
  readonly "interface-contract-members-private": DocumentedRule<readonly [], "nonContractMemberMustBePrivate">;
@@ -983,7 +985,9 @@ declare const PLUGIN: {
983
985
  readonly "no-detached-global-fetch": DocumentedRule<readonly [], "detachedGlobalFetch">;
984
986
  readonly "no-json-stringify-error": DocumentedRule<readonly [], "noJsonStringifyError">;
985
987
  readonly "no-json-stringify-object-equality": DocumentedRule<readonly [], "serializedObjectEquality">;
986
- readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [], "impossibleBounds">;
988
+ readonly "no-impossible-zod-literal-bounds": DocumentedRule<readonly [{
989
+ readonly delegateRealContradictions?: boolean;
990
+ }?], "impossibleBounds">;
987
991
  readonly "no-in-operator-on-built-in-collections": DocumentedRule<readonly [], "ambiguousCollectionIn">;
988
992
  readonly "no-redundant-optional-array-default": DocumentedRule<readonly [], "redundantOptionalArrayDefault">;
989
993
  readonly "interface-contract-members-private": DocumentedRule<readonly [], "nonContractMemberMustBePrivate">;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@sarj/eslint-plugin",
4
- version: "16.9.0",
5
- packageManager: "npm@12.1.0",
4
+ version: "16.11.0",
5
+ packageManager: "npm@12.2.0",
6
6
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
7
7
  type: "module",
8
8
  main: "./dist/index.cjs",
@@ -55,48 +55,49 @@ var package_default = {
55
55
  author: "sarj-ai",
56
56
  license: "MIT",
57
57
  dependencies: {
58
- "@typescript-eslint/typescript-estree": "^8.70.1",
59
- "@typescript-eslint/utils": "^8.70.1",
58
+ "@typescript-eslint/typescript-estree": "^8.71.2",
59
+ "@typescript-eslint/utils": "^8.71.2",
60
60
  "jsonc-parser": "^3.3.1"
61
61
  },
62
62
  devDependencies: {
63
63
  "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
64
64
  "@eslint/compat": "^2.1.1",
65
65
  "@eslint/js": "10.0.1",
66
- "@shadcn/lint": "0.2.0",
67
- "@types/node": "26.6.2",
68
- "@typescript-eslint/parser": "8.70.1",
69
- "@typescript-eslint/rule-tester": "8.70.1",
70
- "@typescript-eslint/utils": "8.70.1",
66
+ "@shadcn/lint": "0.2.1",
67
+ "@types/node": "26.6.5",
68
+ "@typescript-eslint/parser": "8.71.2",
69
+ "@typescript-eslint/rule-tester": "8.71.2",
70
+ "@typescript-eslint/utils": "8.71.2",
71
71
  "@typescript/native": "npm:typescript@7.0.2",
72
72
  "@vitest/eslint-plugin": "1.6.27",
73
- eslint: "10.11.0",
74
- "eslint-node-test": "0.4.0",
75
- "eslint-plugin-better-tailwindcss": "^4.7.0",
76
- "eslint-plugin-jest": "29.16.6",
77
- "eslint-plugin-jsdoc": "64.5.4",
78
- "eslint-plugin-n": "^18.3.0",
73
+ eslint: "10.12.0",
74
+ "eslint-node-test": "1.0.1",
75
+ "eslint-plugin-better-tailwindcss": "^4.9.0",
76
+ "eslint-plugin-jest": "29.17.0",
77
+ "eslint-plugin-jsdoc": "65.3.1",
78
+ "eslint-plugin-n": "^18.4.1",
79
79
  "eslint-plugin-perfectionist": "5.12.1",
80
- "eslint-plugin-playwright": "2.12.0",
80
+ "eslint-plugin-playwright": "2.12.1",
81
81
  "eslint-plugin-promise": "^7.3.0",
82
82
  "eslint-plugin-react": "^7.37.5",
83
83
  "eslint-plugin-react-hooks": "^7.1.1",
84
84
  "eslint-plugin-simple-import-sort": "^14.0.0",
85
85
  "eslint-plugin-testing-library": "7.16.2",
86
- "eslint-plugin-unicorn": "^76.0.0",
87
- "eslint-plugin-zod": "^4.14.2",
88
- globals: "15.15.0",
89
- tsup: "latest",
86
+ "eslint-plugin-unicorn": "^78.0.0",
87
+ "eslint-plugin-zod": "^5.0.1",
88
+ globals: "17.13.0",
89
+ tsup: "8.5.1",
90
90
  typescript: "npm:@typescript/typescript6@6.0.2",
91
- "typescript-eslint": "^8.70.1",
92
- vitest: "5.0.1"
91
+ "typescript-eslint": "^8.71.2",
92
+ vitest: "5.0.4",
93
+ zod: "4.6.7"
93
94
  },
94
95
  peerDependencies: {
95
96
  eslint: ">=9.39.4",
96
97
  typescript: ">=5.0.0"
97
98
  },
98
99
  engines: {
99
- node: ">=22.22.2"
100
+ node: "^22.22.2 || >=24.15.0"
100
101
  },
101
102
  allowScripts: {
102
103
  "esbuild@0.28.2": true,
@@ -108,12 +109,12 @@ var package_default = {
108
109
  }
109
110
  },
110
111
  overrides: {
111
- "brace-expansion@^5.0.0": "5.0.9",
112
+ "brace-expansion@^5.0.0": "5.0.12",
112
113
  esbuild: "0.28.2",
113
- "nanoid@<3.3.18": "3.3.18",
114
114
  "eslint-plugin-react": {
115
115
  eslint: "$eslint"
116
- }
116
+ },
117
+ "nanoid@<3.3.20": "3.3.20"
117
118
  }
118
119
  };
119
120
 
@@ -7679,7 +7680,8 @@ var NO_IMPOSSIBLE_ZOD_LITERAL_BOUNDS_DOCUMENTATION = {
7679
7680
  remediation: "Choose compatible lower and upper bounds, or remove the constraint that does not express the intended domain.",
7680
7681
  category: "correctness",
7681
7682
  limitations: [
7682
- "Only finite numeric literals in a single number, string, or array schema chain are compared; explicit int() chains also require an admitted integer.",
7683
+ "Only finite numeric literals in a single schema chain are compared; number().int(), int(), int32(), uint32(), strings and arrays require an admitted integer value or length.",
7684
+ "int32() and uint32() include their intrinsic ranges. The managed preset delegates explicit real-bound conflicts to upstream zod/no-conflicting-checks to avoid duplicate findings.",
7683
7685
  "Integer-domain comparisons abstain near the JavaScript safe-integer boundary.",
7684
7686
  "Chains with dynamic bounds, non-bound validators, transforms, pipes, or preprocessors are skipped.",
7685
7687
  "Test and generated files are excluded."
@@ -7715,7 +7717,14 @@ var NO_IMPOSSIBLE_ZOD_LITERAL_BOUNDS_DOCUMENTATION = {
7715
7717
  }
7716
7718
  ]
7717
7719
  };
7718
- var KINDS = /* @__PURE__ */ new Set(["array", "number", "string"]);
7720
+ var KINDS = /* @__PURE__ */ new Map([
7721
+ ["array", "array"],
7722
+ ["number", "number"],
7723
+ ["string", "string"],
7724
+ ["int", "integer"],
7725
+ ["int32", "int32"],
7726
+ ["uint32", "uint32"]
7727
+ ]);
7719
7728
  var NUMBER_METHODS = /* @__PURE__ */ new Set([
7720
7729
  "gt",
7721
7730
  "gte",
@@ -7761,11 +7770,14 @@ function strongerUpper(current, candidate2) {
7761
7770
  }
7762
7771
  return current;
7763
7772
  }
7773
+ function isRealEmpty(lower, upper) {
7774
+ if (lower === null || upper === null) return false;
7775
+ return lower.value > upper.value || lower.value === upper.value && (lower.exclusive || upper.exclusive);
7776
+ }
7764
7777
  function isEmpty(lower, upper, integer) {
7765
7778
  if (lower === null || upper === null) return false;
7766
- const realEmpty = lower.value > upper.value || lower.value === upper.value && (lower.exclusive || upper.exclusive);
7779
+ if (isRealEmpty(lower, upper)) return true;
7767
7780
  if (integer) {
7768
- if (realEmpty) return false;
7769
7781
  const safeMargin = Number.MAX_SAFE_INTEGER - 1;
7770
7782
  if (Math.abs(lower.value) >= safeMargin || Math.abs(upper.value) >= safeMargin) {
7771
7783
  return false;
@@ -7774,11 +7786,12 @@ function isEmpty(lower, upper, integer) {
7774
7786
  const maximum = upper.exclusive ? Math.ceil(upper.value) - 1 : Math.floor(upper.value);
7775
7787
  return minimum > maximum;
7776
7788
  }
7777
- return realEmpty;
7789
+ return false;
7778
7790
  }
7779
7791
  function isOutermostCall(node) {
7780
- const parent = node.parent;
7781
- return !(parent?.type === AST_NODE_TYPES37.MemberExpression && parent.object === node && parent.parent?.type === AST_NODE_TYPES37.CallExpression && parent.parent.callee === parent);
7792
+ const expression = outerExpression(node);
7793
+ const parent = expression.parent;
7794
+ return !(parent?.type === AST_NODE_TYPES37.MemberExpression && parent.object === expression && parent.parent?.type === AST_NODE_TYPES37.CallExpression && parent.parent.callee === parent);
7782
7795
  }
7783
7796
  var no_impossible_zod_literal_bounds_default = createRule({
7784
7797
  name: "no-impossible-zod-literal-bounds",
@@ -7788,13 +7801,24 @@ var no_impossible_zod_literal_bounds_default = createRule({
7788
7801
  docs: {
7789
7802
  description: "Disallow same-chain literal Zod bounds whose accepted set is mathematically empty."
7790
7803
  },
7791
- schema: [],
7804
+ schema: [
7805
+ {
7806
+ type: "object",
7807
+ additionalProperties: false,
7808
+ properties: {
7809
+ delegateRealContradictions: {
7810
+ type: "boolean",
7811
+ description: "Let enabled upstream zod/no-conflicting-checks own explicit real-bound contradictions."
7812
+ }
7813
+ }
7814
+ }
7815
+ ],
7792
7816
  messages: {
7793
7817
  impossibleBounds: "This Zod {{kind}} schema accepts no values: {{lower}} conflicts with {{upper}}."
7794
7818
  }
7795
7819
  },
7796
- defaultOptions: [],
7797
- create(context) {
7820
+ defaultOptions: [{}],
7821
+ create(context, [options]) {
7798
7822
  const sourceCode = context.sourceCode;
7799
7823
  if (isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
7800
7824
  return {};
@@ -7808,9 +7832,13 @@ var no_impossible_zod_literal_bounds_default = createRule({
7808
7832
  if (binding === void 0) return false;
7809
7833
  let scope = sourceCode.getScope(node);
7810
7834
  while (scope !== null) {
7811
- const variable = scope.variables.find((candidate2) => candidate2.name === node.name);
7835
+ const variable = scope.variables.find(
7836
+ (candidate2) => candidate2.name === node.name
7837
+ );
7812
7838
  if (variable !== void 0) {
7813
- return variable.defs.some((definition) => definition.name === binding);
7839
+ return variable.defs.some(
7840
+ (definition) => definition.name === binding
7841
+ );
7814
7842
  }
7815
7843
  scope = scope.upper;
7816
7844
  }
@@ -7843,7 +7871,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7843
7871
  return null;
7844
7872
  }
7845
7873
  const name = ASTUtils17.getPropertyName(callee);
7846
- return name !== null && KINDS.has(name) ? name : null;
7874
+ return name === null ? null : KINDS.get(name) ?? null;
7847
7875
  }
7848
7876
  function isInsideReshapingCall(node) {
7849
7877
  let child = node;
@@ -7865,10 +7893,33 @@ var no_impossible_zod_literal_bounds_default = createRule({
7865
7893
  return false;
7866
7894
  }
7867
7895
  function contradiction(chain) {
7868
- const allowed = chain.kind === "number" ? NUMBER_METHODS : LENGTH_METHODS;
7896
+ const bounds = readConstraints(chain);
7897
+ if (bounds === null) return null;
7898
+ let { lower, upper } = bounds;
7899
+ const { integer, numeric } = bounds;
7900
+ if (isRealEmpty(lower, upper) && (numeric && integer || options?.delegateRealContradictions === true)) {
7901
+ return null;
7902
+ }
7903
+ if (chain.kind === "int32" || chain.kind === "uint32") {
7904
+ lower = strongerLower(lower, {
7905
+ exclusive: false,
7906
+ label: `${chain.kind}() minimum`,
7907
+ value: chain.kind === "int32" ? -(2 ** 31) : 0
7908
+ });
7909
+ upper = strongerUpper(upper, {
7910
+ exclusive: false,
7911
+ label: `${chain.kind}() maximum`,
7912
+ value: chain.kind === "int32" ? 2 ** 31 - 1 : 2 ** 32 - 1
7913
+ });
7914
+ }
7915
+ return isEmpty(lower, upper, integer) && lower !== null && upper !== null ? { lower, upper } : null;
7916
+ }
7917
+ function readConstraints(chain) {
7918
+ const numeric = chain.kind !== "string" && chain.kind !== "array";
7919
+ const allowed = numeric ? NUMBER_METHODS : LENGTH_METHODS;
7869
7920
  let lower = null;
7870
7921
  let upper = null;
7871
- let integer = false;
7922
+ let integer = chain.kind !== "number";
7872
7923
  function addBound(method, semantic, value) {
7873
7924
  const label = `${method}(${String(value)})`;
7874
7925
  if (semantic !== null) {
@@ -7912,13 +7963,13 @@ var no_impossible_zod_literal_bounds_default = createRule({
7912
7963
  const semantic = zeroBoundSemantic(method);
7913
7964
  const value = semantic?.value ?? finiteNumber(node.arguments[0]);
7914
7965
  if (value === null) return null;
7915
- if (chain.kind !== "number" && (!Number.isInteger(value) || value < 0)) {
7966
+ if (!numeric && value < 0) {
7916
7967
  return null;
7917
7968
  }
7918
7969
  if (semantic !== null && node.arguments.length !== 0) return null;
7919
7970
  addBound(method, semantic, value);
7920
7971
  }
7921
- return isEmpty(lower, upper, integer) && lower !== null && upper !== null ? { lower, upper } : null;
7972
+ return { integer, lower, numeric, upper };
7922
7973
  }
7923
7974
  return {
7924
7975
  ImportDeclaration(node) {
@@ -7934,7 +7985,9 @@ var no_impossible_zod_literal_bounds_default = createRule({
7934
7985
  namespaces.add(specifier.local.name);
7935
7986
  importBindings.set(specifier.local.name, specifier.local);
7936
7987
  } else if (imported !== null && KINDS.has(imported)) {
7937
- constructors.set(specifier.local.name, imported);
7988
+ const kind = KINDS.get(imported);
7989
+ if (kind !== void 0)
7990
+ constructors.set(specifier.local.name, kind);
7938
7991
  importBindings.set(specifier.local.name, specifier.local);
7939
7992
  } else if (imported === "preprocess") {
7940
7993
  preprocessors.add(specifier.local.name);
@@ -21679,7 +21732,7 @@ var PREFER_ZOD_PARSE_OUTPUT_TYPE_DOCUMENTATION = {
21679
21732
  focusPath: "src/row.ts",
21680
21733
  expectedCount: 0,
21681
21734
  public: true,
21682
- installedDependencies: [{ module: "zod", version: "4.6.5" }]
21735
+ installedDependencies: [{ module: "zod", version: "4.6.7" }]
21683
21736
  },
21684
21737
  {
21685
21738
  id: "hand-written-parsed-return",
@@ -21698,7 +21751,7 @@ var PREFER_ZOD_PARSE_OUTPUT_TYPE_DOCUMENTATION = {
21698
21751
  focusPath: "src/row.ts",
21699
21752
  expectedCount: 1,
21700
21753
  public: true,
21701
- installedDependencies: [{ module: "zod", version: "4.6.5" }]
21754
+ installedDependencies: [{ module: "zod", version: "4.6.7" }]
21702
21755
  }
21703
21756
  ]
21704
21757
  };
@@ -22156,16 +22209,48 @@ var require_assert_never_default = createRule({
22156
22209
  });
22157
22210
 
22158
22211
  // src/rules/require-fetch-timeout.ts
22159
- import { AST_NODE_TYPES as AST_NODE_TYPES103, ASTUtils as ASTUtils64 } from "@typescript-eslint/utils";
22212
+ import {
22213
+ AST_NODE_TYPES as AST_NODE_TYPES103,
22214
+ ASTUtils as ASTUtils64
22215
+ } from "@typescript-eslint/utils";
22160
22216
  var REQUIRE_FETCH_TIMEOUT_DOCUMENTATION = {
22161
22217
  summary: "Require an explicit abort signal on locally analyzable global fetch calls.",
22162
22218
  rationale: "An unbounded request can occupy work indefinitely when an upstream stalls.",
22163
22219
  remediation: "Pass an abort signal, such as `AbortSignal.timeout(ms)`, in the fetch init.",
22164
22220
  category: "correctness",
22165
- limitations: ["Signal presence establishes an explicit cancellation path, not a guaranteed timeout. Literal null, global undefined, and void 0 do not provide a signal. Unknown spreads and dynamic values are not inferred. Forwarded Request objects can carry an existing signal."],
22221
+ limitations: [
22222
+ "Signal presence establishes an explicit cancellation path, not a guaranteed timeout. Literal null, global undefined, and void 0 do not provide a signal. Erased TypeScript wrappers preserve the same analysis. Unknown spreads and dynamic values are not inferred. Forwarded Request objects retain their signal unless an init explicitly resets it to null.",
22223
+ "Accessors and unproved prototypes are excluded because native option conversion can attach or replace a signal before it is read."
22224
+ ],
22166
22225
  examples: [
22167
- { id: "bounded-fetch", title: "Bound the request", outcome: "no-match", files: [{ path: "src/client.ts", source: "await fetch(url, { signal: AbortSignal.timeout(5000) });" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
22168
- { id: "unbounded-fetch", title: "Do not leave fetch unbounded", outcome: "match", files: [{ path: "src/client.ts", source: "await fetch('https://api.example.com/items');" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
22226
+ {
22227
+ id: "bounded-fetch",
22228
+ title: "Bound the request",
22229
+ outcome: "no-match",
22230
+ files: [
22231
+ {
22232
+ path: "src/client.ts",
22233
+ source: "await fetch(url, { signal: AbortSignal.timeout(5000) });"
22234
+ }
22235
+ ],
22236
+ focusPath: "src/client.ts",
22237
+ expectedCount: 0,
22238
+ public: true
22239
+ },
22240
+ {
22241
+ id: "unbounded-fetch",
22242
+ title: "Do not leave fetch unbounded",
22243
+ outcome: "match",
22244
+ files: [
22245
+ {
22246
+ path: "src/client.ts",
22247
+ source: "await fetch('https://api.example.com/items');"
22248
+ }
22249
+ ],
22250
+ focusPath: "src/client.ts",
22251
+ expectedCount: 1,
22252
+ public: true
22253
+ }
22169
22254
  ]
22170
22255
  };
22171
22256
  var GLOBAL_OBJECTS2 = /* @__PURE__ */ new Set([
@@ -22182,23 +22267,50 @@ function matchesAnyPattern3(filename, patterns) {
22182
22267
  }
22183
22268
  return false;
22184
22269
  }
22185
- function initProvablyLacksSignal(init, resolvesToGlobal) {
22186
- if (init.type !== AST_NODE_TYPES103.ObjectExpression) return false;
22187
- let lacksSignal = true;
22270
+ function emptySignal(expression, resolvesToGlobal) {
22271
+ const value = unwrapExpression(expression);
22272
+ if (value.type === AST_NODE_TYPES103.Literal && value.value === null)
22273
+ return "null";
22274
+ if (value.type === AST_NODE_TYPES103.Identifier && value.name === "undefined" && resolvesToGlobal(value))
22275
+ return "undefined";
22276
+ if (value.type === AST_NODE_TYPES103.UnaryExpression && value.operator === "void") {
22277
+ const argument = unwrapExpression(value.argument);
22278
+ if (argument.type === AST_NODE_TYPES103.Literal && argument.value === 0)
22279
+ return "undefined";
22280
+ }
22281
+ return null;
22282
+ }
22283
+ function initSignalState(init, resolvesToGlobal) {
22284
+ init = unwrapExpression(init);
22285
+ if (init.type !== AST_NODE_TYPES103.ObjectExpression) return "unknown";
22286
+ let state = "absent";
22188
22287
  for (const prop of init.properties) {
22189
22288
  if (prop.type === AST_NODE_TYPES103.SpreadElement) {
22190
- lacksSignal = false;
22289
+ state = "unknown";
22191
22290
  continue;
22192
22291
  }
22292
+ if (prop.kind !== "init" || hasUnprovedPrototype(prop)) return "unknown";
22193
22293
  const isSignal = !prop.computed && prop.key.type === AST_NODE_TYPES103.Identifier && prop.key.name === "signal" || prop.key.type === AST_NODE_TYPES103.Literal && prop.key.value === "signal";
22194
22294
  if (isSignal) {
22195
- const value = prop.value;
22196
- lacksSignal = prop.kind === "init" && !prop.method && (value.type === AST_NODE_TYPES103.Literal && value.value === null || value.type === AST_NODE_TYPES103.Identifier && value.name === "undefined" && resolvesToGlobal(value) || value.type === AST_NODE_TYPES103.UnaryExpression && value.operator === "void" && value.argument.type === AST_NODE_TYPES103.Literal && value.argument.value === 0);
22295
+ state = !prop.method ? emptySignal(prop.value, resolvesToGlobal) ?? "unknown" : "unknown";
22296
+ } else if (!prop.computed && ASTUtils64.getPropertyName(prop) === "__proto__" && state === "absent") {
22297
+ state = "unknown";
22197
22298
  } else if (prop.computed && prop.key.type !== AST_NODE_TYPES103.Literal) {
22198
- lacksSignal = false;
22299
+ state = "unknown";
22199
22300
  }
22200
22301
  }
22201
- return lacksSignal;
22302
+ return state;
22303
+ }
22304
+ function hasUnprovedPrototype(property) {
22305
+ if (property.computed || property.method || property.shorthand || ASTUtils64.getPropertyName(property) !== "__proto__")
22306
+ return false;
22307
+ const prototype = unwrapExpression(property.value);
22308
+ if (prototype.type === AST_NODE_TYPES103.Literal && prototype.value === null)
22309
+ return false;
22310
+ if (prototype.type !== AST_NODE_TYPES103.ObjectExpression) return true;
22311
+ return prototype.properties.some(
22312
+ (member) => member.type === AST_NODE_TYPES103.SpreadElement || member.kind !== "init" || !member.computed && ASTUtils64.getPropertyName(member) === "__proto__" || member.computed && member.key.type !== AST_NODE_TYPES103.Literal
22313
+ );
22202
22314
  }
22203
22315
  function isInlineUrl(node, resolvesToGlobal) {
22204
22316
  node = unwrapExpression(node);
@@ -22244,40 +22356,50 @@ var require_fetch_timeout_default = createRule({
22244
22356
  return variable === null || variable.defs.length === 0;
22245
22357
  }
22246
22358
  function isGlobalFetchCall2(callee) {
22359
+ callee = unwrapExpression(callee);
22247
22360
  if (callee.type === AST_NODE_TYPES103.Identifier) {
22248
22361
  return callee.name === "fetch" && resolvesToGlobal(callee);
22249
22362
  }
22250
- return callee.type === AST_NODE_TYPES103.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES103.Identifier && callee.property.name === "fetch" && callee.object.type === AST_NODE_TYPES103.Identifier && GLOBAL_OBJECTS2.has(callee.object.name) && resolvesToGlobal(callee.object);
22363
+ const receiver = callee.type === AST_NODE_TYPES103.MemberExpression ? unwrapExpression(callee.object) : null;
22364
+ return callee.type === AST_NODE_TYPES103.MemberExpression && ASTUtils64.getPropertyName(callee) === "fetch" && receiver?.type === AST_NODE_TYPES103.Identifier && GLOBAL_OBJECTS2.has(receiver.name) && resolvesToGlobal(receiver);
22251
22365
  }
22252
- function localConstInitProvablyLacksSignal(identifier) {
22366
+ function localConstInitSignalState(identifier) {
22253
22367
  const variable = ASTUtils64.findVariable(
22254
22368
  context.sourceCode.getScope(identifier),
22255
22369
  identifier.name
22256
22370
  );
22257
- if (variable?.defs.length !== 1) return false;
22371
+ if (variable?.defs.length !== 1) return "unknown";
22258
22372
  const definition = variable.defs[0];
22259
- if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init?.type !== AST_NODE_TYPES103.ObjectExpression || !initProvablyLacksSignal(definition.node.init, resolvesToGlobal)) {
22260
- return false;
22373
+ if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || unwrapExpression(definition.node.init).type !== AST_NODE_TYPES103.ObjectExpression) {
22374
+ return "unknown";
22261
22375
  }
22262
22376
  for (const reference of variable.references) {
22263
22377
  const ref = reference.identifier;
22264
22378
  if (ref === identifier || ref === definition.name) continue;
22265
22379
  const member = ref.parent;
22266
- if (member.type !== AST_NODE_TYPES103.MemberExpression || member.object !== ref || member.computed || member.property.type !== AST_NODE_TYPES103.Identifier || member.property.name === "signal" || member.parent.type !== AST_NODE_TYPES103.AssignmentExpression || member.parent.left !== member) {
22267
- return false;
22380
+ if (member.type !== AST_NODE_TYPES103.MemberExpression || member.object !== ref || member.computed || member.property.type !== AST_NODE_TYPES103.Identifier || member.property.name === "signal" || member.property.name === "__proto__" || member.parent.type !== AST_NODE_TYPES103.AssignmentExpression || member.parent.left !== member) {
22381
+ return "unknown";
22268
22382
  }
22269
22383
  }
22270
- return true;
22384
+ return initSignalState(definition.node.init, resolvesToGlobal);
22271
22385
  }
22272
22386
  function isForwardedRequest(argument) {
22273
- let value = argument;
22387
+ let value = unwrapExpression(argument);
22274
22388
  if (value.type === AST_NODE_TYPES103.Identifier) {
22275
- const binding = ASTUtils64.findVariable(context.sourceCode.getScope(value), value.name);
22389
+ const binding = ASTUtils64.findVariable(
22390
+ context.sourceCode.getScope(value),
22391
+ value.name
22392
+ );
22276
22393
  const definition = binding?.defs.length === 1 ? binding.defs[0] : void 0;
22277
- if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || binding?.references.some((reference) => reference.isWrite() && reference.init !== true)) return false;
22278
- value = definition.node.init;
22394
+ if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init === null || binding?.references.some(
22395
+ (reference) => reference.isWrite() && reference.init !== true
22396
+ ))
22397
+ return false;
22398
+ value = unwrapExpression(definition.node.init);
22279
22399
  }
22280
- return value.type === AST_NODE_TYPES103.NewExpression && value.callee.type === AST_NODE_TYPES103.Identifier && value.callee.name === "Request" && resolvesToGlobal(value.callee);
22400
+ if (value.type !== AST_NODE_TYPES103.NewExpression) return false;
22401
+ const callee = unwrapExpression(value.callee);
22402
+ return callee.type === AST_NODE_TYPES103.Identifier && callee.name === "Request" && resolvesToGlobal(callee);
22281
22403
  }
22282
22404
  return {
22283
22405
  CallExpression(node) {
@@ -22285,11 +22407,13 @@ var require_fetch_timeout_default = createRule({
22285
22407
  if (!isGlobalFetchCall2(unwrappedNodeCallee)) {
22286
22408
  return;
22287
22409
  }
22288
- const [first, init] = node.arguments;
22289
- if (first !== void 0 && (node.arguments.length === 1 && !isInlineUrl(first, resolvesToGlobal) || isForwardedRequest(first))) {
22410
+ const [first, rawInit] = node.arguments;
22411
+ const init = rawInit === void 0 ? void 0 : unwrapExpression(rawInit);
22412
+ const state = init === void 0 ? "unknown" : init.type === AST_NODE_TYPES103.Identifier ? localConstInitSignalState(init) : initSignalState(init, resolvesToGlobal);
22413
+ if (first !== void 0 && (node.arguments.length === 1 && !isInlineUrl(first, resolvesToGlobal) || isForwardedRequest(first) && state !== "null")) {
22290
22414
  return;
22291
22415
  }
22292
- if (init === void 0 || initProvablyLacksSignal(init, resolvesToGlobal) || init.type === AST_NODE_TYPES103.Identifier && localConstInitProvablyLacksSignal(init)) {
22416
+ if (init === void 0 || state !== "unknown" || first !== void 0 && isInlineUrl(first, resolvesToGlobal) && emptySignal(init, resolvesToGlobal) !== null) {
22293
22417
  context.report({ node, messageId: "missingSignal" });
22294
22418
  }
22295
22419
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.9.0",
4
- "packageManager": "npm@12.1.0",
3
+ "version": "16.11.0",
4
+ "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",
7
7
  "main": "./dist/index.cjs",
@@ -54,48 +54,49 @@
54
54
  "author": "sarj-ai",
55
55
  "license": "MIT",
56
56
  "dependencies": {
57
- "@typescript-eslint/typescript-estree": "^8.70.1",
58
- "@typescript-eslint/utils": "^8.70.1",
57
+ "@typescript-eslint/typescript-estree": "^8.71.2",
58
+ "@typescript-eslint/utils": "^8.71.2",
59
59
  "jsonc-parser": "^3.3.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
63
63
  "@eslint/compat": "^2.1.1",
64
64
  "@eslint/js": "10.0.1",
65
- "@shadcn/lint": "0.2.0",
66
- "@types/node": "26.6.2",
67
- "@typescript-eslint/parser": "8.70.1",
68
- "@typescript-eslint/rule-tester": "8.70.1",
69
- "@typescript-eslint/utils": "8.70.1",
65
+ "@shadcn/lint": "0.2.1",
66
+ "@types/node": "26.6.5",
67
+ "@typescript-eslint/parser": "8.71.2",
68
+ "@typescript-eslint/rule-tester": "8.71.2",
69
+ "@typescript-eslint/utils": "8.71.2",
70
70
  "@typescript/native": "npm:typescript@7.0.2",
71
71
  "@vitest/eslint-plugin": "1.6.27",
72
- "eslint": "10.11.0",
73
- "eslint-node-test": "0.4.0",
74
- "eslint-plugin-better-tailwindcss": "^4.7.0",
75
- "eslint-plugin-jest": "29.16.6",
76
- "eslint-plugin-jsdoc": "64.5.4",
77
- "eslint-plugin-n": "^18.3.0",
72
+ "eslint": "10.12.0",
73
+ "eslint-node-test": "1.0.1",
74
+ "eslint-plugin-better-tailwindcss": "^4.9.0",
75
+ "eslint-plugin-jest": "29.17.0",
76
+ "eslint-plugin-jsdoc": "65.3.1",
77
+ "eslint-plugin-n": "^18.4.1",
78
78
  "eslint-plugin-perfectionist": "5.12.1",
79
- "eslint-plugin-playwright": "2.12.0",
79
+ "eslint-plugin-playwright": "2.12.1",
80
80
  "eslint-plugin-promise": "^7.3.0",
81
81
  "eslint-plugin-react": "^7.37.5",
82
82
  "eslint-plugin-react-hooks": "^7.1.1",
83
83
  "eslint-plugin-simple-import-sort": "^14.0.0",
84
84
  "eslint-plugin-testing-library": "7.16.2",
85
- "eslint-plugin-unicorn": "^76.0.0",
86
- "eslint-plugin-zod": "^4.14.2",
87
- "globals": "15.15.0",
88
- "tsup": "latest",
85
+ "eslint-plugin-unicorn": "^78.0.0",
86
+ "eslint-plugin-zod": "^5.0.1",
87
+ "globals": "17.13.0",
88
+ "tsup": "8.5.1",
89
89
  "typescript": "npm:@typescript/typescript6@6.0.2",
90
- "typescript-eslint": "^8.70.1",
91
- "vitest": "5.0.1"
90
+ "typescript-eslint": "^8.71.2",
91
+ "vitest": "5.0.4",
92
+ "zod": "4.6.7"
92
93
  },
93
94
  "peerDependencies": {
94
95
  "eslint": ">=9.39.4",
95
96
  "typescript": ">=5.0.0"
96
97
  },
97
98
  "engines": {
98
- "node": ">=22.22.2"
99
+ "node": "^22.22.2 || >=24.15.0"
99
100
  },
100
101
  "allowScripts": {
101
102
  "esbuild@0.28.2": true,
@@ -107,11 +108,11 @@
107
108
  }
108
109
  },
109
110
  "overrides": {
110
- "brace-expansion@^5.0.0": "5.0.9",
111
+ "brace-expansion@^5.0.0": "5.0.12",
111
112
  "esbuild": "0.28.2",
112
- "nanoid@<3.3.18": "3.3.18",
113
113
  "eslint-plugin-react": {
114
114
  "eslint": "$eslint"
115
- }
115
+ },
116
+ "nanoid@<3.3.20": "3.3.20"
116
117
  }
117
118
  }