eslint-plugin-react-web-api 3.0.0-next.6 → 3.0.0-next.60
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.d.ts +5 -16
- package/dist/index.js +65 -67
- package/package.json +9 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ESLint, Linter } from "eslint";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
plugins: {};
|
|
8
|
-
name?: string;
|
|
9
|
-
rules?: Record<string, _eslint_react_shared0.RuleConfig>;
|
|
10
|
-
settings?: _eslint_react_shared0.SettingsConfig;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
13
|
-
meta: {
|
|
14
|
-
name: string;
|
|
15
|
-
version: string;
|
|
16
|
-
};
|
|
17
|
-
rules: Record<string, _eslint_react_shared0.CompatibleRule>;
|
|
4
|
+
type ConfigName = "recommended";
|
|
5
|
+
declare const finalPlugin: ESLint.Plugin & {
|
|
6
|
+
configs: Record<ConfigName, Linter.Config>;
|
|
18
7
|
};
|
|
19
8
|
//#endregion
|
|
20
|
-
export {
|
|
9
|
+
export { finalPlugin as default };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { DEFAULT_ESLINT_REACT_SETTINGS, WEBSITE_URL,
|
|
1
|
+
import { DEFAULT_ESLINT_REACT_SETTINGS, WEBSITE_URL, defineRuleListener } from "@eslint-react/shared";
|
|
2
2
|
import * as ast from "@eslint-react/ast";
|
|
3
3
|
import * as core from "@eslint-react/core";
|
|
4
4
|
import { dual, or, unit } from "@eslint-react/eff";
|
|
5
|
-
import { findEnclosingAssignmentTarget,
|
|
5
|
+
import { findEnclosingAssignmentTarget, findVariable, getVariableInitializer, isAssignmentTargetEqual, isValueEqual } from "@eslint-react/var";
|
|
6
6
|
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
|
|
7
7
|
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
8
8
|
import { P, isMatching, match } from "ts-pattern";
|
|
@@ -24,26 +24,10 @@ var __exportAll = (all, no_symbols) => {
|
|
|
24
24
|
return target;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/configs/recommended.ts
|
|
29
|
-
var recommended_exports = /* @__PURE__ */ __exportAll({
|
|
30
|
-
name: () => name$1,
|
|
31
|
-
rules: () => rules,
|
|
32
|
-
settings: () => settings
|
|
33
|
-
});
|
|
34
|
-
const name$1 = "react-web-api/recommended";
|
|
35
|
-
const rules = {
|
|
36
|
-
"react-web-api/no-leaked-event-listener": "warn",
|
|
37
|
-
"react-web-api/no-leaked-interval": "warn",
|
|
38
|
-
"react-web-api/no-leaked-resize-observer": "warn",
|
|
39
|
-
"react-web-api/no-leaked-timeout": "warn"
|
|
40
|
-
};
|
|
41
|
-
const settings = { "react-x": DEFAULT_ESLINT_REACT_SETTINGS };
|
|
42
|
-
|
|
43
27
|
//#endregion
|
|
44
28
|
//#region package.json
|
|
45
|
-
var name = "eslint-plugin-react-web-api";
|
|
46
|
-
var version = "3.0.0-next.
|
|
29
|
+
var name$1 = "eslint-plugin-react-web-api";
|
|
30
|
+
var version = "3.0.0-next.60";
|
|
47
31
|
|
|
48
32
|
//#endregion
|
|
49
33
|
//#region src/types/component-phase.ts
|
|
@@ -64,7 +48,7 @@ function getDocsUrl(ruleName) {
|
|
|
64
48
|
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
65
49
|
|
|
66
50
|
//#endregion
|
|
67
|
-
//#region src/rules/no-leaked-event-listener.ts
|
|
51
|
+
//#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
|
|
68
52
|
const RULE_NAME$3 = "no-leaked-event-listener";
|
|
69
53
|
const defaultOptions = {
|
|
70
54
|
capture: false,
|
|
@@ -83,19 +67,16 @@ function getFunctionKind$1(node) {
|
|
|
83
67
|
function getSignalValueExpression(node, initialScope) {
|
|
84
68
|
if (node == null) return unit;
|
|
85
69
|
switch (node.type) {
|
|
86
|
-
case AST_NODE_TYPES.Identifier: return getSignalValueExpression(
|
|
70
|
+
case AST_NODE_TYPES.Identifier: return getSignalValueExpression(getVariableInitializer(findVariable(node, initialScope), 0), initialScope);
|
|
87
71
|
case AST_NODE_TYPES.MemberExpression: return node;
|
|
88
72
|
default: return unit;
|
|
89
73
|
}
|
|
90
74
|
}
|
|
91
75
|
function getOptions(node, initialScope) {
|
|
92
|
-
function findProp(properties, propName) {
|
|
93
|
-
return findProperty(propName, properties, initialScope);
|
|
94
|
-
}
|
|
95
76
|
function getOpts(node) {
|
|
96
77
|
switch (node.type) {
|
|
97
78
|
case AST_NODE_TYPES.Identifier: {
|
|
98
|
-
const variableNode =
|
|
79
|
+
const variableNode = getVariableInitializer(findVariable(node, initialScope), 0);
|
|
99
80
|
if (variableNode?.type === AST_NODE_TYPES.ObjectExpression) return getOpts(variableNode);
|
|
100
81
|
return defaultOptions;
|
|
101
82
|
}
|
|
@@ -104,14 +85,14 @@ function getOptions(node, initialScope) {
|
|
|
104
85
|
capture: Boolean(node.value)
|
|
105
86
|
};
|
|
106
87
|
case AST_NODE_TYPES.ObjectExpression: {
|
|
107
|
-
const vCapture = match(
|
|
88
|
+
const vCapture = match(ast.findProperty(node.properties, "capture")).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
|
|
108
89
|
const value = prop.value;
|
|
109
90
|
switch (value.type) {
|
|
110
91
|
case AST_NODE_TYPES.Literal: return Boolean(value.value);
|
|
111
92
|
default: return Boolean(getStaticValue(value, initialScope)?.value);
|
|
112
93
|
}
|
|
113
94
|
}).otherwise(() => false);
|
|
114
|
-
const pSignal =
|
|
95
|
+
const pSignal = ast.findProperty(node.properties, "signal");
|
|
115
96
|
return {
|
|
116
97
|
capture: vCapture,
|
|
117
98
|
signal: pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(pSignal.value, initialScope) : unit
|
|
@@ -154,19 +135,19 @@ function create$3(context) {
|
|
|
154
135
|
const { type: aType, callee: aCallee, capture: aCapture, listener: aListener, phase: aPhase } = aEntry;
|
|
155
136
|
const { type: rType, callee: rCallee, capture: rCapture, listener: rListener, phase: rPhase } = rEntry;
|
|
156
137
|
if (!isInversePhase(aPhase, rPhase)) return false;
|
|
157
|
-
return isSameObject(aCallee, rCallee) && ast.isNodeEqual(aListener, rListener) &&
|
|
138
|
+
return isSameObject(aCallee, rCallee) && ast.isNodeEqual(aListener, rListener) && isValueEqual(aType, rType, [context.sourceCode.getScope(aType), context.sourceCode.getScope(rType)]) && aCapture === rCapture;
|
|
158
139
|
}
|
|
159
140
|
function checkInlineFunction(node, callKind, options) {
|
|
160
141
|
const listener = node.arguments.at(1);
|
|
161
142
|
if (!ast.isFunction(listener)) return;
|
|
162
143
|
if (options.signal != null) return;
|
|
163
144
|
context.report({
|
|
145
|
+
data: { eventMethodKind: callKind },
|
|
164
146
|
messageId: "unexpectedInlineFunction",
|
|
165
|
-
node: listener
|
|
166
|
-
data: { eventMethodKind: callKind }
|
|
147
|
+
node: listener
|
|
167
148
|
});
|
|
168
149
|
}
|
|
169
|
-
return {
|
|
150
|
+
return defineRuleListener({
|
|
170
151
|
[":function"](node) {
|
|
171
152
|
const kind = getFunctionKind$1(node);
|
|
172
153
|
fEntries.push({
|
|
@@ -191,10 +172,10 @@ function create$3(context) {
|
|
|
191
172
|
aEntries.push({
|
|
192
173
|
...opts,
|
|
193
174
|
type,
|
|
194
|
-
node,
|
|
195
175
|
callee,
|
|
196
176
|
listener,
|
|
197
177
|
method: "addEventListener",
|
|
178
|
+
node,
|
|
198
179
|
phase: fKind
|
|
199
180
|
});
|
|
200
181
|
}).with("removeEventListener", (callKind) => {
|
|
@@ -206,10 +187,10 @@ function create$3(context) {
|
|
|
206
187
|
rEntries.push({
|
|
207
188
|
...opts,
|
|
208
189
|
type,
|
|
209
|
-
node,
|
|
210
190
|
callee,
|
|
211
191
|
listener,
|
|
212
192
|
method: "removeEventListener",
|
|
193
|
+
node,
|
|
213
194
|
phase: fKind
|
|
214
195
|
});
|
|
215
196
|
}).with("abort", () => {
|
|
@@ -224,9 +205,9 @@ function create$3(context) {
|
|
|
224
205
|
case "setup":
|
|
225
206
|
case "cleanup":
|
|
226
207
|
context.report({
|
|
208
|
+
data: { effectMethodKind: "useEffect" },
|
|
227
209
|
messageId: "expectedRemoveEventListenerInCleanup",
|
|
228
|
-
node: aEntry.node
|
|
229
|
-
data: { effectMethodKind: "useEffect" }
|
|
210
|
+
node: aEntry.node
|
|
230
211
|
});
|
|
231
212
|
continue;
|
|
232
213
|
case "mount":
|
|
@@ -239,11 +220,11 @@ function create$3(context) {
|
|
|
239
220
|
}
|
|
240
221
|
}
|
|
241
222
|
}
|
|
242
|
-
};
|
|
223
|
+
});
|
|
243
224
|
}
|
|
244
225
|
|
|
245
226
|
//#endregion
|
|
246
|
-
//#region src/rules/no-leaked-interval.ts
|
|
227
|
+
//#region src/rules/no-leaked-interval/no-leaked-interval.ts
|
|
247
228
|
const RULE_NAME$2 = "no-leaked-interval";
|
|
248
229
|
function getCallKind$2(node) {
|
|
249
230
|
switch (true) {
|
|
@@ -275,7 +256,7 @@ function create$2(context) {
|
|
|
275
256
|
function isInverseEntry(a, b) {
|
|
276
257
|
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
|
|
277
258
|
}
|
|
278
|
-
return {
|
|
259
|
+
return defineRuleListener({
|
|
279
260
|
[":function"](node) {
|
|
280
261
|
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
281
262
|
fEntries.push({
|
|
@@ -302,8 +283,8 @@ function create$2(context) {
|
|
|
302
283
|
}
|
|
303
284
|
sEntries.push({
|
|
304
285
|
kind: "interval",
|
|
305
|
-
node,
|
|
306
286
|
callee: node.callee,
|
|
287
|
+
node,
|
|
307
288
|
phase: fEntry.kind,
|
|
308
289
|
timerId: intervalIdNode
|
|
309
290
|
});
|
|
@@ -317,8 +298,8 @@ function create$2(context) {
|
|
|
317
298
|
if (intervalIdNode == null) break;
|
|
318
299
|
cEntries.push({
|
|
319
300
|
kind: "interval",
|
|
320
|
-
node,
|
|
321
301
|
callee: node.callee,
|
|
302
|
+
node,
|
|
322
303
|
phase: fEntry.kind,
|
|
323
304
|
timerId: intervalIdNode
|
|
324
305
|
});
|
|
@@ -333,34 +314,34 @@ function create$2(context) {
|
|
|
333
314
|
case "setup":
|
|
334
315
|
case "cleanup":
|
|
335
316
|
context.report({
|
|
317
|
+
data: { kind: "useEffect" },
|
|
336
318
|
messageId: "expectedClearIntervalInCleanup",
|
|
337
|
-
node: sEntry.node
|
|
338
|
-
data: { kind: "useEffect" }
|
|
319
|
+
node: sEntry.node
|
|
339
320
|
});
|
|
340
321
|
continue;
|
|
341
322
|
case "mount":
|
|
342
323
|
case "unmount":
|
|
343
324
|
context.report({
|
|
325
|
+
data: { kind: "componentDidMount" },
|
|
344
326
|
messageId: "expectedClearIntervalInUnmount",
|
|
345
|
-
node: sEntry.node
|
|
346
|
-
data: { kind: "componentDidMount" }
|
|
327
|
+
node: sEntry.node
|
|
347
328
|
});
|
|
348
329
|
continue;
|
|
349
330
|
}
|
|
350
331
|
}
|
|
351
332
|
}
|
|
352
|
-
};
|
|
333
|
+
});
|
|
353
334
|
}
|
|
354
335
|
|
|
355
336
|
//#endregion
|
|
356
|
-
//#region src/rules/no-leaked-resize-observer.ts
|
|
337
|
+
//#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
|
|
357
338
|
const RULE_NAME$1 = "no-leaked-resize-observer";
|
|
358
339
|
function isNewResizeObserver(node) {
|
|
359
340
|
return node?.type === AST_NODE_TYPES.NewExpression && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "ResizeObserver";
|
|
360
341
|
}
|
|
361
342
|
function isFromObserver(context, node) {
|
|
362
343
|
switch (true) {
|
|
363
|
-
case node.type === AST_NODE_TYPES.Identifier: return isNewResizeObserver(
|
|
344
|
+
case node.type === AST_NODE_TYPES.Identifier: return isNewResizeObserver(getVariableInitializer(findVariable(node, context.sourceCode.getScope(node)), 0));
|
|
364
345
|
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
|
|
365
346
|
default: return false;
|
|
366
347
|
}
|
|
@@ -397,7 +378,7 @@ function create$1(context) {
|
|
|
397
378
|
const oEntries = [];
|
|
398
379
|
const uEntries = [];
|
|
399
380
|
const dEntries = [];
|
|
400
|
-
return {
|
|
381
|
+
return defineRuleListener({
|
|
401
382
|
[":function"](node) {
|
|
402
383
|
const kind = getFunctionKind(node);
|
|
403
384
|
fEntries.push({
|
|
@@ -416,9 +397,9 @@ function create$1(context) {
|
|
|
416
397
|
match(getCallKind$1(context, node)).with("disconnect", () => {
|
|
417
398
|
dEntries.push({
|
|
418
399
|
kind: "ResizeObserver",
|
|
419
|
-
node,
|
|
420
400
|
callee: node.callee,
|
|
421
401
|
method: "disconnect",
|
|
402
|
+
node,
|
|
422
403
|
observer: object,
|
|
423
404
|
phase: fKind
|
|
424
405
|
});
|
|
@@ -427,10 +408,10 @@ function create$1(context) {
|
|
|
427
408
|
if (element == null) return;
|
|
428
409
|
oEntries.push({
|
|
429
410
|
kind: "ResizeObserver",
|
|
430
|
-
node,
|
|
431
411
|
callee: node.callee,
|
|
432
412
|
element,
|
|
433
413
|
method: "observe",
|
|
414
|
+
node,
|
|
434
415
|
observer: object,
|
|
435
416
|
phase: fKind
|
|
436
417
|
});
|
|
@@ -439,10 +420,10 @@ function create$1(context) {
|
|
|
439
420
|
if (element == null) return;
|
|
440
421
|
uEntries.push({
|
|
441
422
|
kind: "ResizeObserver",
|
|
442
|
-
node,
|
|
443
423
|
callee: node.callee,
|
|
444
424
|
element,
|
|
445
425
|
method: "unobserve",
|
|
426
|
+
node,
|
|
446
427
|
observer: object,
|
|
447
428
|
phase: fKind
|
|
448
429
|
});
|
|
@@ -491,11 +472,11 @@ function create$1(context) {
|
|
|
491
472
|
}
|
|
492
473
|
}
|
|
493
474
|
}
|
|
494
|
-
};
|
|
475
|
+
});
|
|
495
476
|
}
|
|
496
477
|
|
|
497
478
|
//#endregion
|
|
498
|
-
//#region src/rules/no-leaked-timeout.ts
|
|
479
|
+
//#region src/rules/no-leaked-timeout/no-leaked-timeout.ts
|
|
499
480
|
const RULE_NAME = "no-leaked-timeout";
|
|
500
481
|
function getCallKind(node) {
|
|
501
482
|
switch (true) {
|
|
@@ -527,7 +508,7 @@ function create(context) {
|
|
|
527
508
|
function isInverseEntry(a, b) {
|
|
528
509
|
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
|
|
529
510
|
}
|
|
530
|
-
return {
|
|
511
|
+
return defineRuleListener({
|
|
531
512
|
[":function"](node) {
|
|
532
513
|
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
533
514
|
fEntries.push({
|
|
@@ -553,8 +534,8 @@ function create(context) {
|
|
|
553
534
|
}
|
|
554
535
|
sEntries.push({
|
|
555
536
|
kind: "timeout",
|
|
556
|
-
node,
|
|
557
537
|
callee: node.callee,
|
|
538
|
+
node,
|
|
558
539
|
phase: fEntry.kind,
|
|
559
540
|
timerId: timeoutIdNode
|
|
560
541
|
});
|
|
@@ -565,8 +546,8 @@ function create(context) {
|
|
|
565
546
|
if (timeoutIdNode == null) break;
|
|
566
547
|
rEntries.push({
|
|
567
548
|
kind: "timeout",
|
|
568
|
-
node,
|
|
569
549
|
callee: node.callee,
|
|
550
|
+
node,
|
|
570
551
|
phase: fEntry.kind,
|
|
571
552
|
timerId: timeoutIdNode
|
|
572
553
|
});
|
|
@@ -581,30 +562,30 @@ function create(context) {
|
|
|
581
562
|
case "setup":
|
|
582
563
|
case "cleanup":
|
|
583
564
|
context.report({
|
|
565
|
+
data: { kind: "useEffect" },
|
|
584
566
|
messageId: "expectedClearTimeoutInCleanup",
|
|
585
|
-
node: sEntry.node
|
|
586
|
-
data: { kind: "useEffect" }
|
|
567
|
+
node: sEntry.node
|
|
587
568
|
});
|
|
588
569
|
continue;
|
|
589
570
|
case "mount":
|
|
590
571
|
case "unmount":
|
|
591
572
|
context.report({
|
|
573
|
+
data: { kind: "componentDidMount" },
|
|
592
574
|
messageId: "expectedClearTimeoutInUnmount",
|
|
593
|
-
node: sEntry.node
|
|
594
|
-
data: { kind: "componentDidMount" }
|
|
575
|
+
node: sEntry.node
|
|
595
576
|
});
|
|
596
577
|
continue;
|
|
597
578
|
}
|
|
598
579
|
}
|
|
599
580
|
}
|
|
600
|
-
};
|
|
581
|
+
});
|
|
601
582
|
}
|
|
602
583
|
|
|
603
584
|
//#endregion
|
|
604
585
|
//#region src/plugin.ts
|
|
605
586
|
const plugin = {
|
|
606
587
|
meta: {
|
|
607
|
-
name,
|
|
588
|
+
name: name$1,
|
|
608
589
|
version
|
|
609
590
|
},
|
|
610
591
|
rules: {
|
|
@@ -615,13 +596,30 @@ const plugin = {
|
|
|
615
596
|
}
|
|
616
597
|
};
|
|
617
598
|
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/configs/recommended.ts
|
|
601
|
+
var recommended_exports = /* @__PURE__ */ __exportAll({
|
|
602
|
+
name: () => name,
|
|
603
|
+
plugins: () => plugins,
|
|
604
|
+
rules: () => rules,
|
|
605
|
+
settings: () => settings
|
|
606
|
+
});
|
|
607
|
+
const name = "react-web-api/recommended";
|
|
608
|
+
const rules = {
|
|
609
|
+
"react-web-api/no-leaked-event-listener": "warn",
|
|
610
|
+
"react-web-api/no-leaked-interval": "warn",
|
|
611
|
+
"react-web-api/no-leaked-resize-observer": "warn",
|
|
612
|
+
"react-web-api/no-leaked-timeout": "warn"
|
|
613
|
+
};
|
|
614
|
+
const plugins = { "react-web-api": plugin };
|
|
615
|
+
const settings = { "react-x": DEFAULT_ESLINT_REACT_SETTINGS };
|
|
616
|
+
|
|
618
617
|
//#endregion
|
|
619
618
|
//#region src/index.ts
|
|
620
|
-
const
|
|
621
|
-
var src_default = {
|
|
619
|
+
const finalPlugin = {
|
|
622
620
|
...plugin,
|
|
623
|
-
configs: { ["recommended"]:
|
|
621
|
+
configs: { ["recommended"]: recommended_exports }
|
|
624
622
|
};
|
|
625
623
|
|
|
626
624
|
//#endregion
|
|
627
|
-
export {
|
|
625
|
+
export { finalPlugin as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-web-api",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.60",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for interacting with Web APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -43,16 +43,17 @@
|
|
|
43
43
|
"@typescript-eslint/utils": "canary",
|
|
44
44
|
"birecord": "^0.1.1",
|
|
45
45
|
"ts-pattern": "^5.9.0",
|
|
46
|
-
"@eslint-react/
|
|
47
|
-
"@eslint-react/
|
|
48
|
-
"@eslint-react/
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/
|
|
46
|
+
"@eslint-react/ast": "3.0.0-next.60",
|
|
47
|
+
"@eslint-react/shared": "3.0.0-next.60",
|
|
48
|
+
"@eslint-react/eff": "3.0.0-next.60",
|
|
49
|
+
"@eslint-react/var": "3.0.0-next.60",
|
|
50
|
+
"@eslint-react/core": "3.0.0-next.60"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/react": "^19.2.14",
|
|
54
54
|
"@types/react-dom": "^19.2.3",
|
|
55
|
-
"
|
|
55
|
+
"eslint": "^10.0.2",
|
|
56
|
+
"tsdown": "^0.21.0-beta.2",
|
|
56
57
|
"@local/configs": "0.0.0"
|
|
57
58
|
},
|
|
58
59
|
"peerDependencies": {
|
|
@@ -68,6 +69,6 @@
|
|
|
68
69
|
"scripts": {
|
|
69
70
|
"build": "tsdown",
|
|
70
71
|
"lint:publish": "publint",
|
|
71
|
-
"lint:ts": "
|
|
72
|
+
"lint:ts": "tsl"
|
|
72
73
|
}
|
|
73
74
|
}
|