source: imaps-frontend/node_modules/eslint/lib/rule-tester/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: 47.1 KB
Line 
1/**
2 * @fileoverview Mocha test wrapper
3 * @author Ilya Volodin
4 */
5"use strict";
6
7/* globals describe, it -- Mocha globals */
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
43const
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 CodePath = require("../linter/code-path-analysis/code-path");
53
54const ajv = require("../shared/ajv")({ strictDefaults: true });
55
56const espreePath = require.resolve("espree");
57const parserSymbol = Symbol.for("eslint.RuleTester.parser");
58
59const { SourceCode } = require("../source-code");
60
61//------------------------------------------------------------------------------
62// Typedefs
63//------------------------------------------------------------------------------
64
65/** @typedef {import("../shared/types").Parser} Parser */
66/** @typedef {import("../shared/types").Rule} Rule */
67
68
69/**
70 * A test case that is expected to pass lint.
71 * @typedef {Object} ValidTestCase
72 * @property {string} [name] Name for the test case.
73 * @property {string} code Code for the test case.
74 * @property {any[]} [options] Options for the test case.
75 * @property {{ [name: string]: any }} [settings] Settings for the test case.
76 * @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 * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
82 */
83
84/**
85 * A test case that is expected to fail lint.
86 * @typedef {Object} InvalidTestCase
87 * @property {string} [name] Name for the test case.
88 * @property {string} code Code for the test case.
89 * @property {number | Array<TestCaseError | string | RegExp>} errors Expected errors.
90 * @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.
91 * @property {any[]} [options] Options for the test case.
92 * @property {{ [name: string]: any }} [settings] Settings for the test case.
93 * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
94 * @property {string} [parser] The absolute path for the parser.
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.
98 * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
99 */
100
101/**
102 * A description of a reported error used in a rule tester test.
103 * @typedef {Object} TestCaseError
104 * @property {string | RegExp} [message] Message.
105 * @property {string} [messageId] Message ID.
106 * @property {string} [type] The type of the reported AST node.
107 * @property {{ [name: string]: string }} [data] The data used to fill the message template.
108 * @property {number} [line] The 1-based line number of the reported start location.
109 * @property {number} [column] The 1-based column number of the reported start location.
110 * @property {number} [endLine] The 1-based line number of the reported end location.
111 * @property {number} [endColumn] The 1-based column number of the reported end location.
112 */
113
114//------------------------------------------------------------------------------
115// Private Members
116//------------------------------------------------------------------------------
117
118/*
119 * testerDefaultConfig must not be modified as it allows to reset the tester to
120 * the initial default configuration
121 */
122const testerDefaultConfig = { rules: {} };
123let defaultConfig = { rules: {} };
124
125/*
126 * List every parameters possible on a test case that are not related to eslint
127 * configuration
128 */
129const RuleTesterParameters = [
130 "name",
131 "code",
132 "filename",
133 "options",
134 "errors",
135 "output",
136 "only"
137];
138
139/*
140 * All allowed property names in error objects.
141 */
142const errorObjectParameters = new Set([
143 "message",
144 "messageId",
145 "data",
146 "type",
147 "line",
148 "column",
149 "endLine",
150 "endColumn",
151 "suggestions"
152]);
153const friendlyErrorObjectParameterList = `[${[...errorObjectParameters].map(key => `'${key}'`).join(", ")}]`;
154
155/*
156 * All allowed property names in suggestion objects.
157 */
158const suggestionObjectParameters = new Set([
159 "desc",
160 "messageId",
161 "data",
162 "output"
163]);
164const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
165
166const forbiddenMethods = [
167 "applyInlineConfig",
168 "applyLanguageOptions",
169 "finalize"
170];
171
172const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
173
174const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
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};
202
203/**
204 * Clones a given value deeply.
205 * Note: This ignores `parent` property.
206 * @param {any} x A value to clone.
207 * @returns {any} A cloned value.
208 */
209function cloneDeeplyExcludesParent(x) {
210 if (typeof x === "object" && x !== null) {
211 if (Array.isArray(x)) {
212 return x.map(cloneDeeplyExcludesParent);
213 }
214
215 const retv = {};
216
217 for (const key in x) {
218 if (key !== "parent" && hasOwnProperty(x, key)) {
219 retv[key] = cloneDeeplyExcludesParent(x[key]);
220 }
221 }
222
223 return retv;
224 }
225
226 return x;
227}
228
229/**
230 * Freezes a given value deeply.
231 * @param {any} x A value to freeze.
232 * @returns {void}
233 */
234function freezeDeeply(x) {
235 if (typeof x === "object" && x !== null) {
236 if (Array.isArray(x)) {
237 x.forEach(freezeDeeply);
238 } else {
239 for (const key in x) {
240 if (key !== "parent" && hasOwnProperty(x, key)) {
241 freezeDeeply(x[key]);
242 }
243 }
244 }
245 Object.freeze(x);
246 }
247}
248
249/**
250 * Replace control characters by `\u00xx` form.
251 * @param {string} text The text to sanitize.
252 * @returns {string} The sanitized text.
253 */
254function sanitize(text) {
255 if (typeof text !== "string") {
256 return "";
257 }
258 return text.replace(
259 /[\u0000-\u0009\u000b-\u001a]/gu, // eslint-disable-line no-control-regex -- Escaping controls
260 c => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`
261 );
262}
263
264/**
265 * Define `start`/`end` properties as throwing error.
266 * @param {string} objName Object name used for error messages.
267 * @param {ASTNode} node The node to define.
268 * @returns {void}
269 */
270function defineStartEndAsError(objName, node) {
271 Object.defineProperties(node, {
272 start: {
273 get() {
274 throw new Error(`Use ${objName}.range[0] instead of ${objName}.start`);
275 },
276 configurable: true,
277 enumerable: false
278 },
279 end: {
280 get() {
281 throw new Error(`Use ${objName}.range[1] instead of ${objName}.end`);
282 },
283 configurable: true,
284 enumerable: false
285 }
286 });
287}
288
289
290/**
291 * Define `start`/`end` properties of all nodes of the given AST as throwing error.
292 * @param {ASTNode} ast The root node to errorize `start`/`end` properties.
293 * @param {Object} [visitorKeys] Visitor keys to be used for traversing the given ast.
294 * @returns {void}
295 */
296function defineStartEndAsErrorInTree(ast, visitorKeys) {
297 Traverser.traverse(ast, { visitorKeys, enter: defineStartEndAsError.bind(null, "node") });
298 ast.tokens.forEach(defineStartEndAsError.bind(null, "token"));
299 ast.comments.forEach(defineStartEndAsError.bind(null, "token"));
300}
301
302/**
303 * Wraps the given parser in order to intercept and modify return values from the `parse` and `parseForESLint` methods, for test purposes.
304 * In particular, to modify ast nodes, tokens and comments to throw on access to their `start` and `end` properties.
305 * @param {Parser} parser Parser object.
306 * @returns {Parser} Wrapped parser object.
307 */
308function wrapParser(parser) {
309
310 if (typeof parser.parseForESLint === "function") {
311 return {
312 [parserSymbol]: parser,
313 parseForESLint(...args) {
314 const ret = parser.parseForESLint(...args);
315
316 defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys);
317 return ret;
318 }
319 };
320 }
321
322 return {
323 [parserSymbol]: parser,
324 parse(...args) {
325 const ast = parser.parse(...args);
326
327 defineStartEndAsErrorInTree(ast);
328 return ast;
329 }
330 };
331}
332
333/**
334 * Function to replace `SourceCode.prototype.getComments`.
335 * @returns {void}
336 * @throws {Error} Deprecation message.
337 */
338function 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.
346 * @param {string} methodName The name of the method to forbid.
347 * @returns {Function} The function that throws the error.
348 */
349function throwForbiddenMethodError(methodName) {
350 return () => {
351 throw new Error(
352 `\`SourceCode#${methodName}()\` cannot be called inside a rule.`
353 );
354 };
355}
356
357/**
358 * Emit a deprecation warning if function-style format is being used.
359 * @param {string} ruleName Name of the rule.
360 * @returns {void}
361 */
362function emitLegacyRuleAPIWarning(ruleName) {
363 if (!emitLegacyRuleAPIWarning[`warned-${ruleName}`]) {
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}
371
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 */
377function 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 }
385}
386
387/**
388 * Emit a deprecation warning if a rule uses a deprecated `context` method.
389 * @param {string} ruleName Name of the rule.
390 * @param {string} methodName The name of the method on `context` that was used.
391 * @returns {void}
392 */
393function emitDeprecatedContextMethodWarning(ruleName, methodName) {
394 if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) {
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}
402
403/**
404 * Emit a deprecation warning if rule uses CodePath#currentSegments.
405 * @param {string} ruleName Name of the rule.
406 * @returns {void}
407 */
408function 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 );
415 }
416}
417
418/**
419 * Emit a deprecation warning if `context.parserServices` is used.
420 * @param {string} ruleName Name of the rule.
421 * @returns {void}
422 */
423function 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 }
431}
432
433
434//------------------------------------------------------------------------------
435// Public Interface
436//------------------------------------------------------------------------------
437
438// default separators for testing
439const DESCRIBE = Symbol("describe");
440const IT = Symbol("it");
441const IT_ONLY = Symbol("itOnly");
442
443/**
444 * This is `it` default handler if `it` don't exist.
445 * @this {Mocha}
446 * @param {string} text The description of the test case.
447 * @param {Function} method The logic of the test case.
448 * @throws {Error} Any error upon execution of `method`.
449 * @returns {any} Returned value of `method`.
450 */
451function itDefaultHandler(text, method) {
452 try {
453 return method.call(this);
454 } catch (err) {
455 if (err instanceof assert.AssertionError) {
456 err.message += ` (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})`;
457 }
458 throw err;
459 }
460}
461
462/**
463 * This is `describe` default handler if `describe` don't exist.
464 * @this {Mocha}
465 * @param {string} text The description of the test case.
466 * @param {Function} method The logic of the test case.
467 * @returns {any} Returned value of `method`.
468 */
469function describeDefaultHandler(text, method) {
470 return method.call(this);
471}
472
473/**
474 * Mocha test wrapper.
475 */
476class RuleTester {
477
478 /**
479 * Creates a new instance of RuleTester.
480 * @param {Object} [testerConfig] Optional, extra configuration for the tester
481 */
482 constructor(testerConfig) {
483
484 /**
485 * The configuration to use for this tester. Combination of the tester
486 * configuration and the default configuration.
487 * @type {Object}
488 */
489 this.testerConfig = merge(
490 {},
491 defaultConfig,
492 testerConfig,
493 { rules: { "rule-tester/validate-ast": "error" } }
494 );
495
496 /**
497 * Rule definitions to define before tests.
498 * @type {Object}
499 */
500 this.rules = {};
501 this.linter = new Linter();
502 }
503
504 /**
505 * Set the configuration to use for all future tests
506 * @param {Object} config the configuration to use.
507 * @throws {TypeError} If non-object config.
508 * @returns {void}
509 */
510 static setDefaultConfig(config) {
511 if (typeof config !== "object" || config === null) {
512 throw new TypeError("RuleTester.setDefaultConfig: config must be an object");
513 }
514 defaultConfig = config;
515
516 // Make sure the rules object exists since it is assumed to exist later
517 defaultConfig.rules = defaultConfig.rules || {};
518 }
519
520 /**
521 * Get the current configuration used for all tests
522 * @returns {Object} the current configuration
523 */
524 static getDefaultConfig() {
525 return defaultConfig;
526 }
527
528 /**
529 * Reset the configuration to the initial configuration of the tester removing
530 * any changes made until now.
531 * @returns {void}
532 */
533 static resetDefaultConfig() {
534 defaultConfig = merge({}, testerDefaultConfig);
535 }
536
537
538 /*
539 * If people use `mocha test.js --watch` command, `describe` and `it` function
540 * instances are different for each execution. So `describe` and `it` should get fresh instance
541 * always.
542 */
543 static get describe() {
544 return (
545 this[DESCRIBE] ||
546 (typeof describe === "function" ? describe : describeDefaultHandler)
547 );
548 }
549
550 static set describe(value) {
551 this[DESCRIBE] = value;
552 }
553
554 static get it() {
555 return (
556 this[IT] ||
557 (typeof it === "function" ? it : itDefaultHandler)
558 );
559 }
560
561 static set it(value) {
562 this[IT] = value;
563 }
564
565 /**
566 * Adds the `only` property to a test to run it in isolation.
567 * @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
568 * @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
569 */
570 static only(item) {
571 if (typeof item === "string") {
572 return { code: item, only: true };
573 }
574
575 return { ...item, only: true };
576 }
577
578 static get itOnly() {
579 if (typeof this[IT_ONLY] === "function") {
580 return this[IT_ONLY];
581 }
582 if (typeof this[IT] === "function" && typeof this[IT].only === "function") {
583 return Function.bind.call(this[IT].only, this[IT]);
584 }
585 if (typeof it === "function" && typeof it.only === "function") {
586 return Function.bind.call(it.only, it);
587 }
588
589 if (typeof this[DESCRIBE] === "function" || typeof this[IT] === "function") {
590 throw new Error(
591 "Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
592 "See https://eslint.org/docs/latest/integrate/nodejs-api#customizing-ruletester for more."
593 );
594 }
595 if (typeof it === "function") {
596 throw new Error("The current test framework does not support exclusive tests with `only`.");
597 }
598 throw new Error("To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.");
599 }
600
601 static set itOnly(value) {
602 this[IT_ONLY] = value;
603 }
604
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
618 /**
619 * Adds a new rule test to execute.
620 * @param {string} ruleName The name of the rule to run.
621 * @param {Function | Rule} rule The rule to test.
622 * @param {{
623 * valid: (ValidTestCase | string)[],
624 * invalid: InvalidTestCase[]
625 * }} test The collection of tests to run.
626 * @throws {TypeError|Error} If non-object `test`, or if a required
627 * scenario of the given type is missing.
628 * @returns {void}
629 */
630 run(ruleName, rule, test) {
631
632 const testerConfig = this.testerConfig,
633 requiredScenarios = ["valid", "invalid"],
634 scenarioErrors = [],
635 linter = this.linter;
636
637 if (!test || typeof test !== "object") {
638 throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
639 }
640
641 requiredScenarios.forEach(scenarioType => {
642 if (!test[scenarioType]) {
643 scenarioErrors.push(`Could not find any ${scenarioType} test scenarios`);
644 }
645 });
646
647 if (scenarioErrors.length > 0) {
648 throw new Error([
649 `Test Scenarios for rule ${ruleName} is invalid:`
650 ].concat(scenarioErrors).join("\n"));
651 }
652
653 if (typeof rule === "function") {
654 emitLegacyRuleAPIWarning(ruleName);
655 }
656
657 linter.defineRule(ruleName, Object.assign({}, rule, {
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 );
683
684 // emit warning about context.parserServices
685 const parserServices = context.parserServices;
686
687 Object.defineProperty(newContext, "parserServices", {
688 get() {
689 emitParserServicesWarning(ruleName);
690 return parserServices;
691 }
692 });
693
694 Object.freeze(newContext);
695
696 return (typeof rule === "function" ? rule : rule.create)(newContext);
697 }
698 }));
699
700 linter.defineRules(this.rules);
701
702 /**
703 * Run the rule for the given item
704 * @param {string|Object} item Item to run the rule against
705 * @throws {Error} If an invalid schema.
706 * @returns {Object} Eslint run result
707 * @private
708 */
709 function runRuleForItem(item) {
710 let config = merge({}, testerConfig),
711 code, filename, output, beforeAST, afterAST;
712
713 if (typeof item === "string") {
714 code = item;
715 } else {
716 code = item.code;
717
718 /*
719 * Assumes everything on the item is a config except for the
720 * parameters used by this tester
721 */
722 const itemConfig = { ...item };
723
724 for (const parameter of RuleTesterParameters) {
725 delete itemConfig[parameter];
726 }
727
728 /*
729 * Create the config object from the tester config and this item
730 * specific configurations.
731 */
732 config = merge(
733 config,
734 itemConfig
735 );
736 }
737
738 if (item.filename) {
739 filename = item.filename;
740 }
741
742 if (hasOwnProperty(item, "options")) {
743 assert(Array.isArray(item.options), "options must be an array");
744 if (
745 item.options.length > 0 &&
746 typeof rule === "object" &&
747 (
748 !rule.meta || (rule.meta && (typeof rule.meta.schema === "undefined" || rule.meta.schema === null))
749 )
750 ) {
751 emitMissingSchemaWarning(ruleName);
752 }
753 config.rules[ruleName] = [1].concat(item.options);
754 } else {
755 config.rules[ruleName] = 1;
756 }
757
758 const schema = getRuleOptionsSchema(rule);
759
760 /*
761 * Setup AST getters.
762 * The goal is to check whether or not AST was modified when
763 * running the rule under test.
764 */
765 linter.defineRule("rule-tester/validate-ast", {
766 create() {
767 return {
768 Program(node) {
769 beforeAST = cloneDeeplyExcludesParent(node);
770 },
771 "Program:exit"(node) {
772 afterAST = node;
773 }
774 };
775 }
776 });
777
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 if (schema) {
787 ajv.validateSchema(schema);
788
789 if (ajv.errors) {
790 const errors = ajv.errors.map(error => {
791 const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
792
793 return `\t${field}: ${error.message}`;
794 }).join("\n");
795
796 throw new Error([`Schema for rule ${ruleName} is invalid:`, errors]);
797 }
798
799 /*
800 * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
801 * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
802 * 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,
803 * the schema is compiled here separately from checking for `validateSchema` errors.
804 */
805 try {
806 ajv.compile(schema);
807 } catch (err) {
808 throw new Error(`Schema for rule ${ruleName} is invalid: ${err.message}`);
809 }
810 }
811
812 validate(config, "rule-tester", id => (id === ruleName ? rule : null));
813
814 // Verify the code.
815 const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype;
816 const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments");
817 let messages;
818
819 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 forbiddenMethods.forEach(methodName => {
829 SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName);
830 });
831
832 messages = linter.verify(code, config, filename);
833 } finally {
834 SourceCode.prototype.getComments = getComments;
835 Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
836 SourceCode.prototype.applyInlineConfig = applyInlineConfig;
837 SourceCode.prototype.applyLanguageOptions = applyLanguageOptions;
838 SourceCode.prototype.finalize = finalize;
839 }
840
841 const fatalErrorMessage = messages.find(m => m.fatal);
842
843 assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
844
845 // Verify if autofix makes a syntax error or not.
846 if (messages.some(m => m.fix)) {
847 output = SourceCodeFixer.applyFixes(code, messages).output;
848 const errorMessageInFix = linter.verify(output, config, filename).find(m => m.fatal);
849
850 assert(!errorMessageInFix, [
851 "A fatal parsing error occurred in autofix.",
852 `Error: ${errorMessageInFix && errorMessageInFix.message}`,
853 "Autofix output:",
854 output
855 ].join("\n"));
856 } else {
857 output = code;
858 }
859
860 return {
861 messages,
862 output,
863 beforeAST,
864 afterAST: cloneDeeplyExcludesParent(afterAST)
865 };
866 }
867
868 /**
869 * Check if the AST was changed
870 * @param {ASTNode} beforeAST AST node before running
871 * @param {ASTNode} afterAST AST node after running
872 * @returns {void}
873 * @private
874 */
875 function assertASTDidntChange(beforeAST, afterAST) {
876 if (!equal(beforeAST, afterAST)) {
877 assert.fail("Rule should not modify AST.");
878 }
879 }
880
881 /**
882 * Check if the template is valid or not
883 * all valid cases go through this
884 * @param {string|Object} item Item to run the rule against
885 * @returns {void}
886 * @private
887 */
888 function testValidTemplate(item) {
889 const code = typeof item === "object" ? item.code : item;
890
891 assert.ok(typeof code === "string", "Test case must specify a string value for 'code'");
892 if (item.name) {
893 assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
894 }
895
896 const result = runRuleForItem(item);
897 const messages = result.messages;
898
899 assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s",
900 messages.length,
901 util.inspect(messages)));
902
903 assertASTDidntChange(result.beforeAST, result.afterAST);
904 }
905
906 /**
907 * Asserts that the message matches its expected value. If the expected
908 * value is a regular expression, it is checked against the actual
909 * value.
910 * @param {string} actual Actual value
911 * @param {string|RegExp} expected Expected value
912 * @returns {void}
913 * @private
914 */
915 function assertMessageMatches(actual, expected) {
916 if (expected instanceof RegExp) {
917
918 // assert.js doesn't have a built-in RegExp match function
919 assert.ok(
920 expected.test(actual),
921 `Expected '${actual}' to match ${expected}`
922 );
923 } else {
924 assert.strictEqual(actual, expected);
925 }
926 }
927
928 /**
929 * Check if the template is invalid or not
930 * all invalid cases go through this.
931 * @param {string|Object} item Item to run the rule against
932 * @returns {void}
933 * @private
934 */
935 function testInvalidTemplate(item) {
936 assert.ok(typeof item.code === "string", "Test case must specify a string value for 'code'");
937 if (item.name) {
938 assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
939 }
940 assert.ok(item.errors || item.errors === 0,
941 `Did not specify errors for an invalid test of ${ruleName}`);
942
943 if (Array.isArray(item.errors) && item.errors.length === 0) {
944 assert.fail("Invalid cases must have at least one error");
945 }
946
947 const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
948 const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
949
950 const result = runRuleForItem(item);
951 const messages = result.messages;
952
953 if (typeof item.errors === "number") {
954
955 if (item.errors === 0) {
956 assert.fail("Invalid cases must have 'error' value greater than 0");
957 }
958
959 assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
960 item.errors,
961 item.errors === 1 ? "" : "s",
962 messages.length,
963 util.inspect(messages)));
964 } else {
965 assert.strictEqual(
966 messages.length, item.errors.length, util.format(
967 "Should have %d error%s but had %d: %s",
968 item.errors.length,
969 item.errors.length === 1 ? "" : "s",
970 messages.length,
971 util.inspect(messages)
972 )
973 );
974
975 const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName);
976
977 for (let i = 0, l = item.errors.length; i < l; i++) {
978 const error = item.errors[i];
979 const message = messages[i];
980
981 assert(hasMessageOfThisRule, "Error rule name should be the same as the name of the rule being tested");
982
983 if (typeof error === "string" || error instanceof RegExp) {
984
985 // Just an error message.
986 assertMessageMatches(message.message, error);
987 } else if (typeof error === "object" && error !== null) {
988
989 /*
990 * Error object.
991 * This may have a message, messageId, data, node type, line, and/or
992 * column.
993 */
994
995 Object.keys(error).forEach(propertyName => {
996 assert.ok(
997 errorObjectParameters.has(propertyName),
998 `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`
999 );
1000 });
1001
1002 if (hasOwnProperty(error, "message")) {
1003 assert.ok(!hasOwnProperty(error, "messageId"), "Error should not specify both 'message' and a 'messageId'.");
1004 assert.ok(!hasOwnProperty(error, "data"), "Error should not specify both 'data' and 'message'.");
1005 assertMessageMatches(message.message, error.message);
1006 } else if (hasOwnProperty(error, "messageId")) {
1007 assert.ok(
1008 ruleHasMetaMessages,
1009 "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'."
1010 );
1011 if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
1012 assert(false, `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`);
1013 }
1014 assert.strictEqual(
1015 message.messageId,
1016 error.messageId,
1017 `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
1018 );
1019 if (hasOwnProperty(error, "data")) {
1020
1021 /*
1022 * if data was provided, then directly compare the returned message to a synthetic
1023 * interpolated message using the same message ID and data provided in the test.
1024 * See https://github.com/eslint/eslint/issues/9890 for context.
1025 */
1026 const unformattedOriginalMessage = rule.meta.messages[error.messageId];
1027 const rehydratedMessage = interpolate(unformattedOriginalMessage, error.data);
1028
1029 assert.strictEqual(
1030 message.message,
1031 rehydratedMessage,
1032 `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
1033 );
1034 }
1035 }
1036
1037 assert.ok(
1038 hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
1039 "Error must specify 'messageId' if 'data' is used."
1040 );
1041
1042 if (error.type) {
1043 assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
1044 }
1045
1046 if (hasOwnProperty(error, "line")) {
1047 assert.strictEqual(message.line, error.line, `Error line should be ${error.line}`);
1048 }
1049
1050 if (hasOwnProperty(error, "column")) {
1051 assert.strictEqual(message.column, error.column, `Error column should be ${error.column}`);
1052 }
1053
1054 if (hasOwnProperty(error, "endLine")) {
1055 assert.strictEqual(message.endLine, error.endLine, `Error endLine should be ${error.endLine}`);
1056 }
1057
1058 if (hasOwnProperty(error, "endColumn")) {
1059 assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
1060 }
1061
1062 if (hasOwnProperty(error, "suggestions")) {
1063
1064 // Support asserting there are no suggestions
1065 if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) {
1066 if (Array.isArray(message.suggestions) && message.suggestions.length > 0) {
1067 assert.fail(`Error should have no suggestions on error with message: "${message.message}"`);
1068 }
1069 } else {
1070 assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`);
1071 assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
1072
1073 error.suggestions.forEach((expectedSuggestion, index) => {
1074 assert.ok(
1075 typeof expectedSuggestion === "object" && expectedSuggestion !== null,
1076 "Test suggestion in 'suggestions' array must be an object."
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")) {
1089 assert.ok(
1090 !hasOwnProperty(expectedSuggestion, "data"),
1091 `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
1092 );
1093 assert.strictEqual(
1094 actualSuggestion.desc,
1095 expectedSuggestion.desc,
1096 `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
1097 );
1098 }
1099
1100 if (hasOwnProperty(expectedSuggestion, "messageId")) {
1101 assert.ok(
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);
1117
1118 assert.strictEqual(
1119 actualSuggestion.desc,
1120 rehydratedDesc,
1121 `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
1122 );
1123 }
1124 } else {
1125 assert.ok(
1126 !hasOwnProperty(expectedSuggestion, "data"),
1127 `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
1128 );
1129 }
1130
1131 if (hasOwnProperty(expectedSuggestion, "output")) {
1132 const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
1133
1134 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 });
1137 }
1138 }
1139 } else {
1140
1141 // Message was an unexpected type
1142 assert.fail(`Error should be a string, object, or RegExp, but found (${util.inspect(message)})`);
1143 }
1144 }
1145 }
1146
1147 if (hasOwnProperty(item, "output")) {
1148 if (item.output === null) {
1149 assert.strictEqual(
1150 result.output,
1151 item.code,
1152 "Expected no autofixes to be suggested"
1153 );
1154 } else {
1155 assert.strictEqual(result.output, item.output, "Output is incorrect.");
1156 }
1157 } else {
1158 assert.strictEqual(
1159 result.output,
1160 item.code,
1161 "The rule fixed the code. Please add 'output' property."
1162 );
1163 }
1164
1165 assertASTDidntChange(result.beforeAST, result.afterAST);
1166 }
1167
1168 /*
1169 * This creates a mocha test suite and pipes all supplied info through
1170 * one of the templates above.
1171 * The test suites for valid/invalid are created conditionally as
1172 * test runners (eg. vitest) fail for empty test suites.
1173 */
1174 this.constructor.describe(ruleName, () => {
1175 if (test.valid.length > 0) {
1176 this.constructor.describe("valid", () => {
1177 test.valid.forEach(valid => {
1178 this.constructor[valid.only ? "itOnly" : "it"](
1179 sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
1180 () => {
1181 testValidTemplate(valid);
1182 }
1183 );
1184 });
1185 });
1186 }
1187
1188 if (test.invalid.length > 0) {
1189 this.constructor.describe("invalid", () => {
1190 test.invalid.forEach(invalid => {
1191 this.constructor[invalid.only ? "itOnly" : "it"](
1192 sanitize(invalid.name || invalid.code),
1193 () => {
1194 testInvalidTemplate(invalid);
1195 }
1196 );
1197 });
1198 });
1199 }
1200 });
1201 }
1202}
1203
1204RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null;
1205
1206module.exports = RuleTester;
Note: See TracBrowser for help on using the repository browser.