@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 +194 -73
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +198 -74
- package/package.json +27 -26
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.
|
|
56
|
-
packageManager: "npm@12.
|
|
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.
|
|
110
|
-
"@typescript-eslint/utils": "^8.
|
|
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.
|
|
118
|
-
"@types/node": "26.6.
|
|
119
|
-
"@typescript-eslint/parser": "8.
|
|
120
|
-
"@typescript-eslint/rule-tester": "8.
|
|
121
|
-
"@typescript-eslint/utils": "8.
|
|
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.
|
|
125
|
-
"eslint-node-test": "0.
|
|
126
|
-
"eslint-plugin-better-tailwindcss": "^4.
|
|
127
|
-
"eslint-plugin-jest": "29.
|
|
128
|
-
"eslint-plugin-jsdoc": "
|
|
129
|
-
"eslint-plugin-n": "^18.
|
|
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.
|
|
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": "^
|
|
138
|
-
"eslint-plugin-zod": "^
|
|
139
|
-
globals: "
|
|
140
|
-
tsup: "
|
|
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.
|
|
143
|
-
vitest: "5.0.
|
|
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: "
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7815
|
+
return false;
|
|
7804
7816
|
}
|
|
7805
7817
|
function isOutermostCall(node) {
|
|
7806
|
-
const
|
|
7807
|
-
|
|
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(
|
|
7861
|
+
const variable = scope.variables.find(
|
|
7862
|
+
(candidate2) => candidate2.name === node.name
|
|
7863
|
+
);
|
|
7838
7864
|
if (variable !== void 0) {
|
|
7839
|
-
return variable.defs.some(
|
|
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
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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: [
|
|
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
|
-
{
|
|
22150
|
-
|
|
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
|
|
22168
|
-
|
|
22169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22178
|
-
|
|
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
|
-
|
|
22278
|
+
state = "unknown";
|
|
22181
22279
|
}
|
|
22182
22280
|
}
|
|
22183
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
22242
|
-
return
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
22260
|
-
|
|
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
|
-
|
|
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,
|
|
22271
|
-
|
|
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 ||
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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.
|
|
5
|
-
packageManager: "npm@12.
|
|
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.
|
|
59
|
-
"@typescript-eslint/utils": "^8.
|
|
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.
|
|
67
|
-
"@types/node": "26.6.
|
|
68
|
-
"@typescript-eslint/parser": "8.
|
|
69
|
-
"@typescript-eslint/rule-tester": "8.
|
|
70
|
-
"@typescript-eslint/utils": "8.
|
|
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.
|
|
74
|
-
"eslint-node-test": "0.
|
|
75
|
-
"eslint-plugin-better-tailwindcss": "^4.
|
|
76
|
-
"eslint-plugin-jest": "29.
|
|
77
|
-
"eslint-plugin-jsdoc": "
|
|
78
|
-
"eslint-plugin-n": "^18.
|
|
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.
|
|
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": "^
|
|
87
|
-
"eslint-plugin-zod": "^
|
|
88
|
-
globals: "
|
|
89
|
-
tsup: "
|
|
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.
|
|
92
|
-
vitest: "5.0.
|
|
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: "
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7789
|
+
return false;
|
|
7778
7790
|
}
|
|
7779
7791
|
function isOutermostCall(node) {
|
|
7780
|
-
const
|
|
7781
|
-
|
|
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(
|
|
7835
|
+
const variable = scope.variables.find(
|
|
7836
|
+
(candidate2) => candidate2.name === node.name
|
|
7837
|
+
);
|
|
7812
7838
|
if (variable !== void 0) {
|
|
7813
|
-
return variable.defs.some(
|
|
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
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
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: [
|
|
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
|
-
{
|
|
22168
|
-
|
|
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
|
|
22186
|
-
|
|
22187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22196
|
-
|
|
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
|
-
|
|
22299
|
+
state = "unknown";
|
|
22199
22300
|
}
|
|
22200
22301
|
}
|
|
22201
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
22260
|
-
return
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
22278
|
-
|
|
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
|
-
|
|
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,
|
|
22289
|
-
|
|
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 ||
|
|
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.
|
|
4
|
-
"packageManager": "npm@12.
|
|
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.
|
|
58
|
-
"@typescript-eslint/utils": "^8.
|
|
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.
|
|
66
|
-
"@types/node": "26.6.
|
|
67
|
-
"@typescript-eslint/parser": "8.
|
|
68
|
-
"@typescript-eslint/rule-tester": "8.
|
|
69
|
-
"@typescript-eslint/utils": "8.
|
|
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.
|
|
73
|
-
"eslint-node-test": "0.
|
|
74
|
-
"eslint-plugin-better-tailwindcss": "^4.
|
|
75
|
-
"eslint-plugin-jest": "29.
|
|
76
|
-
"eslint-plugin-jsdoc": "
|
|
77
|
-
"eslint-plugin-n": "^18.
|
|
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.
|
|
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": "^
|
|
86
|
-
"eslint-plugin-zod": "^
|
|
87
|
-
"globals": "
|
|
88
|
-
"tsup": "
|
|
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.
|
|
91
|
-
"vitest": "5.0.
|
|
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": "
|
|
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.
|
|
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
|
}
|