eslint 9.22.0 → 9.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/README.md +47 -45
  2. package/bin/eslint.js +92 -90
  3. package/conf/default-cli-options.js +22 -22
  4. package/conf/ecma-version.js +1 -1
  5. package/conf/globals.js +97 -98
  6. package/conf/replacements.json +24 -20
  7. package/conf/rule-type-list.json +88 -92
  8. package/lib/api.js +12 -12
  9. package/lib/cli-engine/cli-engine.js +828 -808
  10. package/lib/cli-engine/file-enumerator.js +381 -387
  11. package/lib/cli-engine/formatters/formatters-meta.json +16 -16
  12. package/lib/cli-engine/formatters/html.js +107 -99
  13. package/lib/cli-engine/formatters/json-with-metadata.js +5 -5
  14. package/lib/cli-engine/formatters/json.js +2 -2
  15. package/lib/cli-engine/formatters/stylish.js +96 -75
  16. package/lib/cli-engine/hash.js +1 -1
  17. package/lib/cli-engine/index.js +1 -1
  18. package/lib/cli-engine/lint-result-cache.js +144 -145
  19. package/lib/cli-engine/load-rules.js +16 -16
  20. package/lib/cli.js +541 -457
  21. package/lib/config/config-loader.js +648 -618
  22. package/lib/config/config.js +247 -221
  23. package/lib/config/default-config.js +54 -45
  24. package/lib/config/flat-config-array.js +167 -172
  25. package/lib/config/flat-config-helpers.js +65 -68
  26. package/lib/config/flat-config-schema.js +375 -368
  27. package/lib/config/rule-validator.js +139 -144
  28. package/lib/config-api.js +2 -2
  29. package/lib/eslint/eslint-helpers.js +709 -679
  30. package/lib/eslint/eslint.js +944 -886
  31. package/lib/eslint/index.js +2 -2
  32. package/lib/eslint/legacy-eslint.js +576 -532
  33. package/lib/languages/js/index.js +263 -264
  34. package/lib/languages/js/source-code/index.js +1 -1
  35. package/lib/languages/js/source-code/source-code.js +1128 -1057
  36. package/lib/languages/js/source-code/token-store/backward-token-comment-cursor.js +39 -35
  37. package/lib/languages/js/source-code/token-store/backward-token-cursor.js +35 -36
  38. package/lib/languages/js/source-code/token-store/cursor.js +36 -36
  39. package/lib/languages/js/source-code/token-store/cursors.js +80 -52
  40. package/lib/languages/js/source-code/token-store/decorative-cursor.js +17 -18
  41. package/lib/languages/js/source-code/token-store/filter-cursor.js +19 -20
  42. package/lib/languages/js/source-code/token-store/forward-token-comment-cursor.js +40 -32
  43. package/lib/languages/js/source-code/token-store/forward-token-cursor.js +40 -41
  44. package/lib/languages/js/source-code/token-store/index.js +592 -498
  45. package/lib/languages/js/source-code/token-store/limit-cursor.js +17 -18
  46. package/lib/languages/js/source-code/token-store/padded-token-cursor.js +23 -16
  47. package/lib/languages/js/source-code/token-store/skip-cursor.js +19 -20
  48. package/lib/languages/js/source-code/token-store/utils.js +63 -60
  49. package/lib/languages/js/validate-language-options.js +104 -89
  50. package/lib/linter/apply-disable-directives.js +467 -383
  51. package/lib/linter/code-path-analysis/code-path-analyzer.js +650 -672
  52. package/lib/linter/code-path-analysis/code-path-segment.js +215 -216
  53. package/lib/linter/code-path-analysis/code-path-state.js +2118 -2096
  54. package/lib/linter/code-path-analysis/code-path.js +307 -319
  55. package/lib/linter/code-path-analysis/debug-helpers.js +183 -163
  56. package/lib/linter/code-path-analysis/fork-context.js +296 -271
  57. package/lib/linter/code-path-analysis/id-generator.js +22 -23
  58. package/lib/linter/file-context.js +119 -120
  59. package/lib/linter/index.js +3 -3
  60. package/lib/linter/interpolate.js +16 -16
  61. package/lib/linter/linter.js +2402 -2044
  62. package/lib/linter/node-event-generator.js +284 -225
  63. package/lib/linter/report-translator.js +256 -219
  64. package/lib/linter/rule-fixer.js +122 -124
  65. package/lib/linter/rules.js +35 -35
  66. package/lib/linter/safe-emitter.js +18 -18
  67. package/lib/linter/source-code-fixer.js +94 -92
  68. package/lib/linter/timing.js +104 -101
  69. package/lib/linter/vfile.js +70 -73
  70. package/lib/options.js +375 -361
  71. package/lib/rule-tester/index.js +1 -1
  72. package/lib/rule-tester/rule-tester.js +1307 -1045
  73. package/lib/rules/accessor-pairs.js +297 -262
  74. package/lib/rules/array-bracket-newline.js +249 -237
  75. package/lib/rules/array-bracket-spacing.js +262 -223
  76. package/lib/rules/array-callback-return.js +401 -355
  77. package/lib/rules/array-element-newline.js +357 -312
  78. package/lib/rules/arrow-body-style.js +399 -280
  79. package/lib/rules/arrow-parens.js +205 -172
  80. package/lib/rules/arrow-spacing.js +168 -162
  81. package/lib/rules/block-scoped-var.js +124 -122
  82. package/lib/rules/block-spacing.js +185 -175
  83. package/lib/rules/brace-style.js +261 -198
  84. package/lib/rules/callback-return.js +202 -189
  85. package/lib/rules/camelcase.js +402 -391
  86. package/lib/rules/capitalized-comments.js +252 -231
  87. package/lib/rules/class-methods-use-this.js +179 -171
  88. package/lib/rules/comma-dangle.js +378 -345
  89. package/lib/rules/comma-spacing.js +192 -194
  90. package/lib/rules/comma-style.js +374 -315
  91. package/lib/rules/complexity.js +172 -168
  92. package/lib/rules/computed-property-spacing.js +235 -210
  93. package/lib/rules/consistent-return.js +180 -169
  94. package/lib/rules/consistent-this.js +166 -146
  95. package/lib/rules/constructor-super.js +411 -403
  96. package/lib/rules/curly.js +406 -331
  97. package/lib/rules/default-case-last.js +37 -30
  98. package/lib/rules/default-case.js +88 -84
  99. package/lib/rules/default-param-last.js +68 -53
  100. package/lib/rules/dot-location.js +121 -109
  101. package/lib/rules/dot-notation.js +191 -155
  102. package/lib/rules/eol-last.js +121 -119
  103. package/lib/rules/eqeqeq.js +167 -154
  104. package/lib/rules/for-direction.js +145 -120
  105. package/lib/rules/func-call-spacing.js +260 -230
  106. package/lib/rules/func-name-matching.js +292 -208
  107. package/lib/rules/func-names.js +164 -163
  108. package/lib/rules/func-style.js +158 -126
  109. package/lib/rules/function-call-argument-newline.js +151 -128
  110. package/lib/rules/function-paren-newline.js +348 -290
  111. package/lib/rules/generator-star-spacing.js +228 -209
  112. package/lib/rules/getter-return.js +207 -171
  113. package/lib/rules/global-require.js +84 -73
  114. package/lib/rules/grouped-accessor-pairs.js +169 -149
  115. package/lib/rules/guard-for-in.js +71 -62
  116. package/lib/rules/handle-callback-err.js +107 -102
  117. package/lib/rules/id-blacklist.js +181 -198
  118. package/lib/rules/id-denylist.js +167 -186
  119. package/lib/rules/id-length.js +196 -170
  120. package/lib/rules/id-match.js +343 -288
  121. package/lib/rules/implicit-arrow-linebreak.js +101 -78
  122. package/lib/rules/indent-legacy.js +1343 -1117
  123. package/lib/rules/indent.js +2271 -1758
  124. package/lib/rules/index.js +317 -292
  125. package/lib/rules/init-declarations.js +115 -106
  126. package/lib/rules/jsx-quotes.js +93 -81
  127. package/lib/rules/key-spacing.js +749 -632
  128. package/lib/rules/keyword-spacing.js +647 -604
  129. package/lib/rules/line-comment-position.js +141 -127
  130. package/lib/rules/linebreak-style.js +106 -105
  131. package/lib/rules/lines-around-comment.js +539 -447
  132. package/lib/rules/lines-around-directive.js +232 -202
  133. package/lib/rules/lines-between-class-members.js +304 -233
  134. package/lib/rules/logical-assignment-operators.js +581 -398
  135. package/lib/rules/max-classes-per-file.js +68 -67
  136. package/lib/rules/max-depth.js +145 -142
  137. package/lib/rules/max-len.js +472 -433
  138. package/lib/rules/max-lines-per-function.js +200 -175
  139. package/lib/rules/max-lines.js +157 -161
  140. package/lib/rules/max-nested-callbacks.js +101 -103
  141. package/lib/rules/max-params.js +77 -75
  142. package/lib/rules/max-statements-per-line.js +204 -197
  143. package/lib/rules/max-statements.js +167 -163
  144. package/lib/rules/multiline-comment-style.js +636 -478
  145. package/lib/rules/multiline-ternary.js +240 -175
  146. package/lib/rules/new-cap.js +232 -212
  147. package/lib/rules/new-parens.js +87 -78
  148. package/lib/rules/newline-after-var.js +286 -249
  149. package/lib/rules/newline-before-return.js +228 -221
  150. package/lib/rules/newline-per-chained-call.js +141 -126
  151. package/lib/rules/no-alert.js +89 -78
  152. package/lib/rules/no-array-constructor.js +121 -112
  153. package/lib/rules/no-async-promise-executor.js +29 -23
  154. package/lib/rules/no-await-in-loop.js +68 -70
  155. package/lib/rules/no-bitwise.js +123 -99
  156. package/lib/rules/no-buffer-constructor.js +54 -46
  157. package/lib/rules/no-caller.js +38 -32
  158. package/lib/rules/no-case-declarations.js +60 -56
  159. package/lib/rules/no-catch-shadow.js +75 -72
  160. package/lib/rules/no-class-assign.js +50 -47
  161. package/lib/rules/no-compare-neg-zero.js +61 -47
  162. package/lib/rules/no-cond-assign.js +147 -131
  163. package/lib/rules/no-confusing-arrow.js +97 -80
  164. package/lib/rules/no-console.js +201 -198
  165. package/lib/rules/no-const-assign.js +46 -40
  166. package/lib/rules/no-constant-binary-expression.js +499 -404
  167. package/lib/rules/no-constant-condition.js +157 -142
  168. package/lib/rules/no-constructor-return.js +48 -48
  169. package/lib/rules/no-continue.js +24 -26
  170. package/lib/rules/no-control-regex.js +124 -120
  171. package/lib/rules/no-debugger.js +27 -29
  172. package/lib/rules/no-delete-var.js +28 -28
  173. package/lib/rules/no-div-regex.js +46 -40
  174. package/lib/rules/no-dupe-args.js +67 -68
  175. package/lib/rules/no-dupe-class-members.js +92 -88
  176. package/lib/rules/no-dupe-else-if.js +99 -76
  177. package/lib/rules/no-dupe-keys.js +132 -109
  178. package/lib/rules/no-duplicate-case.js +49 -42
  179. package/lib/rules/no-duplicate-imports.js +178 -175
  180. package/lib/rules/no-else-return.js +429 -384
  181. package/lib/rules/no-empty-character-class.js +56 -49
  182. package/lib/rules/no-empty-function.js +126 -127
  183. package/lib/rules/no-empty-pattern.js +62 -57
  184. package/lib/rules/no-empty-static-block.js +36 -34
  185. package/lib/rules/no-empty.js +97 -85
  186. package/lib/rules/no-eq-null.js +36 -31
  187. package/lib/rules/no-eval.js +255 -249
  188. package/lib/rules/no-ex-assign.js +41 -38
  189. package/lib/rules/no-extend-native.js +160 -158
  190. package/lib/rules/no-extra-bind.js +200 -189
  191. package/lib/rules/no-extra-boolean-cast.js +397 -347
  192. package/lib/rules/no-extra-label.js +149 -130
  193. package/lib/rules/no-extra-parens.js +1653 -1324
  194. package/lib/rules/no-extra-semi.js +145 -143
  195. package/lib/rules/no-fallthrough.js +198 -156
  196. package/lib/rules/no-floating-decimal.js +73 -65
  197. package/lib/rules/no-func-assign.js +53 -54
  198. package/lib/rules/no-global-assign.js +77 -72
  199. package/lib/rules/no-implicit-coercion.js +348 -292
  200. package/lib/rules/no-implicit-globals.js +157 -134
  201. package/lib/rules/no-implied-eval.js +139 -111
  202. package/lib/rules/no-import-assign.js +144 -158
  203. package/lib/rules/no-inline-comments.js +100 -94
  204. package/lib/rules/no-inner-declarations.js +114 -100
  205. package/lib/rules/no-invalid-regexp.js +221 -189
  206. package/lib/rules/no-invalid-this.js +122 -116
  207. package/lib/rules/no-irregular-whitespace.js +265 -251
  208. package/lib/rules/no-iterator.js +28 -32
  209. package/lib/rules/no-label-var.js +58 -61
  210. package/lib/rules/no-labels.js +137 -132
  211. package/lib/rules/no-lone-blocks.js +126 -122
  212. package/lib/rules/no-lonely-if.js +107 -76
  213. package/lib/rules/no-loop-func.js +233 -212
  214. package/lib/rules/no-loss-of-precision.js +215 -200
  215. package/lib/rules/no-magic-numbers.js +245 -217
  216. package/lib/rules/no-misleading-character-class.js +498 -445
  217. package/lib/rules/no-mixed-operators.js +187 -181
  218. package/lib/rules/no-mixed-requires.js +252 -239
  219. package/lib/rules/no-mixed-spaces-and-tabs.js +133 -120
  220. package/lib/rules/no-multi-assign.js +45 -43
  221. package/lib/rules/no-multi-spaces.js +162 -142
  222. package/lib/rules/no-multi-str.js +41 -40
  223. package/lib/rules/no-multiple-empty-lines.js +195 -157
  224. package/lib/rules/no-native-reassign.js +89 -84
  225. package/lib/rules/no-negated-condition.js +78 -74
  226. package/lib/rules/no-negated-in-lhs.js +44 -42
  227. package/lib/rules/no-nested-ternary.js +32 -31
  228. package/lib/rules/no-new-func.js +70 -61
  229. package/lib/rules/no-new-native-nonconstructor.js +42 -38
  230. package/lib/rules/no-new-object.js +47 -47
  231. package/lib/rules/no-new-require.js +47 -46
  232. package/lib/rules/no-new-symbol.js +51 -49
  233. package/lib/rules/no-new-wrappers.js +42 -40
  234. package/lib/rules/no-new.js +27 -28
  235. package/lib/rules/no-nonoctal-decimal-escape.js +140 -120
  236. package/lib/rules/no-obj-calls.js +65 -52
  237. package/lib/rules/no-object-constructor.js +103 -96
  238. package/lib/rules/no-octal-escape.js +39 -42
  239. package/lib/rules/no-octal.js +31 -31
  240. package/lib/rules/no-param-reassign.js +234 -216
  241. package/lib/rules/no-path-concat.js +65 -66
  242. package/lib/rules/no-plusplus.js +59 -60
  243. package/lib/rules/no-process-env.js +48 -47
  244. package/lib/rules/no-process-exit.js +53 -49
  245. package/lib/rules/no-promise-executor-return.js +213 -181
  246. package/lib/rules/no-proto.js +25 -28
  247. package/lib/rules/no-prototype-builtins.js +145 -123
  248. package/lib/rules/no-redeclare.js +153 -151
  249. package/lib/rules/no-regex-spaces.js +182 -160
  250. package/lib/rules/no-restricted-exports.js +207 -184
  251. package/lib/rules/no-restricted-globals.js +110 -111
  252. package/lib/rules/no-restricted-imports.js +656 -536
  253. package/lib/rules/no-restricted-modules.js +221 -201
  254. package/lib/rules/no-restricted-properties.js +180 -152
  255. package/lib/rules/no-restricted-syntax.js +55 -51
  256. package/lib/rules/no-return-assign.js +54 -49
  257. package/lib/rules/no-return-await.js +147 -123
  258. package/lib/rules/no-script-url.js +51 -44
  259. package/lib/rules/no-self-assign.js +147 -145
  260. package/lib/rules/no-self-compare.js +62 -45
  261. package/lib/rules/no-sequences.js +134 -115
  262. package/lib/rules/no-setter-return.js +184 -151
  263. package/lib/rules/no-shadow-restricted-names.js +60 -45
  264. package/lib/rules/no-shadow.js +341 -315
  265. package/lib/rules/no-spaced-func.js +81 -76
  266. package/lib/rules/no-sparse-arrays.js +53 -58
  267. package/lib/rules/no-sync.js +60 -59
  268. package/lib/rules/no-tabs.js +82 -71
  269. package/lib/rules/no-template-curly-in-string.js +32 -31
  270. package/lib/rules/no-ternary.js +24 -28
  271. package/lib/rules/no-this-before-super.js +320 -318
  272. package/lib/rules/no-throw-literal.js +30 -35
  273. package/lib/rules/no-trailing-spaces.js +198 -190
  274. package/lib/rules/no-undef-init.js +75 -60
  275. package/lib/rules/no-undef.js +50 -47
  276. package/lib/rules/no-undefined.js +72 -74
  277. package/lib/rules/no-underscore-dangle.js +369 -326
  278. package/lib/rules/no-unexpected-multiline.js +111 -101
  279. package/lib/rules/no-unmodified-loop-condition.js +253 -253
  280. package/lib/rules/no-unneeded-ternary.js +211 -146
  281. package/lib/rules/no-unreachable-loop.js +144 -141
  282. package/lib/rules/no-unreachable.js +254 -247
  283. package/lib/rules/no-unsafe-finally.js +92 -84
  284. package/lib/rules/no-unsafe-negation.js +104 -82
  285. package/lib/rules/no-unsafe-optional-chaining.js +191 -177
  286. package/lib/rules/no-unused-expressions.js +177 -161
  287. package/lib/rules/no-unused-labels.js +138 -123
  288. package/lib/rules/no-unused-private-class-members.js +205 -181
  289. package/lib/rules/no-unused-vars.js +1668 -1448
  290. package/lib/rules/no-use-before-define.js +228 -230
  291. package/lib/rules/no-useless-assignment.js +589 -510
  292. package/lib/rules/no-useless-backreference.js +211 -192
  293. package/lib/rules/no-useless-call.js +57 -52
  294. package/lib/rules/no-useless-catch.js +39 -39
  295. package/lib/rules/no-useless-computed-key.js +143 -114
  296. package/lib/rules/no-useless-concat.js +64 -59
  297. package/lib/rules/no-useless-constructor.js +157 -110
  298. package/lib/rules/no-useless-escape.js +341 -290
  299. package/lib/rules/no-useless-rename.js +182 -155
  300. package/lib/rules/no-useless-return.js +343 -311
  301. package/lib/rules/no-var.js +232 -211
  302. package/lib/rules/no-void.js +49 -47
  303. package/lib/rules/no-warning-comments.js +190 -185
  304. package/lib/rules/no-whitespace-before-property.js +130 -114
  305. package/lib/rules/no-with.js +23 -25
  306. package/lib/rules/nonblock-statement-body-position.js +148 -129
  307. package/lib/rules/object-curly-newline.js +305 -264
  308. package/lib/rules/object-curly-spacing.js +359 -313
  309. package/lib/rules/object-property-newline.js +136 -105
  310. package/lib/rules/object-shorthand.js +606 -501
  311. package/lib/rules/one-var-declaration-per-line.js +103 -99
  312. package/lib/rules/one-var.js +652 -536
  313. package/lib/rules/operator-assignment.js +218 -160
  314. package/lib/rules/operator-linebreak.js +294 -250
  315. package/lib/rules/padded-blocks.js +345 -307
  316. package/lib/rules/padding-line-between-statements.js +442 -438
  317. package/lib/rules/prefer-arrow-callback.js +361 -312
  318. package/lib/rules/prefer-const.js +417 -376
  319. package/lib/rules/prefer-destructuring.js +300 -278
  320. package/lib/rules/prefer-exponentiation-operator.js +175 -132
  321. package/lib/rules/prefer-named-capture-group.js +152 -139
  322. package/lib/rules/prefer-numeric-literals.js +120 -112
  323. package/lib/rules/prefer-object-has-own.js +115 -81
  324. package/lib/rules/prefer-object-spread.js +212 -192
  325. package/lib/rules/prefer-promise-reject-errors.js +139 -121
  326. package/lib/rules/prefer-reflect.js +126 -106
  327. package/lib/rules/prefer-regex-literals.js +577 -465
  328. package/lib/rules/prefer-rest-params.js +78 -79
  329. package/lib/rules/prefer-spread.js +46 -43
  330. package/lib/rules/prefer-template.js +265 -194
  331. package/lib/rules/quote-props.js +372 -306
  332. package/lib/rules/quotes.js +373 -325
  333. package/lib/rules/radix.js +151 -135
  334. package/lib/rules/require-atomic-updates.js +315 -284
  335. package/lib/rules/require-await.js +143 -115
  336. package/lib/rules/require-unicode-regexp.js +281 -176
  337. package/lib/rules/require-yield.js +52 -53
  338. package/lib/rules/rest-spread-spacing.js +127 -115
  339. package/lib/rules/semi-spacing.js +280 -249
  340. package/lib/rules/semi-style.js +175 -133
  341. package/lib/rules/semi.js +455 -435
  342. package/lib/rules/sort-imports.js +305 -232
  343. package/lib/rules/sort-keys.js +218 -187
  344. package/lib/rules/sort-vars.js +126 -92
  345. package/lib/rules/space-before-blocks.js +198 -188
  346. package/lib/rules/space-before-function-paren.js +185 -165
  347. package/lib/rules/space-in-parens.js +358 -287
  348. package/lib/rules/space-infix-ops.js +236 -200
  349. package/lib/rules/space-unary-ops.js +355 -297
  350. package/lib/rules/spaced-comment.js +362 -318
  351. package/lib/rules/strict.js +264 -229
  352. package/lib/rules/switch-colon-spacing.js +129 -121
  353. package/lib/rules/symbol-description.js +44 -47
  354. package/lib/rules/template-curly-spacing.js +147 -141
  355. package/lib/rules/template-tag-spacing.js +97 -87
  356. package/lib/rules/unicode-bom.js +53 -55
  357. package/lib/rules/use-isnan.js +236 -205
  358. package/lib/rules/utils/ast-utils.js +2039 -1860
  359. package/lib/rules/utils/char-source.js +162 -155
  360. package/lib/rules/utils/fix-tracker.js +83 -80
  361. package/lib/rules/utils/keywords.js +59 -59
  362. package/lib/rules/utils/lazy-loading-rule-map.js +79 -76
  363. package/lib/rules/utils/regular-expressions.js +32 -24
  364. package/lib/rules/utils/unicode/index.js +4 -4
  365. package/lib/rules/utils/unicode/is-combining-character.js +1 -1
  366. package/lib/rules/utils/unicode/is-emoji-modifier.js +1 -1
  367. package/lib/rules/utils/unicode/is-regional-indicator-symbol.js +1 -1
  368. package/lib/rules/utils/unicode/is-surrogate-pair.js +1 -1
  369. package/lib/rules/valid-typeof.js +152 -110
  370. package/lib/rules/vars-on-top.js +151 -144
  371. package/lib/rules/wrap-iife.js +203 -190
  372. package/lib/rules/wrap-regex.js +69 -57
  373. package/lib/rules/yield-star-spacing.js +144 -133
  374. package/lib/rules/yoda.js +282 -271
  375. package/lib/services/parser-service.js +35 -35
  376. package/lib/services/processor-service.js +66 -73
  377. package/lib/shared/ajv.js +14 -14
  378. package/lib/shared/assert.js +3 -4
  379. package/lib/shared/ast-utils.js +7 -6
  380. package/lib/shared/deep-merge-arrays.js +24 -22
  381. package/lib/shared/directives.js +3 -2
  382. package/lib/shared/flags.js +46 -17
  383. package/lib/shared/logging.js +24 -25
  384. package/lib/shared/option-utils.js +43 -36
  385. package/lib/shared/runtime-info.js +136 -127
  386. package/lib/shared/serialization.js +27 -27
  387. package/lib/shared/severity.js +22 -22
  388. package/lib/shared/stats.js +5 -5
  389. package/lib/shared/string-utils.js +16 -16
  390. package/lib/shared/text-table.js +28 -27
  391. package/lib/shared/traverser.js +153 -146
  392. package/lib/types/index.d.ts +2010 -1559
  393. package/lib/types/rules.d.ts +5253 -5140
  394. package/lib/types/use-at-your-own-risk.d.ts +32 -30
  395. package/lib/unsupported-api.js +5 -5
  396. package/messages/all-files-ignored.js +3 -3
  397. package/messages/all-matched-files-ignored.js +3 -3
  398. package/messages/config-file-missing.js +2 -2
  399. package/messages/config-plugin-missing.js +3 -3
  400. package/messages/config-serialize-function.js +9 -7
  401. package/messages/eslintrc-incompat.js +13 -15
  402. package/messages/eslintrc-plugins.js +3 -4
  403. package/messages/extend-config-missing.js +3 -3
  404. package/messages/failed-to-read-json.js +3 -3
  405. package/messages/file-not-found.js +3 -3
  406. package/messages/invalid-rule-options.js +2 -2
  407. package/messages/invalid-rule-severity.js +2 -2
  408. package/messages/no-config-found.js +3 -3
  409. package/messages/plugin-conflict.js +8 -8
  410. package/messages/plugin-invalid.js +3 -3
  411. package/messages/plugin-missing.js +3 -3
  412. package/messages/print-config-with-directory-path.js +2 -2
  413. package/messages/shared.js +6 -1
  414. package/messages/whitespace-found.js +3 -3
  415. package/package.json +11 -17
