flat-rule-tester.js 41 KB

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