rule-tester.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579
  1. /**
  2. * @fileoverview Mocha/Jest test wrapper
  3. * @author Ilya Volodin
  4. */
  5. "use strict";
  6. /* globals describe, it -- Mocha globals */
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const assert = require("node:assert"),
  11. util = require("node:util"),
  12. path = require("node:path"),
  13. equal = require("fast-deep-equal"),
  14. Traverser = require("../shared/traverser"),
  15. { Config } = require("../config/config"),
  16. { Linter, SourceCodeFixer } = require("../linter"),
  17. { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
  18. stringify = require("json-stable-stringify-without-jsonify");
  19. const { FlatConfigArray } = require("../config/flat-config-array");
  20. const {
  21. defaultConfig,
  22. defaultRuleTesterConfig,
  23. } = require("../config/default-config");
  24. const ajv = require("../shared/ajv")({ strictDefaults: true });
  25. const parserSymbol = Symbol.for("eslint.RuleTester.parser");
  26. const { ConfigArraySymbol } = require("@eslint/config-array");
  27. const { isSerializable } = require("../shared/serialization");
  28. const jslang = require("../languages/js");
  29. const { SourceCode } = require("../languages/js/source-code");
  30. //------------------------------------------------------------------------------
  31. // Typedefs
  32. //------------------------------------------------------------------------------
  33. /** @import { LanguageOptions, RuleDefinition } from "@eslint/core" */
  34. /** @typedef {import("../types").Linter.Parser} Parser */
  35. /**
  36. * A test case that is expected to pass lint.
  37. * @typedef {Object} ValidTestCase
  38. * @property {string} [name] Name for the test case.
  39. * @property {string} code Code for the test case.
  40. * @property {any[]} [options] Options for the test case.
  41. * @property {Function} [before] Function to execute before testing the case.
  42. * @property {Function} [after] Function to execute after testing the case regardless of its result.
  43. * @property {LanguageOptions} [languageOptions] The language options to use in the test case.
  44. * @property {{ [name: string]: any }} [settings] Settings for the test case.
  45. * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
  46. * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
  47. */
  48. /**
  49. * A test case that is expected to fail lint.
  50. * @typedef {Object} InvalidTestCase
  51. * @property {string} [name] Name for the test case.
  52. * @property {string} code Code for the test case.
  53. * @property {number | Array<TestCaseError | string | RegExp>} errors Expected errors.
  54. * @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.
  55. * @property {any[]} [options] Options for the test case.
  56. * @property {Function} [before] Function to execute before testing the case.
  57. * @property {Function} [after] Function to execute after testing the case regardless of its result.
  58. * @property {{ [name: string]: any }} [settings] Settings for the test case.
  59. * @property {string} [filename] The fake filename for the test case. Useful for rules that make assertion about filenames.
  60. * @property {LanguageOptions} [languageOptions] The language options to use in the test case.
  61. * @property {boolean} [only] Run only this test case or the subset of test cases with this property.
  62. */
  63. /**
  64. * A description of a reported error used in a rule tester test.
  65. * @typedef {Object} TestCaseError
  66. * @property {string | RegExp} [message] Message.
  67. * @property {string} [messageId] Message ID.
  68. * @property {string} [type] The type of the reported AST node.
  69. * @property {{ [name: string]: string }} [data] The data used to fill the message template.
  70. * @property {number} [line] The 1-based line number of the reported start location.
  71. * @property {number} [column] The 1-based column number of the reported start location.
  72. * @property {number} [endLine] The 1-based line number of the reported end location.
  73. * @property {number} [endColumn] The 1-based column number of the reported end location.
  74. */
  75. //------------------------------------------------------------------------------
  76. // Private Members
  77. //------------------------------------------------------------------------------
  78. /*
  79. * testerDefaultConfig must not be modified as it allows to reset the tester to
  80. * the initial default configuration
  81. */
  82. const testerDefaultConfig = { rules: {} };
  83. /*
  84. * RuleTester uses this config as its default. This can be overwritten via
  85. * setDefaultConfig().
  86. */
  87. let sharedDefaultConfig = { rules: {} };
  88. /*
  89. * List every parameters possible on a test case that are not related to eslint
  90. * configuration
  91. */
  92. const RuleTesterParameters = [
  93. "name",
  94. "code",
  95. "filename",
  96. "options",
  97. "before",
  98. "after",
  99. "errors",
  100. "output",
  101. "only",
  102. ];
  103. /*
  104. * All allowed property names in error objects.
  105. */
  106. const errorObjectParameters = new Set([
  107. "message",
  108. "messageId",
  109. "data",
  110. "type",
  111. "line",
  112. "column",
  113. "endLine",
  114. "endColumn",
  115. "suggestions",
  116. ]);
  117. const friendlyErrorObjectParameterList = `[${[...errorObjectParameters].map(key => `'${key}'`).join(", ")}]`;
  118. /*
  119. * All allowed property names in suggestion objects.
  120. */
  121. const suggestionObjectParameters = new Set([
  122. "desc",
  123. "messageId",
  124. "data",
  125. "output",
  126. ]);
  127. const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
  128. /*
  129. * Ignored test case properties when checking for test case duplicates.
  130. */
  131. const duplicationIgnoredParameters = new Set(["name", "errors", "output"]);
  132. const forbiddenMethods = [
  133. "applyInlineConfig",
  134. "applyLanguageOptions",
  135. "finalize",
  136. ];
  137. /** @type {Map<string,WeakSet>} */
  138. const forbiddenMethodCalls = new Map(
  139. forbiddenMethods.map(methodName => [methodName, new WeakSet()]),
  140. );
  141. const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
  142. /**
  143. * Clones a given value deeply.
  144. * Note: This ignores `parent` property.
  145. * @param {any} x A value to clone.
  146. * @returns {any} A cloned value.
  147. */
  148. function cloneDeeplyExcludesParent(x) {
  149. if (typeof x === "object" && x !== null) {
  150. if (Array.isArray(x)) {
  151. return x.map(cloneDeeplyExcludesParent);
  152. }
  153. const retv = {};
  154. for (const key in x) {
  155. if (key !== "parent" && hasOwnProperty(x, key)) {
  156. retv[key] = cloneDeeplyExcludesParent(x[key]);
  157. }
  158. }
  159. return retv;
  160. }
  161. return x;
  162. }
  163. /**
  164. * Freezes a given value deeply.
  165. * @param {any} x A value to freeze.
  166. * @param {Set<Object>} seenObjects Objects already seen during the traversal.
  167. * @returns {void}
  168. */
  169. function freezeDeeply(x, seenObjects = new Set()) {
  170. if (typeof x === "object" && x !== null) {
  171. if (seenObjects.has(x)) {
  172. return; // skip to avoid infinite recursion
  173. }
  174. seenObjects.add(x);
  175. if (Array.isArray(x)) {
  176. x.forEach(element => {
  177. freezeDeeply(element, seenObjects);
  178. });
  179. } else {
  180. for (const key in x) {
  181. if (key !== "parent" && hasOwnProperty(x, key)) {
  182. freezeDeeply(x[key], seenObjects);
  183. }
  184. }
  185. }
  186. Object.freeze(x);
  187. }
  188. }
  189. /**
  190. * Replace control characters by `\u00xx` form.
  191. * @param {string} text The text to sanitize.
  192. * @returns {string} The sanitized text.
  193. */
  194. function sanitize(text) {
  195. if (typeof text !== "string") {
  196. return "";
  197. }
  198. return text.replace(
  199. /[\u0000-\u0009\u000b-\u001a]/gu, // eslint-disable-line no-control-regex -- Escaping controls
  200. c => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`,
  201. );
  202. }
  203. /**
  204. * Define `start`/`end` properties as throwing error.
  205. * @param {string} objName Object name used for error messages.
  206. * @param {ASTNode} node The node to define.
  207. * @returns {void}
  208. */
  209. function defineStartEndAsError(objName, node) {
  210. Object.defineProperties(node, {
  211. start: {
  212. get() {
  213. throw new Error(
  214. `Use ${objName}.range[0] instead of ${objName}.start`,
  215. );
  216. },
  217. configurable: true,
  218. enumerable: false,
  219. },
  220. end: {
  221. get() {
  222. throw new Error(
  223. `Use ${objName}.range[1] instead of ${objName}.end`,
  224. );
  225. },
  226. configurable: true,
  227. enumerable: false,
  228. },
  229. });
  230. }
  231. /**
  232. * Define `start`/`end` properties of all nodes of the given AST as throwing error.
  233. * @param {ASTNode} ast The root node to errorize `start`/`end` properties.
  234. * @param {Object} [visitorKeys] Visitor keys to be used for traversing the given ast.
  235. * @returns {void}
  236. */
  237. function defineStartEndAsErrorInTree(ast, visitorKeys) {
  238. Traverser.traverse(ast, {
  239. visitorKeys,
  240. enter: defineStartEndAsError.bind(null, "node"),
  241. });
  242. ast.tokens.forEach(defineStartEndAsError.bind(null, "token"));
  243. ast.comments.forEach(defineStartEndAsError.bind(null, "token"));
  244. }
  245. /**
  246. * Wraps the given parser in order to intercept and modify return values from the `parse` and `parseForESLint` methods, for test purposes.
  247. * In particular, to modify ast nodes, tokens and comments to throw on access to their `start` and `end` properties.
  248. * @param {Parser} parser Parser object.
  249. * @returns {Parser} Wrapped parser object.
  250. */
  251. function wrapParser(parser) {
  252. if (typeof parser.parseForESLint === "function") {
  253. return {
  254. [parserSymbol]: parser,
  255. parseForESLint(...args) {
  256. const ret = parser.parseForESLint(...args);
  257. defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys);
  258. return ret;
  259. },
  260. };
  261. }
  262. return {
  263. [parserSymbol]: parser,
  264. parse(...args) {
  265. const ast = parser.parse(...args);
  266. defineStartEndAsErrorInTree(ast);
  267. return ast;
  268. },
  269. };
  270. }
  271. /**
  272. * Function to replace forbidden `SourceCode` methods. Allows just one call per method.
  273. * @param {string} methodName The name of the method to forbid.
  274. * @param {Function} prototype The prototype with the original method to call.
  275. * @returns {Function} The function that throws the error.
  276. */
  277. function throwForbiddenMethodError(methodName, prototype) {
  278. const original = prototype[methodName];
  279. return function (...args) {
  280. const called = forbiddenMethodCalls.get(methodName);
  281. /* eslint-disable no-invalid-this -- needed to operate as a method. */
  282. if (!called.has(this)) {
  283. called.add(this);
  284. return original.apply(this, args);
  285. }
  286. /* eslint-enable no-invalid-this -- not needed past this point */
  287. throw new Error(
  288. `\`SourceCode#${methodName}()\` cannot be called inside a rule.`,
  289. );
  290. };
  291. }
  292. /**
  293. * Extracts names of {{ placeholders }} from the reported message.
  294. * @param {string} message Reported message
  295. * @returns {string[]} Array of placeholder names
  296. */
  297. function getMessagePlaceholders(message) {
  298. const matcher = getPlaceholderMatcher();
  299. return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
  300. }
  301. /**
  302. * Returns the placeholders in the reported messages but
  303. * only includes the placeholders available in the raw message and not in the provided data.
  304. * @param {string} message The reported message
  305. * @param {string} raw The raw message specified in the rule meta.messages
  306. * @param {undefined|Record<unknown, unknown>} data The passed
  307. * @returns {string[]} Missing placeholder names
  308. */
  309. function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) {
  310. const unsubstituted = getMessagePlaceholders(message);
  311. if (unsubstituted.length === 0) {
  312. return [];
  313. }
  314. // Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property
  315. const known = getMessagePlaceholders(raw);
  316. const provided = Object.keys(data);
  317. return unsubstituted.filter(
  318. name => known.includes(name) && !provided.includes(name),
  319. );
  320. }
  321. const metaSchemaDescription = `
  322. \t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.
  323. \t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule.
  324. \t- You can also set \`meta.schema\` to \`false\` to opt-out of options validation (not recommended).
  325. \thttps://eslint.org/docs/latest/extend/custom-rules#options-schemas
  326. `;
  327. //------------------------------------------------------------------------------
  328. // Public Interface
  329. //------------------------------------------------------------------------------
  330. // default separators for testing
  331. const DESCRIBE = Symbol("describe");
  332. const IT = Symbol("it");
  333. const IT_ONLY = Symbol("itOnly");
  334. /**
  335. * This is `it` default handler if `it` don't exist.
  336. * @this {Mocha}
  337. * @param {string} text The description of the test case.
  338. * @param {Function} method The logic of the test case.
  339. * @throws {Error} Any error upon execution of `method`.
  340. * @returns {any} Returned value of `method`.
  341. */
  342. function itDefaultHandler(text, method) {
  343. try {
  344. return method.call(this);
  345. } catch (err) {
  346. if (err instanceof assert.AssertionError) {
  347. err.message += ` (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})`;
  348. }
  349. throw err;
  350. }
  351. }
  352. /**
  353. * This is `describe` default handler if `describe` don't exist.
  354. * @this {Mocha}
  355. * @param {string} text The description of the test case.
  356. * @param {Function} method The logic of the test case.
  357. * @returns {any} Returned value of `method`.
  358. */
  359. function describeDefaultHandler(text, method) {
  360. return method.call(this);
  361. }
  362. /**
  363. * Mocha test wrapper.
  364. */
  365. class RuleTester {
  366. /**
  367. * Creates a new instance of RuleTester.
  368. * @param {Object} [testerConfig] Optional, extra configuration for the tester
  369. */
  370. constructor(testerConfig = {}) {
  371. /**
  372. * The configuration to use for this tester. Combination of the tester
  373. * configuration and the default configuration.
  374. * @type {Object}
  375. */
  376. this.testerConfig = [
  377. sharedDefaultConfig,
  378. testerConfig,
  379. { rules: { "rule-tester/validate-ast": "error" } },
  380. ];
  381. this.linter = new Linter({ configType: "flat" });
  382. }
  383. /**
  384. * Set the configuration to use for all future tests
  385. * @param {Object} config the configuration to use.
  386. * @throws {TypeError} If non-object config.
  387. * @returns {void}
  388. */
  389. static setDefaultConfig(config) {
  390. if (typeof config !== "object" || config === null) {
  391. throw new TypeError(
  392. "RuleTester.setDefaultConfig: config must be an object",
  393. );
  394. }
  395. sharedDefaultConfig = config;
  396. // Make sure the rules object exists since it is assumed to exist later
  397. sharedDefaultConfig.rules = sharedDefaultConfig.rules || {};
  398. }
  399. /**
  400. * Get the current configuration used for all tests
  401. * @returns {Object} the current configuration
  402. */
  403. static getDefaultConfig() {
  404. return sharedDefaultConfig;
  405. }
  406. /**
  407. * Reset the configuration to the initial configuration of the tester removing
  408. * any changes made until now.
  409. * @returns {void}
  410. */
  411. static resetDefaultConfig() {
  412. sharedDefaultConfig = {
  413. rules: {
  414. ...testerDefaultConfig.rules,
  415. },
  416. };
  417. }
  418. /*
  419. * If people use `mocha test.js --watch` command, `describe` and `it` function
  420. * instances are different for each execution. So `describe` and `it` should get fresh instance
  421. * always.
  422. */
  423. static get describe() {
  424. return (
  425. this[DESCRIBE] ||
  426. (typeof describe === "function" ? describe : describeDefaultHandler)
  427. );
  428. }
  429. static set describe(value) {
  430. this[DESCRIBE] = value;
  431. }
  432. static get it() {
  433. return this[IT] || (typeof it === "function" ? it : itDefaultHandler);
  434. }
  435. static set it(value) {
  436. this[IT] = value;
  437. }
  438. /**
  439. * Adds the `only` property to a test to run it in isolation.
  440. * @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
  441. * @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
  442. */
  443. static only(item) {
  444. if (typeof item === "string") {
  445. return { code: item, only: true };
  446. }
  447. return { ...item, only: true };
  448. }
  449. static get itOnly() {
  450. if (typeof this[IT_ONLY] === "function") {
  451. return this[IT_ONLY];
  452. }
  453. if (
  454. typeof this[IT] === "function" &&
  455. typeof this[IT].only === "function"
  456. ) {
  457. return Function.bind.call(this[IT].only, this[IT]);
  458. }
  459. if (typeof it === "function" && typeof it.only === "function") {
  460. return Function.bind.call(it.only, it);
  461. }
  462. if (
  463. typeof this[DESCRIBE] === "function" ||
  464. typeof this[IT] === "function"
  465. ) {
  466. throw new Error(
  467. "Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
  468. "See https://eslint.org/docs/latest/integrate/nodejs-api#customizing-ruletester for more.",
  469. );
  470. }
  471. if (typeof it === "function") {
  472. throw new Error(
  473. "The current test framework does not support exclusive tests with `only`.",
  474. );
  475. }
  476. throw new Error(
  477. "To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.",
  478. );
  479. }
  480. static set itOnly(value) {
  481. this[IT_ONLY] = value;
  482. }
  483. /**
  484. * Adds a new rule test to execute.
  485. * @param {string} ruleName The name of the rule to run.
  486. * @param {RuleDefinition} rule The rule to test.
  487. * @param {{
  488. * valid: (ValidTestCase | string)[],
  489. * invalid: InvalidTestCase[]
  490. * }} test The collection of tests to run.
  491. * @throws {TypeError|Error} If `rule` is not an object with a `create` method,
  492. * or if non-object `test`, or if a required scenario of the given type is missing.
  493. * @returns {void}
  494. */
  495. run(ruleName, rule, test) {
  496. const testerConfig = this.testerConfig,
  497. requiredScenarios = ["valid", "invalid"],
  498. scenarioErrors = [],
  499. linter = this.linter,
  500. ruleId = `rule-to-test/${ruleName}`;
  501. const seenValidTestCases = new Set();
  502. const seenInvalidTestCases = new Set();
  503. if (
  504. !rule ||
  505. typeof rule !== "object" ||
  506. typeof rule.create !== "function"
  507. ) {
  508. throw new TypeError(
  509. "Rule must be an object with a `create` method",
  510. );
  511. }
  512. if (!test || typeof test !== "object") {
  513. throw new TypeError(
  514. `Test Scenarios for rule ${ruleName} : Could not find test scenario object`,
  515. );
  516. }
  517. requiredScenarios.forEach(scenarioType => {
  518. if (!test[scenarioType]) {
  519. scenarioErrors.push(
  520. `Could not find any ${scenarioType} test scenarios`,
  521. );
  522. }
  523. });
  524. if (scenarioErrors.length > 0) {
  525. throw new Error(
  526. [`Test Scenarios for rule ${ruleName} is invalid:`]
  527. .concat(scenarioErrors)
  528. .join("\n"),
  529. );
  530. }
  531. const baseConfig = [
  532. {
  533. plugins: {
  534. // copy root plugin over
  535. "@": {
  536. /*
  537. * Parsers are wrapped to detect more errors, so this needs
  538. * to be a new object for each call to run(), otherwise the
  539. * parsers will be wrapped multiple times.
  540. */
  541. parsers: {
  542. ...defaultConfig[0].plugins["@"].parsers,
  543. },
  544. /*
  545. * The rules key on the default plugin is a proxy to lazy-load
  546. * just the rules that are needed. So, don't create a new object
  547. * here, just use the default one to keep that performance
  548. * enhancement.
  549. */
  550. rules: defaultConfig[0].plugins["@"].rules,
  551. languages: defaultConfig[0].plugins["@"].languages,
  552. },
  553. "rule-to-test": {
  554. rules: {
  555. [ruleName]: Object.assign({}, rule, {
  556. // Create a wrapper rule that freezes the `context` properties.
  557. create(context) {
  558. freezeDeeply(context.options);
  559. freezeDeeply(context.settings);
  560. freezeDeeply(context.parserOptions);
  561. // freezeDeeply(context.languageOptions);
  562. return rule.create(context);
  563. },
  564. }),
  565. },
  566. },
  567. },
  568. language: defaultConfig[0].language,
  569. },
  570. ...defaultRuleTesterConfig,
  571. ];
  572. /**
  573. * Runs a hook on the given item when it's assigned to the given property
  574. * @param {string|Object} item Item to run the hook on
  575. * @param {string} prop The property having the hook assigned to
  576. * @throws {Error} If the property is not a function or that function throws an error
  577. * @returns {void}
  578. * @private
  579. */
  580. function runHook(item, prop) {
  581. if (typeof item === "object" && hasOwnProperty(item, prop)) {
  582. assert.strictEqual(
  583. typeof item[prop],
  584. "function",
  585. `Optional test case property '${prop}' must be a function`,
  586. );
  587. item[prop]();
  588. }
  589. }
  590. /**
  591. * Run the rule for the given item
  592. * @param {string|Object} item Item to run the rule against
  593. * @throws {Error} If an invalid schema.
  594. * @returns {Object} Eslint run result
  595. * @private
  596. */
  597. function runRuleForItem(item) {
  598. const flatConfigArrayOptions = {
  599. baseConfig,
  600. };
  601. if (item.filename) {
  602. flatConfigArrayOptions.basePath =
  603. path.parse(item.filename).root || void 0;
  604. }
  605. const configs = new FlatConfigArray(
  606. testerConfig,
  607. flatConfigArrayOptions,
  608. );
  609. /*
  610. * Modify the returned config so that the parser is wrapped to catch
  611. * access of the start/end properties. This method is called just
  612. * once per code snippet being tested, so each test case gets a clean
  613. * parser.
  614. */
  615. configs[ConfigArraySymbol.finalizeConfig] = function (...args) {
  616. // can't do super here :(
  617. const proto = Object.getPrototypeOf(this);
  618. const calculatedConfig = proto[
  619. ConfigArraySymbol.finalizeConfig
  620. ].apply(this, args);
  621. // wrap the parser to catch start/end property access
  622. if (calculatedConfig.language === jslang) {
  623. calculatedConfig.languageOptions.parser = wrapParser(
  624. calculatedConfig.languageOptions.parser,
  625. );
  626. }
  627. return calculatedConfig;
  628. };
  629. let code, filename, output, beforeAST, afterAST;
  630. if (typeof item === "string") {
  631. code = item;
  632. } else {
  633. code = item.code;
  634. /*
  635. * Assumes everything on the item is a config except for the
  636. * parameters used by this tester
  637. */
  638. const itemConfig = { ...item };
  639. for (const parameter of RuleTesterParameters) {
  640. delete itemConfig[parameter];
  641. }
  642. /*
  643. * Create the config object from the tester config and this item
  644. * specific configurations.
  645. */
  646. configs.push(itemConfig);
  647. }
  648. if (hasOwnProperty(item, "only")) {
  649. assert.ok(
  650. typeof item.only === "boolean",
  651. "Optional test case property 'only' must be a boolean",
  652. );
  653. }
  654. if (hasOwnProperty(item, "filename")) {
  655. assert.ok(
  656. typeof item.filename === "string",
  657. "Optional test case property 'filename' must be a string",
  658. );
  659. filename = item.filename;
  660. }
  661. let ruleConfig = 1;
  662. if (hasOwnProperty(item, "options")) {
  663. assert(Array.isArray(item.options), "options must be an array");
  664. ruleConfig = [1, ...item.options];
  665. }
  666. configs.push({
  667. rules: {
  668. [ruleId]: ruleConfig,
  669. },
  670. });
  671. let schema;
  672. try {
  673. schema = Config.getRuleOptionsSchema(rule);
  674. } catch (err) {
  675. err.message += metaSchemaDescription;
  676. throw err;
  677. }
  678. /*
  679. * Check and throw an error if the schema is an empty object (`schema:{}`), because such schema
  680. * doesn't validate or enforce anything and is therefore considered a possible error. If the intent
  681. * was to skip options validation, `schema:false` should be set instead (explicit opt-out).
  682. *
  683. * For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed
  684. * properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well,
  685. * it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea
  686. * to use inherited properties in schemas because schemas that differ only in inherited properties would end up
  687. * having the same cache entry that would be correct for only one of them.
  688. *
  689. * At this point, `schema` can only be an object or `null`.
  690. */
  691. if (schema && Object.keys(schema).length === 0) {
  692. throw new Error(
  693. `\`schema: {}\` is a no-op${metaSchemaDescription}`,
  694. );
  695. }
  696. /*
  697. * Setup AST getters.
  698. * The goal is to check whether or not AST was modified when
  699. * running the rule under test.
  700. */
  701. configs.push({
  702. plugins: {
  703. "rule-tester": {
  704. rules: {
  705. "validate-ast": {
  706. create() {
  707. return {
  708. Program(node) {
  709. beforeAST =
  710. cloneDeeplyExcludesParent(node);
  711. },
  712. "Program:exit"(node) {
  713. afterAST = node;
  714. },
  715. };
  716. },
  717. },
  718. },
  719. },
  720. },
  721. });
  722. if (schema) {
  723. ajv.validateSchema(schema);
  724. if (ajv.errors) {
  725. const errors = ajv.errors
  726. .map(error => {
  727. const field =
  728. error.dataPath[0] === "."
  729. ? error.dataPath.slice(1)
  730. : error.dataPath;
  731. return `\t${field}: ${error.message}`;
  732. })
  733. .join("\n");
  734. throw new Error([
  735. `Schema for rule ${ruleName} is invalid:`,
  736. errors,
  737. ]);
  738. }
  739. /*
  740. * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
  741. * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
  742. * 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,
  743. * the schema is compiled here separately from checking for `validateSchema` errors.
  744. */
  745. try {
  746. ajv.compile(schema);
  747. } catch (err) {
  748. throw new Error(
  749. `Schema for rule ${ruleName} is invalid: ${err.message}`,
  750. {
  751. cause: err,
  752. },
  753. );
  754. }
  755. }
  756. // check for validation errors
  757. try {
  758. configs.normalizeSync();
  759. configs.getConfig("test.js");
  760. } catch (error) {
  761. error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`;
  762. throw error;
  763. }
  764. // Verify the code.
  765. const { applyLanguageOptions, applyInlineConfig, finalize } =
  766. SourceCode.prototype;
  767. let messages;
  768. try {
  769. forbiddenMethods.forEach(methodName => {
  770. SourceCode.prototype[methodName] =
  771. throwForbiddenMethodError(
  772. methodName,
  773. SourceCode.prototype,
  774. );
  775. });
  776. messages = linter.verify(code, configs, filename);
  777. } finally {
  778. SourceCode.prototype.applyInlineConfig = applyInlineConfig;
  779. SourceCode.prototype.applyLanguageOptions =
  780. applyLanguageOptions;
  781. SourceCode.prototype.finalize = finalize;
  782. }
  783. const fatalErrorMessage = messages.find(m => m.fatal);
  784. assert(
  785. !fatalErrorMessage,
  786. `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`,
  787. );
  788. // Verify if autofix makes a syntax error or not.
  789. if (messages.some(m => m.fix)) {
  790. output = SourceCodeFixer.applyFixes(code, messages).output;
  791. const errorMessageInFix = linter
  792. .verify(output, configs, filename)
  793. .find(m => m.fatal);
  794. assert(
  795. !errorMessageInFix,
  796. [
  797. "A fatal parsing error occurred in autofix.",
  798. `Error: ${errorMessageInFix && errorMessageInFix.message}`,
  799. "Autofix output:",
  800. output,
  801. ].join("\n"),
  802. );
  803. } else {
  804. output = code;
  805. }
  806. return {
  807. messages,
  808. output,
  809. beforeAST,
  810. afterAST: cloneDeeplyExcludesParent(afterAST),
  811. configs,
  812. filename,
  813. };
  814. }
  815. /**
  816. * Check if the AST was changed
  817. * @param {ASTNode} beforeAST AST node before running
  818. * @param {ASTNode} afterAST AST node after running
  819. * @returns {void}
  820. * @private
  821. */
  822. function assertASTDidntChange(beforeAST, afterAST) {
  823. if (!equal(beforeAST, afterAST)) {
  824. assert.fail("Rule should not modify AST.");
  825. }
  826. }
  827. /**
  828. * Check if this test case is a duplicate of one we have seen before.
  829. * @param {string|Object} item test case object
  830. * @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
  831. * @returns {void}
  832. * @private
  833. */
  834. function checkDuplicateTestCase(item, seenTestCases) {
  835. if (!isSerializable(item)) {
  836. /*
  837. * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
  838. * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
  839. */
  840. return;
  841. }
  842. const normalizedItem =
  843. typeof item === "string" ? { code: item } : item;
  844. const serializedTestCase = stringify(normalizedItem, {
  845. replacer(key, value) {
  846. // "this" is the currently stringified object --> only ignore top-level properties
  847. return normalizedItem !== this ||
  848. !duplicationIgnoredParameters.has(key)
  849. ? value
  850. : void 0;
  851. },
  852. });
  853. assert(
  854. !seenTestCases.has(serializedTestCase),
  855. "detected duplicate test case",
  856. );
  857. seenTestCases.add(serializedTestCase);
  858. }
  859. /**
  860. * Check if the template is valid or not
  861. * all valid cases go through this
  862. * @param {string|Object} item Item to run the rule against
  863. * @returns {void}
  864. * @private
  865. */
  866. function testValidTemplate(item) {
  867. const code = typeof item === "object" ? item.code : item;
  868. assert.ok(
  869. typeof code === "string",
  870. "Test case must specify a string value for 'code'",
  871. );
  872. if (item.name) {
  873. assert.ok(
  874. typeof item.name === "string",
  875. "Optional test case property 'name' must be a string",
  876. );
  877. }
  878. checkDuplicateTestCase(item, seenValidTestCases);
  879. const result = runRuleForItem(item);
  880. const messages = result.messages;
  881. assert.strictEqual(
  882. messages.length,
  883. 0,
  884. util.format(
  885. "Should have no errors but had %d: %s",
  886. messages.length,
  887. util.inspect(messages),
  888. ),
  889. );
  890. assertASTDidntChange(result.beforeAST, result.afterAST);
  891. }
  892. /**
  893. * Asserts that the message matches its expected value. If the expected
  894. * value is a regular expression, it is checked against the actual
  895. * value.
  896. * @param {string} actual Actual value
  897. * @param {string|RegExp} expected Expected value
  898. * @returns {void}
  899. * @private
  900. */
  901. function assertMessageMatches(actual, expected) {
  902. if (expected instanceof RegExp) {
  903. // assert.js doesn't have a built-in RegExp match function
  904. assert.ok(
  905. expected.test(actual),
  906. `Expected '${actual}' to match ${expected}`,
  907. );
  908. } else {
  909. assert.strictEqual(actual, expected);
  910. }
  911. }
  912. /**
  913. * Check if the template is invalid or not
  914. * all invalid cases go through this.
  915. * @param {string|Object} item Item to run the rule against
  916. * @returns {void}
  917. * @private
  918. */
  919. function testInvalidTemplate(item) {
  920. assert.ok(
  921. typeof item.code === "string",
  922. "Test case must specify a string value for 'code'",
  923. );
  924. if (item.name) {
  925. assert.ok(
  926. typeof item.name === "string",
  927. "Optional test case property 'name' must be a string",
  928. );
  929. }
  930. assert.ok(
  931. item.errors || item.errors === 0,
  932. `Did not specify errors for an invalid test of ${ruleName}`,
  933. );
  934. if (Array.isArray(item.errors) && item.errors.length === 0) {
  935. assert.fail("Invalid cases must have at least one error");
  936. }
  937. checkDuplicateTestCase(item, seenInvalidTestCases);
  938. const ruleHasMetaMessages =
  939. hasOwnProperty(rule, "meta") &&
  940. hasOwnProperty(rule.meta, "messages");
  941. const friendlyIDList = ruleHasMetaMessages
  942. ? `[${Object.keys(rule.meta.messages)
  943. .map(key => `'${key}'`)
  944. .join(", ")}]`
  945. : null;
  946. const result = runRuleForItem(item);
  947. const messages = result.messages;
  948. for (const message of messages) {
  949. if (hasOwnProperty(message, "suggestions")) {
  950. /** @type {Map<string, number>} */
  951. const seenMessageIndices = new Map();
  952. for (let i = 0; i < message.suggestions.length; i += 1) {
  953. const suggestionMessage = message.suggestions[i].desc;
  954. const previous =
  955. seenMessageIndices.get(suggestionMessage);
  956. assert.ok(
  957. !seenMessageIndices.has(suggestionMessage),
  958. `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`,
  959. );
  960. seenMessageIndices.set(suggestionMessage, i);
  961. }
  962. }
  963. }
  964. if (typeof item.errors === "number") {
  965. if (item.errors === 0) {
  966. assert.fail(
  967. "Invalid cases must have 'error' value greater than 0",
  968. );
  969. }
  970. assert.strictEqual(
  971. messages.length,
  972. item.errors,
  973. util.format(
  974. "Should have %d error%s but had %d: %s",
  975. item.errors,
  976. item.errors === 1 ? "" : "s",
  977. messages.length,
  978. util.inspect(messages),
  979. ),
  980. );
  981. } else {
  982. assert.strictEqual(
  983. messages.length,
  984. item.errors.length,
  985. util.format(
  986. "Should have %d error%s but had %d: %s",
  987. item.errors.length,
  988. item.errors.length === 1 ? "" : "s",
  989. messages.length,
  990. util.inspect(messages),
  991. ),
  992. );
  993. const hasMessageOfThisRule = messages.some(
  994. m => m.ruleId === ruleId,
  995. );
  996. for (let i = 0, l = item.errors.length; i < l; i++) {
  997. const error = item.errors[i];
  998. const message = messages[i];
  999. assert(
  1000. hasMessageOfThisRule,
  1001. "Error rule name should be the same as the name of the rule being tested",
  1002. );
  1003. if (typeof error === "string" || error instanceof RegExp) {
  1004. // Just an error message.
  1005. assertMessageMatches(message.message, error);
  1006. assert.ok(
  1007. message.suggestions === void 0,
  1008. `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`,
  1009. );
  1010. } else if (typeof error === "object" && error !== null) {
  1011. /*
  1012. * Error object.
  1013. * This may have a message, messageId, data, node type, line, and/or
  1014. * column.
  1015. */
  1016. Object.keys(error).forEach(propertyName => {
  1017. assert.ok(
  1018. errorObjectParameters.has(propertyName),
  1019. `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`,
  1020. );
  1021. });
  1022. if (hasOwnProperty(error, "message")) {
  1023. assert.ok(
  1024. !hasOwnProperty(error, "messageId"),
  1025. "Error should not specify both 'message' and a 'messageId'.",
  1026. );
  1027. assert.ok(
  1028. !hasOwnProperty(error, "data"),
  1029. "Error should not specify both 'data' and 'message'.",
  1030. );
  1031. assertMessageMatches(
  1032. message.message,
  1033. error.message,
  1034. );
  1035. } else if (hasOwnProperty(error, "messageId")) {
  1036. assert.ok(
  1037. ruleHasMetaMessages,
  1038. "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'.",
  1039. );
  1040. if (
  1041. !hasOwnProperty(
  1042. rule.meta.messages,
  1043. error.messageId,
  1044. )
  1045. ) {
  1046. assert(
  1047. false,
  1048. `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`,
  1049. );
  1050. }
  1051. assert.strictEqual(
  1052. message.messageId,
  1053. error.messageId,
  1054. `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`,
  1055. );
  1056. const unsubstitutedPlaceholders =
  1057. getUnsubstitutedMessagePlaceholders(
  1058. message.message,
  1059. rule.meta.messages[message.messageId],
  1060. error.data,
  1061. );
  1062. assert.ok(
  1063. unsubstitutedPlaceholders.length === 0,
  1064. `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.`,
  1065. );
  1066. if (hasOwnProperty(error, "data")) {
  1067. /*
  1068. * if data was provided, then directly compare the returned message to a synthetic
  1069. * interpolated message using the same message ID and data provided in the test.
  1070. * See https://github.com/eslint/eslint/issues/9890 for context.
  1071. */
  1072. const unformattedOriginalMessage =
  1073. rule.meta.messages[error.messageId];
  1074. const rehydratedMessage = interpolate(
  1075. unformattedOriginalMessage,
  1076. error.data,
  1077. );
  1078. assert.strictEqual(
  1079. message.message,
  1080. rehydratedMessage,
  1081. `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
  1082. );
  1083. }
  1084. } else {
  1085. assert.fail(
  1086. "Test error must specify either a 'messageId' or 'message'.",
  1087. );
  1088. }
  1089. if (error.type) {
  1090. assert.strictEqual(
  1091. message.nodeType,
  1092. error.type,
  1093. `Error type should be ${error.type}, found ${message.nodeType}`,
  1094. );
  1095. }
  1096. const actualLocation = {};
  1097. const expectedLocation = {};
  1098. if (hasOwnProperty(error, "line")) {
  1099. actualLocation.line = message.line;
  1100. expectedLocation.line = error.line;
  1101. }
  1102. if (hasOwnProperty(error, "column")) {
  1103. actualLocation.column = message.column;
  1104. expectedLocation.column = error.column;
  1105. }
  1106. if (hasOwnProperty(error, "endLine")) {
  1107. actualLocation.endLine = message.endLine;
  1108. expectedLocation.endLine = error.endLine;
  1109. }
  1110. if (hasOwnProperty(error, "endColumn")) {
  1111. actualLocation.endColumn = message.endColumn;
  1112. expectedLocation.endColumn = error.endColumn;
  1113. }
  1114. if (Object.keys(expectedLocation).length > 0) {
  1115. assert.deepStrictEqual(
  1116. actualLocation,
  1117. expectedLocation,
  1118. "Actual error location does not match expected error location.",
  1119. );
  1120. }
  1121. assert.ok(
  1122. !message.suggestions ||
  1123. hasOwnProperty(error, "suggestions"),
  1124. `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`,
  1125. );
  1126. if (hasOwnProperty(error, "suggestions")) {
  1127. // Support asserting there are no suggestions
  1128. const expectsSuggestions = Array.isArray(
  1129. error.suggestions,
  1130. )
  1131. ? error.suggestions.length > 0
  1132. : Boolean(error.suggestions);
  1133. const hasSuggestions =
  1134. message.suggestions !== void 0;
  1135. if (!hasSuggestions && expectsSuggestions) {
  1136. assert.ok(
  1137. !error.suggestions,
  1138. `Error should have suggestions on error with message: "${message.message}"`,
  1139. );
  1140. } else if (hasSuggestions) {
  1141. assert.ok(
  1142. expectsSuggestions,
  1143. `Error should have no suggestions on error with message: "${message.message}"`,
  1144. );
  1145. if (typeof error.suggestions === "number") {
  1146. assert.strictEqual(
  1147. message.suggestions.length,
  1148. error.suggestions,
  1149. `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`,
  1150. );
  1151. } else if (Array.isArray(error.suggestions)) {
  1152. assert.strictEqual(
  1153. message.suggestions.length,
  1154. error.suggestions.length,
  1155. `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`,
  1156. );
  1157. error.suggestions.forEach(
  1158. (expectedSuggestion, index) => {
  1159. assert.ok(
  1160. typeof expectedSuggestion ===
  1161. "object" &&
  1162. expectedSuggestion !== null,
  1163. "Test suggestion in 'suggestions' array must be an object.",
  1164. );
  1165. Object.keys(
  1166. expectedSuggestion,
  1167. ).forEach(propertyName => {
  1168. assert.ok(
  1169. suggestionObjectParameters.has(
  1170. propertyName,
  1171. ),
  1172. `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`,
  1173. );
  1174. });
  1175. const actualSuggestion =
  1176. message.suggestions[index];
  1177. const suggestionPrefix = `Error Suggestion at index ${index}:`;
  1178. if (
  1179. hasOwnProperty(
  1180. expectedSuggestion,
  1181. "desc",
  1182. )
  1183. ) {
  1184. assert.ok(
  1185. !hasOwnProperty(
  1186. expectedSuggestion,
  1187. "data",
  1188. ),
  1189. `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`,
  1190. );
  1191. assert.ok(
  1192. !hasOwnProperty(
  1193. expectedSuggestion,
  1194. "messageId",
  1195. ),
  1196. `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`,
  1197. );
  1198. assert.strictEqual(
  1199. actualSuggestion.desc,
  1200. expectedSuggestion.desc,
  1201. `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`,
  1202. );
  1203. } else if (
  1204. hasOwnProperty(
  1205. expectedSuggestion,
  1206. "messageId",
  1207. )
  1208. ) {
  1209. assert.ok(
  1210. ruleHasMetaMessages,
  1211. `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`,
  1212. );
  1213. assert.ok(
  1214. hasOwnProperty(
  1215. rule.meta.messages,
  1216. expectedSuggestion.messageId,
  1217. ),
  1218. `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`,
  1219. );
  1220. assert.strictEqual(
  1221. actualSuggestion.messageId,
  1222. expectedSuggestion.messageId,
  1223. `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`,
  1224. );
  1225. const unsubstitutedPlaceholders =
  1226. getUnsubstitutedMessagePlaceholders(
  1227. actualSuggestion.desc,
  1228. rule.meta.messages[
  1229. expectedSuggestion
  1230. .messageId
  1231. ],
  1232. expectedSuggestion.data,
  1233. );
  1234. assert.ok(
  1235. unsubstitutedPlaceholders.length ===
  1236. 0,
  1237. `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.`,
  1238. );
  1239. if (
  1240. hasOwnProperty(
  1241. expectedSuggestion,
  1242. "data",
  1243. )
  1244. ) {
  1245. const unformattedMetaMessage =
  1246. rule.meta.messages[
  1247. expectedSuggestion
  1248. .messageId
  1249. ];
  1250. const rehydratedDesc =
  1251. interpolate(
  1252. unformattedMetaMessage,
  1253. expectedSuggestion.data,
  1254. );
  1255. assert.strictEqual(
  1256. actualSuggestion.desc,
  1257. rehydratedDesc,
  1258. `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`,
  1259. );
  1260. }
  1261. } else if (
  1262. hasOwnProperty(
  1263. expectedSuggestion,
  1264. "data",
  1265. )
  1266. ) {
  1267. assert.fail(
  1268. `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`,
  1269. );
  1270. } else {
  1271. assert.fail(
  1272. `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`,
  1273. );
  1274. }
  1275. assert.ok(
  1276. hasOwnProperty(
  1277. expectedSuggestion,
  1278. "output",
  1279. ),
  1280. `${suggestionPrefix} The "output" property is required.`,
  1281. );
  1282. const codeWithAppliedSuggestion =
  1283. SourceCodeFixer.applyFixes(
  1284. item.code,
  1285. [actualSuggestion],
  1286. ).output;
  1287. // Verify if suggestion fix makes a syntax error or not.
  1288. const errorMessageInSuggestion =
  1289. linter
  1290. .verify(
  1291. codeWithAppliedSuggestion,
  1292. result.configs,
  1293. result.filename,
  1294. )
  1295. .find(m => m.fatal);
  1296. assert(
  1297. !errorMessageInSuggestion,
  1298. [
  1299. "A fatal parsing error occurred in suggestion fix.",
  1300. `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
  1301. "Suggestion output:",
  1302. codeWithAppliedSuggestion,
  1303. ].join("\n"),
  1304. );
  1305. assert.strictEqual(
  1306. codeWithAppliedSuggestion,
  1307. expectedSuggestion.output,
  1308. `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`,
  1309. );
  1310. assert.notStrictEqual(
  1311. expectedSuggestion.output,
  1312. item.code,
  1313. `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`,
  1314. );
  1315. },
  1316. );
  1317. } else {
  1318. assert.fail(
  1319. "Test error object property 'suggestions' should be an array or a number",
  1320. );
  1321. }
  1322. }
  1323. }
  1324. } else {
  1325. // Message was an unexpected type
  1326. assert.fail(
  1327. `Error should be a string, object, or RegExp, but found (${util.inspect(message)})`,
  1328. );
  1329. }
  1330. }
  1331. }
  1332. if (hasOwnProperty(item, "output")) {
  1333. if (item.output === null) {
  1334. assert.strictEqual(
  1335. result.output,
  1336. item.code,
  1337. "Expected no autofixes to be suggested",
  1338. );
  1339. } else {
  1340. assert.strictEqual(
  1341. result.output,
  1342. item.output,
  1343. "Output is incorrect.",
  1344. );
  1345. assert.notStrictEqual(
  1346. item.code,
  1347. item.output,
  1348. "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.",
  1349. );
  1350. }
  1351. } else {
  1352. assert.strictEqual(
  1353. result.output,
  1354. item.code,
  1355. "The rule fixed the code. Please add 'output' property.",
  1356. );
  1357. }
  1358. assertASTDidntChange(result.beforeAST, result.afterAST);
  1359. }
  1360. /*
  1361. * This creates a mocha test suite and pipes all supplied info through
  1362. * one of the templates above.
  1363. * The test suites for valid/invalid are created conditionally as
  1364. * test runners (eg. vitest) fail for empty test suites.
  1365. */
  1366. this.constructor.describe(ruleName, () => {
  1367. if (test.valid.length > 0) {
  1368. this.constructor.describe("valid", () => {
  1369. test.valid.forEach(valid => {
  1370. this.constructor[valid.only ? "itOnly" : "it"](
  1371. sanitize(
  1372. typeof valid === "object"
  1373. ? valid.name || valid.code
  1374. : valid,
  1375. ),
  1376. () => {
  1377. try {
  1378. runHook(valid, "before");
  1379. testValidTemplate(valid);
  1380. } finally {
  1381. runHook(valid, "after");
  1382. }
  1383. },
  1384. );
  1385. });
  1386. });
  1387. }
  1388. if (test.invalid.length > 0) {
  1389. this.constructor.describe("invalid", () => {
  1390. test.invalid.forEach(invalid => {
  1391. this.constructor[invalid.only ? "itOnly" : "it"](
  1392. sanitize(invalid.name || invalid.code),
  1393. () => {
  1394. try {
  1395. runHook(invalid, "before");
  1396. testInvalidTemplate(invalid);
  1397. } finally {
  1398. runHook(invalid, "after");
  1399. }
  1400. },
  1401. );
  1402. });
  1403. });
  1404. }
  1405. });
  1406. }
  1407. }
  1408. RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null;
  1409. module.exports = RuleTester;