]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rule-tester/rule-tester.js
7f590a5ea70995e81d19b123e08ac043032bdc17
[pve-eslint.git] / eslint / lib / rule-tester / rule-tester.js
1 /**
2 * @fileoverview Mocha test wrapper
3 * @author Ilya Volodin
4 */
5 "use strict";
6
7 /* eslint-env mocha -- Mocha wrapper */
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 //------------------------------------------------------------------------------
40 // Requirements
41 //------------------------------------------------------------------------------
42
43 const
44 assert = require("assert"),
45 path = require("path"),
46 util = require("util"),
47 merge = require("lodash.merge"),
48 equal = require("fast-deep-equal"),
49 Traverser = require("../../lib/shared/traverser"),
50 { getRuleOptionsSchema, validate } = require("../shared/config-validator"),
51 { Linter, SourceCodeFixer, interpolate } = require("../linter");
52
53 const ajv = require("../shared/ajv")({ strictDefaults: true });
54
55 const espreePath = require.resolve("espree");
56 const parserSymbol = Symbol.for("eslint.RuleTester.parser");
57
58 const { SourceCode } = require("../source-code");
59
60 //------------------------------------------------------------------------------
61 // Typedefs
62 //------------------------------------------------------------------------------
63
64 /** @typedef {import("../shared/types").Parser} Parser */
65
66 /* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
67 /**
68 * A test case that is expected to pass lint.
69 * @typedef {Object} ValidTestCase
70 * @property {string} [name] Name for the test case.
71 * @property {string} code Code for the test case.
72 * @property {any[]} [options] Options for the test case.
73 * @property {{ [name: string]: any }} [settings] Settings for the test case.
74 * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
75 * @property {string} [parser] The absolute path for the parser.
76 * @property {{ [name: string]: any }} [parserOptions] Options for the parser.
77 * @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
78 * @property {{ [name: string]: boolean }} [env] Environments for the test case.
79 * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
80 */
81
82 /**
83 * A test case that is expected to fail lint.
84 * @typedef {Object} InvalidTestCase
85 * @property {string} [name] Name for the test case.
86 * @property {string} code Code for the test case.
87 * @property {number | Array<TestCaseError | string | RegExp>} errors Expected errors.
88 * @property {string | null} [output] The expected code after autofixes are applied. If set to `null`, the test runner will assert that no autofix is suggested.
89 * @property {any[]} [options] Options for the test case.
90 * @property {{ [name: string]: any }} [settings] Settings for the test case.
91 * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
92 * @property {string} [parser] The absolute path for the parser.
93 * @property {{ [name: string]: any }} [parserOptions] Options for the parser.
94 * @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
95 * @property {{ [name: string]: boolean }} [env] Environments for the test case.
96 * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
97 */
98
99 /**
100 * A description of a reported error used in a rule tester test.
101 * @typedef {Object} TestCaseError
102 * @property {string | RegExp} [message] Message.
103 * @property {string} [messageId] Message ID.
104 * @property {string} [type] The type of the reported AST node.
105 * @property {{ [name: string]: string }} [data] The data used to fill the message template.
106 * @property {number} [line] The 1-based line number of the reported start location.
107 * @property {number} [column] The 1-based column number of the reported start location.
108 * @property {number} [endLine] The 1-based line number of the reported end location.
109 * @property {number} [endColumn] The 1-based column number of the reported end location.
110 */
111 /* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
112
113 //------------------------------------------------------------------------------
114 // Private Members
115 //------------------------------------------------------------------------------
116
117 /*
118 * testerDefaultConfig must not be modified as it allows to reset the tester to
119 * the initial default configuration
120 */
121 const testerDefaultConfig = { rules: {} };
122 let defaultConfig = { rules: {} };
123
124 /*
125 * List every parameters possible on a test case that are not related to eslint
126 * configuration
127 */
128 const RuleTesterParameters = [
129 "name",
130 "code",
131 "filename",
132 "options",
133 "errors",
134 "output",
135 "only"
136 ];
137
138 /*
139 * All allowed property names in error objects.
140 */
141 const errorObjectParameters = new Set([
142 "message",
143 "messageId",
144 "data",
145 "type",
146 "line",
147 "column",
148 "endLine",
149 "endColumn",
150 "suggestions"
151 ]);
152 const friendlyErrorObjectParameterList = `[${[...errorObjectParameters].map(key => `'${key}'`).join(", ")}]`;
153
154 /*
155 * All allowed property names in suggestion objects.
156 */
157 const suggestionObjectParameters = new Set([
158 "desc",
159 "messageId",
160 "data",
161 "output"
162 ]);
163 const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
164
165 const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
166
167 /**
168 * Clones a given value deeply.
169 * Note: This ignores `parent` property.
170 * @param {any} x A value to clone.
171 * @returns {any} A cloned value.
172 */
173 function cloneDeeplyExcludesParent(x) {
174 if (typeof x === "object" && x !== null) {
175 if (Array.isArray(x)) {
176 return x.map(cloneDeeplyExcludesParent);
177 }
178
179 const retv = {};
180
181 for (const key in x) {
182 if (key !== "parent" && hasOwnProperty(x, key)) {
183 retv[key] = cloneDeeplyExcludesParent(x[key]);
184 }
185 }
186
187 return retv;
188 }
189
190 return x;
191 }
192
193 /**
194 * Freezes a given value deeply.
195 * @param {any} x A value to freeze.
196 * @returns {void}
197 */
198 function freezeDeeply(x) {
199 if (typeof x === "object" && x !== null) {
200 if (Array.isArray(x)) {
201 x.forEach(freezeDeeply);
202 } else {
203 for (const key in x) {
204 if (key !== "parent" && hasOwnProperty(x, key)) {
205 freezeDeeply(x[key]);
206 }
207 }
208 }
209 Object.freeze(x);
210 }
211 }
212
213 /**
214 * Replace control characters by `\u00xx` form.
215 * @param {string} text The text to sanitize.
216 * @returns {string} The sanitized text.
217 */
218 function sanitize(text) {
219 return text.replace(
220 /[\u0000-\u0009\u000b-\u001a]/gu, // eslint-disable-line no-control-regex -- Escaping controls
221 c => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`
222 );
223 }
224
225 /**
226 * Define `start`/`end` properties as throwing error.
227 * @param {string} objName Object name used for error messages.
228 * @param {ASTNode} node The node to define.
229 * @returns {void}
230 */
231 function defineStartEndAsError(objName, node) {
232 Object.defineProperties(node, {
233 start: {
234 get() {
235 throw new Error(`Use ${objName}.range[0] instead of ${objName}.start`);
236 },
237 configurable: true,
238 enumerable: false
239 },
240 end: {
241 get() {
242 throw new Error(`Use ${objName}.range[1] instead of ${objName}.end`);
243 },
244 configurable: true,
245 enumerable: false
246 }
247 });
248 }
249
250
251 /**
252 * Define `start`/`end` properties of all nodes of the given AST as throwing error.
253 * @param {ASTNode} ast The root node to errorize `start`/`end` properties.
254 * @param {Object} [visitorKeys] Visitor keys to be used for traversing the given ast.
255 * @returns {void}
256 */
257 function defineStartEndAsErrorInTree(ast, visitorKeys) {
258 Traverser.traverse(ast, { visitorKeys, enter: defineStartEndAsError.bind(null, "node") });
259 ast.tokens.forEach(defineStartEndAsError.bind(null, "token"));
260 ast.comments.forEach(defineStartEndAsError.bind(null, "token"));
261 }
262
263 /**
264 * Wraps the given parser in order to intercept and modify return values from the `parse` and `parseForESLint` methods, for test purposes.
265 * In particular, to modify ast nodes, tokens and comments to throw on access to their `start` and `end` properties.
266 * @param {Parser} parser Parser object.
267 * @returns {Parser} Wrapped parser object.
268 */
269 function wrapParser(parser) {
270
271 if (typeof parser.parseForESLint === "function") {
272 return {
273 [parserSymbol]: parser,
274 parseForESLint(...args) {
275 const ret = parser.parseForESLint(...args);
276
277 defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys);
278 return ret;
279 }
280 };
281 }
282
283 return {
284 [parserSymbol]: parser,
285 parse(...args) {
286 const ast = parser.parse(...args);
287
288 defineStartEndAsErrorInTree(ast);
289 return ast;
290 }
291 };
292 }
293
294 /**
295 * Function to replace `SourceCode.prototype.getComments`.
296 * @returns {void}
297 * @throws {Error} Deprecation message.
298 */
299 function getCommentsDeprecation() {
300 throw new Error(
301 "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead."
302 );
303 }
304
305 //------------------------------------------------------------------------------
306 // Public Interface
307 //------------------------------------------------------------------------------
308
309 // default separators for testing
310 const DESCRIBE = Symbol("describe");
311 const IT = Symbol("it");
312 const IT_ONLY = Symbol("itOnly");
313
314 /**
315 * This is `it` default handler if `it` don't exist.
316 * @this {Mocha}
317 * @param {string} text The description of the test case.
318 * @param {Function} method The logic of the test case.
319 * @throws {Error} Any error upon execution of `method`.
320 * @returns {any} Returned value of `method`.
321 */
322 function itDefaultHandler(text, method) {
323 try {
324 return method.call(this);
325 } catch (err) {
326 if (err instanceof assert.AssertionError) {
327 err.message += ` (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})`;
328 }
329 throw err;
330 }
331 }
332
333 /**
334 * This is `describe` default handler if `describe` don't exist.
335 * @this {Mocha}
336 * @param {string} text The description of the test case.
337 * @param {Function} method The logic of the test case.
338 * @returns {any} Returned value of `method`.
339 */
340 function describeDefaultHandler(text, method) {
341 return method.call(this);
342 }
343
344 /**
345 * Mocha test wrapper.
346 */
347 class RuleTester {
348
349 /**
350 * Creates a new instance of RuleTester.
351 * @param {Object} [testerConfig] Optional, extra configuration for the tester
352 */
353 constructor(testerConfig) {
354
355 /**
356 * The configuration to use for this tester. Combination of the tester
357 * configuration and the default configuration.
358 * @type {Object}
359 */
360 this.testerConfig = merge(
361 {},
362 defaultConfig,
363 testerConfig,
364 { rules: { "rule-tester/validate-ast": "error" } }
365 );
366
367 /**
368 * Rule definitions to define before tests.
369 * @type {Object}
370 */
371 this.rules = {};
372 this.linter = new Linter();
373 }
374
375 /**
376 * Set the configuration to use for all future tests
377 * @param {Object} config the configuration to use.
378 * @throws {TypeError} If non-object config.
379 * @returns {void}
380 */
381 static setDefaultConfig(config) {
382 if (typeof config !== "object") {
383 throw new TypeError("RuleTester.setDefaultConfig: config must be an object");
384 }
385 defaultConfig = config;
386
387 // Make sure the rules object exists since it is assumed to exist later
388 defaultConfig.rules = defaultConfig.rules || {};
389 }
390
391 /**
392 * Get the current configuration used for all tests
393 * @returns {Object} the current configuration
394 */
395 static getDefaultConfig() {
396 return defaultConfig;
397 }
398
399 /**
400 * Reset the configuration to the initial configuration of the tester removing
401 * any changes made until now.
402 * @returns {void}
403 */
404 static resetDefaultConfig() {
405 defaultConfig = merge({}, testerDefaultConfig);
406 }
407
408
409 /*
410 * If people use `mocha test.js --watch` command, `describe` and `it` function
411 * instances are different for each execution. So `describe` and `it` should get fresh instance
412 * always.
413 */
414 static get describe() {
415 return (
416 this[DESCRIBE] ||
417 (typeof describe === "function" ? describe : describeDefaultHandler)
418 );
419 }
420
421 static set describe(value) {
422 this[DESCRIBE] = value;
423 }
424
425 static get it() {
426 return (
427 this[IT] ||
428 (typeof it === "function" ? it : itDefaultHandler)
429 );
430 }
431
432 static set it(value) {
433 this[IT] = value;
434 }
435
436 /**
437 * Adds the `only` property to a test to run it in isolation.
438 * @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
439 * @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
440 */
441 static only(item) {
442 if (typeof item === "string") {
443 return { code: item, only: true };
444 }
445
446 return { ...item, only: true };
447 }
448
449 static get itOnly() {
450 if (typeof this[IT_ONLY] === "function") {
451 return this[IT_ONLY];
452 }
453 if (typeof this[IT] === "function" && typeof this[IT].only === "function") {
454 return Function.bind.call(this[IT].only, this[IT]);
455 }
456 if (typeof it === "function" && typeof it.only === "function") {
457 return Function.bind.call(it.only, it);
458 }
459
460 if (typeof this[DESCRIBE] === "function" || typeof this[IT] === "function") {
461 throw new Error(
462 "Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
463 "See https://eslint.org/docs/developer-guide/nodejs-api#customizing-ruletester for more."
464 );
465 }
466 if (typeof it === "function") {
467 throw new Error("The current test framework does not support exclusive tests with `only`.");
468 }
469 throw new Error("To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.");
470 }
471
472 static set itOnly(value) {
473 this[IT_ONLY] = value;
474 }
475
476 /**
477 * Define a rule for one particular run of tests.
478 * @param {string} name The name of the rule to define.
479 * @param {Function} rule The rule definition.
480 * @returns {void}
481 */
482 defineRule(name, rule) {
483 this.rules[name] = rule;
484 }
485
486 /**
487 * Adds a new rule test to execute.
488 * @param {string} ruleName The name of the rule to run.
489 * @param {Function} rule The rule to test.
490 * @param {{
491 * valid: (ValidTestCase | string)[],
492 * invalid: InvalidTestCase[]
493 * }} test The collection of tests to run.
494 * @throws {TypeError|Error} If non-object `test`, or if a required
495 * scenario of the given type is missing.
496 * @returns {void}
497 */
498 run(ruleName, rule, test) {
499
500 const testerConfig = this.testerConfig,
501 requiredScenarios = ["valid", "invalid"],
502 scenarioErrors = [],
503 linter = this.linter;
504
505 if (!test || typeof test !== "object") {
506 throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
507 }
508
509 requiredScenarios.forEach(scenarioType => {
510 if (!test[scenarioType]) {
511 scenarioErrors.push(`Could not find any ${scenarioType} test scenarios`);
512 }
513 });
514
515 if (scenarioErrors.length > 0) {
516 throw new Error([
517 `Test Scenarios for rule ${ruleName} is invalid:`
518 ].concat(scenarioErrors).join("\n"));
519 }
520
521
522 linter.defineRule(ruleName, Object.assign({}, rule, {
523
524 // Create a wrapper rule that freezes the `context` properties.
525 create(context) {
526 freezeDeeply(context.options);
527 freezeDeeply(context.settings);
528 freezeDeeply(context.parserOptions);
529
530 return (typeof rule === "function" ? rule : rule.create)(context);
531 }
532 }));
533
534 linter.defineRules(this.rules);
535
536 /**
537 * Run the rule for the given item
538 * @param {string|Object} item Item to run the rule against
539 * @throws {Error} If an invalid schema.
540 * @returns {Object} Eslint run result
541 * @private
542 */
543 function runRuleForItem(item) {
544 let config = merge({}, testerConfig),
545 code, filename, output, beforeAST, afterAST;
546
547 if (typeof item === "string") {
548 code = item;
549 } else {
550 code = item.code;
551
552 /*
553 * Assumes everything on the item is a config except for the
554 * parameters used by this tester
555 */
556 const itemConfig = { ...item };
557
558 for (const parameter of RuleTesterParameters) {
559 delete itemConfig[parameter];
560 }
561
562 /*
563 * Create the config object from the tester config and this item
564 * specific configurations.
565 */
566 config = merge(
567 config,
568 itemConfig
569 );
570 }
571
572 if (item.filename) {
573 filename = item.filename;
574 }
575
576 if (hasOwnProperty(item, "options")) {
577 assert(Array.isArray(item.options), "options must be an array");
578 config.rules[ruleName] = [1].concat(item.options);
579 } else {
580 config.rules[ruleName] = 1;
581 }
582
583 const schema = getRuleOptionsSchema(rule);
584
585 /*
586 * Setup AST getters.
587 * The goal is to check whether or not AST was modified when
588 * running the rule under test.
589 */
590 linter.defineRule("rule-tester/validate-ast", () => ({
591 Program(node) {
592 beforeAST = cloneDeeplyExcludesParent(node);
593 },
594 "Program:exit"(node) {
595 afterAST = node;
596 }
597 }));
598
599 if (typeof config.parser === "string") {
600 assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths");
601 } else {
602 config.parser = espreePath;
603 }
604
605 linter.defineParser(config.parser, wrapParser(require(config.parser)));
606
607 if (schema) {
608 ajv.validateSchema(schema);
609
610 if (ajv.errors) {
611 const errors = ajv.errors.map(error => {
612 const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
613
614 return `\t${field}: ${error.message}`;
615 }).join("\n");
616
617 throw new Error([`Schema for rule ${ruleName} is invalid:`, errors]);
618 }
619
620 /*
621 * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
622 * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
623 * the schema (e.g. using invalid defaults in a schema), and only one of these errors can be reported at a time. As a result,
624 * the schema is compiled here separately from checking for `validateSchema` errors.
625 */
626 try {
627 ajv.compile(schema);
628 } catch (err) {
629 throw new Error(`Schema for rule ${ruleName} is invalid: ${err.message}`);
630 }
631 }
632
633 validate(config, "rule-tester", id => (id === ruleName ? rule : null));
634
635 // Verify the code.
636 const { getComments } = SourceCode.prototype;
637 let messages;
638
639 try {
640 SourceCode.prototype.getComments = getCommentsDeprecation;
641 messages = linter.verify(code, config, filename);
642 } finally {
643 SourceCode.prototype.getComments = getComments;
644 }
645
646 const fatalErrorMessage = messages.find(m => m.fatal);
647
648 assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
649
650 // Verify if autofix makes a syntax error or not.
651 if (messages.some(m => m.fix)) {
652 output = SourceCodeFixer.applyFixes(code, messages).output;
653 const errorMessageInFix = linter.verify(output, config, filename).find(m => m.fatal);
654
655 assert(!errorMessageInFix, [
656 "A fatal parsing error occurred in autofix.",
657 `Error: ${errorMessageInFix && errorMessageInFix.message}`,
658 "Autofix output:",
659 output
660 ].join("\n"));
661 } else {
662 output = code;
663 }
664
665 return {
666 messages,
667 output,
668 beforeAST,
669 afterAST: cloneDeeplyExcludesParent(afterAST)
670 };
671 }
672
673 /**
674 * Check if the AST was changed
675 * @param {ASTNode} beforeAST AST node before running
676 * @param {ASTNode} afterAST AST node after running
677 * @returns {void}
678 * @private
679 */
680 function assertASTDidntChange(beforeAST, afterAST) {
681 if (!equal(beforeAST, afterAST)) {
682 assert.fail("Rule should not modify AST.");
683 }
684 }
685
686 /**
687 * Check if the template is valid or not
688 * all valid cases go through this
689 * @param {string|Object} item Item to run the rule against
690 * @returns {void}
691 * @private
692 */
693 function testValidTemplate(item) {
694 const result = runRuleForItem(item);
695 const messages = result.messages;
696
697 assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s",
698 messages.length,
699 util.inspect(messages)));
700
701 assertASTDidntChange(result.beforeAST, result.afterAST);
702 }
703
704 /**
705 * Asserts that the message matches its expected value. If the expected
706 * value is a regular expression, it is checked against the actual
707 * value.
708 * @param {string} actual Actual value
709 * @param {string|RegExp} expected Expected value
710 * @returns {void}
711 * @private
712 */
713 function assertMessageMatches(actual, expected) {
714 if (expected instanceof RegExp) {
715
716 // assert.js doesn't have a built-in RegExp match function
717 assert.ok(
718 expected.test(actual),
719 `Expected '${actual}' to match ${expected}`
720 );
721 } else {
722 assert.strictEqual(actual, expected);
723 }
724 }
725
726 /**
727 * Check if the template is invalid or not
728 * all invalid cases go through this.
729 * @param {string|Object} item Item to run the rule against
730 * @returns {void}
731 * @private
732 */
733 function testInvalidTemplate(item) {
734 assert.ok(item.errors || item.errors === 0,
735 `Did not specify errors for an invalid test of ${ruleName}`);
736
737 if (Array.isArray(item.errors) && item.errors.length === 0) {
738 assert.fail("Invalid cases must have at least one error");
739 }
740
741 const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
742 const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
743
744 const result = runRuleForItem(item);
745 const messages = result.messages;
746
747 if (typeof item.errors === "number") {
748
749 if (item.errors === 0) {
750 assert.fail("Invalid cases must have 'error' value greater than 0");
751 }
752
753 assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
754 item.errors,
755 item.errors === 1 ? "" : "s",
756 messages.length,
757 util.inspect(messages)));
758 } else {
759 assert.strictEqual(
760 messages.length, item.errors.length, util.format(
761 "Should have %d error%s but had %d: %s",
762 item.errors.length,
763 item.errors.length === 1 ? "" : "s",
764 messages.length,
765 util.inspect(messages)
766 )
767 );
768
769 const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName);
770
771 for (let i = 0, l = item.errors.length; i < l; i++) {
772 const error = item.errors[i];
773 const message = messages[i];
774
775 assert(hasMessageOfThisRule, "Error rule name should be the same as the name of the rule being tested");
776
777 if (typeof error === "string" || error instanceof RegExp) {
778
779 // Just an error message.
780 assertMessageMatches(message.message, error);
781 } else if (typeof error === "object" && error !== null) {
782
783 /*
784 * Error object.
785 * This may have a message, messageId, data, node type, line, and/or
786 * column.
787 */
788
789 Object.keys(error).forEach(propertyName => {
790 assert.ok(
791 errorObjectParameters.has(propertyName),
792 `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`
793 );
794 });
795
796 if (hasOwnProperty(error, "message")) {
797 assert.ok(!hasOwnProperty(error, "messageId"), "Error should not specify both 'message' and a 'messageId'.");
798 assert.ok(!hasOwnProperty(error, "data"), "Error should not specify both 'data' and 'message'.");
799 assertMessageMatches(message.message, error.message);
800 } else if (hasOwnProperty(error, "messageId")) {
801 assert.ok(
802 ruleHasMetaMessages,
803 "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'."
804 );
805 if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
806 assert(false, `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`);
807 }
808 assert.strictEqual(
809 message.messageId,
810 error.messageId,
811 `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
812 );
813 if (hasOwnProperty(error, "data")) {
814
815 /*
816 * if data was provided, then directly compare the returned message to a synthetic
817 * interpolated message using the same message ID and data provided in the test.
818 * See https://github.com/eslint/eslint/issues/9890 for context.
819 */
820 const unformattedOriginalMessage = rule.meta.messages[error.messageId];
821 const rehydratedMessage = interpolate(unformattedOriginalMessage, error.data);
822
823 assert.strictEqual(
824 message.message,
825 rehydratedMessage,
826 `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
827 );
828 }
829 }
830
831 assert.ok(
832 hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
833 "Error must specify 'messageId' if 'data' is used."
834 );
835
836 if (error.type) {
837 assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
838 }
839
840 if (hasOwnProperty(error, "line")) {
841 assert.strictEqual(message.line, error.line, `Error line should be ${error.line}`);
842 }
843
844 if (hasOwnProperty(error, "column")) {
845 assert.strictEqual(message.column, error.column, `Error column should be ${error.column}`);
846 }
847
848 if (hasOwnProperty(error, "endLine")) {
849 assert.strictEqual(message.endLine, error.endLine, `Error endLine should be ${error.endLine}`);
850 }
851
852 if (hasOwnProperty(error, "endColumn")) {
853 assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
854 }
855
856 if (hasOwnProperty(error, "suggestions")) {
857
858 // Support asserting there are no suggestions
859 if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) {
860 if (Array.isArray(message.suggestions) && message.suggestions.length > 0) {
861 assert.fail(`Error should have no suggestions on error with message: "${message.message}"`);
862 }
863 } else {
864 assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`);
865 assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
866
867 error.suggestions.forEach((expectedSuggestion, index) => {
868 assert.ok(
869 typeof expectedSuggestion === "object" && expectedSuggestion !== null,
870 "Test suggestion in 'suggestions' array must be an object."
871 );
872 Object.keys(expectedSuggestion).forEach(propertyName => {
873 assert.ok(
874 suggestionObjectParameters.has(propertyName),
875 `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
876 );
877 });
878
879 const actualSuggestion = message.suggestions[index];
880 const suggestionPrefix = `Error Suggestion at index ${index} :`;
881
882 if (hasOwnProperty(expectedSuggestion, "desc")) {
883 assert.ok(
884 !hasOwnProperty(expectedSuggestion, "data"),
885 `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
886 );
887 assert.strictEqual(
888 actualSuggestion.desc,
889 expectedSuggestion.desc,
890 `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
891 );
892 }
893
894 if (hasOwnProperty(expectedSuggestion, "messageId")) {
895 assert.ok(
896 ruleHasMetaMessages,
897 `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
898 );
899 assert.ok(
900 hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
901 `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
902 );
903 assert.strictEqual(
904 actualSuggestion.messageId,
905 expectedSuggestion.messageId,
906 `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
907 );
908 if (hasOwnProperty(expectedSuggestion, "data")) {
909 const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
910 const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
911
912 assert.strictEqual(
913 actualSuggestion.desc,
914 rehydratedDesc,
915 `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
916 );
917 }
918 } else {
919 assert.ok(
920 !hasOwnProperty(expectedSuggestion, "data"),
921 `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
922 );
923 }
924
925 if (hasOwnProperty(expectedSuggestion, "output")) {
926 const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
927
928 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}"`);
929 }
930 });
931 }
932 }
933 } else {
934
935 // Message was an unexpected type
936 assert.fail(`Error should be a string, object, or RegExp, but found (${util.inspect(message)})`);
937 }
938 }
939 }
940
941 if (hasOwnProperty(item, "output")) {
942 if (item.output === null) {
943 assert.strictEqual(
944 result.output,
945 item.code,
946 "Expected no autofixes to be suggested"
947 );
948 } else {
949 assert.strictEqual(result.output, item.output, "Output is incorrect.");
950 }
951 } else {
952 assert.strictEqual(
953 result.output,
954 item.code,
955 "The rule fixed the code. Please add 'output' property."
956 );
957 }
958
959 assertASTDidntChange(result.beforeAST, result.afterAST);
960 }
961
962 /*
963 * This creates a mocha test suite and pipes all supplied info through
964 * one of the templates above.
965 */
966 RuleTester.describe(ruleName, () => {
967 RuleTester.describe("valid", () => {
968 test.valid.forEach(valid => {
969 RuleTester[valid.only ? "itOnly" : "it"](
970 sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
971 () => {
972 testValidTemplate(valid);
973 }
974 );
975 });
976 });
977
978 RuleTester.describe("invalid", () => {
979 test.invalid.forEach(invalid => {
980 RuleTester[invalid.only ? "itOnly" : "it"](
981 sanitize(invalid.name || invalid.code),
982 () => {
983 testInvalidTemplate(invalid);
984 }
985 );
986 });
987 });
988 });
989 }
990 }
991
992 RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null;
993
994 module.exports = RuleTester;