eslint 9.25.1 → 9.27.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/README.md +44 -39
- package/bin/eslint.js +15 -0
- package/conf/rule-type-list.json +2 -1
- package/lib/cli-engine/cli-engine.js +8 -8
- package/lib/cli.js +6 -5
- package/lib/config/config-loader.js +10 -18
- package/lib/config/config.js +328 -5
- package/lib/eslint/eslint-helpers.js +3 -1
- package/lib/eslint/eslint.js +31 -17
- package/lib/eslint/legacy-eslint.js +7 -7
- package/lib/languages/js/index.js +4 -3
- package/lib/languages/js/source-code/source-code.js +10 -6
- package/lib/linter/apply-disable-directives.js +1 -1
- package/lib/linter/esquery.js +329 -0
- package/lib/linter/file-context.js +11 -0
- package/lib/linter/linter.js +81 -89
- package/lib/linter/node-event-generator.js +94 -251
- package/lib/linter/report-translator.js +2 -1
- package/lib/options.js +11 -0
- package/lib/rule-tester/rule-tester.js +17 -9
- package/lib/rules/eqeqeq.js +31 -8
- package/lib/rules/index.js +2 -1
- package/lib/rules/max-params.js +32 -7
- package/lib/rules/no-array-constructor.js +51 -1
- package/lib/rules/no-shadow-restricted-names.js +25 -2
- package/lib/rules/no-unassigned-vars.js +72 -0
- package/lib/rules/no-unused-expressions.js +7 -1
- package/lib/rules/no-useless-escape.js +24 -2
- package/lib/rules/prefer-named-capture-group.js +7 -1
- package/lib/rules/utils/lazy-loading-rule-map.js +2 -2
- package/lib/services/processor-service.js +1 -2
- package/lib/services/suppressions-service.js +5 -3
- package/lib/shared/flags.js +1 -0
- package/lib/shared/serialization.js +29 -6
- package/lib/types/index.d.ts +126 -6
- package/lib/types/rules.d.ts +33 -2
- package/package.json +7 -6
- package/lib/config/flat-config-helpers.js +0 -128
- package/lib/config/rule-validator.js +0 -199
- package/lib/shared/types.js +0 -246
package/lib/rules/max-params.js
CHANGED
@@ -20,6 +20,8 @@ const { upperCaseFirst } = require("../shared/string-utils");
|
|
20
20
|
module.exports = {
|
21
21
|
meta: {
|
22
22
|
type: "suggestion",
|
23
|
+
dialects: ["typescript", "javascript"],
|
24
|
+
language: "javascript",
|
23
25
|
|
24
26
|
docs: {
|
25
27
|
description:
|
@@ -46,6 +48,11 @@ module.exports = {
|
|
46
48
|
type: "integer",
|
47
49
|
minimum: 0,
|
48
50
|
},
|
51
|
+
countVoidThis: {
|
52
|
+
type: "boolean",
|
53
|
+
description:
|
54
|
+
"Whether to count a `this` declaration when the type is `void`.",
|
55
|
+
},
|
49
56
|
},
|
50
57
|
additionalProperties: false,
|
51
58
|
},
|
@@ -61,12 +68,16 @@ module.exports = {
|
|
61
68
|
const sourceCode = context.sourceCode;
|
62
69
|
const option = context.options[0];
|
63
70
|
let numParams = 3;
|
71
|
+
let countVoidThis = false;
|
64
72
|
|
65
|
-
if (
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
73
|
+
if (typeof option === "object") {
|
74
|
+
if (
|
75
|
+
Object.hasOwn(option, "maximum") ||
|
76
|
+
Object.hasOwn(option, "max")
|
77
|
+
) {
|
78
|
+
numParams = option.maximum || option.max;
|
79
|
+
}
|
80
|
+
countVoidThis = option.countVoidThis;
|
70
81
|
}
|
71
82
|
if (typeof option === "number") {
|
72
83
|
numParams = option;
|
@@ -79,7 +90,19 @@ module.exports = {
|
|
79
90
|
* @private
|
80
91
|
*/
|
81
92
|
function checkFunction(node) {
|
82
|
-
|
93
|
+
const hasVoidThisParam =
|
94
|
+
node.params.length > 0 &&
|
95
|
+
node.params[0].type === "Identifier" &&
|
96
|
+
node.params[0].name === "this" &&
|
97
|
+
node.params[0].typeAnnotation?.typeAnnotation.type ===
|
98
|
+
"TSVoidKeyword";
|
99
|
+
|
100
|
+
const effectiveParamCount =
|
101
|
+
hasVoidThisParam && !countVoidThis
|
102
|
+
? node.params.length - 1
|
103
|
+
: node.params.length;
|
104
|
+
|
105
|
+
if (effectiveParamCount > numParams) {
|
83
106
|
context.report({
|
84
107
|
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
|
85
108
|
node,
|
@@ -88,7 +111,7 @@ module.exports = {
|
|
88
111
|
name: upperCaseFirst(
|
89
112
|
astUtils.getFunctionNameWithKind(node),
|
90
113
|
),
|
91
|
-
count:
|
114
|
+
count: effectiveParamCount,
|
92
115
|
max: numParams,
|
93
116
|
},
|
94
117
|
});
|
@@ -99,6 +122,8 @@ module.exports = {
|
|
99
122
|
FunctionDeclaration: checkFunction,
|
100
123
|
ArrowFunctionExpression: checkFunction,
|
101
124
|
FunctionExpression: checkFunction,
|
125
|
+
TSDeclareFunction: checkFunction,
|
126
|
+
TSFunctionType: checkFunction,
|
102
127
|
};
|
103
128
|
},
|
104
129
|
};
|
@@ -34,6 +34,8 @@ module.exports = {
|
|
34
34
|
url: "https://eslint.org/docs/latest/rules/no-array-constructor",
|
35
35
|
},
|
36
36
|
|
37
|
+
fixable: "code",
|
38
|
+
|
37
39
|
hasSuggestions: true,
|
38
40
|
|
39
41
|
schema: [],
|
@@ -49,6 +51,30 @@ module.exports = {
|
|
49
51
|
create(context) {
|
50
52
|
const sourceCode = context.sourceCode;
|
51
53
|
|
54
|
+
/**
|
55
|
+
* Checks if there are comments in Array constructor expressions.
|
56
|
+
* @param {ASTNode} node A CallExpression or NewExpression node.
|
57
|
+
* @returns {boolean} True if there are comments, false otherwise.
|
58
|
+
*/
|
59
|
+
function hasCommentsInArrayConstructor(node) {
|
60
|
+
const firstToken = sourceCode.getFirstToken(node);
|
61
|
+
const lastToken = sourceCode.getLastToken(node);
|
62
|
+
|
63
|
+
let lastRelevantToken = sourceCode.getLastToken(node.callee);
|
64
|
+
|
65
|
+
while (
|
66
|
+
lastRelevantToken !== lastToken &&
|
67
|
+
!isOpeningParenToken(lastRelevantToken)
|
68
|
+
) {
|
69
|
+
lastRelevantToken = sourceCode.getTokenAfter(lastRelevantToken);
|
70
|
+
}
|
71
|
+
|
72
|
+
return sourceCode.commentsExistBetween(
|
73
|
+
firstToken,
|
74
|
+
lastRelevantToken,
|
75
|
+
);
|
76
|
+
}
|
77
|
+
|
52
78
|
/**
|
53
79
|
* Gets the text between the calling parentheses of a CallExpression or NewExpression.
|
54
80
|
* @param {ASTNode} node A CallExpression or NewExpression node.
|
@@ -107,6 +133,17 @@ module.exports = {
|
|
107
133
|
let fixText;
|
108
134
|
let messageId;
|
109
135
|
|
136
|
+
const nonSpreadCount = node.arguments.reduce(
|
137
|
+
(count, arg) =>
|
138
|
+
arg.type !== "SpreadElement" ? count + 1 : count,
|
139
|
+
0,
|
140
|
+
);
|
141
|
+
|
142
|
+
const shouldSuggest =
|
143
|
+
node.optional ||
|
144
|
+
(node.arguments.length > 0 && nonSpreadCount < 2) ||
|
145
|
+
hasCommentsInArrayConstructor(node);
|
146
|
+
|
110
147
|
/*
|
111
148
|
* Check if the suggested change should include a preceding semicolon or not.
|
112
149
|
* Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically
|
@@ -127,10 +164,23 @@ module.exports = {
|
|
127
164
|
context.report({
|
128
165
|
node,
|
129
166
|
messageId: "preferLiteral",
|
167
|
+
fix(fixer) {
|
168
|
+
if (shouldSuggest) {
|
169
|
+
return null;
|
170
|
+
}
|
171
|
+
|
172
|
+
return fixer.replaceText(node, fixText);
|
173
|
+
},
|
130
174
|
suggest: [
|
131
175
|
{
|
132
176
|
messageId,
|
133
|
-
fix
|
177
|
+
fix(fixer) {
|
178
|
+
if (shouldSuggest) {
|
179
|
+
return fixer.replaceText(node, fixText);
|
180
|
+
}
|
181
|
+
|
182
|
+
return null;
|
183
|
+
},
|
134
184
|
},
|
135
185
|
],
|
136
186
|
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/**
|
2
|
-
* @fileoverview Disallow shadowing of NaN, undefined, and Infinity (
|
2
|
+
* @fileoverview Disallow shadowing of globalThis, NaN, undefined, and Infinity (ES2020 section 18.1)
|
3
3
|
* @author Michael Ficarra
|
4
4
|
*/
|
5
5
|
"use strict";
|
@@ -32,13 +32,29 @@ module.exports = {
|
|
32
32
|
meta: {
|
33
33
|
type: "suggestion",
|
34
34
|
|
35
|
+
defaultOptions: [
|
36
|
+
{
|
37
|
+
reportGlobalThis: false,
|
38
|
+
},
|
39
|
+
],
|
40
|
+
|
35
41
|
docs: {
|
36
42
|
description: "Disallow identifiers from shadowing restricted names",
|
37
43
|
recommended: true,
|
38
44
|
url: "https://eslint.org/docs/latest/rules/no-shadow-restricted-names",
|
39
45
|
},
|
40
46
|
|
41
|
-
schema: [
|
47
|
+
schema: [
|
48
|
+
{
|
49
|
+
type: "object",
|
50
|
+
properties: {
|
51
|
+
reportGlobalThis: {
|
52
|
+
type: "boolean",
|
53
|
+
},
|
54
|
+
},
|
55
|
+
additionalProperties: false,
|
56
|
+
},
|
57
|
+
],
|
42
58
|
|
43
59
|
messages: {
|
44
60
|
shadowingRestrictedName: "Shadowing of global property '{{name}}'.",
|
@@ -46,6 +62,8 @@ module.exports = {
|
|
46
62
|
},
|
47
63
|
|
48
64
|
create(context) {
|
65
|
+
const [{ reportGlobalThis }] = context.options;
|
66
|
+
|
49
67
|
const RESTRICTED = new Set([
|
50
68
|
"undefined",
|
51
69
|
"NaN",
|
@@ -53,6 +71,11 @@ module.exports = {
|
|
53
71
|
"arguments",
|
54
72
|
"eval",
|
55
73
|
]);
|
74
|
+
|
75
|
+
if (reportGlobalThis) {
|
76
|
+
RESTRICTED.add("globalThis");
|
77
|
+
}
|
78
|
+
|
56
79
|
const sourceCode = context.sourceCode;
|
57
80
|
|
58
81
|
// Track reported nodes to avoid duplicate reports. For example, on class declarations.
|
@@ -0,0 +1,72 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Rule to flag variables that are never assigned
|
3
|
+
* @author Jacob Bandes-Storch <https://github.com/jtbandes>
|
4
|
+
*/
|
5
|
+
"use strict";
|
6
|
+
|
7
|
+
//------------------------------------------------------------------------------
|
8
|
+
// Rule Definition
|
9
|
+
//------------------------------------------------------------------------------
|
10
|
+
|
11
|
+
/** @type {import('../types').Rule.RuleModule} */
|
12
|
+
module.exports = {
|
13
|
+
meta: {
|
14
|
+
type: "problem",
|
15
|
+
dialects: ["typescript", "javascript"],
|
16
|
+
language: "javascript",
|
17
|
+
|
18
|
+
docs: {
|
19
|
+
description:
|
20
|
+
"Disallow `let` or `var` variables that are read but never assigned",
|
21
|
+
recommended: false,
|
22
|
+
url: "https://eslint.org/docs/latest/rules/no-unassigned-vars",
|
23
|
+
},
|
24
|
+
|
25
|
+
schema: [],
|
26
|
+
messages: {
|
27
|
+
unassigned:
|
28
|
+
"'{{name}}' is always 'undefined' because it's never assigned.",
|
29
|
+
},
|
30
|
+
},
|
31
|
+
|
32
|
+
create(context) {
|
33
|
+
const sourceCode = context.sourceCode;
|
34
|
+
|
35
|
+
return {
|
36
|
+
VariableDeclarator(node) {
|
37
|
+
/** @type {import('estree').VariableDeclaration} */
|
38
|
+
const declaration = node.parent;
|
39
|
+
const shouldCheck =
|
40
|
+
!node.init &&
|
41
|
+
node.id.type === "Identifier" &&
|
42
|
+
declaration.kind !== "const" &&
|
43
|
+
!declaration.declare;
|
44
|
+
if (!shouldCheck) {
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
const [variable] = sourceCode.getDeclaredVariables(node);
|
48
|
+
if (!variable) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
let hasRead = false;
|
52
|
+
for (const reference of variable.references) {
|
53
|
+
if (reference.isWrite()) {
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
if (reference.isRead()) {
|
57
|
+
hasRead = true;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
if (!hasRead) {
|
61
|
+
// Variables that are never read should be flagged by no-unused-vars instead
|
62
|
+
return;
|
63
|
+
}
|
64
|
+
context.report({
|
65
|
+
node,
|
66
|
+
messageId: "unassigned",
|
67
|
+
data: { name: node.id.name },
|
68
|
+
});
|
69
|
+
},
|
70
|
+
};
|
71
|
+
},
|
72
|
+
};
|
@@ -55,6 +55,9 @@ module.exports = {
|
|
55
55
|
enforceForJSX: {
|
56
56
|
type: "boolean",
|
57
57
|
},
|
58
|
+
ignoreDirectives: {
|
59
|
+
type: "boolean",
|
60
|
+
},
|
58
61
|
},
|
59
62
|
additionalProperties: false,
|
60
63
|
},
|
@@ -66,6 +69,7 @@ module.exports = {
|
|
66
69
|
allowTernary: false,
|
67
70
|
allowTaggedTemplates: false,
|
68
71
|
enforceForJSX: false,
|
72
|
+
ignoreDirectives: false,
|
69
73
|
},
|
70
74
|
],
|
71
75
|
|
@@ -82,6 +86,7 @@ module.exports = {
|
|
82
86
|
allowTernary,
|
83
87
|
allowTaggedTemplates,
|
84
88
|
enforceForJSX,
|
89
|
+
ignoreDirectives,
|
85
90
|
},
|
86
91
|
] = context.options;
|
87
92
|
|
@@ -211,7 +216,8 @@ module.exports = {
|
|
211
216
|
ExpressionStatement(node) {
|
212
217
|
if (
|
213
218
|
Checker.isDisallowed(node.expression) &&
|
214
|
-
!isDirective(node)
|
219
|
+
!astUtils.isDirective(node) &&
|
220
|
+
!(ignoreDirectives && isDirective(node))
|
215
221
|
) {
|
216
222
|
context.report({ node, messageId: "unusedExpression" });
|
217
223
|
}
|
@@ -60,6 +60,12 @@ module.exports = {
|
|
60
60
|
meta: {
|
61
61
|
type: "suggestion",
|
62
62
|
|
63
|
+
defaultOptions: [
|
64
|
+
{
|
65
|
+
allowRegexCharacters: [],
|
66
|
+
},
|
67
|
+
],
|
68
|
+
|
63
69
|
docs: {
|
64
70
|
description: "Disallow unnecessary escape characters",
|
65
71
|
recommended: true,
|
@@ -78,11 +84,26 @@ module.exports = {
|
|
78
84
|
"Replace the `\\` with `\\\\` to include the actual backslash character.",
|
79
85
|
},
|
80
86
|
|
81
|
-
schema: [
|
87
|
+
schema: [
|
88
|
+
{
|
89
|
+
type: "object",
|
90
|
+
properties: {
|
91
|
+
allowRegexCharacters: {
|
92
|
+
type: "array",
|
93
|
+
items: {
|
94
|
+
type: "string",
|
95
|
+
},
|
96
|
+
uniqueItems: true,
|
97
|
+
},
|
98
|
+
},
|
99
|
+
additionalProperties: false,
|
100
|
+
},
|
101
|
+
],
|
82
102
|
},
|
83
103
|
|
84
104
|
create(context) {
|
85
105
|
const sourceCode = context.sourceCode;
|
106
|
+
const [{ allowRegexCharacters }] = context.options;
|
86
107
|
const parser = new RegExpParser();
|
87
108
|
|
88
109
|
/**
|
@@ -217,7 +238,8 @@ module.exports = {
|
|
217
238
|
|
218
239
|
if (
|
219
240
|
escapedChar !==
|
220
|
-
|
241
|
+
String.fromCodePoint(characterNode.value) ||
|
242
|
+
allowRegexCharacters.includes(escapedChar)
|
221
243
|
) {
|
222
244
|
// It's a valid escape.
|
223
245
|
return;
|
@@ -17,6 +17,12 @@ const {
|
|
17
17
|
} = require("@eslint-community/eslint-utils");
|
18
18
|
const regexpp = require("@eslint-community/regexpp");
|
19
19
|
|
20
|
+
//------------------------------------------------------------------------------
|
21
|
+
// Typedefs
|
22
|
+
//------------------------------------------------------------------------------
|
23
|
+
|
24
|
+
/** @import { SuggestedEdit } from "@eslint/core"; */
|
25
|
+
|
20
26
|
//------------------------------------------------------------------------------
|
21
27
|
// Helpers
|
22
28
|
//------------------------------------------------------------------------------
|
@@ -29,7 +35,7 @@ const parser = new regexpp.RegExpParser();
|
|
29
35
|
* @param {string} pattern The regular expression pattern to be checked.
|
30
36
|
* @param {string} rawText Source text of the regexNode.
|
31
37
|
* @param {ASTNode} regexNode AST node which contains the regular expression.
|
32
|
-
* @returns {Array<
|
38
|
+
* @returns {Array<SuggestedEdit>} Fixer suggestions for the regex, if statically determinable.
|
33
39
|
*/
|
34
40
|
function suggestIfPossible(groupStart, pattern, rawText, regexNode) {
|
35
41
|
switch (regexNode.type) {
|
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
const debug = require("debug")("eslint:rules");
|
8
8
|
|
9
|
-
/** @typedef {import("../../
|
9
|
+
/** @typedef {import("../../types").Rule.RuleModule} Rule */
|
10
10
|
|
11
11
|
/**
|
12
12
|
* The `Map` object that loads each rule when it's accessed.
|
@@ -19,7 +19,7 @@ const debug = require("debug")("eslint:rules");
|
|
19
19
|
*
|
20
20
|
* rules.get("semi"); // call `() => require("semi")` here.
|
21
21
|
*
|
22
|
-
* @extends {Map<string,
|
22
|
+
* @extends {Map<string, Rule>}
|
23
23
|
*/
|
24
24
|
class LazyLoadingRuleMap extends Map {
|
25
25
|
/**
|
@@ -17,10 +17,9 @@ const { VFile } = require("../linter/vfile.js");
|
|
17
17
|
// Types
|
18
18
|
//-----------------------------------------------------------------------------
|
19
19
|
|
20
|
-
/** @typedef {import("../
|
20
|
+
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
|
21
21
|
/** @typedef {import("../linter/vfile.js").VFile} VFile */
|
22
22
|
/** @typedef {import("@eslint/core").Language} Language */
|
23
|
-
/** @typedef {import("@eslint/core").LanguageOptions} LanguageOptions */
|
24
23
|
/** @typedef {import("eslint").Linter.Processor} Processor */
|
25
24
|
|
26
25
|
//-----------------------------------------------------------------------------
|
@@ -12,14 +12,16 @@
|
|
12
12
|
const fs = require("node:fs");
|
13
13
|
const path = require("node:path");
|
14
14
|
const { calculateStatsPerFile } = require("../eslint/eslint-helpers");
|
15
|
+
const stringify = require("json-stable-stringify-without-jsonify");
|
15
16
|
|
16
17
|
//------------------------------------------------------------------------------
|
17
18
|
// Typedefs
|
18
19
|
//------------------------------------------------------------------------------
|
19
20
|
|
20
21
|
// For VSCode IntelliSense
|
21
|
-
/** @typedef {import("../
|
22
|
-
/** @typedef {import("../
|
22
|
+
/** @typedef {import("../types").Linter.LintMessage} LintMessage */
|
23
|
+
/** @typedef {import("../types").ESLint.LintResult} LintResult */
|
24
|
+
/** @typedef {Record<string, Record<string, { count: number; }>>} SuppressedViolations */
|
23
25
|
|
24
26
|
//-----------------------------------------------------------------------------
|
25
27
|
// Exports
|
@@ -224,7 +226,7 @@ class SuppressionsService {
|
|
224
226
|
save(suppressions) {
|
225
227
|
return fs.promises.writeFile(
|
226
228
|
this.filePath,
|
227
|
-
|
229
|
+
stringify(suppressions, { space: 2 }),
|
228
230
|
);
|
229
231
|
}
|
230
232
|
|
package/lib/shared/flags.js
CHANGED
@@ -26,21 +26,44 @@ function isSerializablePrimitiveOrPlainObject(val) {
|
|
26
26
|
* Check if a value is serializable.
|
27
27
|
* Functions or objects like RegExp cannot be serialized by JSON.stringify().
|
28
28
|
* Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript
|
29
|
-
* @param {any} val
|
30
|
-
* @
|
29
|
+
* @param {any} val The value
|
30
|
+
* @param {Set<Object>} seenObjects Objects already seen in this path from the root object.
|
31
|
+
* @returns {boolean} `true` if the value is serializable
|
31
32
|
*/
|
32
|
-
function isSerializable(val) {
|
33
|
+
function isSerializable(val, seenObjects = new Set()) {
|
33
34
|
if (!isSerializablePrimitiveOrPlainObject(val)) {
|
34
35
|
return false;
|
35
36
|
}
|
36
|
-
if (typeof val === "object") {
|
37
|
+
if (typeof val === "object" && val !== null) {
|
38
|
+
if (seenObjects.has(val)) {
|
39
|
+
/*
|
40
|
+
* Since this is a depth-first traversal, encountering
|
41
|
+
* the same object again means there is a circular reference.
|
42
|
+
* Objects with circular references are not serializable.
|
43
|
+
*/
|
44
|
+
return false;
|
45
|
+
}
|
37
46
|
for (const property in val) {
|
38
47
|
if (Object.hasOwn(val, property)) {
|
39
48
|
if (!isSerializablePrimitiveOrPlainObject(val[property])) {
|
40
49
|
return false;
|
41
50
|
}
|
42
|
-
if (
|
43
|
-
|
51
|
+
if (
|
52
|
+
typeof val[property] === "object" &&
|
53
|
+
val[property] !== null
|
54
|
+
) {
|
55
|
+
if (
|
56
|
+
/*
|
57
|
+
* We're creating a new Set of seen objects because we want to
|
58
|
+
* ensure that `val` doesn't appear again in this path, but it can appear
|
59
|
+
* in other paths. This allows for resuing objects in the graph, as long as
|
60
|
+
* there are no cycles.
|
61
|
+
*/
|
62
|
+
!isSerializable(
|
63
|
+
val[property],
|
64
|
+
new Set([...seenObjects, val]),
|
65
|
+
)
|
66
|
+
) {
|
44
67
|
return false;
|
45
68
|
}
|
46
69
|
}
|