eslint 8.57.0 → 9.2.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 +31 -28
- package/bin/eslint.js +4 -3
- package/conf/ecma-version.js +16 -0
- package/conf/globals.js +1 -0
- package/conf/rule-type-list.json +3 -1
- package/lib/api.js +7 -11
- package/lib/cli-engine/cli-engine.js +14 -3
- package/lib/cli-engine/formatters/formatters-meta.json +1 -29
- package/lib/cli-engine/lint-result-cache.js +2 -2
- package/lib/cli.js +115 -36
- package/lib/config/default-config.js +3 -0
- package/lib/config/flat-config-array.js +110 -24
- package/lib/config/flat-config-helpers.js +41 -20
- package/lib/config/flat-config-schema.js +1 -7
- package/lib/config/rule-validator.js +42 -6
- package/lib/eslint/eslint-helpers.js +116 -58
- package/lib/eslint/eslint.js +892 -377
- package/lib/eslint/index.js +2 -2
- package/lib/eslint/legacy-eslint.js +728 -0
- package/lib/linter/apply-disable-directives.js +59 -31
- package/lib/linter/code-path-analysis/code-path-analyzer.js +0 -1
- package/lib/linter/code-path-analysis/code-path.js +32 -30
- package/lib/linter/code-path-analysis/fork-context.js +1 -1
- package/lib/linter/config-comment-parser.js +8 -11
- package/lib/linter/index.js +1 -3
- package/lib/linter/interpolate.js +24 -2
- package/lib/linter/linter.js +428 -207
- package/lib/linter/report-translator.js +3 -3
- package/lib/linter/rules.js +6 -15
- package/lib/linter/source-code-fixer.js +1 -1
- package/lib/linter/timing.js +16 -8
- package/lib/options.js +35 -3
- package/lib/rule-tester/index.js +3 -1
- package/lib/rule-tester/rule-tester.js +424 -347
- package/lib/rules/array-bracket-newline.js +1 -1
- package/lib/rules/array-bracket-spacing.js +1 -1
- package/lib/rules/block-scoped-var.js +1 -1
- package/lib/rules/callback-return.js +2 -2
- package/lib/rules/camelcase.js +3 -5
- package/lib/rules/capitalized-comments.js +10 -7
- package/lib/rules/comma-dangle.js +1 -1
- package/lib/rules/comma-style.js +2 -2
- package/lib/rules/complexity.js +14 -1
- package/lib/rules/constructor-super.js +99 -100
- package/lib/rules/default-case.js +1 -1
- package/lib/rules/eol-last.js +2 -2
- package/lib/rules/function-paren-newline.js +2 -2
- package/lib/rules/indent-legacy.js +5 -5
- package/lib/rules/indent.js +5 -5
- package/lib/rules/index.js +1 -2
- package/lib/rules/key-spacing.js +2 -2
- package/lib/rules/line-comment-position.js +1 -1
- package/lib/rules/lines-around-directive.js +2 -2
- package/lib/rules/max-depth.js +1 -1
- package/lib/rules/max-len.js +3 -3
- package/lib/rules/max-lines.js +3 -3
- package/lib/rules/max-nested-callbacks.js +1 -1
- package/lib/rules/max-params.js +1 -1
- package/lib/rules/max-statements.js +1 -1
- package/lib/rules/multiline-comment-style.js +7 -7
- package/lib/rules/new-cap.js +1 -1
- package/lib/rules/newline-after-var.js +1 -1
- package/lib/rules/newline-before-return.js +1 -1
- package/lib/rules/no-case-declarations.js +13 -1
- package/lib/rules/no-constant-binary-expression.js +7 -8
- package/lib/rules/no-constant-condition.js +18 -7
- package/lib/rules/no-constructor-return.js +2 -2
- package/lib/rules/no-dupe-class-members.js +2 -2
- package/lib/rules/no-else-return.js +1 -1
- package/lib/rules/no-empty-function.js +2 -2
- package/lib/rules/no-empty-static-block.js +1 -1
- package/lib/rules/no-extend-native.js +1 -2
- package/lib/rules/no-extra-semi.js +1 -1
- package/lib/rules/no-fallthrough.js +41 -16
- package/lib/rules/no-implicit-coercion.js +66 -24
- package/lib/rules/no-inner-declarations.js +23 -2
- package/lib/rules/no-invalid-regexp.js +1 -1
- package/lib/rules/no-invalid-this.js +1 -1
- package/lib/rules/no-lone-blocks.js +3 -3
- package/lib/rules/no-loss-of-precision.js +1 -1
- package/lib/rules/no-misleading-character-class.js +225 -69
- package/lib/rules/no-mixed-spaces-and-tabs.js +1 -1
- package/lib/rules/no-multiple-empty-lines.js +1 -1
- package/lib/rules/no-new-native-nonconstructor.js +1 -1
- package/lib/rules/no-new-symbol.js +8 -1
- package/lib/rules/no-restricted-globals.js +1 -1
- package/lib/rules/no-restricted-imports.js +186 -40
- package/lib/rules/no-restricted-modules.js +2 -2
- package/lib/rules/no-return-await.js +1 -1
- package/lib/rules/no-sequences.js +1 -0
- package/lib/rules/no-this-before-super.js +45 -13
- package/lib/rules/no-trailing-spaces.js +2 -3
- package/lib/rules/no-unneeded-ternary.js +1 -1
- package/lib/rules/no-unsafe-optional-chaining.js +1 -1
- package/lib/rules/no-unused-private-class-members.js +1 -1
- package/lib/rules/no-unused-vars.js +197 -36
- package/lib/rules/no-useless-assignment.js +566 -0
- package/lib/rules/no-useless-backreference.js +1 -1
- package/lib/rules/no-useless-computed-key.js +2 -2
- package/lib/rules/no-useless-return.js +7 -2
- package/lib/rules/object-curly-spacing.js +3 -3
- package/lib/rules/object-property-newline.js +1 -1
- package/lib/rules/one-var.js +5 -5
- package/lib/rules/padded-blocks.js +7 -7
- package/lib/rules/prefer-arrow-callback.js +3 -3
- package/lib/rules/prefer-reflect.js +1 -1
- package/lib/rules/prefer-regex-literals.js +1 -1
- package/lib/rules/prefer-template.js +1 -1
- package/lib/rules/radix.js +2 -2
- package/lib/rules/semi-style.js +1 -1
- package/lib/rules/sort-imports.js +1 -1
- package/lib/rules/sort-keys.js +1 -1
- package/lib/rules/sort-vars.js +1 -1
- package/lib/rules/space-unary-ops.js +1 -1
- package/lib/rules/strict.js +1 -1
- package/lib/rules/use-isnan.js +101 -7
- package/lib/rules/utils/ast-utils.js +16 -7
- package/lib/rules/utils/char-source.js +240 -0
- package/lib/rules/utils/lazy-loading-rule-map.js +1 -1
- package/lib/rules/utils/unicode/index.js +9 -4
- package/lib/rules/yield-star-spacing.js +1 -1
- package/lib/shared/runtime-info.js +1 -0
- package/lib/shared/serialization.js +55 -0
- package/lib/shared/stats.js +30 -0
- package/lib/shared/string-utils.js +9 -11
- package/lib/shared/types.js +35 -1
- package/lib/source-code/index.js +3 -1
- package/lib/source-code/source-code.js +299 -85
- package/lib/source-code/token-store/backward-token-cursor.js +3 -3
- package/lib/source-code/token-store/cursors.js +4 -2
- package/lib/source-code/token-store/forward-token-comment-cursor.js +3 -3
- package/lib/source-code/token-store/forward-token-cursor.js +3 -3
- package/lib/source-code/token-store/index.js +2 -2
- package/lib/unsupported-api.js +3 -5
- package/messages/no-config-found.js +1 -1
- package/messages/plugin-conflict.js +1 -1
- package/messages/plugin-invalid.js +1 -1
- package/messages/plugin-missing.js +1 -1
- package/package.json +32 -29
- package/conf/config-schema.js +0 -93
- package/lib/cli-engine/formatters/checkstyle.js +0 -60
- package/lib/cli-engine/formatters/compact.js +0 -60
- package/lib/cli-engine/formatters/jslint-xml.js +0 -41
- package/lib/cli-engine/formatters/junit.js +0 -82
- package/lib/cli-engine/formatters/tap.js +0 -95
- package/lib/cli-engine/formatters/unix.js +0 -58
- package/lib/cli-engine/formatters/visualstudio.js +0 -63
- package/lib/cli-engine/xml-escape.js +0 -34
- package/lib/eslint/flat-eslint.js +0 -1155
- package/lib/rule-tester/flat-rule-tester.js +0 -1131
- package/lib/rules/require-jsdoc.js +0 -122
- package/lib/rules/utils/patterns/letters.js +0 -36
- package/lib/rules/valid-jsdoc.js +0 -516
- package/lib/shared/config-validator.js +0 -347
- package/lib/shared/deprecation-warnings.js +0 -58
- package/lib/shared/relative-module-resolver.js +0 -50
@@ -1,68 +1,42 @@
|
|
1
1
|
/**
|
2
|
-
* @fileoverview Mocha test wrapper
|
2
|
+
* @fileoverview Mocha/Jest test wrapper
|
3
3
|
* @author Ilya Volodin
|
4
4
|
*/
|
5
5
|
"use strict";
|
6
6
|
|
7
7
|
/* globals describe, it -- Mocha globals */
|
8
8
|
|
9
|
-
/*
|
10
|
-
* This is a wrapper around mocha to allow for DRY unittests for eslint
|
11
|
-
* Format:
|
12
|
-
* RuleTester.run("{ruleName}", {
|
13
|
-
* valid: [
|
14
|
-
* "{code}",
|
15
|
-
* { code: "{code}", options: {options}, globals: {globals}, parser: "{parser}", settings: {settings} }
|
16
|
-
* ],
|
17
|
-
* invalid: [
|
18
|
-
* { code: "{code}", errors: {numErrors} },
|
19
|
-
* { code: "{code}", errors: ["{errorMessage}"] },
|
20
|
-
* { code: "{code}", options: {options}, globals: {globals}, parser: "{parser}", settings: {settings}, errors: [{ message: "{errorMessage}", type: "{errorNodeType}"}] }
|
21
|
-
* ]
|
22
|
-
* });
|
23
|
-
*
|
24
|
-
* Variables:
|
25
|
-
* {code} - String that represents the code to be tested
|
26
|
-
* {options} - Arguments that are passed to the configurable rules.
|
27
|
-
* {globals} - An object representing a list of variables that are
|
28
|
-
* registered as globals
|
29
|
-
* {parser} - String representing the parser to use
|
30
|
-
* {settings} - An object representing global settings for all rules
|
31
|
-
* {numErrors} - If failing case doesn't need to check error message,
|
32
|
-
* this integer will specify how many errors should be
|
33
|
-
* received
|
34
|
-
* {errorMessage} - Message that is returned by the rule on failure
|
35
|
-
* {errorNodeType} - AST node type that is returned by they rule as
|
36
|
-
* a cause of the failure.
|
37
|
-
*/
|
38
|
-
|
39
9
|
//------------------------------------------------------------------------------
|
40
10
|
// Requirements
|
41
11
|
//------------------------------------------------------------------------------
|
42
12
|
|
43
13
|
const
|
44
14
|
assert = require("assert"),
|
45
|
-
path = require("path"),
|
46
15
|
util = require("util"),
|
47
|
-
|
16
|
+
path = require("path"),
|
48
17
|
equal = require("fast-deep-equal"),
|
49
|
-
Traverser = require("
|
50
|
-
{ getRuleOptionsSchema
|
51
|
-
{ Linter, SourceCodeFixer
|
52
|
-
|
18
|
+
Traverser = require("../shared/traverser"),
|
19
|
+
{ getRuleOptionsSchema } = require("../config/flat-config-helpers"),
|
20
|
+
{ Linter, SourceCodeFixer } = require("../linter"),
|
21
|
+
{ interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
|
22
|
+
stringify = require("json-stable-stringify-without-jsonify");
|
23
|
+
|
24
|
+
const { FlatConfigArray } = require("../config/flat-config-array");
|
25
|
+
const { defaultConfig } = require("../config/default-config");
|
53
26
|
|
54
27
|
const ajv = require("../shared/ajv")({ strictDefaults: true });
|
55
28
|
|
56
|
-
const espreePath = require.resolve("espree");
|
57
29
|
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
|
58
|
-
|
59
30
|
const { SourceCode } = require("../source-code");
|
31
|
+
const { ConfigArraySymbol } = require("@humanwhocodes/config-array");
|
32
|
+
const { isSerializable } = require("../shared/serialization");
|
60
33
|
|
61
34
|
//------------------------------------------------------------------------------
|
62
35
|
// Typedefs
|
63
36
|
//------------------------------------------------------------------------------
|
64
37
|
|
65
38
|
/** @typedef {import("../shared/types").Parser} Parser */
|
39
|
+
/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
|
66
40
|
/** @typedef {import("../shared/types").Rule} Rule */
|
67
41
|
|
68
42
|
|
@@ -72,12 +46,9 @@ const { SourceCode } = require("../source-code");
|
|
72
46
|
* @property {string} [name] Name for the test case.
|
73
47
|
* @property {string} code Code for the test case.
|
74
48
|
* @property {any[]} [options] Options for the test case.
|
49
|
+
* @property {LanguageOptions} [languageOptions] The language options to use in the test case.
|
75
50
|
* @property {{ [name: string]: any }} [settings] Settings for the test case.
|
76
51
|
* @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
|
77
|
-
* @property {string} [parser] The absolute path for the parser.
|
78
|
-
* @property {{ [name: string]: any }} [parserOptions] Options for the parser.
|
79
|
-
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
|
80
|
-
* @property {{ [name: string]: boolean }} [env] Environments for the test case.
|
81
52
|
* @property {boolean} [only] Run only this test case or the subset of test cases with this property.
|
82
53
|
*/
|
83
54
|
|
@@ -91,10 +62,7 @@ const { SourceCode } = require("../source-code");
|
|
91
62
|
* @property {any[]} [options] Options for the test case.
|
92
63
|
* @property {{ [name: string]: any }} [settings] Settings for the test case.
|
93
64
|
* @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
|
94
|
-
* @property {
|
95
|
-
* @property {{ [name: string]: any }} [parserOptions] Options for the parser.
|
96
|
-
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
|
97
|
-
* @property {{ [name: string]: boolean }} [env] Environments for the test case.
|
65
|
+
* @property {LanguageOptions} [languageOptions] The language options to use in the test case.
|
98
66
|
* @property {boolean} [only] Run only this test case or the subset of test cases with this property.
|
99
67
|
*/
|
100
68
|
|
@@ -120,7 +88,12 @@ const { SourceCode } = require("../source-code");
|
|
120
88
|
* the initial default configuration
|
121
89
|
*/
|
122
90
|
const testerDefaultConfig = { rules: {} };
|
123
|
-
|
91
|
+
|
92
|
+
/*
|
93
|
+
* RuleTester uses this config as its default. This can be overwritten via
|
94
|
+
* setDefaultConfig().
|
95
|
+
*/
|
96
|
+
let sharedDefaultConfig = { rules: {} };
|
124
97
|
|
125
98
|
/*
|
126
99
|
* List every parameters possible on a test case that are not related to eslint
|
@@ -163,42 +136,25 @@ const suggestionObjectParameters = new Set([
|
|
163
136
|
]);
|
164
137
|
const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
|
165
138
|
|
139
|
+
/*
|
140
|
+
* Ignored test case properties when checking for test case duplicates.
|
141
|
+
*/
|
142
|
+
const duplicationIgnoredParameters = new Set([
|
143
|
+
"name",
|
144
|
+
"errors",
|
145
|
+
"output"
|
146
|
+
]);
|
147
|
+
|
166
148
|
const forbiddenMethods = [
|
167
149
|
"applyInlineConfig",
|
168
150
|
"applyLanguageOptions",
|
169
151
|
"finalize"
|
170
152
|
];
|
171
153
|
|
172
|
-
|
154
|
+
/** @type {Map<string,WeakSet>} */
|
155
|
+
const forbiddenMethodCalls = new Map(forbiddenMethods.map(methodName => ([methodName, new WeakSet()])));
|
173
156
|
|
174
|
-
const
|
175
|
-
getSource: "getText",
|
176
|
-
getSourceLines: "getLines",
|
177
|
-
getAllComments: "getAllComments",
|
178
|
-
getNodeByRangeIndex: "getNodeByRangeIndex",
|
179
|
-
|
180
|
-
// getComments: "getComments", -- already handled by a separate error
|
181
|
-
getCommentsBefore: "getCommentsBefore",
|
182
|
-
getCommentsAfter: "getCommentsAfter",
|
183
|
-
getCommentsInside: "getCommentsInside",
|
184
|
-
getJSDocComment: "getJSDocComment",
|
185
|
-
getFirstToken: "getFirstToken",
|
186
|
-
getFirstTokens: "getFirstTokens",
|
187
|
-
getLastToken: "getLastToken",
|
188
|
-
getLastTokens: "getLastTokens",
|
189
|
-
getTokenAfter: "getTokenAfter",
|
190
|
-
getTokenBefore: "getTokenBefore",
|
191
|
-
getTokenByRangeStart: "getTokenByRangeStart",
|
192
|
-
getTokens: "getTokens",
|
193
|
-
getTokensAfter: "getTokensAfter",
|
194
|
-
getTokensBefore: "getTokensBefore",
|
195
|
-
getTokensBetween: "getTokensBetween",
|
196
|
-
|
197
|
-
getScope: "getScope",
|
198
|
-
getAncestors: "getAncestors",
|
199
|
-
getDeclaredVariables: "getDeclaredVariables",
|
200
|
-
markVariableAsUsed: "markVariableAsUsed"
|
201
|
-
};
|
157
|
+
const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
|
202
158
|
|
203
159
|
/**
|
204
160
|
* Clones a given value deeply.
|
@@ -331,23 +287,27 @@ function wrapParser(parser) {
|
|
331
287
|
}
|
332
288
|
|
333
289
|
/**
|
334
|
-
* Function to replace `SourceCode.
|
335
|
-
* @returns {void}
|
336
|
-
* @throws {Error} Deprecation message.
|
337
|
-
*/
|
338
|
-
function getCommentsDeprecation() {
|
339
|
-
throw new Error(
|
340
|
-
"`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead."
|
341
|
-
);
|
342
|
-
}
|
343
|
-
|
344
|
-
/**
|
345
|
-
* Function to replace forbidden `SourceCode` methods.
|
290
|
+
* Function to replace forbidden `SourceCode` methods. Allows just one call per method.
|
346
291
|
* @param {string} methodName The name of the method to forbid.
|
292
|
+
* @param {Function} prototype The prototype with the original method to call.
|
347
293
|
* @returns {Function} The function that throws the error.
|
348
294
|
*/
|
349
|
-
function throwForbiddenMethodError(methodName) {
|
350
|
-
|
295
|
+
function throwForbiddenMethodError(methodName, prototype) {
|
296
|
+
|
297
|
+
const original = prototype[methodName];
|
298
|
+
|
299
|
+
return function(...args) {
|
300
|
+
|
301
|
+
const called = forbiddenMethodCalls.get(methodName);
|
302
|
+
|
303
|
+
/* eslint-disable no-invalid-this -- needed to operate as a method. */
|
304
|
+
if (!called.has(this)) {
|
305
|
+
called.add(this);
|
306
|
+
|
307
|
+
return original.apply(this, args);
|
308
|
+
}
|
309
|
+
/* eslint-enable no-invalid-this -- not needed past this point */
|
310
|
+
|
351
311
|
throw new Error(
|
352
312
|
`\`SourceCode#${methodName}()\` cannot be called inside a rule.`
|
353
313
|
);
|
@@ -355,81 +315,45 @@ function throwForbiddenMethodError(methodName) {
|
|
355
315
|
}
|
356
316
|
|
357
317
|
/**
|
358
|
-
*
|
359
|
-
* @param
|
360
|
-
* @returns {
|
318
|
+
* Extracts names of {{ placeholders }} from the reported message.
|
319
|
+
* @param {string} message Reported message
|
320
|
+
* @returns {string[]} Array of placeholder names
|
361
321
|
*/
|
362
|
-
function
|
363
|
-
|
364
|
-
emitLegacyRuleAPIWarning[`warned-${ruleName}`] = true;
|
365
|
-
process.emitWarning(
|
366
|
-
`"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules`,
|
367
|
-
"DeprecationWarning"
|
368
|
-
);
|
369
|
-
}
|
370
|
-
}
|
322
|
+
function getMessagePlaceholders(message) {
|
323
|
+
const matcher = getPlaceholderMatcher();
|
371
324
|
|
372
|
-
|
373
|
-
* Emit a deprecation warning if rule has options but is missing the "meta.schema" property
|
374
|
-
* @param {string} ruleName Name of the rule.
|
375
|
-
* @returns {void}
|
376
|
-
*/
|
377
|
-
function emitMissingSchemaWarning(ruleName) {
|
378
|
-
if (!emitMissingSchemaWarning[`warned-${ruleName}`]) {
|
379
|
-
emitMissingSchemaWarning[`warned-${ruleName}`] = true;
|
380
|
-
process.emitWarning(
|
381
|
-
`"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas`,
|
382
|
-
"DeprecationWarning"
|
383
|
-
);
|
384
|
-
}
|
325
|
+
return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
|
385
326
|
}
|
386
327
|
|
387
328
|
/**
|
388
|
-
*
|
389
|
-
*
|
390
|
-
* @param {string}
|
391
|
-
* @
|
329
|
+
* Returns the placeholders in the reported messages but
|
330
|
+
* only includes the placeholders available in the raw message and not in the provided data.
|
331
|
+
* @param {string} message The reported message
|
332
|
+
* @param {string} raw The raw message specified in the rule meta.messages
|
333
|
+
* @param {undefined|Record<unknown, unknown>} data The passed
|
334
|
+
* @returns {string[]} Missing placeholder names
|
392
335
|
*/
|
393
|
-
function
|
394
|
-
|
395
|
-
emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true;
|
396
|
-
process.emitWarning(
|
397
|
-
`"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`,
|
398
|
-
"DeprecationWarning"
|
399
|
-
);
|
400
|
-
}
|
401
|
-
}
|
336
|
+
function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) {
|
337
|
+
const unsubstituted = getMessagePlaceholders(message);
|
402
338
|
|
403
|
-
|
404
|
-
|
405
|
-
* @param {string} ruleName Name of the rule.
|
406
|
-
* @returns {void}
|
407
|
-
*/
|
408
|
-
function emitCodePathCurrentSegmentsWarning(ruleName) {
|
409
|
-
if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) {
|
410
|
-
emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true;
|
411
|
-
process.emitWarning(
|
412
|
-
`"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`,
|
413
|
-
"DeprecationWarning"
|
414
|
-
);
|
339
|
+
if (unsubstituted.length === 0) {
|
340
|
+
return [];
|
415
341
|
}
|
416
|
-
}
|
417
342
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
function emitParserServicesWarning(ruleName) {
|
424
|
-
if (!emitParserServicesWarning[`warned-${ruleName}`]) {
|
425
|
-
emitParserServicesWarning[`warned-${ruleName}`] = true;
|
426
|
-
process.emitWarning(
|
427
|
-
`"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`,
|
428
|
-
"DeprecationWarning"
|
429
|
-
);
|
430
|
-
}
|
343
|
+
// Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property
|
344
|
+
const known = getMessagePlaceholders(raw);
|
345
|
+
const provided = Object.keys(data);
|
346
|
+
|
347
|
+
return unsubstituted.filter(name => known.includes(name) && !provided.includes(name));
|
431
348
|
}
|
432
349
|
|
350
|
+
const metaSchemaDescription = `
|
351
|
+
\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.
|
352
|
+
\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule.
|
353
|
+
\t- You can also set \`meta.schema\` to \`false\` to opt-out of options validation (not recommended).
|
354
|
+
|
355
|
+
\thttps://eslint.org/docs/latest/extend/custom-rules#options-schemas
|
356
|
+
`;
|
433
357
|
|
434
358
|
//------------------------------------------------------------------------------
|
435
359
|
// Public Interface
|
@@ -479,26 +403,20 @@ class RuleTester {
|
|
479
403
|
* Creates a new instance of RuleTester.
|
480
404
|
* @param {Object} [testerConfig] Optional, extra configuration for the tester
|
481
405
|
*/
|
482
|
-
constructor(testerConfig) {
|
406
|
+
constructor(testerConfig = {}) {
|
483
407
|
|
484
408
|
/**
|
485
409
|
* The configuration to use for this tester. Combination of the tester
|
486
410
|
* configuration and the default configuration.
|
487
411
|
* @type {Object}
|
488
412
|
*/
|
489
|
-
this.testerConfig =
|
490
|
-
|
491
|
-
defaultConfig,
|
413
|
+
this.testerConfig = [
|
414
|
+
sharedDefaultConfig,
|
492
415
|
testerConfig,
|
493
416
|
{ rules: { "rule-tester/validate-ast": "error" } }
|
494
|
-
|
417
|
+
];
|
495
418
|
|
496
|
-
|
497
|
-
* Rule definitions to define before tests.
|
498
|
-
* @type {Object}
|
499
|
-
*/
|
500
|
-
this.rules = {};
|
501
|
-
this.linter = new Linter();
|
419
|
+
this.linter = new Linter({ configType: "flat" });
|
502
420
|
}
|
503
421
|
|
504
422
|
/**
|
@@ -511,10 +429,10 @@ class RuleTester {
|
|
511
429
|
if (typeof config !== "object" || config === null) {
|
512
430
|
throw new TypeError("RuleTester.setDefaultConfig: config must be an object");
|
513
431
|
}
|
514
|
-
|
432
|
+
sharedDefaultConfig = config;
|
515
433
|
|
516
434
|
// Make sure the rules object exists since it is assumed to exist later
|
517
|
-
|
435
|
+
sharedDefaultConfig.rules = sharedDefaultConfig.rules || {};
|
518
436
|
}
|
519
437
|
|
520
438
|
/**
|
@@ -522,7 +440,7 @@ class RuleTester {
|
|
522
440
|
* @returns {Object} the current configuration
|
523
441
|
*/
|
524
442
|
static getDefaultConfig() {
|
525
|
-
return
|
443
|
+
return sharedDefaultConfig;
|
526
444
|
}
|
527
445
|
|
528
446
|
/**
|
@@ -531,7 +449,11 @@ class RuleTester {
|
|
531
449
|
* @returns {void}
|
532
450
|
*/
|
533
451
|
static resetDefaultConfig() {
|
534
|
-
|
452
|
+
sharedDefaultConfig = {
|
453
|
+
rules: {
|
454
|
+
...testerDefaultConfig.rules
|
455
|
+
}
|
456
|
+
};
|
535
457
|
}
|
536
458
|
|
537
459
|
|
@@ -602,29 +524,17 @@ class RuleTester {
|
|
602
524
|
this[IT_ONLY] = value;
|
603
525
|
}
|
604
526
|
|
605
|
-
/**
|
606
|
-
* Define a rule for one particular run of tests.
|
607
|
-
* @param {string} name The name of the rule to define.
|
608
|
-
* @param {Function | Rule} rule The rule definition.
|
609
|
-
* @returns {void}
|
610
|
-
*/
|
611
|
-
defineRule(name, rule) {
|
612
|
-
if (typeof rule === "function") {
|
613
|
-
emitLegacyRuleAPIWarning(name);
|
614
|
-
}
|
615
|
-
this.rules[name] = rule;
|
616
|
-
}
|
617
527
|
|
618
528
|
/**
|
619
529
|
* Adds a new rule test to execute.
|
620
530
|
* @param {string} ruleName The name of the rule to run.
|
621
|
-
* @param {
|
531
|
+
* @param {Rule} rule The rule to test.
|
622
532
|
* @param {{
|
623
533
|
* valid: (ValidTestCase | string)[],
|
624
534
|
* invalid: InvalidTestCase[]
|
625
535
|
* }} test The collection of tests to run.
|
626
|
-
* @throws {TypeError|Error} If
|
627
|
-
* scenario of the given type is missing.
|
536
|
+
* @throws {TypeError|Error} If `rule` is not an object with a `create` method,
|
537
|
+
* or if non-object `test`, or if a required scenario of the given type is missing.
|
628
538
|
* @returns {void}
|
629
539
|
*/
|
630
540
|
run(ruleName, rule, test) {
|
@@ -632,7 +542,15 @@ class RuleTester {
|
|
632
542
|
const testerConfig = this.testerConfig,
|
633
543
|
requiredScenarios = ["valid", "invalid"],
|
634
544
|
scenarioErrors = [],
|
635
|
-
linter = this.linter
|
545
|
+
linter = this.linter,
|
546
|
+
ruleId = `rule-to-test/${ruleName}`;
|
547
|
+
|
548
|
+
const seenValidTestCases = new Set();
|
549
|
+
const seenInvalidTestCases = new Set();
|
550
|
+
|
551
|
+
if (!rule || typeof rule !== "object" || typeof rule.create !== "function") {
|
552
|
+
throw new TypeError("Rule must be an object with a `create` method");
|
553
|
+
}
|
636
554
|
|
637
555
|
if (!test || typeof test !== "object") {
|
638
556
|
throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
|
@@ -650,54 +568,55 @@ class RuleTester {
|
|
650
568
|
].concat(scenarioErrors).join("\n"));
|
651
569
|
}
|
652
570
|
|
653
|
-
|
654
|
-
|
655
|
-
|
571
|
+
const baseConfig = [
|
572
|
+
{ files: ["**"] }, // Make sure the default config matches for all files
|
573
|
+
{
|
574
|
+
plugins: {
|
656
575
|
|
657
|
-
|
658
|
-
|
659
|
-
// Create a wrapper rule that freezes the `context` properties.
|
660
|
-
create(context) {
|
661
|
-
freezeDeeply(context.options);
|
662
|
-
freezeDeeply(context.settings);
|
663
|
-
freezeDeeply(context.parserOptions);
|
664
|
-
|
665
|
-
// wrap all deprecated methods
|
666
|
-
const newContext = Object.create(
|
667
|
-
context,
|
668
|
-
Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [
|
669
|
-
methodName,
|
670
|
-
{
|
671
|
-
value(...args) {
|
672
|
-
|
673
|
-
// emit deprecation warning
|
674
|
-
emitDeprecatedContextMethodWarning(ruleName, methodName);
|
675
|
-
|
676
|
-
// call the original method
|
677
|
-
return context[methodName].call(this, ...args);
|
678
|
-
},
|
679
|
-
enumerable: true
|
680
|
-
}
|
681
|
-
]))
|
682
|
-
);
|
576
|
+
// copy root plugin over
|
577
|
+
"@": {
|
683
578
|
|
684
|
-
|
685
|
-
|
579
|
+
/*
|
580
|
+
* Parsers are wrapped to detect more errors, so this needs
|
581
|
+
* to be a new object for each call to run(), otherwise the
|
582
|
+
* parsers will be wrapped multiple times.
|
583
|
+
*/
|
584
|
+
parsers: {
|
585
|
+
...defaultConfig[0].plugins["@"].parsers
|
586
|
+
},
|
686
587
|
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
588
|
+
/*
|
589
|
+
* The rules key on the default plugin is a proxy to lazy-load
|
590
|
+
* just the rules that are needed. So, don't create a new object
|
591
|
+
* here, just use the default one to keep that performance
|
592
|
+
* enhancement.
|
593
|
+
*/
|
594
|
+
rules: defaultConfig[0].plugins["@"].rules
|
595
|
+
},
|
596
|
+
"rule-to-test": {
|
597
|
+
rules: {
|
598
|
+
[ruleName]: Object.assign({}, rule, {
|
693
599
|
|
694
|
-
|
600
|
+
// Create a wrapper rule that freezes the `context` properties.
|
601
|
+
create(context) {
|
602
|
+
freezeDeeply(context.options);
|
603
|
+
freezeDeeply(context.settings);
|
604
|
+
freezeDeeply(context.parserOptions);
|
695
605
|
|
696
|
-
|
697
|
-
}
|
698
|
-
}));
|
606
|
+
// freezeDeeply(context.languageOptions);
|
699
607
|
|
700
|
-
|
608
|
+
return rule.create(context);
|
609
|
+
}
|
610
|
+
})
|
611
|
+
}
|
612
|
+
}
|
613
|
+
},
|
614
|
+
languageOptions: {
|
615
|
+
...defaultConfig[0].languageOptions
|
616
|
+
}
|
617
|
+
},
|
618
|
+
...defaultConfig.slice(1)
|
619
|
+
];
|
701
620
|
|
702
621
|
/**
|
703
622
|
* Run the rule for the given item
|
@@ -707,8 +626,34 @@ class RuleTester {
|
|
707
626
|
* @private
|
708
627
|
*/
|
709
628
|
function runRuleForItem(item) {
|
710
|
-
|
711
|
-
|
629
|
+
const flatConfigArrayOptions = {
|
630
|
+
baseConfig
|
631
|
+
};
|
632
|
+
|
633
|
+
if (item.filename) {
|
634
|
+
flatConfigArrayOptions.basePath = path.parse(item.filename).root;
|
635
|
+
}
|
636
|
+
|
637
|
+
const configs = new FlatConfigArray(testerConfig, flatConfigArrayOptions);
|
638
|
+
|
639
|
+
/*
|
640
|
+
* Modify the returned config so that the parser is wrapped to catch
|
641
|
+
* access of the start/end properties. This method is called just
|
642
|
+
* once per code snippet being tested, so each test case gets a clean
|
643
|
+
* parser.
|
644
|
+
*/
|
645
|
+
configs[ConfigArraySymbol.finalizeConfig] = function(...args) {
|
646
|
+
|
647
|
+
// can't do super here :(
|
648
|
+
const proto = Object.getPrototypeOf(this);
|
649
|
+
const calculatedConfig = proto[ConfigArraySymbol.finalizeConfig].apply(this, args);
|
650
|
+
|
651
|
+
// wrap the parser to catch start/end property access
|
652
|
+
calculatedConfig.languageOptions.parser = wrapParser(calculatedConfig.languageOptions.parser);
|
653
|
+
return calculatedConfig;
|
654
|
+
};
|
655
|
+
|
656
|
+
let code, filename, output, beforeAST, afterAST;
|
712
657
|
|
713
658
|
if (typeof item === "string") {
|
714
659
|
code = item;
|
@@ -725,64 +670,97 @@ class RuleTester {
|
|
725
670
|
delete itemConfig[parameter];
|
726
671
|
}
|
727
672
|
|
673
|
+
// wrap any parsers
|
674
|
+
if (itemConfig.languageOptions && itemConfig.languageOptions.parser) {
|
675
|
+
|
676
|
+
const parser = itemConfig.languageOptions.parser;
|
677
|
+
|
678
|
+
if (parser && typeof parser !== "object") {
|
679
|
+
throw new Error("Parser must be an object with a parse() or parseForESLint() method.");
|
680
|
+
}
|
681
|
+
|
682
|
+
}
|
683
|
+
|
728
684
|
/*
|
729
685
|
* Create the config object from the tester config and this item
|
730
686
|
* specific configurations.
|
731
687
|
*/
|
732
|
-
|
733
|
-
config,
|
734
|
-
itemConfig
|
735
|
-
);
|
688
|
+
configs.push(itemConfig);
|
736
689
|
}
|
737
690
|
|
738
|
-
if (item
|
691
|
+
if (hasOwnProperty(item, "only")) {
|
692
|
+
assert.ok(typeof item.only === "boolean", "Optional test case property 'only' must be a boolean");
|
693
|
+
}
|
694
|
+
if (hasOwnProperty(item, "filename")) {
|
695
|
+
assert.ok(typeof item.filename === "string", "Optional test case property 'filename' must be a string");
|
739
696
|
filename = item.filename;
|
740
697
|
}
|
741
698
|
|
699
|
+
let ruleConfig = 1;
|
700
|
+
|
742
701
|
if (hasOwnProperty(item, "options")) {
|
743
702
|
assert(Array.isArray(item.options), "options must be an array");
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
) {
|
751
|
-
emitMissingSchemaWarning(ruleName);
|
703
|
+
ruleConfig = [1, ...item.options];
|
704
|
+
}
|
705
|
+
|
706
|
+
configs.push({
|
707
|
+
rules: {
|
708
|
+
[ruleId]: ruleConfig
|
752
709
|
}
|
753
|
-
|
754
|
-
|
755
|
-
|
710
|
+
});
|
711
|
+
|
712
|
+
let schema;
|
713
|
+
|
714
|
+
try {
|
715
|
+
schema = getRuleOptionsSchema(rule);
|
716
|
+
} catch (err) {
|
717
|
+
err.message += metaSchemaDescription;
|
718
|
+
throw err;
|
756
719
|
}
|
757
720
|
|
758
|
-
|
721
|
+
/*
|
722
|
+
* Check and throw an error if the schema is an empty object (`schema:{}`), because such schema
|
723
|
+
* doesn't validate or enforce anything and is therefore considered a possible error. If the intent
|
724
|
+
* was to skip options validation, `schema:false` should be set instead (explicit opt-out).
|
725
|
+
*
|
726
|
+
* For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed
|
727
|
+
* properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well,
|
728
|
+
* it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea
|
729
|
+
* to use inherited properties in schemas because schemas that differ only in inherited properties would end up
|
730
|
+
* having the same cache entry that would be correct for only one of them.
|
731
|
+
*
|
732
|
+
* At this point, `schema` can only be an object or `null`.
|
733
|
+
*/
|
734
|
+
if (schema && Object.keys(schema).length === 0) {
|
735
|
+
throw new Error(`\`schema: {}\` is a no-op${metaSchemaDescription}`);
|
736
|
+
}
|
759
737
|
|
760
738
|
/*
|
761
739
|
* Setup AST getters.
|
762
740
|
* The goal is to check whether or not AST was modified when
|
763
741
|
* running the rule under test.
|
764
742
|
*/
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
743
|
+
configs.push({
|
744
|
+
plugins: {
|
745
|
+
"rule-tester": {
|
746
|
+
rules: {
|
747
|
+
"validate-ast": {
|
748
|
+
create() {
|
749
|
+
return {
|
750
|
+
Program(node) {
|
751
|
+
beforeAST = cloneDeeplyExcludesParent(node);
|
752
|
+
},
|
753
|
+
"Program:exit"(node) {
|
754
|
+
afterAST = node;
|
755
|
+
}
|
756
|
+
};
|
757
|
+
}
|
758
|
+
}
|
773
759
|
}
|
774
|
-
}
|
760
|
+
}
|
775
761
|
}
|
776
762
|
});
|
777
763
|
|
778
|
-
if (typeof config.parser === "string") {
|
779
|
-
assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths");
|
780
|
-
} else {
|
781
|
-
config.parser = espreePath;
|
782
|
-
}
|
783
|
-
|
784
|
-
linter.defineParser(config.parser, wrapParser(require(config.parser)));
|
785
|
-
|
786
764
|
if (schema) {
|
787
765
|
ajv.validateSchema(schema);
|
788
766
|
|
@@ -809,35 +787,32 @@ class RuleTester {
|
|
809
787
|
}
|
810
788
|
}
|
811
789
|
|
812
|
-
|
790
|
+
// check for validation errors
|
791
|
+
try {
|
792
|
+
configs.normalizeSync();
|
793
|
+
configs.getConfig("test.js");
|
794
|
+
} catch (error) {
|
795
|
+
error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`;
|
796
|
+
throw error;
|
797
|
+
}
|
813
798
|
|
814
799
|
// Verify the code.
|
815
|
-
const {
|
816
|
-
const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments");
|
800
|
+
const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype;
|
817
801
|
let messages;
|
818
802
|
|
819
803
|
try {
|
820
|
-
SourceCode.prototype.getComments = getCommentsDeprecation;
|
821
|
-
Object.defineProperty(CodePath.prototype, "currentSegments", {
|
822
|
-
get() {
|
823
|
-
emitCodePathCurrentSegmentsWarning(ruleName);
|
824
|
-
return originalCurrentSegments.get.call(this);
|
825
|
-
}
|
826
|
-
});
|
827
|
-
|
828
804
|
forbiddenMethods.forEach(methodName => {
|
829
|
-
SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName);
|
805
|
+
SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype);
|
830
806
|
});
|
831
807
|
|
832
|
-
messages = linter.verify(code,
|
808
|
+
messages = linter.verify(code, configs, filename);
|
833
809
|
} finally {
|
834
|
-
SourceCode.prototype.getComments = getComments;
|
835
|
-
Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
|
836
810
|
SourceCode.prototype.applyInlineConfig = applyInlineConfig;
|
837
811
|
SourceCode.prototype.applyLanguageOptions = applyLanguageOptions;
|
838
812
|
SourceCode.prototype.finalize = finalize;
|
839
813
|
}
|
840
814
|
|
815
|
+
|
841
816
|
const fatalErrorMessage = messages.find(m => m.fatal);
|
842
817
|
|
843
818
|
assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
|
@@ -845,7 +820,7 @@ class RuleTester {
|
|
845
820
|
// Verify if autofix makes a syntax error or not.
|
846
821
|
if (messages.some(m => m.fix)) {
|
847
822
|
output = SourceCodeFixer.applyFixes(code, messages).output;
|
848
|
-
const errorMessageInFix = linter.verify(output,
|
823
|
+
const errorMessageInFix = linter.verify(output, configs, filename).find(m => m.fatal);
|
849
824
|
|
850
825
|
assert(!errorMessageInFix, [
|
851
826
|
"A fatal parsing error occurred in autofix.",
|
@@ -861,7 +836,9 @@ class RuleTester {
|
|
861
836
|
messages,
|
862
837
|
output,
|
863
838
|
beforeAST,
|
864
|
-
afterAST: cloneDeeplyExcludesParent(afterAST)
|
839
|
+
afterAST: cloneDeeplyExcludesParent(afterAST),
|
840
|
+
configs,
|
841
|
+
filename
|
865
842
|
};
|
866
843
|
}
|
867
844
|
|
@@ -878,6 +855,39 @@ class RuleTester {
|
|
878
855
|
}
|
879
856
|
}
|
880
857
|
|
858
|
+
/**
|
859
|
+
* Check if this test case is a duplicate of one we have seen before.
|
860
|
+
* @param {string|Object} item test case object
|
861
|
+
* @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
|
862
|
+
* @returns {void}
|
863
|
+
* @private
|
864
|
+
*/
|
865
|
+
function checkDuplicateTestCase(item, seenTestCases) {
|
866
|
+
if (!isSerializable(item)) {
|
867
|
+
|
868
|
+
/*
|
869
|
+
* If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
|
870
|
+
* This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
|
871
|
+
*/
|
872
|
+
return;
|
873
|
+
}
|
874
|
+
|
875
|
+
const normalizedItem = typeof item === "string" ? { code: item } : item;
|
876
|
+
const serializedTestCase = stringify(normalizedItem, {
|
877
|
+
replacer(key, value) {
|
878
|
+
|
879
|
+
// "this" is the currently stringified object --> only ignore top-level properties
|
880
|
+
return (normalizedItem !== this || !duplicationIgnoredParameters.has(key)) ? value : void 0;
|
881
|
+
}
|
882
|
+
});
|
883
|
+
|
884
|
+
assert(
|
885
|
+
!seenTestCases.has(serializedTestCase),
|
886
|
+
"detected duplicate test case"
|
887
|
+
);
|
888
|
+
seenTestCases.add(serializedTestCase);
|
889
|
+
}
|
890
|
+
|
881
891
|
/**
|
882
892
|
* Check if the template is valid or not
|
883
893
|
* all valid cases go through this
|
@@ -893,6 +903,8 @@ class RuleTester {
|
|
893
903
|
assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
|
894
904
|
}
|
895
905
|
|
906
|
+
checkDuplicateTestCase(item, seenValidTestCases);
|
907
|
+
|
896
908
|
const result = runRuleForItem(item);
|
897
909
|
const messages = result.messages;
|
898
910
|
|
@@ -944,12 +956,30 @@ class RuleTester {
|
|
944
956
|
assert.fail("Invalid cases must have at least one error");
|
945
957
|
}
|
946
958
|
|
959
|
+
checkDuplicateTestCase(item, seenInvalidTestCases);
|
960
|
+
|
947
961
|
const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
|
948
962
|
const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
|
949
963
|
|
950
964
|
const result = runRuleForItem(item);
|
951
965
|
const messages = result.messages;
|
952
966
|
|
967
|
+
for (const message of messages) {
|
968
|
+
if (hasOwnProperty(message, "suggestions")) {
|
969
|
+
|
970
|
+
/** @type {Map<string, number>} */
|
971
|
+
const seenMessageIndices = new Map();
|
972
|
+
|
973
|
+
for (let i = 0; i < message.suggestions.length; i += 1) {
|
974
|
+
const suggestionMessage = message.suggestions[i].desc;
|
975
|
+
const previous = seenMessageIndices.get(suggestionMessage);
|
976
|
+
|
977
|
+
assert.ok(!seenMessageIndices.has(suggestionMessage), `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`);
|
978
|
+
seenMessageIndices.set(suggestionMessage, i);
|
979
|
+
}
|
980
|
+
}
|
981
|
+
}
|
982
|
+
|
953
983
|
if (typeof item.errors === "number") {
|
954
984
|
|
955
985
|
if (item.errors === 0) {
|
@@ -972,7 +1002,7 @@ class RuleTester {
|
|
972
1002
|
)
|
973
1003
|
);
|
974
1004
|
|
975
|
-
const hasMessageOfThisRule = messages.some(m => m.ruleId ===
|
1005
|
+
const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleId);
|
976
1006
|
|
977
1007
|
for (let i = 0, l = item.errors.length; i < l; i++) {
|
978
1008
|
const error = item.errors[i];
|
@@ -984,6 +1014,7 @@ class RuleTester {
|
|
984
1014
|
|
985
1015
|
// Just an error message.
|
986
1016
|
assertMessageMatches(message.message, error);
|
1017
|
+
assert.ok(message.suggestions === void 0, `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`);
|
987
1018
|
} else if (typeof error === "object" && error !== null) {
|
988
1019
|
|
989
1020
|
/*
|
@@ -1016,6 +1047,18 @@ class RuleTester {
|
|
1016
1047
|
error.messageId,
|
1017
1048
|
`messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
|
1018
1049
|
);
|
1050
|
+
|
1051
|
+
const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
|
1052
|
+
message.message,
|
1053
|
+
rule.meta.messages[message.messageId],
|
1054
|
+
error.data
|
1055
|
+
);
|
1056
|
+
|
1057
|
+
assert.ok(
|
1058
|
+
unsubstitutedPlaceholders.length === 0,
|
1059
|
+
`The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.`
|
1060
|
+
);
|
1061
|
+
|
1019
1062
|
if (hasOwnProperty(error, "data")) {
|
1020
1063
|
|
1021
1064
|
/*
|
@@ -1032,13 +1075,10 @@ class RuleTester {
|
|
1032
1075
|
`Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
|
1033
1076
|
);
|
1034
1077
|
}
|
1078
|
+
} else {
|
1079
|
+
assert.fail("Test error must specify either a 'messageId' or 'message'.");
|
1035
1080
|
}
|
1036
1081
|
|
1037
|
-
assert.ok(
|
1038
|
-
hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
|
1039
|
-
"Error must specify 'messageId' if 'data' is used."
|
1040
|
-
);
|
1041
|
-
|
1042
1082
|
if (error.type) {
|
1043
1083
|
assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
|
1044
1084
|
}
|
@@ -1059,81 +1099,117 @@ class RuleTester {
|
|
1059
1099
|
assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
|
1060
1100
|
}
|
1061
1101
|
|
1102
|
+
assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`);
|
1062
1103
|
if (hasOwnProperty(error, "suggestions")) {
|
1063
1104
|
|
1064
1105
|
// Support asserting there are no suggestions
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
assert.
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
)
|
1078
|
-
Object.keys(expectedSuggestion).forEach(propertyName => {
|
1079
|
-
assert.ok(
|
1080
|
-
suggestionObjectParameters.has(propertyName),
|
1081
|
-
`Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
|
1082
|
-
);
|
1083
|
-
});
|
1084
|
-
|
1085
|
-
const actualSuggestion = message.suggestions[index];
|
1086
|
-
const suggestionPrefix = `Error Suggestion at index ${index} :`;
|
1087
|
-
|
1088
|
-
if (hasOwnProperty(expectedSuggestion, "desc")) {
|
1106
|
+
const expectsSuggestions = Array.isArray(error.suggestions) ? error.suggestions.length > 0 : Boolean(error.suggestions);
|
1107
|
+
const hasSuggestions = message.suggestions !== void 0;
|
1108
|
+
|
1109
|
+
if (!hasSuggestions && expectsSuggestions) {
|
1110
|
+
assert.ok(!error.suggestions, `Error should have suggestions on error with message: "${message.message}"`);
|
1111
|
+
} else if (hasSuggestions) {
|
1112
|
+
assert.ok(expectsSuggestions, `Error should have no suggestions on error with message: "${message.message}"`);
|
1113
|
+
if (typeof error.suggestions === "number") {
|
1114
|
+
assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`);
|
1115
|
+
} else if (Array.isArray(error.suggestions)) {
|
1116
|
+
assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
|
1117
|
+
|
1118
|
+
error.suggestions.forEach((expectedSuggestion, index) => {
|
1089
1119
|
assert.ok(
|
1090
|
-
|
1091
|
-
|
1092
|
-
);
|
1093
|
-
assert.strictEqual(
|
1094
|
-
actualSuggestion.desc,
|
1095
|
-
expectedSuggestion.desc,
|
1096
|
-
`${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
|
1120
|
+
typeof expectedSuggestion === "object" && expectedSuggestion !== null,
|
1121
|
+
"Test suggestion in 'suggestions' array must be an object."
|
1097
1122
|
);
|
1098
|
-
|
1123
|
+
Object.keys(expectedSuggestion).forEach(propertyName => {
|
1124
|
+
assert.ok(
|
1125
|
+
suggestionObjectParameters.has(propertyName),
|
1126
|
+
`Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
|
1127
|
+
);
|
1128
|
+
});
|
1099
1129
|
|
1100
|
-
|
1101
|
-
|
1102
|
-
ruleHasMetaMessages,
|
1103
|
-
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
|
1104
|
-
);
|
1105
|
-
assert.ok(
|
1106
|
-
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
|
1107
|
-
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
|
1108
|
-
);
|
1109
|
-
assert.strictEqual(
|
1110
|
-
actualSuggestion.messageId,
|
1111
|
-
expectedSuggestion.messageId,
|
1112
|
-
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
|
1113
|
-
);
|
1114
|
-
if (hasOwnProperty(expectedSuggestion, "data")) {
|
1115
|
-
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
|
1116
|
-
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
|
1130
|
+
const actualSuggestion = message.suggestions[index];
|
1131
|
+
const suggestionPrefix = `Error Suggestion at index ${index}:`;
|
1117
1132
|
|
1133
|
+
if (hasOwnProperty(expectedSuggestion, "desc")) {
|
1134
|
+
assert.ok(
|
1135
|
+
!hasOwnProperty(expectedSuggestion, "data"),
|
1136
|
+
`${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
|
1137
|
+
);
|
1138
|
+
assert.ok(
|
1139
|
+
!hasOwnProperty(expectedSuggestion, "messageId"),
|
1140
|
+
`${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`
|
1141
|
+
);
|
1118
1142
|
assert.strictEqual(
|
1119
1143
|
actualSuggestion.desc,
|
1120
|
-
|
1121
|
-
`${suggestionPrefix}
|
1144
|
+
expectedSuggestion.desc,
|
1145
|
+
`${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
|
1146
|
+
);
|
1147
|
+
} else if (hasOwnProperty(expectedSuggestion, "messageId")) {
|
1148
|
+
assert.ok(
|
1149
|
+
ruleHasMetaMessages,
|
1150
|
+
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
|
1151
|
+
);
|
1152
|
+
assert.ok(
|
1153
|
+
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
|
1154
|
+
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
|
1155
|
+
);
|
1156
|
+
assert.strictEqual(
|
1157
|
+
actualSuggestion.messageId,
|
1158
|
+
expectedSuggestion.messageId,
|
1159
|
+
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
|
1160
|
+
);
|
1161
|
+
|
1162
|
+
const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
|
1163
|
+
actualSuggestion.desc,
|
1164
|
+
rule.meta.messages[expectedSuggestion.messageId],
|
1165
|
+
expectedSuggestion.data
|
1166
|
+
);
|
1167
|
+
|
1168
|
+
assert.ok(
|
1169
|
+
unsubstitutedPlaceholders.length === 0,
|
1170
|
+
`The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.`
|
1171
|
+
);
|
1172
|
+
|
1173
|
+
if (hasOwnProperty(expectedSuggestion, "data")) {
|
1174
|
+
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
|
1175
|
+
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
|
1176
|
+
|
1177
|
+
assert.strictEqual(
|
1178
|
+
actualSuggestion.desc,
|
1179
|
+
rehydratedDesc,
|
1180
|
+
`${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
|
1181
|
+
);
|
1182
|
+
}
|
1183
|
+
} else if (hasOwnProperty(expectedSuggestion, "data")) {
|
1184
|
+
assert.fail(
|
1185
|
+
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
|
1186
|
+
);
|
1187
|
+
} else {
|
1188
|
+
assert.fail(
|
1189
|
+
`${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`
|
1122
1190
|
);
|
1123
1191
|
}
|
1124
|
-
} else {
|
1125
|
-
assert.ok(
|
1126
|
-
!hasOwnProperty(expectedSuggestion, "data"),
|
1127
|
-
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
|
1128
|
-
);
|
1129
|
-
}
|
1130
1192
|
|
1131
|
-
|
1193
|
+
assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`);
|
1132
1194
|
const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
|
1133
1195
|
|
1196
|
+
// Verify if suggestion fix makes a syntax error or not.
|
1197
|
+
const errorMessageInSuggestion =
|
1198
|
+
linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
|
1199
|
+
|
1200
|
+
assert(!errorMessageInSuggestion, [
|
1201
|
+
"A fatal parsing error occurred in suggestion fix.",
|
1202
|
+
`Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
|
1203
|
+
"Suggestion output:",
|
1204
|
+
codeWithAppliedSuggestion
|
1205
|
+
].join("\n"));
|
1206
|
+
|
1134
1207
|
assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`);
|
1135
|
-
|
1136
|
-
|
1208
|
+
assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`);
|
1209
|
+
});
|
1210
|
+
} else {
|
1211
|
+
assert.fail("Test error object property 'suggestions' should be an array or a number");
|
1212
|
+
}
|
1137
1213
|
}
|
1138
1214
|
}
|
1139
1215
|
} else {
|
@@ -1153,6 +1229,7 @@ class RuleTester {
|
|
1153
1229
|
);
|
1154
1230
|
} else {
|
1155
1231
|
assert.strictEqual(result.output, item.output, "Output is incorrect.");
|
1232
|
+
assert.notStrictEqual(item.code, item.output, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.");
|
1156
1233
|
}
|
1157
1234
|
} else {
|
1158
1235
|
assert.strictEqual(
|