@@ -10,19 +10,21 @@
10
10
  // Requirements
11
11
  //------------------------------------------------------------------------------
12
12
 
13
- const
14
- assert = require("node:assert"),
15
- util = require("node:util"),
16
- path = require("node:path"),
17
- equal = require("fast-deep-equal"),
18
- Traverser = require("../shared/traverser"),
19
- { getRuleOptionsSchema } = require("../config/flat-config-helpers"),
20
- { Linter, SourceCodeFixer } = require("../linter"),
21
- { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
22
- stringify = require("json-stable-stringify-without-jsonify");
13
+ const assert = require("node:assert"),
14
+ util = require("node:util"),
15
+ path = require("node:path"),
16
+ equal = require("fast-deep-equal"),
17
+ Traverser = require("../shared/traverser"),
18
+ { getRuleOptionsSchema } = require("../config/flat-config-helpers"),
19
+ { Linter, SourceCodeFixer } = require("../linter"),
20
+ { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
21
+ stringify = require("json-stable-stringify-without-jsonify");
23
22
 
24
23
  const { FlatConfigArray } = require("../config/flat-config-array");
25
- const { defaultConfig } = require("../config/default-config");
24
+ const {
25
+ defaultConfig,
26
+ defaultRuleTesterConfig,
27
+ } = require("../config/default-config");
26
28
 
27
29
  const ajv = require("../shared/ajv")({ strictDefaults: true });
28
30
 
@@ -41,7 +43,6 @@ const { SourceCode } = require("../languages/js/source-code");
41
43
  /** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
42
44
  /** @typedef {import("../shared/types").Rule} Rule */
43
45
 
44
-
45
46
  /**
46
47
  * A test case that is expected to pass lint.
47
48
  * @typedef {Object} ValidTestCase
@@ -106,30 +107,30 @@ let sharedDefaultConfig = { rules: {} };
106
107
  * configuration
107
108
  */
108
109
  const RuleTesterParameters = [
109
- "name",
110
- "code",
111
- "filename",
112
- "options",
113
- "before",
114
- "after",
115
- "errors",
116
- "output",
117
- "only"
110
+ "name",
111
+ "code",
112
+ "filename",
113
+ "options",
114
+ "before",
115
+ "after",
116
+ "errors",
117
+ "output",
118
+ "only",
118
119
  ];
119
120
 
120
121
  /*
121
122
  * All allowed property names in error objects.
122
123
  */
123
124
  const errorObjectParameters = new Set([
124
- "message",
125
- "messageId",
126
- "data",
127
- "type",
128
- "line",
129
- "column",
130
- "endLine",
131
- "endColumn",
132
- "suggestions"
125
+ "message",
126
+ "messageId",
127
+ "data",
128
+ "type",
129
+ "line",
130
+ "column",
131
+ "endLine",
132
+ "endColumn",
133
+ "suggestions",
133
134
  ]);
134
135
  const friendlyErrorObjectParameterList = `[${[...errorObjectParameters].map(key => `'${key}'`).join(", ")}]`;
135
136
 
@@ -137,30 +138,28 @@ const friendlyErrorObjectParameterList = `[${[...errorObjectParameters].map(key
137
138
  * All allowed property names in suggestion objects.
138
139
  */
139
140
  const suggestionObjectParameters = new Set([
140
- "desc",
141
- "messageId",
142
- "data",
143
- "output"
141
+ "desc",
142
+ "messageId",
143
+ "data",
144
+ "output",
144
145
  ]);
145
146
  const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
146
147
 
147
148
  /*
148
149
  * Ignored test case properties when checking for test case duplicates.
149
150
  */
150
- const duplicationIgnoredParameters = new Set([
151
- "name",
152
- "errors",
153
- "output"
154
- ]);
151
+ const duplicationIgnoredParameters = new Set(["name", "errors", "output"]);
155
152
 
156
153
  const forbiddenMethods = [
157
- "applyInlineConfig",
158
- "applyLanguageOptions",
159
- "finalize"
154
+ "applyInlineConfig",
155
+ "applyLanguageOptions",
156
+ "finalize",
160
157
  ];
161
158
 
162
159
  /** @type {Map<string,WeakSet>} */
163
- const forbiddenMethodCalls = new Map(forbiddenMethods.map(methodName => ([methodName, new WeakSet()])));
160
+ const forbiddenMethodCalls = new Map(
161
+ forbiddenMethods.map(methodName => [methodName, new WeakSet()]),
162
+ );
164
163
 
165
164
  const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
166
165
 
@@ -171,23 +170,23 @@ const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
171
170
  * @returns {any} A cloned value.
172
171
  */
173
172
  function cloneDeeplyExcludesParent(x) {
174
- if (typeof x === "object" && x !== null) {
175
- if (Array.isArray(x)) {
176
- return x.map(cloneDeeplyExcludesParent);
177
- }
173
+ if (typeof x === "object" && x !== null) {
174
+ if (Array.isArray(x)) {
175
+ return x.map(cloneDeeplyExcludesParent);
176
+ }
178
177
 
179
- const retv = {};
178
+ const retv = {};
180
179
 
181
- for (const key in x) {
182
- if (key !== "parent" && hasOwnProperty(x, key)) {
183
- retv[key] = cloneDeeplyExcludesParent(x[key]);
184
- }
185
- }
180
+ for (const key in x) {
181
+ if (key !== "parent" && hasOwnProperty(x, key)) {
182
+ retv[key] = cloneDeeplyExcludesParent(x[key]);
183
+ }
184
+ }
186
185
 
187
- return retv;
188
- }
186
+ return retv;
187
+ }
189
188
 
190
- return x;
189
+ return x;
191
190
  }
192
191
 
193
192
  /**
@@ -196,18 +195,18 @@ function cloneDeeplyExcludesParent(x) {
196
195
  * @returns {void}
197
196
  */
198
197
  function freezeDeeply(x) {
199
- if (typeof x === "object" && x !== null) {
200
- if (Array.isArray(x)) {
201
- x.forEach(freezeDeeply);
202
- } else {
203
- for (const key in x) {
204
- if (key !== "parent" && hasOwnProperty(x, key)) {
205
- freezeDeeply(x[key]);
206
- }
207
- }
208
- }
209
- Object.freeze(x);
210
- }
198
+ if (typeof x === "object" && x !== null) {
199
+ if (Array.isArray(x)) {
200
+ x.forEach(freezeDeeply);
201
+ } else {
202
+ for (const key in x) {
203
+ if (key !== "parent" && hasOwnProperty(x, key)) {
204
+ freezeDeeply(x[key]);
205
+ }
206
+ }
207
+ }
208
+ Object.freeze(x);
209
+ }
211
210
  }
212
211
 
213
212
  /**
@@ -216,13 +215,13 @@ function freezeDeeply(x) {
216
215
  * @returns {string} The sanitized text.
217
216
  */
218
217
  function sanitize(text) {
219
- if (typeof text !== "string") {
220
- return "";
221
- }
222
- return text.replace(
223
- /[\u0000-\u0009\u000b-\u001a]/gu, // eslint-disable-line no-control-regex -- Escaping controls
224
- c => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`
225
- );
218
+ if (typeof text !== "string") {
219
+ return "";
220
+ }
221
+ return text.replace(
222
+ /[\u0000-\u0009\u000b-\u001a]/gu, // eslint-disable-line no-control-regex -- Escaping controls
223
+ c => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`,
224
+ );
226
225
  }
227
226
 
228
227
  /**
@@ -232,25 +231,28 @@ function sanitize(text) {
232
231
  * @returns {void}
233
232
  */
234
233
  function defineStartEndAsError(objName, node) {
235
- Object.defineProperties(node, {
236
- start: {
237
- get() {
238
- throw new Error(`Use ${objName}.range[0] instead of ${objName}.start`);
239
- },
240
- configurable: true,
241
- enumerable: false
242
- },
243
- end: {
244
- get() {
245
- throw new Error(`Use ${objName}.range[1] instead of ${objName}.end`);
246
- },
247
- configurable: true,
248
- enumerable: false
249
- }
250
- });
234
+ Object.defineProperties(node, {
235
+ start: {
236
+ get() {
237
+ throw new Error(
238
+ `Use ${objName}.range[0] instead of ${objName}.start`,
239
+ );
240
+ },
241
+ configurable: true,
242
+ enumerable: false,
243
+ },
244
+ end: {
245
+ get() {
246
+ throw new Error(
247
+ `Use ${objName}.range[1] instead of ${objName}.end`,
248
+ );
249
+ },
250
+ configurable: true,
251
+ enumerable: false,
252
+ },
253
+ });
251
254
  }
252
255
 
253
-
254
256
  /**
255
257
  * Define `start`/`end` properties of all nodes of the given AST as throwing error.
256
258
  * @param {ASTNode} ast The root node to errorize `start`/`end` properties.
@@ -258,9 +260,12 @@ function defineStartEndAsError(objName, node) {
258
260
  * @returns {void}
259
261
  */
260
262
  function defineStartEndAsErrorInTree(ast, visitorKeys) {
261
- Traverser.traverse(ast, { visitorKeys, enter: defineStartEndAsError.bind(null, "node") });
262
- ast.tokens.forEach(defineStartEndAsError.bind(null, "token"));
263
- ast.comments.forEach(defineStartEndAsError.bind(null, "token"));
263
+ Traverser.traverse(ast, {
264
+ visitorKeys,
265
+ enter: defineStartEndAsError.bind(null, "node"),
266
+ });
267
+ ast.tokens.forEach(defineStartEndAsError.bind(null, "token"));
268
+ ast.comments.forEach(defineStartEndAsError.bind(null, "token"));
264
269
  }
265
270
 
266
271
  /**
@@ -270,28 +275,27 @@ function defineStartEndAsErrorInTree(ast, visitorKeys) {
270
275
  * @returns {Parser} Wrapped parser object.
271
276
  */
272
277
  function wrapParser(parser) {
273
-
274
- if (typeof parser.parseForESLint === "function") {
275
- return {
276
- [parserSymbol]: parser,
277
- parseForESLint(...args) {
278
- const ret = parser.parseForESLint(...args);
279
-
280
- defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys);
281
- return ret;
282
- }
283
- };
284
- }
285
-
286
- return {
287
- [parserSymbol]: parser,
288
- parse(...args) {
289
- const ast = parser.parse(...args);
290
-
291
- defineStartEndAsErrorInTree(ast);
292
- return ast;
293
- }
294
- };
278
+ if (typeof parser.parseForESLint === "function") {
279
+ return {
280
+ [parserSymbol]: parser,
281
+ parseForESLint(...args) {
282
+ const ret = parser.parseForESLint(...args);
283
+
284
+ defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys);
285
+ return ret;
286
+ },
287
+ };
288
+ }
289
+
290
+ return {
291
+ [parserSymbol]: parser,
292
+ parse(...args) {
293
+ const ast = parser.parse(...args);
294
+
295
+ defineStartEndAsErrorInTree(ast);
296
+ return ast;
297
+ },
298
+ };
295
299
  }
296
300
 
297
301
  /**
@@ -301,25 +305,23 @@ function wrapParser(parser) {
301
305
  * @returns {Function} The function that throws the error.
302
306
  */
303
307
  function throwForbiddenMethodError(methodName, prototype) {
308
+ const original = prototype[methodName];
304
309
 
305
- const original = prototype[methodName];
310
+ return function (...args) {
311
+ const called = forbiddenMethodCalls.get(methodName);
306
312
 
307
- return function(...args) {
313
+ /* eslint-disable no-invalid-this -- needed to operate as a method. */
314
+ if (!called.has(this)) {
315
+ called.add(this);
308
316
 
309
- const called = forbiddenMethodCalls.get(methodName);
317
+ return original.apply(this, args);
318
+ }
319
+ /* eslint-enable no-invalid-this -- not needed past this point */
310
320
 
311
- /* eslint-disable no-invalid-this -- needed to operate as a method. */
312
- if (!called.has(this)) {
313
- called.add(this);
314
-
315
- return original.apply(this, args);
316
- }
317
- /* eslint-enable no-invalid-this -- not needed past this point */
318
-
319
- throw new Error(
320
- `\`SourceCode#${methodName}()\` cannot be called inside a rule.`
321
- );
322
- };
321
+ throw new Error(
322
+ `\`SourceCode#${methodName}()\` cannot be called inside a rule.`,
323
+ );
324
+ };
323
325
  }
