eslint 9.22.0 → 9.24.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 (417) hide show
  1. package/README.md +48 -46
  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 +830 -810
  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 +638 -457
  21. package/lib/config/config-loader.js +726 -622
  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 +72 -72
  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 +756 -681
  30. package/lib/eslint/eslint.js +934 -912
  31. package/lib/eslint/index.js +2 -2
  32. package/lib/eslint/legacy-eslint.js +577 -533
  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 +2403 -2045
  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 +36 -36
  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 +404 -361
  71. package/lib/rule-tester/index.js +1 -1
  72. package/lib/rule-tester/rule-tester.js +1308 -1046
  73. package/lib/rules/accessor-pairs.js +298 -263
  74. package/lib/rules/array-bracket-newline.js +250 -238
  75. package/lib/rules/array-bracket-spacing.js +263 -224
  76. package/lib/rules/array-callback-return.js +402 -356
  77. package/lib/rules/array-element-newline.js +358 -313
  78. package/lib/rules/arrow-body-style.js +400 -281
  79. package/lib/rules/arrow-parens.js +206 -173
  80. package/lib/rules/arrow-spacing.js +169 -163
  81. package/lib/rules/block-scoped-var.js +125 -123
  82. package/lib/rules/block-spacing.js +186 -176
  83. package/lib/rules/brace-style.js +262 -199
  84. package/lib/rules/callback-return.js +203 -190
  85. package/lib/rules/camelcase.js +403 -392
  86. package/lib/rules/capitalized-comments.js +253 -232
  87. package/lib/rules/class-methods-use-this.js +224 -172
  88. package/lib/rules/comma-dangle.js +379 -346
  89. package/lib/rules/comma-spacing.js +193 -195
  90. package/lib/rules/comma-style.js +375 -316
  91. package/lib/rules/complexity.js +173 -169
  92. package/lib/rules/computed-property-spacing.js +236 -211
  93. package/lib/rules/consistent-return.js +181 -170
  94. package/lib/rules/consistent-this.js +167 -147
  95. package/lib/rules/constructor-super.js +412 -404
  96. package/lib/rules/curly.js +407 -332
  97. package/lib/rules/default-case-last.js +38 -31
  98. package/lib/rules/default-case.js +89 -85
  99. package/lib/rules/default-param-last.js +69 -54
  100. package/lib/rules/dot-location.js +122 -110
  101. package/lib/rules/dot-notation.js +192 -156
  102. package/lib/rules/eol-last.js +122 -120
  103. package/lib/rules/eqeqeq.js +168 -155
  104. package/lib/rules/for-direction.js +146 -121
  105. package/lib/rules/func-call-spacing.js +261 -231
  106. package/lib/rules/func-name-matching.js +293 -209
  107. package/lib/rules/func-names.js +165 -164
  108. package/lib/rules/func-style.js +159 -127
  109. package/lib/rules/function-call-argument-newline.js +152 -129
  110. package/lib/rules/function-paren-newline.js +349 -291
  111. package/lib/rules/generator-star-spacing.js +229 -210
  112. package/lib/rules/getter-return.js +208 -172
  113. package/lib/rules/global-require.js +85 -74
  114. package/lib/rules/grouped-accessor-pairs.js +170 -150
  115. package/lib/rules/guard-for-in.js +72 -63
  116. package/lib/rules/handle-callback-err.js +108 -103
  117. package/lib/rules/id-blacklist.js +182 -199
  118. package/lib/rules/id-denylist.js +168 -187
  119. package/lib/rules/id-length.js +197 -171
  120. package/lib/rules/id-match.js +344 -289
  121. package/lib/rules/implicit-arrow-linebreak.js +102 -79
  122. package/lib/rules/indent-legacy.js +1344 -1118
  123. package/lib/rules/indent.js +2272 -1759
  124. package/lib/rules/index.js +317 -292
  125. package/lib/rules/init-declarations.js +137 -107
  126. package/lib/rules/jsx-quotes.js +94 -82
  127. package/lib/rules/key-spacing.js +750 -633
  128. package/lib/rules/keyword-spacing.js +648 -605
  129. package/lib/rules/line-comment-position.js +142 -128
  130. package/lib/rules/linebreak-style.js +107 -106
  131. package/lib/rules/lines-around-comment.js +540 -448
  132. package/lib/rules/lines-around-directive.js +233 -203
  133. package/lib/rules/lines-between-class-members.js +305 -234
  134. package/lib/rules/logical-assignment-operators.js +582 -399
  135. package/lib/rules/max-classes-per-file.js +69 -68
  136. package/lib/rules/max-depth.js +146 -143
  137. package/lib/rules/max-len.js +473 -434
  138. package/lib/rules/max-lines-per-function.js +201 -176
  139. package/lib/rules/max-lines.js +158 -162
  140. package/lib/rules/max-nested-callbacks.js +102 -104
  141. package/lib/rules/max-params.js +78 -76
  142. package/lib/rules/max-statements-per-line.js +205 -198
  143. package/lib/rules/max-statements.js +168 -164
  144. package/lib/rules/multiline-comment-style.js +637 -479
  145. package/lib/rules/multiline-ternary.js +241 -176
  146. package/lib/rules/new-cap.js +233 -213
  147. package/lib/rules/new-parens.js +88 -79
  148. package/lib/rules/newline-after-var.js +287 -250
  149. package/lib/rules/newline-before-return.js +229 -222
  150. package/lib/rules/newline-per-chained-call.js +142 -127
  151. package/lib/rules/no-alert.js +90 -79
  152. package/lib/rules/no-array-constructor.js +125 -113
  153. package/lib/rules/no-async-promise-executor.js +30 -24
  154. package/lib/rules/no-await-in-loop.js +69 -71
  155. package/lib/rules/no-bitwise.js +124 -100
  156. package/lib/rules/no-buffer-constructor.js +55 -47
  157. package/lib/rules/no-caller.js +39 -33
  158. package/lib/rules/no-case-declarations.js +61 -57
  159. package/lib/rules/no-catch-shadow.js +76 -73
  160. package/lib/rules/no-class-assign.js +51 -48
  161. package/lib/rules/no-compare-neg-zero.js +62 -48
  162. package/lib/rules/no-cond-assign.js +148 -132
  163. package/lib/rules/no-confusing-arrow.js +98 -81
  164. package/lib/rules/no-console.js +202 -199
  165. package/lib/rules/no-const-assign.js +47 -41
  166. package/lib/rules/no-constant-binary-expression.js +500 -405
  167. package/lib/rules/no-constant-condition.js +158 -143
  168. package/lib/rules/no-constructor-return.js +49 -49
  169. package/lib/rules/no-continue.js +25 -27
  170. package/lib/rules/no-control-regex.js +125 -121
  171. package/lib/rules/no-debugger.js +28 -30
  172. package/lib/rules/no-delete-var.js +29 -29
  173. package/lib/rules/no-div-regex.js +47 -41
  174. package/lib/rules/no-dupe-args.js +68 -69
  175. package/lib/rules/no-dupe-class-members.js +102 -89
  176. package/lib/rules/no-dupe-else-if.js +100 -77
  177. package/lib/rules/no-dupe-keys.js +133 -110
  178. package/lib/rules/no-duplicate-case.js +50 -43
  179. package/lib/rules/no-duplicate-imports.js +179 -176
  180. package/lib/rules/no-else-return.js +430 -385
  181. package/lib/rules/no-empty-character-class.js +57 -50
  182. package/lib/rules/no-empty-function.js +127 -128
  183. package/lib/rules/no-empty-pattern.js +63 -58
  184. package/lib/rules/no-empty-static-block.js +37 -35
  185. package/lib/rules/no-empty.js +98 -86
  186. package/lib/rules/no-eq-null.js +37 -32
  187. package/lib/rules/no-eval.js +256 -250
  188. package/lib/rules/no-ex-assign.js +42 -39
  189. package/lib/rules/no-extend-native.js +161 -159
  190. package/lib/rules/no-extra-bind.js +201 -190
  191. package/lib/rules/no-extra-boolean-cast.js +398 -348
  192. package/lib/rules/no-extra-label.js +150 -131
  193. package/lib/rules/no-extra-parens.js +1654 -1325
  194. package/lib/rules/no-extra-semi.js +146 -144
  195. package/lib/rules/no-fallthrough.js +199 -157
  196. package/lib/rules/no-floating-decimal.js +74 -66
  197. package/lib/rules/no-func-assign.js +54 -55
  198. package/lib/rules/no-global-assign.js +78 -73
  199. package/lib/rules/no-implicit-coercion.js +349 -293
  200. package/lib/rules/no-implicit-globals.js +158 -135
  201. package/lib/rules/no-implied-eval.js +140 -112
  202. package/lib/rules/no-import-assign.js +145 -159
  203. package/lib/rules/no-inline-comments.js +101 -95
  204. package/lib/rules/no-inner-declarations.js +115 -101
  205. package/lib/rules/no-invalid-regexp.js +222 -190
  206. package/lib/rules/no-invalid-this.js +123 -117
  207. package/lib/rules/no-irregular-whitespace.js +266 -252
  208. package/lib/rules/no-iterator.js +29 -33
  209. package/lib/rules/no-label-var.js +59 -62
  210. package/lib/rules/no-labels.js +138 -133
  211. package/lib/rules/no-lone-blocks.js +127 -123
  212. package/lib/rules/no-lonely-if.js +108 -77
  213. package/lib/rules/no-loop-func.js +238 -213
  214. package/lib/rules/no-loss-of-precision.js +218 -201
  215. package/lib/rules/no-magic-numbers.js +246 -218
  216. package/lib/rules/no-misleading-character-class.js +499 -446
  217. package/lib/rules/no-mixed-operators.js +188 -182
  218. package/lib/rules/no-mixed-requires.js +253 -240
  219. package/lib/rules/no-mixed-spaces-and-tabs.js +134 -121
  220. package/lib/rules/no-multi-assign.js +46 -44
  221. package/lib/rules/no-multi-spaces.js +163 -143
  222. package/lib/rules/no-multi-str.js +42 -41
  223. package/lib/rules/no-multiple-empty-lines.js +196 -158
  224. package/lib/rules/no-native-reassign.js +90 -85
  225. package/lib/rules/no-negated-condition.js +79 -75
  226. package/lib/rules/no-negated-in-lhs.js +45 -43
  227. package/lib/rules/no-nested-ternary.js +33 -32
  228. package/lib/rules/no-new-func.js +71 -62
  229. package/lib/rules/no-new-native-nonconstructor.js +43 -39
  230. package/lib/rules/no-new-object.js +48 -48
  231. package/lib/rules/no-new-require.js +48 -47
  232. package/lib/rules/no-new-symbol.js +52 -50
  233. package/lib/rules/no-new-wrappers.js +43 -41
  234. package/lib/rules/no-new.js +28 -29
  235. package/lib/rules/no-nonoctal-decimal-escape.js +141 -121
  236. package/lib/rules/no-obj-calls.js +66 -53
  237. package/lib/rules/no-object-constructor.js +104 -97
  238. package/lib/rules/no-octal-escape.js +40 -43
  239. package/lib/rules/no-octal.js +32 -32
  240. package/lib/rules/no-param-reassign.js +235 -217
  241. package/lib/rules/no-path-concat.js +66 -67
  242. package/lib/rules/no-plusplus.js +60 -61
  243. package/lib/rules/no-process-env.js +49 -48
  244. package/lib/rules/no-process-exit.js +54 -50
  245. package/lib/rules/no-promise-executor-return.js +214 -182
  246. package/lib/rules/no-proto.js +26 -29
  247. package/lib/rules/no-prototype-builtins.js +146 -124
  248. package/lib/rules/no-redeclare.js +154 -152
  249. package/lib/rules/no-regex-spaces.js +183 -161
  250. package/lib/rules/no-restricted-exports.js +208 -185
  251. package/lib/rules/no-restricted-globals.js +111 -112
  252. package/lib/rules/no-restricted-imports.js +657 -537
  253. package/lib/rules/no-restricted-modules.js +222 -202
  254. package/lib/rules/no-restricted-properties.js +181 -153
  255. package/lib/rules/no-restricted-syntax.js +56 -52
  256. package/lib/rules/no-return-assign.js +55 -50
  257. package/lib/rules/no-return-await.js +148 -124
  258. package/lib/rules/no-script-url.js +52 -45
  259. package/lib/rules/no-self-assign.js +148 -146
  260. package/lib/rules/no-self-compare.js +63 -46
  261. package/lib/rules/no-sequences.js +135 -116
  262. package/lib/rules/no-setter-return.js +185 -152
  263. package/lib/rules/no-shadow-restricted-names.js +61 -46
  264. package/lib/rules/no-shadow.js +342 -316
  265. package/lib/rules/no-spaced-func.js +82 -77
  266. package/lib/rules/no-sparse-arrays.js +54 -59
  267. package/lib/rules/no-sync.js +61 -60
  268. package/lib/rules/no-tabs.js +83 -72
  269. package/lib/rules/no-template-curly-in-string.js +33 -32
  270. package/lib/rules/no-ternary.js +25 -29
  271. package/lib/rules/no-this-before-super.js +321 -319
  272. package/lib/rules/no-throw-literal.js +31 -36
  273. package/lib/rules/no-trailing-spaces.js +199 -191
  274. package/lib/rules/no-undef-init.js +76 -61
  275. package/lib/rules/no-undef.js +51 -48
  276. package/lib/rules/no-undefined.js +73 -75
  277. package/lib/rules/no-underscore-dangle.js +370 -327
  278. package/lib/rules/no-unexpected-multiline.js +112 -102
  279. package/lib/rules/no-unmodified-loop-condition.js +254 -254
  280. package/lib/rules/no-unneeded-ternary.js +212 -147
  281. package/lib/rules/no-unreachable-loop.js +145 -142
  282. package/lib/rules/no-unreachable.js +255 -248
  283. package/lib/rules/no-unsafe-finally.js +93 -85
  284. package/lib/rules/no-unsafe-negation.js +105 -83
  285. package/lib/rules/no-unsafe-optional-chaining.js +192 -178
  286. package/lib/rules/no-unused-expressions.js +178 -162
  287. package/lib/rules/no-unused-labels.js +139 -124
  288. package/lib/rules/no-unused-private-class-members.js +206 -182
  289. package/lib/rules/no-unused-vars.js +1669 -1449
  290. package/lib/rules/no-use-before-define.js +229 -231
  291. package/lib/rules/no-useless-assignment.js +590 -511
  292. package/lib/rules/no-useless-backreference.js +212 -193
  293. package/lib/rules/no-useless-call.js +58 -53
  294. package/lib/rules/no-useless-catch.js +40 -40
  295. package/lib/rules/no-useless-computed-key.js +144 -115
  296. package/lib/rules/no-useless-concat.js +65 -60
  297. package/lib/rules/no-useless-constructor.js +158 -111
  298. package/lib/rules/no-useless-escape.js +342 -291
  299. package/lib/rules/no-useless-rename.js +183 -156
  300. package/lib/rules/no-useless-return.js +344 -312
  301. package/lib/rules/no-var.js +233 -212
  302. package/lib/rules/no-void.js +50 -48
  303. package/lib/rules/no-warning-comments.js +191 -186
  304. package/lib/rules/no-whitespace-before-property.js +131 -115
  305. package/lib/rules/no-with.js +24 -26
  306. package/lib/rules/nonblock-statement-body-position.js +149 -130
  307. package/lib/rules/object-curly-newline.js +306 -265
  308. package/lib/rules/object-curly-spacing.js +360 -314
  309. package/lib/rules/object-property-newline.js +137 -106
  310. package/lib/rules/object-shorthand.js +607 -502
  311. package/lib/rules/one-var-declaration-per-line.js +104 -100
  312. package/lib/rules/one-var.js +653 -537
  313. package/lib/rules/operator-assignment.js +219 -161
  314. package/lib/rules/operator-linebreak.js +295 -251
  315. package/lib/rules/padded-blocks.js +346 -308
  316. package/lib/rules/padding-line-between-statements.js +443 -439
  317. package/lib/rules/prefer-arrow-callback.js +362 -313
  318. package/lib/rules/prefer-const.js +418 -377
  319. package/lib/rules/prefer-destructuring.js +301 -279
  320. package/lib/rules/prefer-exponentiation-operator.js +176 -133
  321. package/lib/rules/prefer-named-capture-group.js +153 -140
  322. package/lib/rules/prefer-numeric-literals.js +121 -113
  323. package/lib/rules/prefer-object-has-own.js +116 -82
  324. package/lib/rules/prefer-object-spread.js +213 -193
  325. package/lib/rules/prefer-promise-reject-errors.js +140 -122
  326. package/lib/rules/prefer-reflect.js +127 -107
  327. package/lib/rules/prefer-regex-literals.js +578 -466
  328. package/lib/rules/prefer-rest-params.js +79 -80
  329. package/lib/rules/prefer-spread.js +47 -44
  330. package/lib/rules/prefer-template.js +266 -195
  331. package/lib/rules/quote-props.js +373 -307
  332. package/lib/rules/quotes.js +374 -326
  333. package/lib/rules/radix.js +152 -136
  334. package/lib/rules/require-atomic-updates.js +316 -285
  335. package/lib/rules/require-await.js +144 -116
  336. package/lib/rules/require-unicode-regexp.js +282 -177
  337. package/lib/rules/require-yield.js +53 -54
  338. package/lib/rules/rest-spread-spacing.js +128 -116
  339. package/lib/rules/semi-spacing.js +281 -250
  340. package/lib/rules/semi-style.js +176 -134
  341. package/lib/rules/semi.js +456 -436
  342. package/lib/rules/sort-imports.js +306 -233
  343. package/lib/rules/sort-keys.js +219 -188
  344. package/lib/rules/sort-vars.js +127 -93
  345. package/lib/rules/space-before-blocks.js +199 -189
  346. package/lib/rules/space-before-function-paren.js +186 -166
  347. package/lib/rules/space-in-parens.js +359 -288
  348. package/lib/rules/space-infix-ops.js +237 -201
  349. package/lib/rules/space-unary-ops.js +356 -298
  350. package/lib/rules/spaced-comment.js +363 -319
  351. package/lib/rules/strict.js +265 -230
  352. package/lib/rules/switch-colon-spacing.js +130 -122
  353. package/lib/rules/symbol-description.js +45 -48
  354. package/lib/rules/template-curly-spacing.js +148 -142
  355. package/lib/rules/template-tag-spacing.js +98 -88
  356. package/lib/rules/unicode-bom.js +54 -56
  357. package/lib/rules/use-isnan.js +237 -206
  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 +153 -111
  370. package/lib/rules/vars-on-top.js +152 -145
  371. package/lib/rules/wrap-iife.js +204 -191
  372. package/lib/rules/wrap-regex.js +70 -58
  373. package/lib/rules/yield-star-spacing.js +145 -134
  374. package/lib/rules/yoda.js +283 -272
  375. package/lib/services/parser-service.js +35 -35
  376. package/lib/services/processor-service.js +66 -73
  377. package/lib/services/suppressions-service.js +289 -0
  378. package/lib/shared/ajv.js +14 -14
  379. package/lib/shared/assert.js +3 -4
  380. package/lib/shared/ast-utils.js +7 -6
  381. package/lib/shared/deep-merge-arrays.js +24 -22
  382. package/lib/shared/directives.js +3 -2
  383. package/lib/shared/flags.js +50 -17
  384. package/lib/shared/logging.js +24 -25
  385. package/lib/shared/option-utils.js +43 -36
  386. package/lib/shared/runtime-info.js +136 -127
  387. package/lib/shared/serialization.js +27 -27
  388. package/lib/shared/severity.js +22 -22
  389. package/lib/shared/stats.js +5 -5
  390. package/lib/shared/string-utils.js +16 -16
  391. package/lib/shared/text-table.js +28 -27
  392. package/lib/shared/traverser.js +153 -146
  393. package/lib/shared/types.js +4 -27
  394. package/lib/types/index.d.ts +2010 -1559
  395. package/lib/types/rules.d.ts +5253 -5140
  396. package/lib/types/use-at-your-own-risk.d.ts +32 -30
  397. package/lib/unsupported-api.js +5 -5
  398. package/messages/all-files-ignored.js +3 -3
  399. package/messages/all-matched-files-ignored.js +3 -3
  400. package/messages/config-file-missing.js +2 -2
  401. package/messages/config-plugin-missing.js +3 -3
  402. package/messages/config-serialize-function.js +9 -7
  403. package/messages/eslintrc-incompat.js +13 -15
  404. package/messages/eslintrc-plugins.js +3 -4
  405. package/messages/extend-config-missing.js +3 -3
  406. package/messages/failed-to-read-json.js +3 -3
  407. package/messages/file-not-found.js +3 -3
  408. package/messages/invalid-rule-options.js +2 -2
  409. package/messages/invalid-rule-severity.js +2 -2
  410. package/messages/no-config-found.js +3 -3
  411. package/messages/plugin-conflict.js +8 -8
  412. package/messages/plugin-invalid.js +3 -3
  413. package/messages/plugin-missing.js +3 -3
  414. package/messages/print-config-with-directory-path.js +2 -2
  415. package/messages/shared.js +6 -1
  416. package/messages/whitespace-found.js +3 -3
  417. package/package.json +14 -20
@@ -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
 
@@ -39,8 +41,7 @@ const { SourceCode } = require("../languages/js/source-code");
39
41
 
40
42
  /** @typedef {import("../shared/types").Parser} Parser */
41
43
  /** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
42
- /** @typedef {import("../shared/types").Rule} Rule */
43
-
44
+ /** @typedef {import("../types").Rule.RuleModule} Rule */
44
45
 
45
46
  /**
46
47
  * A test case that is expected to pass lint.
@@ -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;