eslint 8.57.1 → 9.39.1

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