source: imaps-frontend/node_modules/eslint/lib/rule-tester/flat-rule-tester.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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