324
326
 
325
327
  /**
@@ -328,9 +330,9 @@ function throwForbiddenMethodError(methodName, prototype) {
328
330
  * @returns {string[]} Array of placeholder names
329
331
  */
330
332
  function getMessagePlaceholders(message) {
331
- const matcher = getPlaceholderMatcher();
333
+ const matcher = getPlaceholderMatcher();
332
334
 
333
- return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
335
+ return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
334
336
  }
335
337
 
336
338
  /**
@@ -342,17 +344,19 @@ function getMessagePlaceholders(message) {
342
344
  * @returns {string[]} Missing placeholder names
343
345
  */
344
346
  function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) {
345
- const unsubstituted = getMessagePlaceholders(message);
347
+ const unsubstituted = getMessagePlaceholders(message);
346
348
 
347
- if (unsubstituted.length === 0) {
348
- return [];
349
- }
349
+ if (unsubstituted.length === 0) {
350
+ return [];
351
+ }
350
352
 
351
- // 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
352
- const known = getMessagePlaceholders(raw);
353
- const provided = Object.keys(data);
353
+ // 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
354
+ const known = getMessagePlaceholders(raw);
355
+ const provided = Object.keys(data);
354
356
 
355
- return unsubstituted.filter(name => known.includes(name) && !provided.includes(name));
357
+ return unsubstituted.filter(
358
+ name => known.includes(name) && !provided.includes(name),
359
+ );
356
360
  }
357
361
 
