flat-rule-tester.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  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. return {
  537. Program(node) {
  538. beforeAST = cloneDeeplyExcludesParent(node);
  539. },
  540. "Program:exit"(node) {
  541. afterAST = node;
  542. }
  543. };
  544. }
  545. }
  546. }
  547. }
  548. });
  549. if (schema) {
  550. ajv.validateSchema(schema);
  551. if (ajv.errors) {
  552. const errors = ajv.errors.map(error => {
  553. const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
  554. return `\t${field}: ${error.message}`;
  555. }).join("\n");
  556. throw new Error([`Schema for rule ${ruleName} is invalid:`, errors]);
  557. }
  558. /*
  559. * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
  560. * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
  561. * 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,
  562. * the schema is compiled here separately from checking for `validateSchema` errors.
  563. */
  564. try {
  565. ajv.compile(schema);
  566. } catch (err) {
  567. throw new Error(`Schema for rule ${ruleName} is invalid: ${err.message}`);
  568. }
  569. }
  570. // Verify the code.
  571. const { getComments } = SourceCode.prototype;
  572. let messages;
  573. // check for validation errors
  574. try {
  575. configs.normalizeSync();
  576. configs.getConfig("test.js");
  577. } catch (error) {
  578. error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`;
  579. throw error;
  580. }
  581. try {
  582. SourceCode.prototype.getComments = getCommentsDeprecation;
  583. messages = linter.verify(code, configs, filename);
  584. } finally {
  585. SourceCode.prototype.getComments = getComments;
  586. }
  587. const fatalErrorMessage = messages.find(m => m.fatal);
  588. assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
  589. // Verify if autofix makes a syntax error or not.
  590. if (messages.some(m => m.fix)) {
  591. output = SourceCodeFixer.applyFixes(code, messages).output;
  592. const errorMessageInFix = linter.verify(output, configs, filename).find(m => m.fatal);
  593. assert(!errorMessageInFix, [
  594. "A fatal parsing error occurred in autofix.",
  595. `Error: ${errorMessageInFix && errorMessageInFix.message}`,
  596. "Autofix output:",
  597. output
  598. ].join("\n"));
  599. } else {
  600. output = code;
  601. }
  602. return {
  603. messages,
  604. output,
  605. beforeAST,
  606. afterAST: cloneDeeplyExcludesParent(afterAST)
  607. };
  608. }
  609. /**
  610. * Check if the AST was changed
  611. * @param {ASTNode} beforeAST AST node before running
  612. * @param {ASTNode} afterAST AST node after running
  613. * @returns {void}
  614. * @private
  615. */
  616. function assertASTDidntChange(beforeAST, afterAST) {
  617. if (!equal(beforeAST, afterAST)) {
  618. assert.fail("Rule should not modify AST.");
  619. }
  620. }
  621. /**
  622. * Check if the template is valid or not
  623. * all valid cases go through this
  624. * @param {string|Object} item Item to run the rule against
  625. * @returns {void}
  626. * @private
  627. */
  628. function testValidTemplate(item) {
  629. const code = typeof item === "object" ? item.code : item;
  630. assert.ok(typeof code === "string", "Test case must specify a string value for 'code'");
  631. if (item.name) {
  632. assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
  633. }
  634. const result = runRuleForItem(item);
  635. const messages = result.messages;
  636. assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s",
  637. messages.length,
  638. util.inspect(messages)));
  639. assertASTDidntChange(result.beforeAST, result.afterAST);
  640. }
  641. /**
  642. * Asserts that the message matches its expected value. If the expected
  643. * value is a regular expression, it is checked against the actual
  644. * value.
  645. * @param {string} actual Actual value
  646. * @param {string|RegExp} expected Expected value
  647. * @returns {void}
  648. * @private
  649. */
  650. function assertMessageMatches(actual, expected) {
  651. if (expected instanceof RegExp) {
  652. // assert.js doesn't have a built-in RegExp match function
  653. assert.ok(
  654. expected.test(actual),
  655. `Expected '${actual}' to match ${expected}`
  656. );
  657. } else {
  658. assert.strictEqual(actual, expected);
  659. }
  660. }
  661. /**
  662. * Check if the template is invalid or not
  663. * all invalid cases go through this.
  664. * @param {string|Object} item Item to run the rule against
  665. * @returns {void}
  666. * @private
  667. */
  668. function testInvalidTemplate(item) {
  669. assert.ok(typeof item.code === "string", "Test case must specify a string value for 'code'");
  670. if (item.name) {
  671. assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
  672. }
  673. assert.ok(item.errors || item.errors === 0,
  674. `Did not specify errors for an invalid test of ${ruleName}`);
  675. if (Array.isArray(item.errors) && item.errors.length === 0) {
  676. assert.fail("Invalid cases must have at least one error");
  677. }
  678. const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
  679. const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
  680. const result = runRuleForItem(item);
  681. const messages = result.messages;
  682. if (typeof item.errors === "number") {
  683. if (item.errors === 0) {
  684. assert.fail("Invalid cases must have 'error' value greater than 0");
  685. }
  686. assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
  687. item.errors,
  688. item.errors === 1 ? "" : "s",
  689. messages.length,
  690. util.inspect(messages)));
  691. } else {
  692. assert.strictEqual(
  693. messages.length, item.errors.length, util.format(
  694. "Should have %d error%s but had %d: %s",
  695. item.errors.length,
  696. item.errors.length === 1 ? "" : "s",
  697. messages.length,
  698. util.inspect(messages)
  699. )
  700. );
  701. const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleId);
  702. for (let i = 0, l = item.errors.length; i < l; i++) {
  703. const error = item.errors[i];
  704. const message = messages[i];
  705. assert(hasMessageOfThisRule, "Error rule name should be the same as the name of the rule being tested");
  706. if (typeof error === "string" || error instanceof RegExp) {
  707. // Just an error message.
  708. assertMessageMatches(message.message, error);
  709. } else if (typeof error === "object" && error !== null) {
  710. /*
  711. * Error object.
  712. * This may have a message, messageId, data, node type, line, and/or
  713. * column.
  714. */
  715. Object.keys(error).forEach(propertyName => {
  716. assert.ok(
  717. errorObjectParameters.has(propertyName),
  718. `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`
  719. );
  720. });
  721. if (hasOwnProperty(error, "message")) {
  722. assert.ok(!hasOwnProperty(error, "messageId"), "Error should not specify both 'message' and a 'messageId'.");
  723. assert.ok(!hasOwnProperty(error, "data"), "Error should not specify both 'data' and 'message'.");
  724. assertMessageMatches(message.message, error.message);
  725. } else if (hasOwnProperty(error, "messageId")) {
  726. assert.ok(
  727. ruleHasMetaMessages,
  728. "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'."
  729. );
  730. if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
  731. assert(false, `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`);
  732. }
  733. assert.strictEqual(
  734. message.messageId,
  735. error.messageId,
  736. `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
  737. );
  738. if (hasOwnProperty(error, "data")) {
  739. /*
  740. * if data was provided, then directly compare the returned message to a synthetic
  741. * interpolated message using the same message ID and data provided in the test.
  742. * See https://github.com/eslint/eslint/issues/9890 for context.
  743. */
  744. const unformattedOriginalMessage = rule.meta.messages[error.messageId];
  745. const rehydratedMessage = interpolate(unformattedOriginalMessage, error.data);
  746. assert.strictEqual(
  747. message.message,
  748. rehydratedMessage,
  749. `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
  750. );
  751. }
  752. }
  753. assert.ok(
  754. hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
  755. "Error must specify 'messageId' if 'data' is used."
  756. );
  757. if (error.type) {
  758. assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
  759. }
  760. if (hasOwnProperty(error, "line")) {
  761. assert.strictEqual(message.line, error.line, `Error line should be ${error.line}`);
  762. }
  763. if (hasOwnProperty(error, "column")) {
  764. assert.strictEqual(message.column, error.column, `Error column should be ${error.column}`);
  765. }
  766. if (hasOwnProperty(error, "endLine")) {
  767. assert.strictEqual(message.endLine, error.endLine, `Error endLine should be ${error.endLine}`);
  768. }
  769. if (hasOwnProperty(error, "endColumn")) {
  770. assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
  771. }
  772. if (hasOwnProperty(error, "suggestions")) {
  773. // Support asserting there are no suggestions
  774. if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) {
  775. if (Array.isArray(message.suggestions) && message.suggestions.length > 0) {
  776. assert.fail(`Error should have no suggestions on error with message: "${message.message}"`);
  777. }
  778. } else {
  779. assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`);
  780. assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
  781. error.suggestions.forEach((expectedSuggestion, index) => {
  782. assert.ok(
  783. typeof expectedSuggestion === "object" && expectedSuggestion !== null,
  784. "Test suggestion in 'suggestions' array must be an object."
  785. );
  786. Object.keys(expectedSuggestion).forEach(propertyName => {
  787. assert.ok(
  788. suggestionObjectParameters.has(propertyName),
  789. `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
  790. );
  791. });
  792. const actualSuggestion = message.suggestions[index];
  793. const suggestionPrefix = `Error Suggestion at index ${index} :`;
  794. if (hasOwnProperty(expectedSuggestion, "desc")) {
  795. assert.ok(
  796. !hasOwnProperty(expectedSuggestion, "data"),
  797. `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
  798. );
  799. assert.strictEqual(
  800. actualSuggestion.desc,
  801. expectedSuggestion.desc,
  802. `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
  803. );
  804. }
  805. if (hasOwnProperty(expectedSuggestion, "messageId")) {
  806. assert.ok(
  807. ruleHasMetaMessages,
  808. `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
  809. );
  810. assert.ok(
  811. hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
  812. `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
  813. );
  814. assert.strictEqual(
  815. actualSuggestion.messageId,
  816. expectedSuggestion.messageId,
  817. `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
  818. );
  819. if (hasOwnProperty(expectedSuggestion, "data")) {
  820. const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
  821. const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
  822. assert.strictEqual(
  823. actualSuggestion.desc,
  824. rehydratedDesc,
  825. `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
  826. );
  827. }
  828. } else {
  829. assert.ok(
  830. !hasOwnProperty(expectedSuggestion, "data"),
  831. `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
  832. );
  833. }
  834. if (hasOwnProperty(expectedSuggestion, "output")) {
  835. const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
  836. 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}"`);
  837. }
  838. });
  839. }
  840. }
  841. } else {
  842. // Message was an unexpected type
  843. assert.fail(`Error should be a string, object, or RegExp, but found (${util.inspect(message)})`);
  844. }
  845. }
  846. }
  847. if (hasOwnProperty(item, "output")) {
  848. if (item.output === null) {
  849. assert.strictEqual(
  850. result.output,
  851. item.code,
  852. "Expected no autofixes to be suggested"
  853. );
  854. } else {
  855. assert.strictEqual(result.output, item.output, "Output is incorrect.");
  856. }
  857. } else {
  858. assert.strictEqual(
  859. result.output,
  860. item.code,
  861. "The rule fixed the code. Please add 'output' property."
  862. );
  863. }
  864. assertASTDidntChange(result.beforeAST, result.afterAST);
  865. }
  866. /*
  867. * This creates a mocha test suite and pipes all supplied info through
  868. * one of the templates above.
  869. */
  870. this.constructor.describe(ruleName, () => {
  871. this.constructor.describe("valid", () => {
  872. test.valid.forEach(valid => {
  873. this.constructor[valid.only ? "itOnly" : "it"](
  874. sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
  875. () => {
  876. testValidTemplate(valid);
  877. }
  878. );
  879. });
  880. });
  881. this.constructor.describe("invalid", () => {
  882. test.invalid.forEach(invalid => {
  883. this.constructor[invalid.only ? "itOnly" : "it"](
  884. sanitize(invalid.name || invalid.code),
  885. () => {
  886. testInvalidTemplate(invalid);
  887. }
  888. );
  889. });
  890. });
  891. });
  892. }
  893. }
  894. FlatRuleTester[DESCRIBE] = FlatRuleTester[IT] = FlatRuleTester[IT_ONLY] = null;
  895. module.exports = FlatRuleTester;