diff --git a/apps/oxlint/conformance/init.sh b/apps/oxlint/conformance/init.sh index 6cb9c093471e9..b9ae0cdbc1a87 100755 --- a/apps/oxlint/conformance/init.sh +++ b/apps/oxlint/conformance/init.sh @@ -3,6 +3,7 @@ set -e ESLINT_SHA="8f360ad6a7a743d33a83eed8973ee4a50731e55b" # 10.0.0-rc.0 REACT_SHA="612e371fb215498edde4c853bd1e0c8e9203808f" # 19.2.3 +STYLISTIC_SHA="5c4b512a225a314fa5f41eead9fdc4d51fc243d7" # 5.7.1 # Shallow clone a repo at a specific commit. # Git commands copied from `.github/scripts/clone-parallel.mjs`. @@ -60,3 +61,62 @@ yarn add eslint-plugin-react-hooks # Return to `submodules` directory cd ../../.. + +############################################################################### +# Stylistic +############################################################################### + +# Clone ESLint Stylistic repo into `submodules/stylistic` +clone stylistic https://github.com/eslint-stylistic/eslint-stylistic.git "$STYLISTIC_SHA" + +# Install dependencies. +# No `--ignore-workspace` because `eslint-stylistic` has its own `pnpm-workspace.yaml`. +pnpm install + +# Patch `package.json` files to add Node.js subpath imports. +# ESLint Stylistic uses TypeScript `paths` in `tsconfig.base.json` (e.g. `#test`), +# but Node/tsx doesn't respect tsconfig paths. It needs `imports` in `package.json`. +# +# Read path aliases from `tsconfig.base.json` and add them to `package.json` as `imports`. +node -e ' +const fs = require("fs"); +const path = require("path"); + +const pluginPkgPath = path.resolve("packages/eslint-plugin/package.json"); +const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgPath, "utf8")); + +// Read path aliases from `tsconfig.base.json` +const tsconfig = JSON.parse(fs.readFileSync("tsconfig.base.json", "utf8")); +const tsPaths = tsconfig.compilerOptions.paths; + +// Convert tsconfig paths format to package.json imports format +// tsconfig: { "#test": ["./shared/test-utils/index.ts"] } +// package.json: { "#test": "./shared/test-utils/index.ts" } +pluginPkg.imports = Object.fromEntries( + Object.entries(tsPaths).map(([alias, targets]) => [alias, targets[0]]), +); + +fs.writeFileSync(pluginPkgPath, JSON.stringify(pluginPkg, null, 2) + "\n"); +' + +# Node.js resolves `imports` from the nearest `package.json` with a `name` field, +# and subpath imports can only reference files within the package (no `../` allowed). +# So create a symlink to the `shared` directory within the `eslint-plugin` package. +ln -s ../../shared packages/eslint-plugin/shared + +# Replace top-level await imports in `parsers-jsx.ts` with regular imports. +# `tsx` does not support TLA with CJS output format. +if [[ "$OSTYPE" == darwin* ]]; then + sed -i '' '/^export const.*= await import/d' shared/test-utils/parsers-jsx.ts +else + sed -i '/^export const.*= await import/d' shared/test-utils/parsers-jsx.ts +fi + +cat >> shared/test-utils/parsers-jsx.ts << 'EOF' +import BABEL_ESLINT from '@babel/eslint-parser'; +import TYPESCRIPT_ESLINT from '@typescript-eslint/parser'; +export { BABEL_ESLINT, TYPESCRIPT_ESLINT }; +EOF + +# Return to `submodules` directory +cd .. diff --git a/apps/oxlint/conformance/snapshots/stylistic.md b/apps/oxlint/conformance/snapshots/stylistic.md new file mode 100644 index 0000000000000..3bc731057ea24 --- /dev/null +++ b/apps/oxlint/conformance/snapshots/stylistic.md @@ -0,0 +1,519 @@ +# Conformance test results - stylistic + +## Summary + +### Rules + +| Status | Count | % | +| ----------------- | ----- | ------ | +| Total rules | 129 | 100.0% | +| Fully passing | 127 | 98.4% | +| Partially passing | 2 | 1.6% | +| Fully failing | 0 | 0.0% | +| Load errors | 0 | 0.0% | +| No tests run | 0 | 0.0% | + +### Tests + +| Status | Count | % | +| ----------- | ----- | ------ | +| Total tests | 18416 | 100.0% | +| Passing | 18303 | 99.4% | +| Failing | 7 | 0.0% | +| Skipped | 106 | 0.6% | + +## Fully Passing Rules + +- `array-bracket-newline` (210 tests) (1 skipped) +- `array-bracket-spacing` (143 tests) +- `array-element-newline` (167 tests) +- `arrow-parens` (110 tests) +- `arrow-spacing` (48 tests) +- `block-spacing` (101 tests) +- `brace-style` (164 tests) +- `brace-style` (16 tests) +- `comma-dangle` (333 tests) (2 skipped) +- `comma-dangle` (74 tests) +- `comma-spacing` (173 tests) +- `comma-spacing` (13 tests) (2 skipped) +- `comma-style` (140 tests) (10 skipped) +- `computed-property-spacing` (126 tests) +- `computed-property-spacing` (10 tests) +- `curly-newline` (111 tests) +- `dot-location` (80 tests) +- `dot-location` (4 tests) +- `dot-location` (9 tests) +- `eol-last` (20 tests) (20 skipped) +- `eol-last` (4 tests) (2 skipped) +- `eol-last` (35 tests) +- `function-call-argument-newline` (64 tests) +- `function-call-spacing` (200 tests) +- `function-call-spacing` (27 tests) (1 skipped) +- `function-paren-newline` (198 tests) +- `generator-star-spacing` (203 tests) +- `implicit-arrow-linebreak` (62 tests) +- `indent` (1096 tests) (1 skipped) +- `indent` (413 tests) (17 skipped) +- `indent` (159 tests) +- `indent-binary-ops` (48 tests) (2 skipped) +- `jsx-child-element-spacing` (82 tests) +- `jsx-closing-bracket-location` (327 tests) +- `jsx-closing-tag-location` (44 tests) +- `jsx-curly-brace-presence` (392 tests) +- `jsx-curly-newline` (72 tests) +- `jsx-curly-spacing` (886 tests) +- `jsx-equals-spacing` (72 tests) +- `jsx-first-prop-new-line` (88 tests) +- `jsx-function-call-newline` (81 tests) +- `jsx-indent` (436 tests) (16 skipped) +- `jsx-indent-props` (120 tests) +- `jsx-max-props-per-line` (119 tests) +- `jsx-newline` (84 tests) +- `jsx-one-expression-per-line` (342 tests) +- `jsx-pascal-case` (128 tests) +- `jsx-quotes` (18 tests) +- `jsx-self-closing-comp` (141 tests) +- `jsx-sort-props` (289 tests) +- `jsx-wrap-multilines` (399 tests) +- `key-spacing` (185 tests) +- `key-spacing` (97 tests) +- `keyword-spacing` (1096 tests) +- `keyword-spacing` (81 tests) (1 skipped) +- `line-comment-position` (38 tests) +- `linebreak-style` (12 tests) (12 skipped) +- `linebreak-style` (2 tests) (1 skipped) +- `linebreak-style` (14 tests) +- `lines-around-comment` (199 tests) +- `lines-around-comment` (62 tests) +- `lines-between-class-members` (112 tests) +- `lines-between-class-members` (13 tests) +- `list-style` (9 tests) (9 skipped) +- `list-style` (96 tests) +- `max-len` (108 tests) +- `max-statements-per-line` (99 tests) +- `member-delimiter-style` (160 tests) +- `multiline-comment-style` (124 tests) +- `multiline-ternary` (147 tests) +- `new-parens` (42 tests) +- `newline-per-chained-call` (37 tests) +- `no-confusing-arrow` (30 tests) +- `no-extra-parens` (1114 tests) +- `no-extra-parens` (109 tests) +- `no-extra-semi` (50 tests) +- `no-extra-semi` (10 tests) +- `no-floating-decimal` (8 tests) +- `no-mixed-operators` (40 tests) +- `no-mixed-spaces-and-tabs` (61 tests) +- `no-multi-spaces` (67 tests) +- `no-multi-spaces` (133 tests) +- `no-multiple-empty-lines` (46 tests) +- `no-tabs` (11 tests) +- `no-trailing-spaces` (54 tests) +- `no-whitespace-before-property` (192 tests) +- `no-whitespace-before-property` (9 tests) +- `nonblock-statement-body-position` (48 tests) +- `object-curly-newline` (144 tests) (2 skipped) +- `object-curly-newline` (335 tests) +- `object-curly-spacing` (201 tests) +- `object-curly-spacing` (122 tests) +- `object-property-newline` (73 tests) +- `object-property-newline` (28 tests) +- `one-var-declaration-per-line` (38 tests) (2 skipped) +- `operator-linebreak` (161 tests) +- `padded-blocks` (167 tests) +- `padding-line-between-statements` (685 tests) +- `padding-line-between-statements` (49 tests) +- `quote-props` (142 tests) +- `quote-props` (18 tests) +- `quotes` (132 tests) +- `quotes` (48 tests) +- `rest-spread-spacing` (82 tests) +- `semi` (309 tests) (1 skipped) +- `semi` (42 tests) (2 skipped) +- `semi-spacing` (66 tests) +- `semi-spacing` (17 tests) +- `semi-style` (85 tests) +- `space-before-blocks` (161 tests) +- `space-before-blocks` (26 tests) +- `space-before-function-paren` (87 tests) +- `space-before-function-paren` (13 tests) +- `space-in-parens` (139 tests) +- `space-infix-ops` (74 tests) +- `space-infix-ops` (182 tests) (1 skipped) +- `space-unary-ops` (116 tests) +- `spaced-comment` (102 tests) +- `switch-colon-spacing` (46 tests) +- `template-curly-spacing` (57 tests) +- `template-tag-spacing` (63 tests) +- `type-annotation-spacing` (478 tests) +- `type-generic-spacing` (33 tests) (1 skipped) +- `type-named-tuple-spacing` (16 tests) +- `wrap-iife` (128 tests) +- `wrap-regex` (8 tests) +- `yield-star-spacing` (48 tests) + +## Rules with Failures + +- `jsx-props-no-multi-spaces` - 81 / 82 (98.8%) +- `jsx-tag-spacing` - 211 / 217 (97.2%) + +## Rules with Failures Detail + +### `jsx-props-no-multi-spaces` + +Pass: 81 / 82 (98.8%) +Fail: 1 / 82 (1.2%) +Skip: 0 / 82 (0.0%) + +#### jsx-props-no-multi-spaces > valid + +```js + foo bar /> +// features: [ts,no-babel], parser: @typescript-eslint/parser, , , +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "modules": true, + "legacyDecorators": false + } + }, + "parser": {} + }, + "_parser": { + "specifier": "@typescript-eslint/parser", + "lang": "ts" + } +} +``` + +TypeError: Cannot read properties of undefined (reading 'end') + at hasEmptyLines (apps/oxlint/conformance/submodules/stylistic/packages/eslint-plugin/rules/jsx-props-no-multi-spaces/jsx-props-no-multi-spaces.ts:63:44) + at checkSpacing (apps/oxlint/conformance/submodules/stylistic/packages/eslint-plugin/rules/jsx-props-no-multi-spaces/jsx-props-no-multi-spaces.ts:71:11) + at (apps/oxlint/conformance/submodules/stylistic/packages/eslint-plugin/rules/jsx-props-no-multi-spaces/jsx-props-no-multi-spaces.ts:132:11) + + +### `jsx-tag-spacing` + +Pass: 211 / 217 (97.2%) +Fail: 6 / 217 (2.8%) +Skip: 0 / 217 (0.0%) + +#### jsx-tag-spacing > valid + +```js +< /App> +// features: [no-ts], parser: default, , options: [{"closingSlash":"always","beforeSelfClosing":"allow","afterOpening":"allow","beforeClosing":"allow"}], +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + } + }, + "options": [ + { + "closingSlash": "always", + "beforeSelfClosing": "allow", + "afterOpening": "allow", + "beforeClosing": "allow" + } + ] +} +``` + +Error: Overlapping token/comments: last end: 75, next start: 24 + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) + + +#### jsx-tag-spacing > valid + +```js +< /App> +// features: [no-ts], parser: @babel/eslint-parser, , options: [{"closingSlash":"always","beforeSelfClosing":"allow","afterOpening":"allow","beforeClosing":"allow"}], +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "modules": true, + "legacyDecorators": false + }, + "requireConfigFile": false, + "babelOptions": { + "presets": [ + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-syntax-do-expressions", + "@babel/plugin-syntax-function-bind", + [ + "@babel/plugin-syntax-decorators", + { + "legacy": true + } + ] + ], + "parserOpts": { + "allowSuperOutsideMethod": false, + "allowReturnOutsideFunction": false + } + } + }, + "parser": {} + }, + "options": [ + { + "closingSlash": "always", + "beforeSelfClosing": "allow", + "afterOpening": "allow", + "beforeClosing": "allow" + } + ], + "_parser": { + "specifier": "@babel/eslint-parser", + "lang": "ts" + } +} +``` + +Error: Overlapping token/comments: last end: 88, next start: 24 + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) + + +#### jsx-tag-spacing > invalid + +```js +
< /div>; +// features: [no-ts], parser: default, , options: [{"closingSlash":"never","beforeSelfClosing":"allow","afterOpening":"allow","beforeClosing":"allow"}], +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + } + }, + "output": "
;\n// features: [no-ts], parser: default, , options: [{\"closingSlash\":\"never\",\"beforeSelfClosing\":\"allow\",\"afterOpening\":\"allow\",\"beforeClosing\":\"allow\"}], ", + "errors": [ + { + "messageId": "closeSlashNoSpace" + } + ], + "options": [ + { + "closingSlash": "never", + "beforeSelfClosing": "allow", + "afterOpening": "allow", + "beforeClosing": "allow" + } + ] +} +``` + +Error: Overlapping token/comments: last end: 81, next start: 30 + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) + + +#### jsx-tag-spacing > invalid + +```js +
< /div>; +// features: [no-ts], parser: @babel/eslint-parser, , options: [{"closingSlash":"never","beforeSelfClosing":"allow","afterOpening":"allow","beforeClosing":"allow"}], +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "modules": true, + "legacyDecorators": false + }, + "requireConfigFile": false, + "babelOptions": { + "presets": [ + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-syntax-do-expressions", + "@babel/plugin-syntax-function-bind", + [ + "@babel/plugin-syntax-decorators", + { + "legacy": true + } + ] + ], + "parserOpts": { + "allowSuperOutsideMethod": false, + "allowReturnOutsideFunction": false + } + } + }, + "parser": {} + }, + "output": "
;\n// features: [no-ts], parser: @babel/eslint-parser, , options: [{\"closingSlash\":\"never\",\"beforeSelfClosing\":\"allow\",\"afterOpening\":\"allow\",\"beforeClosing\":\"allow\"}], ", + "errors": [ + { + "messageId": "closeSlashNoSpace" + } + ], + "options": [ + { + "closingSlash": "never", + "beforeSelfClosing": "allow", + "afterOpening": "allow", + "beforeClosing": "allow" + } + ], + "_parser": { + "specifier": "@babel/eslint-parser", + "lang": "ts" + } +} +``` + +Error: Overlapping token/comments: last end: 94, next start: 30 + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) + + +#### jsx-tag-spacing > invalid + +```js + +
< + /div>; + +// features: [no-ts], parser: default, , options: [{"closingSlash":"never","beforeSelfClosing":"allow","afterOpening":"allow","beforeClosing":"allow"}], +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + } + }, + "output": "\n
;\n \n// features: [no-ts], parser: default, , options: [{\"closingSlash\":\"never\",\"beforeSelfClosing\":\"allow\",\"afterOpening\":\"allow\",\"beforeClosing\":\"allow\"}], ", + "errors": [ + { + "messageId": "closeSlashNoSpace" + } + ], + "options": [ + { + "closingSlash": "never", + "beforeSelfClosing": "allow", + "afterOpening": "allow", + "beforeClosing": "allow" + } + ] +} +``` + +Error: Overlapping token/comments: last end: 105, next start: 54 + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) + + +#### jsx-tag-spacing > invalid + +```js + +
< + /div>; + +// features: [no-ts], parser: @babel/eslint-parser, , options: [{"closingSlash":"never","beforeSelfClosing":"allow","afterOpening":"allow","beforeClosing":"allow"}], +``` + +```json +{ + "languageOptions": { + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "modules": true, + "legacyDecorators": false + }, + "requireConfigFile": false, + "babelOptions": { + "presets": [ + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-syntax-do-expressions", + "@babel/plugin-syntax-function-bind", + [ + "@babel/plugin-syntax-decorators", + { + "legacy": true + } + ] + ], + "parserOpts": { + "allowSuperOutsideMethod": false, + "allowReturnOutsideFunction": false + } + } + }, + "parser": {} + }, + "output": "\n
;\n \n// features: [no-ts], parser: @babel/eslint-parser, , options: [{\"closingSlash\":\"never\",\"beforeSelfClosing\":\"allow\",\"afterOpening\":\"allow\",\"beforeClosing\":\"allow\"}], ", + "errors": [ + { + "messageId": "closeSlashNoSpace" + } + ], + "options": [ + { + "closingSlash": "never", + "beforeSelfClosing": "allow", + "afterOpening": "allow", + "beforeClosing": "allow" + } + ], + "_parser": { + "specifier": "@babel/eslint-parser", + "lang": "ts" + } +} +``` + +Error: Overlapping token/comments: last end: 118, next start: 54 + at debugCheckValidRanges (apps/oxlint/dist/lint.js) + at debugCheckTokensAndComments (apps/oxlint/dist/lint.js) + at initTokensAndComments (apps/oxlint/dist/lint.js) + at Object.isSpaceBetween (apps/oxlint/dist/lint.js) + diff --git a/apps/oxlint/conformance/src/groups/index.ts b/apps/oxlint/conformance/src/groups/index.ts index 6bec505f2c45b..eee5ddf27c7d5 100644 --- a/apps/oxlint/conformance/src/groups/index.ts +++ b/apps/oxlint/conformance/src/groups/index.ts @@ -2,5 +2,6 @@ import type { TestGroup } from "../index.ts"; import eslint from "./eslint.ts"; import reactHooks from "./react_hooks.ts"; +import stylistic from "./stylistic.ts"; -export const TEST_GROUPS: TestGroup[] = [eslint, reactHooks]; +export const TEST_GROUPS: TestGroup[] = [eslint, reactHooks, stylistic]; diff --git a/apps/oxlint/conformance/src/groups/stylistic.ts b/apps/oxlint/conformance/src/groups/stylistic.ts new file mode 100644 index 0000000000000..784bd09774fd7 --- /dev/null +++ b/apps/oxlint/conformance/src/groups/stylistic.ts @@ -0,0 +1,328 @@ +import { unindent } from "eslint-vitest-rule-tester"; +import { RuleTester } from "../rule_tester.ts"; + +import type { TestGroup } from "../index.ts"; +import type { + ValidTestCase, + InvalidTestCase, + TestCase, + LanguageOptions, + ParserOptions, +} from "../rule_tester.ts"; +import type { Rule, RuleTester as RuleTesterType } from "#oxlint"; + +type Config = RuleTesterType.Config; + +const group: TestGroup = { + name: "stylistic", + + submoduleName: "stylistic", + testFilesDirPath: "packages/eslint-plugin/rules", + + transformTestFilename(filename: string) { + // Each rule has its own directory in `packages/eslint-plugin/rules`. + // Test files are in those subdirectories. + // e.g. `packages/eslint-plugin/rules/indent/indent.test.ts` + if (!filename.endsWith(".test.ts")) return null; + const parts = filename.split("/"); + if (parts.length !== 2) return null; + return parts[0]; + }, + + prepare(require: NodeJS.Require, mock: (path: string, value: unknown) => void) { + // Load the copy of `@typescript-eslint/parser` which is used by the test cases + const tsEslintParser = require("@typescript-eslint/parser"); + + // Mock `eslint-plugin-stylistic`'s rule tester, to use conformance `RuleTester` + mock("../../../shared/test-utils/runner.ts", createStylisticRuleRunnerMock(tsEslintParser)); + }, + + shouldSkipTest(ruleName: string, test: TestCase, code: string, err: Error): boolean { + // Skip test cases which start with `/* eslint */` comments. + // `RuleTester` does not support enabling other rules beyond the rule under test. + if (code.match(/^\s*\/\*\s*eslint\s/)) return true; + + // Invalid code, cannot parse + if ( + err.message === "Parsing failed" && + ruleName === "comma-spacing" && + ["let foo,", "let foo ,"].includes(code) + ) { + return true; + } + + // Test cases should be parsed as Flow. AST is different when parsed as TS. + if ( + ruleName === "object-curly-newline" && + test._parser?.specifier === "@babel/eslint-parser" && + (test.languageOptions?.parserOptions as any)?.babelOptions?.parserOpts?.plugins?.includes( + "flow", + ) && + [ + "function foo({\n a,\n b\n} : { a : string, b : string }) {}", + "function foo({ a, b } : { a : string, b : string }) {}", + ].includes(code) + ) { + return true; + } + + // Invalid code, Oxc's AST does not match TS-ESLint + if ( + ruleName === "space-infix-ops" && + compact(code) === "class Test {\n accessor optional?= false;\n }" + ) { + return true; + } + + // Code contains `do` expressions which Oxc parser does not support + if ( + ruleName === "jsx-indent" && + err.message === "Parsing failed" && + code.match(/^\s*\s*\{\(?do \{/) + ) { + return true; + } + + // Use custom language plugin + if ( + (ruleName === "eol-last" || ruleName === "linebreak-style") && + (test as any).configs?.plugins !== undefined + ) { + return true; + } + + // Faulty test cases - no message or message ID provided for the errors + if ( + ruleName === "one-var-declaration-per-line" && + err.message === "Test error must specify either a `messageId` or `message`" + ) { + return true; + } + + // `eslint-vitest-rule-tester` does not detect duplicate test cases, and tests do contain duplicates + if (err.message === "Detected duplicate test case") { + return true; + } + + // TS parser incorrectly parses closing JSX tag with whitespace before `/`. + // We don't skip these tests because we should be able to make them pass. + /* + if ( + ruleName === "jsx-tag-spacing" && + (code.startsWith('< /App>') || + code.startsWith('
< /div>;') || + compact(code).startsWith('
<\n /div>;')) + ) { + return true; + } + */ + + return false; + }, + + ruleTesters: [], + parsers: [ + { specifier: "@typescript-eslint/parser", lang: "ts" }, + { specifier: "@babel/eslint-parser", lang: "ts" }, + ], +}; + +export default group; + +/** + * Options passed to `run` function in `eslint-plugin-stylistic`'s rule tester module. + */ +interface StylisticRunOptions { + name: string; + rule: Rule; + valid: (ValidTestCase | string)[]; + invalid: InvalidTestCase[]; + lang?: "js" | "ts" | "json" | "css"; + parserOptions?: StylisticParserOptions; + recursive?: number | false; + linterOptions?: unknown; + configs?: unknown; +} + +/** + * `eslint-vitest-rule-tester` takes a single `parserOptions` object, + * which comprises of options which end up in `languageOptions` and `languageOptions.parserOptions`. + */ +interface StylisticParserOptions extends ParserOptions { + ecmaVersion?: number | "latest"; + sourceType?: "script" | "module" | "commonjs"; +} + +/** + * `eslint-vitest-rule-tester` takes `parserOptions` as a property of test case. + */ +type StylisticTestCase = TestCase & { parserOptions?: StylisticParserOptions }; + +// List of keys that `StylisticRunOptions` can have. +// Must be kept in sync with properties of `StylisticRunOptions`. +// The type constraints enforce this. +const OPTIONS_KEYS_ARRAY = [ + "name", + "rule", + "valid", + "invalid", + "lang", + "parserOptions", + "recursive", + "linterOptions", + "configs", +] as const satisfies readonly (keyof StylisticRunOptions)[]; + +type MissingKeys = Exclude; +type KeysSet = MissingKeys extends never ? Set : never; + +const OPTIONS_KEYS: KeysSet = new Set(OPTIONS_KEYS_ARRAY); + +/** + * Create a module to replace `eslint-plugin-stylistic`'s rule runner module, + * which presents the same API, but used conformance `RuleTester`. + * + * @param tsEslintParser - TSESLint parser module + * @returns Module to replace `eslint-plugin-stylistic`'s rule runner module with + */ +function createStylisticRuleRunnerMock(tsEslintParser: any) { + return { + run(options: StylisticRunOptions) { + // Validate options + const extraKeys = Object.keys(options).filter((key) => !OPTIONS_KEYS.has(key)); + if (extraKeys.length > 0) { + throw new Error(`Unexpected keys in options passed to \`run\`: ${extraKeys.join(", ")}`); + } + + // Get parser from `lang` option. + // If no `lang` option provided, default is TS. + let parser: LanguageOptions["parser"] | null = null; + const { lang } = options; + if (lang === undefined || lang === "ts") { + parser = tsEslintParser; + } else if (lang !== "js") { + // 'json' | 'css'. + // Cause "custom parsers are not supported" error later on by setting an unknown parser. + parser = { _unsupportedParser: true, lang } as LanguageOptions["parser"]; + } + + const config: Config = {}; + if (parser !== null || options.parserOptions != null) { + const languageOptions = + options.parserOptions == null ? {} : getLanguageOptions(options.parserOptions, undefined); + if (parser !== null) languageOptions.parser = parser; + config.languageOptions = languageOptions; + } + + // Add other options to config. + // These don't have any effect, but we add them so they appear in snapshot. + if (options.linterOptions != null) (config as any).linterOptions = options.linterOptions; + if (options.configs != null) (config as any).configs = options.configs; + + // Convert test cases + const valid = options.valid ?? []; + for (const test of valid) { + if (typeof test === "object") modifyValidTestCase(test); + } + + const invalid = options.invalid ?? []; + for (const test of invalid) { + modifyInvalidTestCase(test); + } + + // Run tests + const tester = new RuleTester(config); + tester.run(options.name, options.rule, { valid, invalid }); + }, + + unindent, + $: unindent, + }; +} + +/** + * Modify a valid test case from `eslint-vitest-rule-tester`'s object shape to what `RuleTester` expects. + * @param test - Test case + */ +function modifyValidTestCase(test: ValidTestCase) { + modifyTestCase(test); +} + +/** + * Modify an invalid test case from `eslint-vitest-rule-tester`'s object shape to what `RuleTester` expects. + * @param test - Test case + */ +function modifyInvalidTestCase(test: InvalidTestCase) { + modifyTestCase(test); + + // Handle difference in `errors` property + const { errors } = test; + if (errors == null) { + // `eslint-vitest-rule-tester` allows `errors` prop to be missing. + // Set it to `__unknown__`. `RuleTester` will skip the check that errors match expected. + (test.errors as unknown as string) = "__unknown__"; + } else if (Array.isArray(errors)) { + // `eslint-vitest-rule-tester` treats strings as message IDs + for (let i = 0; i < errors.length; i++) { + const error = errors[i]; + if (typeof error === "string") errors[i] = { messageId: error }; + } + } +} + +/** + * `eslint-vitest-rule-tester` takes `parserOptions` and `parser` as properties of test case. + * Move them to be properties of `languageOptions`. + * @param test - Test case + */ +function modifyTestCase(test: StylisticTestCase) { + const { parserOptions } = test; + if (parserOptions != null) { + delete test.parserOptions; + test.languageOptions = getLanguageOptions(parserOptions, test.languageOptions); + } + + const parser = test.parser as LanguageOptions["parser"]; + if (parser != null) { + if (test.languageOptions == null) test.languageOptions = {}; + test.languageOptions.parser = parser; + delete test.parser; + } +} + +/** + * Get language options from `StylisticParserOptions` and `LanguageOptions`. + * @param parserOptions - Parser options from options passed to `run` function, or included as properties of test case + * @param languageOptions - Language options from test case object + * @returns `LanguageOptions` to add to config / test case, combining `parserOptions` and `languageOptions` + */ +function getLanguageOptions( + parserOptions: StylisticParserOptions, + languageOptions?: LanguageOptions, +): LanguageOptions { + languageOptions = { ...languageOptions }; + parserOptions = { ...parserOptions }; + + if (parserOptions.ecmaVersion != null) { + languageOptions.ecmaVersion ??= parserOptions.ecmaVersion; + delete parserOptions.ecmaVersion; + } + + if (parserOptions.sourceType != null) { + languageOptions.sourceType ??= parserOptions.sourceType; + delete parserOptions.sourceType; + } + + if (Object.keys(parserOptions).length !== 0) languageOptions.parserOptions = parserOptions; + + return languageOptions; +} + +/** + * Compact whitespace in code. + * @param code - Code + * @returns Code with whitespace compacted + */ +function compact(code: string): string { + return code.trim().replace(/\n\s+/g, "\n "); +} diff --git a/apps/oxlint/conformance/src/rule_tester.ts b/apps/oxlint/conformance/src/rule_tester.ts index 5fcbf2e43aac5..03c4df84aeeb1 100644 --- a/apps/oxlint/conformance/src/rule_tester.ts +++ b/apps/oxlint/conformance/src/rule_tester.ts @@ -10,13 +10,18 @@ import { SHOULD_SKIP_CODE } from "./filter.ts"; import type { Rule } from "#oxlint"; import type { ParserDetails } from "./index.ts"; -import type { LanguageOptionsInternal } from "../../src-js/package/rule_tester.ts"; +import type { + LanguageOptionsInternal, + ParserOptionsInternal, +} from "../../src-js/package/rule_tester.ts"; type DescribeFn = RuleTester.DescribeFn; type ItFn = RuleTester.ItFn; type TestCases = RuleTester.TestCases; type Globals = RuleTester.Globals; export type Language = RuleTester.Language; +export type LanguageOptions = LanguageOptionsInternal; +export type ParserOptions = ParserOptionsInternal; interface TestCaseExtension { languageOptions?: LanguageOptionsInternal; diff --git a/apps/oxlint/conformance/tester.ts b/apps/oxlint/conformance/tester.ts index 760c2858e4815..b90ced22c9408 100644 --- a/apps/oxlint/conformance/tester.ts +++ b/apps/oxlint/conformance/tester.ts @@ -8,10 +8,12 @@ // Use the conformance testing version of `RuleTester`, // which modifies test cases before they run import { RuleTester as OxlintRuleTester } from "./src/rule_tester.ts"; -// @ts-expect-error - internal module of ESLint with no types -import { RuleTester as ESLintRuleTester } from "./submodules/eslint/lib/rule-tester/index.js"; +// // @ts-expect-error - internal module of ESLint with no types +// import { RuleTester as ESLintRuleTester } from "./submodules/eslint/lib/rule-tester/index.js"; +import { RuleTester as ESLintRuleTester } from "eslint"; // @ts-expect-error - internal module of ESLint with no types import { builtinRules } from "./submodules/eslint/lib/unsupported-api.js"; +import tsEslintParser from "@typescript-eslint/parser"; import type { Rule } from "#oxlint"; import type { ValidTestCase, InvalidTestCase } from "./src/rule_tester.ts"; @@ -133,6 +135,9 @@ Leave the eslint repo as it was before you started, ready to investigate another -----------------------------------------------------------------------------*/ const ruleName = "block-scoped-var"; +const rule = builtinRules.get(ruleName) as unknown as Rule; + +// import rule from "./submodules/stylistic/packages/eslint-plugin/rules/jsx-props-no-multi-spaces/jsx-props-no-multi-spaces.ts"; const code = `for (var a = 0;;) {} a;`; @@ -157,35 +162,47 @@ const testCase: TestCaseWithoutCode = { // Run test case with both ESLint and Oxlint // ----------------------------------------------------------------------------- -runBoth(ruleName, isInvalid, code, testCase); +runBoth(rule, isInvalid, code, testCase); -function runBoth( - ruleName: string, - isInvalid: boolean, - code: string, - testCase: TestCaseWithoutCode, -) { +function runBoth(rule: Rule, isInvalid: boolean, code: string, testCase: TestCaseWithoutCode) { testCase = { code, ...testCase }; - const valid: ValidTestCase[] = [], - invalid: InvalidTestCase[] = []; + // Run case with ESLint + console.log("--------------------"); + console.log("ESLint"); + console.log("--------------------"); + + const eslintValid: ValidTestCase[] = [], + eslintInvalid: InvalidTestCase[] = []; if (isInvalid) { - invalid.push(testCase as InvalidTestCase); + eslintInvalid.push(testCase as InvalidTestCase); } else { - valid.push(testCase as ValidTestCase); + eslintValid.push(testCase as ValidTestCase); } - const rule = builtinRules.get(ruleName) as unknown as Rule; - - console.log("--------------------"); - console.log("ESLint"); - console.log("--------------------"); - runOne(ESLintRuleTester, rule, valid, invalid); + runOne(ESLintRuleTester, rule, eslintValid, eslintInvalid); + // Run case with Oxlint console.log("\n--------------------"); console.log("Oxlint"); console.log("--------------------"); - runOne(OxlintRuleTester, rule, valid, invalid); + + if (testCase?.languageOptions?.parser === tsEslintParser) { + testCase.languageOptions = { parserOptions: {}, ...testCase.languageOptions }; + delete testCase.languageOptions.parser; + testCase.languageOptions.parserOptions!.lang = + testCase.languageOptions.parserOptions!.ecmaFeatures?.jsx === true ? "tsx" : "ts"; + } + + const oxlintValid: ValidTestCase[] = [], + oxlintInvalid: InvalidTestCase[] = []; + if (isInvalid) { + oxlintInvalid.push(testCase as InvalidTestCase); + } else { + oxlintValid.push(testCase as ValidTestCase); + } + + runOne(OxlintRuleTester, rule, oxlintValid, oxlintInvalid); } function runOne(RuleTester: any, rule: Rule, valid: ValidTestCase[], invalid: InvalidTestCase[]) { diff --git a/apps/oxlint/package.json b/apps/oxlint/package.json index e73c2a50bac70..b9ef749c743be 100644 --- a/apps/oxlint/package.json +++ b/apps/oxlint/package.json @@ -31,10 +31,12 @@ "@types/json-schema": "^7.0.15", "@types/json-stable-stringify-without-jsonify": "^1.0.2", "@types/node": "catalog:", + "@typescript-eslint/parser": "^8.53.1", "@typescript-eslint/scope-manager": "8.53.1", "ajv": "^6.12.6", "cross-env": "catalog:", "eslint": "catalog:", + "eslint-vitest-rule-tester": "^3.0.1", "esquery": "^1.6.0", "execa": "^9.6.0", "json-stable-stringify-without-jsonify": "^1.0.1", diff --git a/apps/oxlint/src-js/package/rule_tester.ts b/apps/oxlint/src-js/package/rule_tester.ts index f370f534e28c8..d13ec82173e3a 100644 --- a/apps/oxlint/src-js/package/rule_tester.ts +++ b/apps/oxlint/src-js/package/rule_tester.ts @@ -193,7 +193,7 @@ interface ParserOptions { * but could be if test cases are ported from ESLint. * For internal use only. */ -interface ParserOptionsInternal extends ParserOptions { +export interface ParserOptionsInternal extends ParserOptions { ecmaFeatures?: EcmaFeaturesInternal; } @@ -568,6 +568,10 @@ function assertInvalidTestCasePasses(test: InvalidTestCase, plugin: Plugin, conf if (typeof errors === "number") { // If `errors` is a number, it's expected error count assertErrorCountIsCorrect(diagnostics, errors); + } else if (CONFORMANCE && (errors as unknown as string) === "__unknown__") { + // In conformance tests, sometimes test cases don't specify `errors` property + // (e.g. `eslint-plugin-stylistic`'s test cases). Conformance tester sets `errors` to `"__unknown__"` + // in those cases. So don't error here. } else { // `errors` is an array of error objects assertErrorCountIsCorrect(diagnostics, errors.length); @@ -1361,6 +1365,10 @@ function assertInvalidTestCaseIsWellFormed( const { errors } = test; if (typeof errors === "number") { assert(errors > 0, "Invalid cases must have `errors` value greater than 0"); + } else if (CONFORMANCE && (errors as unknown as string) === "__unknown__") { + // In conformance tests, sometimes test cases don't specify `errors` property + // (e.g. `eslint-plugin-stylistic`'s test cases). Conformance tester sets `errors` to `"__unknown__"` + // in those cases. So don't error here. } else { assert( errors !== undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a893e2aa660c5..e07bfeb0ef23b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.1.0 + '@typescript-eslint/parser': + specifier: ^8.53.1 + version: 8.53.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': specifier: 8.53.1 version: 8.53.1(patch_hash=15e9da78cb0de614fd78a94fbaca0b199efb09049db999b533c420a2125ab6a3) @@ -140,6 +143,9 @@ importers: eslint: specifier: 'catalog:' version: 9.36.0(jiti@2.6.1) + eslint-vitest-rule-tester: + specifier: ^3.0.1 + version: 3.0.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15) esquery: specifier: ^1.6.0 version: 1.6.0 @@ -1022,6 +1028,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2705,14 +2717,46 @@ packages: '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@typescript-eslint/parser@8.53.1': + resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.53.1': + resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.53.1': resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.53.1': + resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.53.1': resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.53.1': + resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.53.1': + resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.53.1': resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3575,6 +3619,12 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-vitest-rule-tester@3.0.1: + resolution: {integrity: sha512-6VoHuVEqj2A7pVQmJrMY8+FwJjC0cMxuO36DRf50wClN4K36l4OOw6J0KpIP/BCBwF+BHWIJy3taZDkmFo+Kow==} + peerDependencies: + eslint: ^9.10.0 + vitest: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + eslint@9.36.0: resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5225,6 +5275,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tsdown@0.20.1: resolution: {integrity: sha512-Wo1BzqNQVZ6SFQV8rjQBwMmNubO+yV3F+vp2WNTjEaS4S5CT1C1dHtUbeFMrCEasZpGy5w6TshpehNnfTe8QBQ==} engines: {node: '>=20.19.0'} @@ -6203,6 +6259,11 @@ snapshots: eslint: 9.36.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.36.0(jiti@2.6.1))': + dependencies: + eslint: 9.36.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.1': @@ -7842,13 +7903,64 @@ snapshots: '@types/whatwg-mimetype@3.0.2': {} + '@typescript-eslint/parser@8.53.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.53.1(patch_hash=15e9da78cb0de614fd78a94fbaca0b199efb09049db999b533c420a2125ab6a3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.53.1(patch_hash=15e9da78cb0de614fd78a94fbaca0b199efb09049db999b533c420a2125ab6a3)': dependencies: '@typescript-eslint/types': 8.53.1 '@typescript-eslint/visitor-keys': 8.53.1 + '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/types@8.53.1': {} + '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.53.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.36.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.53.1(patch_hash=15e9da78cb0de614fd78a94fbaca0b199efb09049db999b533c420a2125ab6a3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.53.1': dependencies: '@typescript-eslint/types': 8.53.1 @@ -8902,6 +9014,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-vitest-rule-tester@3.0.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.15): + dependencies: + '@typescript-eslint/utils': 8.53.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + vitest: 4.0.15(@types/node@24.1.0)(@vitest/browser-playwright@4.0.15)(happy-dom@20.0.11)(jiti@2.6.1)(terser@5.44.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + - typescript + eslint@9.36.0(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) @@ -10625,6 +10746,10 @@ snapshots: tree-kill@1.2.2: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tsdown@0.20.1(@arethetypeswrong/core@0.18.2)(publint@0.3.15)(typescript@5.9.3): dependencies: ansis: 4.2.0