358
362
  const metaSchemaDescription = `
@@ -381,14 +385,14 @@ const IT_ONLY = Symbol("itOnly");
381
385
  * @returns {any} Returned value of `method`.
382
386
  */
383
387
  function itDefaultHandler(text, method) {
384
- try {
385
- return method.call(this);
386
- } catch (err) {
387
- if (err instanceof assert.AssertionError) {
388
- err.message += ` (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})`;
389
- }
390
- throw err;
391
- }
388
+ try {
389
+ return method.call(this);
390
+ } catch (err) {
391
+ if (err instanceof assert.AssertionError) {
392
+ err.message += ` (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})`;
393
+ }
394
+ throw err;
395
+ }
392
396
  }
393
397
 
394
398
  /**
@@ -399,907 +403,1165 @@ function itDefaultHandler(text, method) {
399
403
  * @returns {any} Returned value of `method`.
400
404
  */
401
405
  function describeDefaultHandler(text, method) {
402
- return method.call(this);
406
+ return method.call(this);
403
407
  }
404
408
 
405
409
  /**
406
410
  * Mocha test wrapper.
407
411
  */
408
412
  class RuleTester {
409
-
410
- /**
411
- * Creates a new instance of RuleTester.
412
- * @param {Object} [testerConfig] Optional, extra configuration for the tester
413
- */
414
- constructor(testerConfig = {}) {
415
-
416
- /**
417
- * The configuration to use for this tester. Combination of the tester
418
- * configuration and the default configuration.
419
- * @type {Object}
420
- */
421
- this.testerConfig = [
422
- sharedDefaultConfig,
423
- testerConfig,
424
- { rules: { "rule-tester/validate-ast": "error" } }
425
- ];
426
-
427
- this.linter = new Linter({ configType: "flat" });
428
- }
429
-
430
- /**
431
- * Set the configuration to use for all future tests
432
- * @param {Object} config the configuration to use.
433
- * @throws {TypeError} If non-object config.
434
- * @returns {void}
435
- */
436
- static setDefaultConfig(config) {
437
- if (typeof config !== "object" || config === null) {
438
- throw new TypeError("RuleTester.setDefaultConfig: config must be an object");
439
- }
440
- sharedDefaultConfig = config;
441
-
442
- // Make sure the rules object exists since it is assumed to exist later
443
- sharedDefaultConfig.rules = sharedDefaultConfig.rules || {};
444
- }
445
-
446
- /**
447
- * Get the current configuration used for all tests
448
- * @returns {Object} the current configuration
449
- */
450
- static getDefaultConfig() {
451
- return sharedDefaultConfig;
452
- }
453
-
454
- /**
455
- * Reset the configuration to the initial configuration of the tester removing
456
- * any changes made until now.
457
- * @returns {void}
458
- */
459
- static resetDefaultConfig() {
460
- sharedDefaultConfig = {
461
- rules: {
462
- ...testerDefaultConfig.rules
463
- }
464
- };
465
- }
466
-
467
-
468
- /*
469
- * If people use `mocha test.js --watch` command, `describe` and `it` function
470
- * instances are different for each execution. So `describe` and `it` should get fresh instance
471
- * always.
472
- */
473
- static get describe() {
474
- return (
475
- this[DESCRIBE] ||
476
- (typeof describe === "function" ? describe : describeDefaultHandler)
477
- );
478
- }
479
-
480
- static set describe(value) {
481
- this[DESCRIBE] = value;
482
- }
483
-
484
- static get it() {
485
- return (
486
- this[IT] ||
487
- (typeof it === "function" ? it : itDefaultHandler)
488
- );
489
- }
490
-
491
- static set it(value) {
492
- this[IT] = value;
493
- }
494
-
495
- /**
496
- * Adds the `only` property to a test to run it in isolation.
497
- * @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
498
- * @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
499
- */
500
- static only(item) {
501
- if (typeof item === "string") {
502
- return { code: item, only: true };
503
- }
504
-
505
- return { ...item, only: true };
506
- }
507
-
508
- static get itOnly() {
509
- if (typeof this[IT_ONLY] === "function") {
510
- return this[IT_ONLY];
511
- }
512
- if (typeof this[IT] === "function" && typeof this[IT].only === "function") {
513
- return Function.bind.call(this[IT].only, this[IT]);
514
- }
515
- if (typeof it === "function" && typeof it.only === "function") {
516
- return Function.bind.call(it.only, it);
517
- }
518
-
519
- if (typeof this[DESCRIBE] === "function" || typeof this[IT] === "function") {
520
- throw new Error(
521
- "Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
522
- "See https://eslint.org/docs/latest/integrate/nodejs-api#customizing-ruletester for more."
523
- );
524
- }
525
- if (typeof it === "function") {
526
- throw new Error("The current test framework does not support exclusive tests with `only`.");
527
- }
528
- throw new Error("To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.");
529
- }
530
-
531
- static set itOnly(value) {
532
- this[IT_ONLY] = value;
533
- }
534
-
535
-
536
- /**
537
- * Adds a new rule test to execute.
538
- * @param {string} ruleName The name of the rule to run.
539
- * @param {Rule} rule The rule to test.
540
- * @param {{
541
- * valid: (ValidTestCase | string)[],
542
- * invalid: InvalidTestCase[]
543
- * }} test The collection of tests to run.
544
- * @throws {TypeError|Error} If `rule` is not an object with a `create` method,
545
- * or if non-object `test`, or if a required scenario of the given type is missing.
546
- * @returns {void}
547
- */
548
- run(ruleName, rule, test) {
549
-
550
- const testerConfig = this.testerConfig,
551
- requiredScenarios = ["valid", "invalid"],
552
- scenarioErrors = [],
553
- linter = this.linter,
554
- ruleId = `rule-to-test/${ruleName}`;
555
-
556
- const seenValidTestCases = new Set();
557
- const seenInvalidTestCases = new Set();
558
-
559
- if (!rule || typeof rule !== "object" || typeof rule.create !== "function") {
560
- throw new TypeError("Rule must be an object with a `create` method");
561
- }
562
-
563
- if (!test || typeof test !== "object") {
564
- throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
565
- }
566
-
567
- requiredScenarios.forEach(scenarioType => {
568
- if (!test[scenarioType]) {
569
- scenarioErrors.push(`Could not find any ${scenarioType} test scenarios`);
570
- }
571
- });
572
-
573
- if (scenarioErrors.length > 0) {
574
- throw new Error([
575
- `Test Scenarios for rule ${ruleName} is invalid:`
576
- ].concat(scenarioErrors).join("\n"));
577
- }
578
-
579
- const baseConfig = [
580
- { files: ["**"] }, // Make sure the default config matches for all files
581
- {
582
- plugins: {
583
-
584
- // copy root plugin over
585
- "@": {
586
-
587
- /*
588
- * Parsers are wrapped to detect more errors, so this needs
589
- * to be a new object for each call to run(), otherwise the
590
- * parsers will be wrapped multiple times.
591
- */
592
- parsers: {
593
- ...defaultConfig[0].plugins["@"].parsers
594
- },
595
-
596
- /*
597
- * The rules key on the default plugin is a proxy to lazy-load
598
- * just the rules that are needed. So, don't create a new object
599
- * here, just use the default one to keep that performance
600
- * enhancement.
601
- */
602
- rules: defaultConfig[0].plugins["@"].rules,
603
- languages: defaultConfig[0].plugins["@"].languages
604
- },
605
- "rule-to-test": {
606
- rules: {
607
- [ruleName]: Object.assign({}, rule, {
608
-
609
- // Create a wrapper rule that freezes the `context` properties.
610
- create(context) {
611
- freezeDeeply(context.options);
612
- freezeDeeply(context.settings);
613
- freezeDeeply(context.parserOptions);
614
-
615
- // freezeDeeply(context.languageOptions);
616
-
617
- return rule.create(context);
618
- }
619
- })
620
- }
621
- }
622
- },
623
- language: defaultConfig[0].language
624
- },
625
- ...defaultConfig.slice(1)
626
- ];
627
-
628
- /**
629
- * Runs a hook on the given item when it's assigned to the given property
630
- * @param {string|Object} item Item to run the hook on
631
- * @param {string} prop The property having the hook assigned to
632
- * @throws {Error} If the property is not a function or that function throws an error
633
- * @returns {void}
634
- * @private
635
- */
636
- function runHook(item, prop) {
637
- if (typeof item === "object" && hasOwnProperty(item, prop)) {
638
- assert.strictEqual(typeof item[prop], "function", `Optional test case property '${prop}' must be a function`);
639
- item[prop]();
640
- }
641
- }
642
-
643
- /**
644
- * Run the rule for the given item
645
- * @param {string|Object} item Item to run the rule against
646
- * @throws {Error} If an invalid schema.
647
- * @returns {Object} Eslint run result
648
- * @private
649
- */
650
- function runRuleForItem(item) {
651
- const flatConfigArrayOptions = {
652
- baseConfig
653
- };
654
-
655
- if (item.filename) {
656
- flatConfigArrayOptions.basePath = path.parse(item.filename).root || void 0;
657
- }
658
-
659
- const configs = new FlatConfigArray(testerConfig, flatConfigArrayOptions);
660
-
661
- /*
662
- * Modify the returned config so that the parser is wrapped to catch
663
- * access of the start/end properties. This method is called just
664
- * once per code snippet being tested, so each test case gets a clean
665
- * parser.
666
- */
667
- configs[ConfigArraySymbol.finalizeConfig] = function(...args) {
668
-
669
- // can't do super here :(
670
- const proto = Object.getPrototypeOf(this);
671
- const calculatedConfig = proto[ConfigArraySymbol.finalizeConfig].apply(this, args);
672
-
673
- // wrap the parser to catch start/end property access
674
- if (calculatedConfig.language === jslang) {
675
- calculatedConfig.languageOptions.parser = wrapParser(calculatedConfig.languageOptions.parser);
676
- }
677
-
678
- return calculatedConfig;
679
- };
680
-
681
- let code, filename, output, beforeAST, afterAST;
682
-
683
- if (typeof item === "string") {
684
- code = item;
685
- } else {
686
- code = item.code;
687
-
688
- /*
689
- * Assumes everything on the item is a config except for the
690
- * parameters used by this tester
691
- */
692
- const itemConfig = { ...item };
693
-
694
- for (const parameter of RuleTesterParameters) {
695
- delete itemConfig[parameter];
696
- }
697
-
698
- /*
699
- * Create the config object from the tester config and this item
700
- * specific configurations.
701
- */
702
- configs.push(itemConfig);
703
- }
704
-
705
- if (hasOwnProperty(item, "only")) {
706
- assert.ok(typeof item.only === "boolean", "Optional test case property 'only' must be a boolean");
707
- }
708
- if (hasOwnProperty(item, "filename")) {
709
- assert.ok(typeof item.filename === "string", "Optional test case property 'filename' must be a string");
710
- filename = item.filename;
711
- }
712
-
713
- let ruleConfig = 1;
714
-
715
- if (hasOwnProperty(item, "options")) {
716
- assert(Array.isArray(item.options), "options must be an array");
717
- ruleConfig = [1, ...item.options];
718
- }
719
-
720
- configs.push({
721
- rules: {
722
- [ruleId]: ruleConfig
723
- }
724
- });
725
-
726
- let schema;
727
-
728
- try {
729
- schema = getRuleOptionsSchema(rule);
730
- } catch (err) {
731
- err.message += metaSchemaDescription;
732
- throw err;
733
- }
734
-
735
- /*
736
- * Check and throw an error if the schema is an empty object (`schema:{}`), because such schema
737
- * doesn't validate or enforce anything and is therefore considered a possible error. If the intent
738
- * was to skip options validation, `schema:false` should be set instead (explicit opt-out).
739
- *
740
- * For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed
741
- * properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well,
742
- * it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea
743
- * to use inherited properties in schemas because schemas that differ only in inherited properties would end up
744
- * having the same cache entry that would be correct for only one of them.
745
- *
746
- * At this point, `schema` can only be an object or `null`.
747
- */
748
- if (schema && Object.keys(schema).length === 0) {
749
- throw new Error(`\`schema: {}\` is a no-op${metaSchemaDescription}`);
750
- }
751
-
752
- /*
753
- * Setup AST getters.
754
- * The goal is to check whether or not AST was modified when
755
- * running the rule under test.
756
- */
757
- configs.push({
758
- plugins: {
759
- "rule-tester": {
760
- rules: {
761
- "validate-ast": {
762
- create() {
763
- return {
764
- Program(node) {
765
- beforeAST = cloneDeeplyExcludesParent(node);
766
- },
767
- "Program:exit"(node) {
768
- afterAST = node;
769
- }
770
- };
771
- }
772
- }
773
- }
774
- }
775
- }
776
- });
777
-
778
- if (schema) {
779
- ajv.validateSchema(schema);
780
-
781
- if (ajv.errors) {
782
- const errors = ajv.errors.map(error => {
783
- const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
784
-
785
- return `\t${field}: ${error.message}`;
786
- }).join("\n");
787
-
788
- throw new Error([`Schema for rule ${ruleName} is invalid:`, errors]);
789
- }
790
-
791
- /*
792
- * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
793
- * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
794
- * 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,
795
- * the schema is compiled here separately from checking for `validateSchema` errors.
796
- */
797
- try {
798
- ajv.compile(schema);
799
- } catch (err) {
800
- throw new Error(`Schema for rule ${ruleName} is invalid: ${err.message}`);
801
- }
802
- }
803
-
804
- // check for validation errors
805
- try {
806
- configs.normalizeSync();
807
- configs.getConfig("test.js");
808
- } catch (error) {
809
- error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`;
810
- throw error;
811
- }
812
-
813
- // Verify the code.
814
- const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype;
815
- let messages;
816
-
817
- try {
818
- forbiddenMethods.forEach(methodName => {
819
- SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype);
820
- });
821
-
822
- messages = linter.verify(code, configs, filename);
823
- } finally {
824
- SourceCode.prototype.applyInlineConfig = applyInlineConfig;
825
- SourceCode.prototype.applyLanguageOptions = applyLanguageOptions;
826
- SourceCode.prototype.finalize = finalize;
827
- }
828
-
829
-
830
- const fatalErrorMessage = messages.find(m => m.fatal);
831
-
832
- assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`);
833
-
834
- // Verify if autofix makes a syntax error or not.
835
- if (messages.some(m => m.fix)) {
836
- output = SourceCodeFixer.applyFixes(code, messages).output;
837
- const errorMessageInFix = linter.verify(output, configs, filename).find(m => m.fatal);
838
-
839
- assert(!errorMessageInFix, [
840
- "A fatal parsing error occurred in autofix.",
841
- `Error: ${errorMessageInFix && errorMessageInFix.message}`,
842
- "Autofix output:",
843
- output
844
- ].join("\n"));
845
- } else {
846
- output = code;
847
- }
848
-
849
- return {
850
- messages,
851
- output,
852
- beforeAST,
853
- afterAST: cloneDeeplyExcludesParent(afterAST),
854
- configs,
855
- filename
856
- };
857
- }
858
-
859
- /**
860
- * Check if the AST was changed
861
- * @param {ASTNode} beforeAST AST node before running
862
- * @param {ASTNode} afterAST AST node after running
863
- * @returns {void}
864
- * @private
865
- */
866
- function assertASTDidntChange(beforeAST, afterAST) {
867
- if (!equal(beforeAST, afterAST)) {
868
- assert.fail("Rule should not modify AST.");
869
- }
870
- }
871
-
872
- /**
873
- * Check if this test case is a duplicate of one we have seen before.
874
- * @param {string|Object} item test case object
875
- * @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
876
- * @returns {void}
877
- * @private
878
- */
879
- function checkDuplicateTestCase(item, seenTestCases) {
880
- if (!isSerializable(item)) {
881
-
882
- /*
883
- * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
884
- * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
885
- */
886
- return;
887
- }
888
-
889
- const normalizedItem = typeof item === "string" ? { code: item } : item;
890
- const serializedTestCase = stringify(normalizedItem, {
891
- replacer(key, value) {
892
-
893
- // "this" is the currently stringified object --> only ignore top-level properties
894
- return (normalizedItem !== this || !duplicationIgnoredParameters.has(key)) ? value : void 0;
895
- }
896
- });
897
-
898
- assert(
899
- !seenTestCases.has(serializedTestCase),
900
- "detected duplicate test case"
901
- );
902
- seenTestCases.add(serializedTestCase);
903
- }
904
-
905
- /**
906
- * Check if the template is valid or not
907
- * all valid cases go through this
908
- * @param {string|Object} item Item to run the rule against
909
- * @returns {void}
910
- * @private
911
- */
912
- function testValidTemplate(item) {
913
- const code = typeof item === "object" ? item.code : item;
914
-
915
- assert.ok(typeof code === "string", "Test case must specify a string value for 'code'");
916
- if (item.name) {
917
- assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
918
- }
919
-
920
- checkDuplicateTestCase(item, seenValidTestCases);
921
-
922
- const result = runRuleForItem(item);
923
- const messages = result.messages;
924
-
925
- assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s",
926
- messages.length,
927
- util.inspect(messages)));
928
-
929
- assertASTDidntChange(result.beforeAST, result.afterAST);
930
- }
931
-
932
- /**
933
- * Asserts that the message matches its expected value. If the expected
934
- * value is a regular expression, it is checked against the actual
935
- * value.
936
- * @param {string} actual Actual value
937
- * @param {string|RegExp} expected Expected value
938
- * @returns {void}
939
- * @private
940
- */
941
- function assertMessageMatches(actual, expected) {
942
- if (expected instanceof RegExp) {
943
-
944
- // assert.js doesn't have a built-in RegExp match function
945
- assert.ok(
946
- expected.test(actual),
947
- `Expected '${actual}' to match ${expected}`
948
- );
949
- } else {
950
- assert.strictEqual(actual, expected);
951
- }
952
- }
953
-
954
- /**
955
- * Check if the template is invalid or not
956
- * all invalid cases go through this.
957
- * @param {string|Object} item Item to run the rule against
958
- * @returns {void}
959
- * @private
960
- */
961
- function testInvalidTemplate(item) {
962
- assert.ok(typeof item.code === "string", "Test case must specify a string value for 'code'");
963
- if (item.name) {
964
- assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
965
- }
966
- assert.ok(item.errors || item.errors === 0,
967
- `Did not specify errors for an invalid test of ${ruleName}`);
968
-
969
- if (Array.isArray(item.errors) && item.errors.length === 0) {
970
- assert.fail("Invalid cases must have at least one error");
971
- }
972
-
973
- checkDuplicateTestCase(item, seenInvalidTestCases);
974
-
975
- const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
976
- const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
977
-
978
- const result = runRuleForItem(item);
979
- const messages = result.messages;
980
-
981
- for (const message of messages) {
982
- if (hasOwnProperty(message, "suggestions")) {
983
-
984
- /** @type {Map<string, number>} */
985
- const seenMessageIndices = new Map();
986
-
987
- for (let i = 0; i < message.suggestions.length; i += 1) {
988
- const suggestionMessage = message.suggestions[i].desc;
989
- const previous = seenMessageIndices.get(suggestionMessage);
990
-
991
- assert.ok(!seenMessageIndices.has(suggestionMessage), `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`);
992
- seenMessageIndices.set(suggestionMessage, i);
993
- }
994
- }
995
- }
996
-
997
- if (typeof item.errors === "number") {
998
-
999
- if (item.errors === 0) {
1000
- assert.fail("Invalid cases must have 'error' value greater than 0");
1001
- }
1002
-
1003
- assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
1004
- item.errors,
1005
- item.errors === 1 ? "" : "s",
1006
- messages.length,
1007
- util.inspect(messages)));
1008
- } else {
1009
- assert.strictEqual(
1010
- messages.length, item.errors.length, util.format(
1011
- "Should have %d error%s but had %d: %s",
1012
- item.errors.length,
1013
- item.errors.length === 1 ? "" : "s",
1014
- messages.length,
1015
- util.inspect(messages)
1016
- )
1017
- );
1018
-
1019
- const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleId);
1020
-
1021
- for (let i = 0, l = item.errors.length; i < l; i++) {
1022
- const error = item.errors[i];
1023
- const message = messages[i];
1024
-
1025
- assert(hasMessageOfThisRule, "Error rule name should be the same as the name of the rule being tested");
1026
-
1027
- if (typeof error === "string" || error instanceof RegExp) {
1028
-
1029
- // Just an error message.
1030
- assertMessageMatches(message.message, error);
1031
- assert.ok(message.suggestions === void 0, `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`);
1032
- } else if (typeof error === "object" && error !== null) {
1033
-
1034
- /*
1035
- * Error object.
1036
- * This may have a message, messageId, data, node type, line, and/or
1037
- * column.
1038
- */
1039
-
1040
- Object.keys(error).forEach(propertyName => {
1041
- assert.ok(
1042
- errorObjectParameters.has(propertyName),
1043
- `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`
1044
- );
1045
- });
1046
-
1047
- if (hasOwnProperty(error, "message")) {
1048
- assert.ok(!hasOwnProperty(error, "messageId"), "Error should not specify both 'message' and a 'messageId'.");
1049
- assert.ok(!hasOwnProperty(error, "data"), "Error should not specify both 'data' and 'message'.");
1050
- assertMessageMatches(message.message, error.message);
1051
- } else if (hasOwnProperty(error, "messageId")) {
1052
- assert.ok(
1053
- ruleHasMetaMessages,
1054
- "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'."
1055
- );
1056
- if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
1057
- assert(false, `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`);
1058
- }
1059
- assert.strictEqual(
1060
- message.messageId,
1061
- error.messageId,
1062
- `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
1063
- );
1064
-
1065
- const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
1066
- message.message,
1067
- rule.meta.messages[message.messageId],
1068
- error.data
1069
- );
1070
-
1071
- assert.ok(
1072
- unsubstitutedPlaceholders.length === 0,
1073
- `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.`
1074
- );
1075
-
1076
- if (hasOwnProperty(error, "data")) {
1077
-
1078
- /*
1079
- * if data was provided, then directly compare the returned message to a synthetic
1080
- * interpolated message using the same message ID and data provided in the test.
1081
- * See https://github.com/eslint/eslint/issues/9890 for context.
1082
- */
1083
- const unformattedOriginalMessage = rule.meta.messages[error.messageId];
1084
- const rehydratedMessage = interpolate(unformattedOriginalMessage, error.data);
1085
-
1086
- assert.strictEqual(
1087
- message.message,
1088
- rehydratedMessage,
1089
- `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`
1090
- );
1091
- }
1092
- } else {
1093
- assert.fail("Test error must specify either a 'messageId' or 'message'.");
1094
- }
1095
-
1096
- if (error.type) {
1097
- assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);
1098
- }
1099
-
1100
- if (hasOwnProperty(error, "line")) {
1101
- assert.strictEqual(message.line, error.line, `Error line should be ${error.line}`);
1102
- }
1103
-
1104
- if (hasOwnProperty(error, "column")) {
1105
- assert.strictEqual(message.column, error.column, `Error column should be ${error.column}`);
1106
- }
1107
-
1108
- if (hasOwnProperty(error, "endLine")) {
1109
- assert.strictEqual(message.endLine, error.endLine, `Error endLine should be ${error.endLine}`);
1110
- }
1111
-
1112
- if (hasOwnProperty(error, "endColumn")) {
1113
- assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`);
1114
- }
1115
-
1116
- assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`);
1117
- if (hasOwnProperty(error, "suggestions")) {
1118
-
1119
- // Support asserting there are no suggestions
1120
- const expectsSuggestions = Array.isArray(error.suggestions) ? error.suggestions.length > 0 : Boolean(error.suggestions);
1121
- const hasSuggestions = message.suggestions !== void 0;
1122
-
1123
- if (!hasSuggestions && expectsSuggestions) {
1124
- assert.ok(!error.suggestions, `Error should have suggestions on error with message: "${message.message}"`);
1125
- } else if (hasSuggestions) {
1126
- assert.ok(expectsSuggestions, `Error should have no suggestions on error with message: "${message.message}"`);
1127
- if (typeof error.suggestions === "number") {
1128
- assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`);
1129
- } else if (Array.isArray(error.suggestions)) {
1130
- assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
1131
-
1132
- error.suggestions.forEach((expectedSuggestion, index) => {
1133
- assert.ok(
1134
- typeof expectedSuggestion === "object" && expectedSuggestion !== null,
1135
- "Test suggestion in 'suggestions' array must be an object."
1136
- );
1137
- Object.keys(expectedSuggestion).forEach(propertyName => {
1138
- assert.ok(
1139
- suggestionObjectParameters.has(propertyName),
1140
- `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
1141
- );
1142
- });
1143
-
1144
- const actualSuggestion = message.suggestions[index];
1145
- const suggestionPrefix = `Error Suggestion at index ${index}:`;
1146
-
1147
- if (hasOwnProperty(expectedSuggestion, "desc")) {
1148
- assert.ok(
1149
- !hasOwnProperty(expectedSuggestion, "data"),
1150
- `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
1151
- );
1152
- assert.ok(
1153
- !hasOwnProperty(expectedSuggestion, "messageId"),
1154
- `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`
1155
- );
1156
- assert.strictEqual(
1157
- actualSuggestion.desc,
1158
- expectedSuggestion.desc,
1159
- `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
1160
- );
1161
- } else if (hasOwnProperty(expectedSuggestion, "messageId")) {
1162
- assert.ok(
1163
- ruleHasMetaMessages,
1164
- `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
1165
- );
1166
- assert.ok(
1167
- hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
1168
- `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
1169
- );
1170
- assert.strictEqual(
1171
- actualSuggestion.messageId,
1172
- expectedSuggestion.messageId,
1173
- `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
1174
- );
1175
-
1176
- const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
1177
- actualSuggestion.desc,
1178
- rule.meta.messages[expectedSuggestion.messageId],
1179
- expectedSuggestion.data
1180
- );
1181
-
1182
- assert.ok(
1183
- unsubstitutedPlaceholders.length === 0,
1184
- `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.`
1185
- );
1186
-
1187
- if (hasOwnProperty(expectedSuggestion, "data")) {
1188
- const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
1189
- const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
1190
-
1191
- assert.strictEqual(
1192
- actualSuggestion.desc,
1193
- rehydratedDesc,
1194
- `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
1195
- );
1196
- }
1197
- } else if (hasOwnProperty(expectedSuggestion, "data")) {
1198
- assert.fail(
1199
- `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
1200
- );
1201
- } else {
1202
- assert.fail(
1203
- `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`
1204
- );
1205
- }
1206
-
1207
- assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`);
1208
- const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;
1209
-
1210
- // Verify if suggestion fix makes a syntax error or not.
1211
- const errorMessageInSuggestion =
1212
- linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
1213
-
1214
- assert(!errorMessageInSuggestion, [
1215
- "A fatal parsing error occurred in suggestion fix.",
1216
- `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
1217
- "Suggestion output:",
1218
- codeWithAppliedSuggestion
1219
- ].join("\n"));
1220
-
1221
- 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}"`);
1222
- assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`);
1223
- });
1224
- } else {
1225
- assert.fail("Test error object property 'suggestions' should be an array or a number");
1226
- }
1227
- }
1228
- }
1229
- } else {
1230
-
1231
- // Message was an unexpected type
1232
- assert.fail(`Error should be a string, object, or RegExp, but found (${util.inspect(message)})`);
1233
- }
1234
- }
1235
- }
1236
-
1237
- if (hasOwnProperty(item, "output")) {
1238
- if (item.output === null) {
1239
- assert.strictEqual(
1240
- result.output,
1241
- item.code,
1242
- "Expected no autofixes to be suggested"
1243
- );
1244
- } else {
1245
- assert.strictEqual(result.output, item.output, "Output is incorrect.");
1246
- assert.notStrictEqual(item.code, item.output, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.");
1247
- }
1248
- } else {
1249
- assert.strictEqual(
1250
- result.output,
1251
- item.code,
1252
- "The rule fixed the code. Please add 'output' property."
1253
- );
1254
- }
1255
-
1256
- assertASTDidntChange(result.beforeAST, result.afterAST);
1257
- }
1258
-
1259
- /*
1260
- * This creates a mocha test suite and pipes all supplied info through
1261
- * one of the templates above.
1262
- * The test suites for valid/invalid are created conditionally as
1263
- * test runners (eg. vitest) fail for empty test suites.
1264
- */
1265
- this.constructor.describe(ruleName, () => {
1266
- if (test.valid.length > 0) {
1267
- this.constructor.describe("valid", () => {
1268
- test.valid.forEach(valid => {
1269
- this.constructor[valid.only ? "itOnly" : "it"](
1270
- sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
1271
- () => {
1272
- try {
1273
- runHook(valid, "before");
1274
- testValidTemplate(valid);
1275
- } finally {
1276
- runHook(valid, "after");
1277
- }
1278
- }
1279
- );
1280
- });
1281
- });
1282
- }
1283
-
1284
- if (test.invalid.length > 0) {
1285
- this.constructor.describe("invalid", () => {
1286
- test.invalid.forEach(invalid => {
1287
- this.constructor[invalid.only ? "itOnly" : "it"](
1288
- sanitize(invalid.name || invalid.code),
1289
- () => {
1290
- try {
1291
- runHook(invalid, "before");
1292
- testInvalidTemplate(invalid);
1293
- } finally {
1294
- runHook(invalid, "after");
1295
- }
1296
- }
1297
- );
1298
- });
1299
- });
1300
- }
1301
- });
1302
- }
413
+ /**
414
+ * Creates a new instance of RuleTester.
415
+ * @param {Object} [testerConfig] Optional, extra configuration for the tester
416
+ */
417
+ constructor(testerConfig = {}) {
418
+ /**
419
+ * The configuration to use for this tester. Combination of the tester
420
+ * configuration and the default configuration.
421
+ * @type {Object}
422
+ */
423
+ this.testerConfig = [
424
+ sharedDefaultConfig,
425
+ testerConfig,
426
+ { rules: { "rule-tester/validate-ast": "error" } },
427
+ ];
428
+
429
+ this.linter = new Linter({ configType: "flat" });
430
+ }
431
+
432
+ /**
433
+ * Set the configuration to use for all future tests
434
+ * @param {Object} config the configuration to use.
435
+ * @throws {TypeError} If non-object config.
436
+ * @returns {void}
437
+ */
438
+ static setDefaultConfig(config) {
439
+ if (typeof config !== "object" || config === null) {
440
+ throw new TypeError(
441
+ "RuleTester.setDefaultConfig: config must be an object",
442
+ );
443
+ }
444
+ sharedDefaultConfig = config;
445
+
446
+ // Make sure the rules object exists since it is assumed to exist later
447
+ sharedDefaultConfig.rules = sharedDefaultConfig.rules || {};
448
+ }
449
+
450
+ /**
451
+ * Get the current configuration used for all tests
452
+ * @returns {Object} the current configuration
453
+ */
454
+ static getDefaultConfig() {
455
+ return sharedDefaultConfig;
456
+ }
457
+
458
+ /**
459
+ * Reset the configuration to the initial configuration of the tester removing
460
+ * any changes made until now.
461
+ * @returns {void}
462
+ */
463
+ static resetDefaultConfig() {
464
+ sharedDefaultConfig = {
465
+ rules: {
466
+ ...testerDefaultConfig.rules,
467
+ },
468
+ };
469
+ }
470
+
471
+ /*
472
+ * If people use `mocha test.js --watch` command, `describe` and `it` function
473
+ * instances are different for each execution. So `describe` and `it` should get fresh instance
474
+ * always.
475
+ */
476
+ static get describe() {
477
+ return (
478
+ this[DESCRIBE] ||
479
+ (typeof describe === "function" ? describe : describeDefaultHandler)
480
+ );
481
+ }
482
+
483
+ static set describe(value) {
484
+ this[DESCRIBE] = value;
485
+ }
486
+
487
+ static get it() {
488
+ return this[IT] || (typeof it === "function" ? it : itDefaultHandler);
489
+ }
490
+
491
+ static set it(value) {
492
+ this[IT] = value;
493
+ }
494
+
495
+ /**
496
+ * Adds the `only` property to a test to run it in isolation.
497
+ * @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
498
+ * @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
499
+ */
500
+ static only(item) {
501
+ if (typeof item === "string") {
502
+ return { code: item, only: true };
503
+ }
504
+
505
+ return { ...item, only: true };
506
+ }
507
+
508
+ static get itOnly() {
509
+ if (typeof this[IT_ONLY] === "function") {
510
+ return this[IT_ONLY];
511
+ }
512
+ if (
513
+ typeof this[IT] === "function" &&
514
+ typeof this[IT].only === "function"
515
+ ) {
516
+ return Function.bind.call(this[IT].only, this[IT]);
517
+ }
518
+ if (typeof it === "function" && typeof it.only === "function") {
519
+ return Function.bind.call(it.only, it);
520
+ }
521
+
522
+ if (
523
+ typeof this[DESCRIBE] === "function" ||
524
+ typeof this[IT] === "function"
525
+ ) {
526
+ throw new Error(
527
+ "Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
528
+ "See https://eslint.org/docs/latest/integrate/nodejs-api#customizing-ruletester for more.",
529
+ );
530
+ }
531
+ if (typeof it === "function") {
532
+ throw new Error(
533
+ "The current test framework does not support exclusive tests with `only`.",
534
+ );
535
+ }
536
+ throw new Error(
537
+ "To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.",
538
+ );
539
+ }
540
+
541
+ static set itOnly(value) {
542
+ this[IT_ONLY] = value;
543
+ }
544
+
545
+ /**
546
+ * Adds a new rule test to execute.
547
+ * @param {string} ruleName The name of the rule to run.
548
+ * @param {Rule} rule The rule to test.
549
+ * @param {{
550
+ * valid: (ValidTestCase | string)[],
551
+ * invalid: InvalidTestCase[]
552
+ * }} test The collection of tests to run.
553
+ * @throws {TypeError|Error} If `rule` is not an object with a `create` method,
554
+ * or if non-object `test`, or if a required scenario of the given type is missing.
555
+ * @returns {void}
556
+ */
557
+ run(ruleName, rule, test) {
558
+ const testerConfig = this.testerConfig,
559
+ requiredScenarios = ["valid", "invalid"],
560
+ scenarioErrors = [],
561
+ linter = this.linter,
562
+ ruleId = `rule-to-test/${ruleName}`;
563
+
564
+ const seenValidTestCases = new Set();
565
+ const seenInvalidTestCases = new Set();
566
+
567
+ if (
568
+ !rule ||
569
+ typeof rule !== "object" ||
570
+ typeof rule.create !== "function"
571
+ ) {
572
+ throw new TypeError(
573
+ "Rule must be an object with a `create` method",
574
+ );
575
+ }
576
+
577
+ if (!test || typeof test !== "object") {
578
+ throw new TypeError(
579
+ `Test Scenarios for rule ${ruleName} : Could not find test scenario object`,
580
+ );
581
+ }
582
+
583
+ requiredScenarios.forEach(scenarioType => {
584
+ if (!test[scenarioType]) {
585
+ scenarioErrors.push(
586
+ `Could not find any ${scenarioType} test scenarios`,
587
+ );
588
+ }
589
+ });
590
+
591
+ if (scenarioErrors.length > 0) {
592
+ throw new Error(
593
+ [`Test Scenarios for rule ${ruleName} is invalid:`]
594
+ .concat(scenarioErrors)
595
+ .join("\n"),
596
+ );
597
+ }
598
+
599
+ const baseConfig = [
600
+ {
601
+ plugins: {
602
+ // copy root plugin over
603
+ "@": {
604
+ /*
605
+ * Parsers are wrapped to detect more errors, so this needs
606
+ * to be a new object for each call to run(), otherwise the
607
+ * parsers will be wrapped multiple times.
608
+ */
609
+ parsers: {
610
+ ...defaultConfig[0].plugins["@"].parsers,
611
+ },
612
+
613
+ /*
614
+ * The rules key on the default plugin is a proxy to lazy-load
615
+ * just the rules that are needed. So, don't create a new object
616
+ * here, just use the default one to keep that performance
617
+ * enhancement.
618
+ */
619
+ rules: defaultConfig[0].plugins["@"].rules,
620
+ languages: defaultConfig[0].plugins["@"].languages,
621
+ },
622
+ "rule-to-test": {
623
+ rules: {
624
+ [ruleName]: Object.assign({}, rule, {
625
+ // Create a wrapper rule that freezes the `context` properties.
626
+ create(context) {
627
+ freezeDeeply(context.options);
628
+ freezeDeeply(context.settings);
629
+ freezeDeeply(context.parserOptions);
630
+
631
+ // freezeDeeply(context.languageOptions);
632
+
633
+ return rule.create(context);
634
+ },
635
+ }),
636
+ },
637
+ },
638
+ },
639
+ language: defaultConfig[0].language,
640
+ },
641
+ ...defaultRuleTesterConfig,
642
+ ];
643
+
644
+ /**
645
+ * Runs a hook on the given item when it's assigned to the given property
646
+ * @param {string|Object} item Item to run the hook on
647
+ * @param {string} prop The property having the hook assigned to
648
+ * @throws {Error} If the property is not a function or that function throws an error
649
+ * @returns {void}
650
+ * @private
651
+ */
652
+ function runHook(item, prop) {
653
+ if (typeof item === "object" && hasOwnProperty(item, prop)) {
654
+ assert.strictEqual(
655
+ typeof item[prop],
656
+ "function",
657
+ `Optional test case property '${prop}' must be a function`,
658
+ );
659
+ item[prop]();
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Run the rule for the given item
665
+ * @param {string|Object} item Item to run the rule against
666
+ * @throws {Error} If an invalid schema.
667
+ * @returns {Object} Eslint run result
668
+ * @private
669
+ */
670
+ function runRuleForItem(item) {
671
+ const flatConfigArrayOptions = {
672
+ baseConfig,
673
+ };
674
+
675
+ if (item.filename) {
676
+ flatConfigArrayOptions.basePath =
677
+ path.parse(item.filename).root || void 0;
678
+ }
679
+
680
+ const configs = new FlatConfigArray(
681
+ testerConfig,
682
+ flatConfigArrayOptions,
683
+ );
684
+
685
+ /*
686
+ * Modify the returned config so that the parser is wrapped to catch
687
+ * access of the start/end properties. This method is called just
688
+ * once per code snippet being tested, so each test case gets a clean
689
+ * parser.
690
+ */
691
+ configs[ConfigArraySymbol.finalizeConfig] = function (...args) {
692
+ // can't do super here :(
693
+ const proto = Object.getPrototypeOf(this);
694
+ const calculatedConfig = proto[
695
+ ConfigArraySymbol.finalizeConfig
696
+ ].apply(this, args);
697
+
698
+ // wrap the parser to catch start/end property access
699
+ if (calculatedConfig.language === jslang) {
700
+ calculatedConfig.languageOptions.parser = wrapParser(
701
+ calculatedConfig.languageOptions.parser,
702
+ );
703
+ }
704
+
705
+ return calculatedConfig;
706
+ };
707
+
708
+ let code, filename, output, beforeAST, afterAST;
709
+
710
+ if (typeof item === "string") {
711
+ code = item;
712
+ } else {
713
+ code = item.code;
714
+
715
+ /*
716
+ * Assumes everything on the item is a config except for the
717
+ * parameters used by this tester
718
+ */
719
+ const itemConfig = { ...item };
720
+
721
+ for (const parameter of RuleTesterParameters) {
722
+ delete itemConfig[parameter];
723
+ }
724
+
725
+ /*
726
+ * Create the config object from the tester config and this item
727
+ * specific configurations.
728
+ */
729
+ configs.push(itemConfig);
730
+ }
731
+
732
+ if (hasOwnProperty(item, "only")) {
733
+ assert.ok(
734
+ typeof item.only === "boolean",
735
+ "Optional test case property 'only' must be a boolean",
736
+ );
737
+ }
738
+ if (hasOwnProperty(item, "filename")) {
739
+ assert.ok(
740
+ typeof item.filename === "string",
741
+ "Optional test case property 'filename' must be a string",
742
+ );
743
+ filename = item.filename;
744
+ }
745
+
746
+ let ruleConfig = 1;
747
+
748
+ if (hasOwnProperty(item, "options")) {
749
+ assert(Array.isArray(item.options), "options must be an array");
750
+ ruleConfig = [1, ...item.options];
751
+ }
752
+
753
+ configs.push({
754
+ rules: {
755
+ [ruleId]: ruleConfig,
756
+ },
757
+ });
758
+
759
+ let schema;
760
+
761
+ try {
762
+ schema = getRuleOptionsSchema(rule);
763
+ } catch (err) {
764
+ err.message += metaSchemaDescription;
765
+ throw err;
766
+ }
767
+
768
+ /*
769
+ * Check and throw an error if the schema is an empty object (`schema:{}`), because such schema
770
+ * doesn't validate or enforce anything and is therefore considered a possible error. If the intent
771
+ * was to skip options validation, `schema:false` should be set instead (explicit opt-out).
772
+ *
773
+ * For this purpose, a schema object is considered empty if it doesn't have any own enumerable string-keyed
774
+ * properties. While `ajv.compile()` does use enumerable properties from the prototype chain as well,
775
+ * it caches compiled schemas by serializing only own enumerable properties, so it's generally not a good idea
776
+ * to use inherited properties in schemas because schemas that differ only in inherited properties would end up
777
+ * having the same cache entry that would be correct for only one of them.
778
+ *
779
+ * At this point, `schema` can only be an object or `null`.
780
+ */
781
+ if (schema && Object.keys(schema).length === 0) {
782
+ throw new Error(
783
+ `\`schema: {}\` is a no-op${metaSchemaDescription}`,
784
+ );
785
+ }
786
+
787
+ /*
788
+ * Setup AST getters.
789
+ * The goal is to check whether or not AST was modified when
790
+ * running the rule under test.
791
+ */
792
+ configs.push({
793
+ plugins: {
794
+ "rule-tester": {
795
+ rules: {
796
+ "validate-ast": {
797
+ create() {
798
+ return {
799
+ Program(node) {
800
+ beforeAST =
801
+ cloneDeeplyExcludesParent(node);
802
+ },
803
+ "Program:exit"(node) {
804
+ afterAST = node;
805
+ },
806
+ };
807
+ },
808
+ },
809
+ },
810
+ },
811
+ },
812
+ });
813
+
814
+ if (schema) {
815
+ ajv.validateSchema(schema);
816
+
817
+ if (ajv.errors) {
818
+ const errors = ajv.errors
819
+ .map(error => {
820
+ const field =
821
+ error.dataPath[0] === "."
822
+ ? error.dataPath.slice(1)
823
+ : error.dataPath;
824
+
825
+ return `\t${field}: ${error.message}`;
826
+ })
827
+ .join("\n");
828
+
829
+ throw new Error([
830
+ `Schema for rule ${ruleName} is invalid:`,
831
+ errors,
832
+ ]);
833
+ }
834
+
835
+ /*
836
+ * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
837
+ * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
838
+ * 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,
839
+ * the schema is compiled here separately from checking for `validateSchema` errors.
840
+ */
841
+ try {
842
+ ajv.compile(schema);
843
+ } catch (err) {
844
+ throw new Error(
845
+ `Schema for rule ${ruleName} is invalid: ${err.message}`,
846
+ );
847
+ }
848
+ }
849
+
850
+ // check for validation errors
851
+ try {
852
+ configs.normalizeSync();
853
+ configs.getConfig("test.js");
854
+ } catch (error) {
855
+ error.message = `ESLint configuration in rule-tester is invalid: ${error.message}`;
856
+ throw error;
857
+ }
858
+
859
+ // Verify the code.
860
+ const { applyLanguageOptions, applyInlineConfig, finalize } =
861
+ SourceCode.prototype;
862
+ let messages;
863
+
864
+ try {
865
+ forbiddenMethods.forEach(methodName => {
866
+ SourceCode.prototype[methodName] =
867
+ throwForbiddenMethodError(
868
+ methodName,
869
+ SourceCode.prototype,
870
+ );
871
+ });
872
+
873
+ messages = linter.verify(code, configs, filename);
874
+ } finally {
875
+ SourceCode.prototype.applyInlineConfig = applyInlineConfig;
876
+ SourceCode.prototype.applyLanguageOptions =
877
+ applyLanguageOptions;
878
+ SourceCode.prototype.finalize = finalize;
879
+ }
880
+
881
+ const fatalErrorMessage = messages.find(m => m.fatal);
882
+
883
+ assert(
884
+ !fatalErrorMessage,
885
+ `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`,
886
+ );
887
+
888
+ // Verify if autofix makes a syntax error or not.
889
+ if (messages.some(m => m.fix)) {
890
+ output = SourceCodeFixer.applyFixes(code, messages).output;
891
+ const errorMessageInFix = linter
892
+ .verify(output, configs, filename)
893
+ .find(m => m.fatal);
894
+
895
+ assert(
896
+ !errorMessageInFix,
897
+ [
898
+ "A fatal parsing error occurred in autofix.",
899
+ `Error: ${errorMessageInFix && errorMessageInFix.message}`,
900
+ "Autofix output:",
901
+ output,
902
+ ].join("\n"),
903
+ );
904
+ } else {
905
+ output = code;
906
+ }
907
+
908
+ return {
909
+ messages,
910
+ output,
911
+ beforeAST,
912
+ afterAST: cloneDeeplyExcludesParent(afterAST),
913
+ configs,
914
+ filename,
915
+ };
916
+ }
917
+
918
+ /**
919
+ * Check if the AST was changed
920
+ * @param {ASTNode} beforeAST AST node before running
921
+ * @param {ASTNode} afterAST AST node after running
922
+ * @returns {void}
923
+ * @private
924
+ */
925
+ function assertASTDidntChange(beforeAST, afterAST) {
926
+ if (!equal(beforeAST, afterAST)) {
927
+ assert.fail("Rule should not modify AST.");
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Check if this test case is a duplicate of one we have seen before.
933
+ * @param {string|Object} item test case object
934
+ * @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
935
+ * @returns {void}
936
+ * @private
937
+ */
938
+ function checkDuplicateTestCase(item, seenTestCases) {
939
+ if (!isSerializable(item)) {
940
+ /*
941
+ * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
942
+ * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
943
+ */
944
+ return;
945
+ }
946
+
947
+ const normalizedItem =
948
+ typeof item === "string" ? { code: item } : item;
949
+ const serializedTestCase = stringify(normalizedItem, {
950
+ replacer(key, value) {
951
+ // "this" is the currently stringified object --> only ignore top-level properties
952
+ return normalizedItem !== this ||
953
+ !duplicationIgnoredParameters.has(key)
954
+ ? value
955
+ : void 0;
956
+ },
957
+ });
958
+
959
+ assert(
960
+ !seenTestCases.has(serializedTestCase),
961
+ "detected duplicate test case",
962
+ );
963
+ seenTestCases.add(serializedTestCase);
964
+ }
965
+
966
+ /**
967
+ * Check if the template is valid or not
968
+ * all valid cases go through this
969
+ * @param {string|Object} item Item to run the rule against
970
+ * @returns {void}
971
+ * @private
972
+ */
973
+ function testValidTemplate(item) {
974
+ const code = typeof item === "object" ? item.code : item;
975
+
976
+ assert.ok(
977
+ typeof code === "string",
978
+ "Test case must specify a string value for 'code'",
979
+ );
980
+ if (item.name) {
981
+ assert.ok(
982
+ typeof item.name === "string",
983
+ "Optional test case property 'name' must be a string",
984
+ );
985
+ }
986
+
987
+ checkDuplicateTestCase(item, seenValidTestCases);
988
+
989
+ const result = runRuleForItem(item);
990
+ const messages = result.messages;
991
+
992
+ assert.strictEqual(
993
+ messages.length,
994
+ 0,
995
+ util.format(
996
+ "Should have no errors but had %d: %s",
997
+ messages.length,
998
+ util.inspect(messages),
999
+ ),
1000
+ );
1001
+
1002
+ assertASTDidntChange(result.beforeAST, result.afterAST);
1003
+ }
1004
+
1005
+ /**
1006
+ * Asserts that the message matches its expected value. If the expected
1007
+ * value is a regular expression, it is checked against the actual
1008
+ * value.
1009
+ * @param {string} actual Actual value
1010
+ * @param {string|RegExp} expected Expected value
1011
+ * @returns {void}
1012
+ * @private
1013
+ */
1014
+ function assertMessageMatches(actual, expected) {
1015
+ if (expected instanceof RegExp) {
1016
+ // assert.js doesn't have a built-in RegExp match function
1017
+ assert.ok(
1018
+ expected.test(actual),
1019
+ `Expected '${actual}' to match ${expected}`,
1020
+ );
1021
+ } else {
1022
+ assert.strictEqual(actual, expected);
1023
+ }
1024
+ }
1025
+
1026
+ /**
1027
+ * Check if the template is invalid or not
1028
+ * all invalid cases go through this.
1029
+ * @param {string|Object} item Item to run the rule against
1030
+ * @returns {void}
1031
+ * @private
1032
+ */
1033
+ function testInvalidTemplate(item) {
1034
+ assert.ok(
1035
+ typeof item.code === "string",
1036
+ "Test case must specify a string value for 'code'",
1037
+ );
1038
+ if (item.name) {
1039
+ assert.ok(
1040
+ typeof item.name === "string",
1041
+ "Optional test case property 'name' must be a string",
1042
+ );
1043
+ }
1044
+ assert.ok(
1045
+ item.errors || item.errors === 0,
1046
+ `Did not specify errors for an invalid test of ${ruleName}`,
1047
+ );
1048
+
1049
+ if (Array.isArray(item.errors) && item.errors.length === 0) {
1050
+ assert.fail("Invalid cases must have at least one error");
1051
+ }
1052
+
1053
+ checkDuplicateTestCase(item, seenInvalidTestCases);
1054
+
1055
+ const ruleHasMetaMessages =
1056
+ hasOwnProperty(rule, "meta") &&
1057
+ hasOwnProperty(rule.meta, "messages");
1058
+ const friendlyIDList = ruleHasMetaMessages
1059
+ ? `[${Object.keys(rule.meta.messages)
1060
+ .map(key => `'${key}'`)
1061
+ .join(", ")}]`
1062
+ : null;
1063
+
1064
+ const result = runRuleForItem(item);
1065
+ const messages = result.messages;
1066
+
1067
+ for (const message of messages) {
1068
+ if (hasOwnProperty(message, "suggestions")) {
1069
+ /** @type {Map<string, number>} */
1070
+ const seenMessageIndices = new Map();
1071
+
1072
+ for (let i = 0; i < message.suggestions.length; i += 1) {
1073
+ const suggestionMessage = message.suggestions[i].desc;
1074
+ const previous =
1075
+ seenMessageIndices.get(suggestionMessage);
1076
+
1077
+ assert.ok(
1078
+ !seenMessageIndices.has(suggestionMessage),
1079
+ `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`,
1080
+ );
1081
+ seenMessageIndices.set(suggestionMessage, i);
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ if (typeof item.errors === "number") {
1087
+ if (item.errors === 0) {
1088
+ assert.fail(
1089
+ "Invalid cases must have 'error' value greater than 0",
1090
+ );
1091
+ }
1092
+
1093
+ assert.strictEqual(
1094
+ messages.length,
1095
+ item.errors,
1096
+ util.format(
1097
+ "Should have %d error%s but had %d: %s",
1098
+ item.errors,
1099
+ item.errors === 1 ? "" : "s",
1100
+ messages.length,
1101
+ util.inspect(messages),
1102
+ ),
1103
+ );
1104
+ } else {
1105
+ assert.strictEqual(
1106
+ messages.length,
1107
+ item.errors.length,
1108
+ util.format(
1109
+ "Should have %d error%s but had %d: %s",
1110
+ item.errors.length,
1111
+ item.errors.length === 1 ? "" : "s",
1112
+ messages.length,
1113
+ util.inspect(messages),
1114
+ ),
1115
+ );
1116
+
1117
+ const hasMessageOfThisRule = messages.some(
1118
+ m => m.ruleId === ruleId,
1119
+ );
1120
+
1121
+ for (let i = 0, l = item.errors.length; i < l; i++) {
1122
+ const error = item.errors[i];
1123
+ const message = messages[i];
1124
+
1125
+ assert(
1126
+ hasMessageOfThisRule,
1127
+ "Error rule name should be the same as the name of the rule being tested",
1128
+ );
1129
+
1130
+ if (typeof error === "string" || error instanceof RegExp) {
1131
+ // Just an error message.
1132
+ assertMessageMatches(message.message, error);
1133
+ assert.ok(
1134
+ message.suggestions === void 0,
1135
+ `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`,
1136
+ );
1137
+ } else if (typeof error === "object" && error !== null) {
1138
+ /*
1139
+ * Error object.
1140
+ * This may have a message, messageId, data, node type, line, and/or
1141
+ * column.
1142
+ */
1143
+
1144
+ Object.keys(error).forEach(propertyName => {
1145
+ assert.ok(
1146
+ errorObjectParameters.has(propertyName),
1147
+ `Invalid error property name '${propertyName}'. Expected one of ${friendlyErrorObjectParameterList}.`,
1148
+ );
1149
+ });
1150
+
1151
+ if (hasOwnProperty(error, "message")) {
1152
+ assert.ok(
1153
+ !hasOwnProperty(error, "messageId"),
1154
+ "Error should not specify both 'message' and a 'messageId'.",
1155
+ );
1156
+ assert.ok(
1157
+ !hasOwnProperty(error, "data"),
1158
+ "Error should not specify both 'data' and 'message'.",
1159
+ );
1160
+ assertMessageMatches(
1161
+ message.message,
1162
+ error.message,
1163
+ );
1164
+ } else if (hasOwnProperty(error, "messageId")) {
1165
+ assert.ok(
1166
+ ruleHasMetaMessages,
1167
+ "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'.",
1168
+ );
1169
+ if (
1170
+ !hasOwnProperty(
1171
+ rule.meta.messages,
1172
+ error.messageId,
1173
+ )
1174
+ ) {
1175
+ assert(
1176
+ false,
1177
+ `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`,
1178
+ );
1179
+ }
1180
+ assert.strictEqual(
1181
+ message.messageId,
1182
+ error.messageId,
1183
+ `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`,
1184
+ );
1185
+
1186
+ const unsubstitutedPlaceholders =
1187
+ getUnsubstitutedMessagePlaceholders(
1188
+ message.message,
1189
+ rule.meta.messages[message.messageId],
1190
+ error.data,
1191
+ );
1192
+
1193
+ assert.ok(
1194
+ unsubstitutedPlaceholders.length === 0,
1195
+ `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.`,
1196
+ );
1197
+
1198
+ if (hasOwnProperty(error, "data")) {
1199
+ /*
1200
+ * if data was provided, then directly compare the returned message to a synthetic
1201
+ * interpolated message using the same message ID and data provided in the test.
1202
+ * See https://github.com/eslint/eslint/issues/9890 for context.
1203
+ */
1204
+ const unformattedOriginalMessage =
1205
+ rule.meta.messages[error.messageId];
1206
+ const rehydratedMessage = interpolate(
1207
+ unformattedOriginalMessage,
1208
+ error.data,
1209
+ );
1210
+
1211
+ assert.strictEqual(
1212
+ message.message,
1213
+ rehydratedMessage,
1214
+ `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
1215
+ );
1216
+ }
1217
+ } else {
1218
+ assert.fail(
1219
+ "Test error must specify either a 'messageId' or 'message'.",
1220
+ );
1221
+ }
1222
+
1223
+ if (error.type) {
1224
+ assert.strictEqual(
1225
+ message.nodeType,
1226
+ error.type,
1227
+ `Error type should be ${error.type}, found ${message.nodeType}`,
1228
+ );
1229
+ }
1230
+
1231
+ if (hasOwnProperty(error, "line")) {
1232
+ assert.strictEqual(
1233
+ message.line,
1234
+ error.line,
1235
+ `Error line should be ${error.line}`,
1236
+ );
1237
+ }
1238
+
1239
+ if (hasOwnProperty(error, "column")) {
1240
+ assert.strictEqual(
1241
+ message.column,
1242
+ error.column,
1243
+ `Error column should be ${error.column}`,
1244
+ );
1245
+ }
1246
+
1247
+ if (hasOwnProperty(error, "endLine")) {
1248
+ assert.strictEqual(
1249
+ message.endLine,
1250
+ error.endLine,
1251
+ `Error endLine should be ${error.endLine}`,
1252
+ );
1253
+ }
1254
+
1255
+ if (hasOwnProperty(error, "endColumn")) {
1256
+ assert.strictEqual(
1257
+ message.endColumn,
1258
+ error.endColumn,
1259
+ `Error endColumn should be ${error.endColumn}`,
1260
+ );
1261
+ }
1262
+
1263
+ assert.ok(
1264
+ !message.suggestions ||
1265
+ hasOwnProperty(error, "suggestions"),
1266
+ `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`,
1267
+ );
1268
+ if (hasOwnProperty(error, "suggestions")) {
1269
+ // Support asserting there are no suggestions
1270
+ const expectsSuggestions = Array.isArray(
1271
+ error.suggestions,
1272
+ )
1273
+ ? error.suggestions.length > 0
1274
+ : Boolean(error.suggestions);
1275
+ const hasSuggestions =
1276
+ message.suggestions !== void 0;
1277
+
1278
+ if (!hasSuggestions && expectsSuggestions) {
1279
+ assert.ok(
1280
+ !error.suggestions,
1281
+ `Error should have suggestions on error with message: "${message.message}"`,
1282
+ );
1283
+ } else if (hasSuggestions) {
1284
+ assert.ok(
1285
+ expectsSuggestions,
1286
+ `Error should have no suggestions on error with message: "${message.message}"`,
1287
+ );
1288
+ if (typeof error.suggestions === "number") {
1289
+ assert.strictEqual(
1290
+ message.suggestions.length,
1291
+ error.suggestions,
1292
+ `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`,
1293
+ );
1294
+ } else if (Array.isArray(error.suggestions)) {
1295
+ assert.strictEqual(
1296
+ message.suggestions.length,
1297
+ error.suggestions.length,
1298
+ `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`,
1299
+ );
1300
+
1301
+ error.suggestions.forEach(
1302
+ (expectedSuggestion, index) => {
1303
+ assert.ok(
1304
+ typeof expectedSuggestion ===
1305
+ "object" &&
1306
+ expectedSuggestion !== null,
1307
+ "Test suggestion in 'suggestions' array must be an object.",
1308
+ );
1309
+ Object.keys(
1310
+ expectedSuggestion,
1311
+ ).forEach(propertyName => {
1312
+ assert.ok(
1313
+ suggestionObjectParameters.has(
1314
+ propertyName,
1315
+ ),
1316
+ `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`,
1317
+ );
1318
+ });
1319
+
1320
+ const actualSuggestion =
1321
+ message.suggestions[index];
1322
+ const suggestionPrefix = `Error Suggestion at index ${index}:`;
1323
+
1324
+ if (
1325
+ hasOwnProperty(
1326
+ expectedSuggestion,
1327
+ "desc",
1328
+ )
1329
+ ) {
1330
+ assert.ok(
1331
+ !hasOwnProperty(
1332
+ expectedSuggestion,
1333
+ "data",
1334
+ ),
1335
+ `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`,
1336
+ );
1337
+ assert.ok(
1338
+ !hasOwnProperty(
1339
+ expectedSuggestion,
1340
+ "messageId",
1341
+ ),
1342
+ `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`,
1343
+ );
1344
+ assert.strictEqual(
1345
+ actualSuggestion.desc,
1346
+ expectedSuggestion.desc,
1347
+ `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`,
1348
+ );
1349
+ } else if (
1350
+ hasOwnProperty(
1351
+ expectedSuggestion,
1352
+ "messageId",
1353
+ )
1354
+ ) {
1355
+ assert.ok(
1356
+ ruleHasMetaMessages,
1357
+ `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`,
1358
+ );
1359
+ assert.ok(
1360
+ hasOwnProperty(
1361
+ rule.meta.messages,
1362
+ expectedSuggestion.messageId,
1363
+ ),
1364
+ `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`,
1365
+ );
1366
+ assert.strictEqual(
1367
+ actualSuggestion.messageId,
1368
+ expectedSuggestion.messageId,
1369
+ `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`,
1370
+ );
1371
+
1372
+ const unsubstitutedPlaceholders =
1373
+ getUnsubstitutedMessagePlaceholders(
1374
+ actualSuggestion.desc,
1375
+ rule.meta.messages[
1376
+ expectedSuggestion
1377
+ .messageId
1378
+ ],
1379
+ expectedSuggestion.data,
1380
+ );
1381
+
1382
+ assert.ok(
1383
+ unsubstitutedPlaceholders.length ===
1384
+ 0,
1385
+ `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.`,
1386
+ );
1387
+
1388
+ if (
1389
+ hasOwnProperty(
1390
+ expectedSuggestion,
1391
+ "data",
1392
+ )
1393
+ ) {
1394
+ const unformattedMetaMessage =
1395
+ rule.meta.messages[
1396
+ expectedSuggestion
1397
+ .messageId
1398
+ ];
1399
+ const rehydratedDesc =
1400
+ interpolate(
1401
+ unformattedMetaMessage,
1402
+ expectedSuggestion.data,
1403
+ );
1404
+
1405
+ assert.strictEqual(
1406
+ actualSuggestion.desc,
1407
+ rehydratedDesc,
1408
+ `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`,
1409
+ );
1410
+ }
1411
+ } else if (
1412
+ hasOwnProperty(
1413
+ expectedSuggestion,
1414
+ "data",
1415
+ )
1416
+ ) {
1417
+ assert.fail(
1418
+ `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`,
1419
+ );
1420
+ } else {
1421
+ assert.fail(
1422
+ `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`,
1423
+ );
1424
+ }
1425
+
1426
+ assert.ok(
1427
+ hasOwnProperty(
1428
+ expectedSuggestion,
1429
+ "output",
1430
+ ),
1431
+ `${suggestionPrefix} The "output" property is required.`,
1432
+ );
1433
+ const codeWithAppliedSuggestion =
1434
+ SourceCodeFixer.applyFixes(
1435
+ item.code,
1436
+ [actualSuggestion],
1437
+ ).output;
1438
+
1439
+ // Verify if suggestion fix makes a syntax error or not.
1440
+ const errorMessageInSuggestion =
1441
+ linter
1442
+ .verify(
1443
+ codeWithAppliedSuggestion,
1444
+ result.configs,
1445
+ result.filename,
1446
+ )
1447
+ .find(m => m.fatal);
1448
+
1449
+ assert(
1450
+ !errorMessageInSuggestion,
1451
+ [
1452
+ "A fatal parsing error occurred in suggestion fix.",
1453
+ `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`,
1454
+ "Suggestion output:",
1455
+ codeWithAppliedSuggestion,
1456
+ ].join("\n"),
1457
+ );
1458
+
1459
+ assert.strictEqual(
1460
+ codeWithAppliedSuggestion,
1461
+ expectedSuggestion.output,
1462
+ `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`,
1463
+ );
1464
+ assert.notStrictEqual(
1465
+ expectedSuggestion.output,
1466
+ item.code,
1467
+ `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`,
1468
+ );
1469
+ },
1470
+ );
1471
+ } else {
1472
+ assert.fail(
1473
+ "Test error object property 'suggestions' should be an array or a number",
1474
+ );
1475
+ }
1476
+ }
1477
+ }
1478
+ } else {
1479
+ // Message was an unexpected type
1480
+ assert.fail(
1481
+ `Error should be a string, object, or RegExp, but found (${util.inspect(message)})`,
1482
+ );
1483
+ }
1484
+ }
1485
+ }
1486
+
1487
+ if (hasOwnProperty(item, "output")) {
1488
+ if (item.output === null) {
1489
+ assert.strictEqual(
1490
+ result.output,
1491
+ item.code,
1492
+ "Expected no autofixes to be suggested",
1493
+ );
1494
+ } else {
1495
+ assert.strictEqual(
1496
+ result.output,
1497
+ item.output,
1498
+ "Output is incorrect.",
1499
+ );
1500
+ assert.notStrictEqual(
1501
+ item.code,
1502
+ item.output,
1503
+ "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.",
1504
+ );
1505
+ }
1506
+ } else {
1507
+ assert.strictEqual(
1508
+ result.output,
1509
+ item.code,
1510
+ "The rule fixed the code. Please add 'output' property.",
1511
+ );
1512
+ }
1513
+
1514
+ assertASTDidntChange(result.beforeAST, result.afterAST);
1515
+ }
1516
+
1517
+ /*
1518
+ * This creates a mocha test suite and pipes all supplied info through
1519
+ * one of the templates above.
1520
+ * The test suites for valid/invalid are created conditionally as
1521
+ * test runners (eg. vitest) fail for empty test suites.
1522
+ */
1523
+ this.constructor.describe(ruleName, () => {
1524
+ if (test.valid.length > 0) {
1525
+ this.constructor.describe("valid", () => {
1526
+ test.valid.forEach(valid => {
1527
+ this.constructor[valid.only ? "itOnly" : "it"](
1528
+ sanitize(
1529
+ typeof valid === "object"
1530
+ ? valid.name || valid.code
1531
+ : valid,
1532
+ ),
1533
+ () => {
1534
+ try {
1535
+ runHook(valid, "before");
1536
+ testValidTemplate(valid);
1537
+ } finally {
1538
+ runHook(valid, "after");
1539
+ }
1540
+ },
1541
+ );
1542
+ });
1543
+ });
1544
+ }
1545
+
1546
+ if (test.invalid.length > 0) {
1547
+ this.constructor.describe("invalid", () => {
1548
+ test.invalid.forEach(invalid => {
1549
+ this.constructor[invalid.only ? "itOnly" : "it"](
1550
+ sanitize(invalid.name || invalid.code),
1551
+ () => {
1552
+ try {
1553
+ runHook(invalid, "before");
1554
+ testInvalidTemplate(invalid);
1555
+ } finally {
1556
+ runHook(invalid, "after");
1557
+ }
1558
+ },
1559
+ );
1560
+ });
1561
+ });
1562
+ }
1563
+ });
1564
+ }
1303
1565
  }
1304
1566
 
1305
1567
  RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null;