diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index fba82fd3..d2fb9ae0 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -25,6 +25,7 @@ jobs: matrix: node-version: ${{ fromJson(needs.matrix.outputs.latest) }} eslint: + - 10 - 9 - 8 - 7 @@ -33,6 +34,42 @@ jobs: - 4 - 3 exclude: + - node-version: 23 + eslint: 10 + - node-version: 21 + eslint: 10 + - node-version: 19 + eslint: 10 + - node-version: 18 + eslint: 10 + - node-version: 17 + eslint: 10 + - node-version: 16 + eslint: 10 + - node-version: 15 + eslint: 10 + - node-version: 14 + eslint: 10 + - node-version: 13 + eslint: 10 + - node-version: 12 + eslint: 10 + - node-version: 11 + eslint: 10 + - node-version: 10 + eslint: 10 + - node-version: 9 + eslint: 10 + - node-version: 8 + eslint: 10 + - node-version: 7 + eslint: 10 + - node-version: 6 + eslint: 10 + - node-version: 5 + eslint: 10 + - node-version: 4 + eslint: 10 - node-version: 16 eslint: 9 - node-version: 15 diff --git a/__tests__/__util__/RuleTester.js b/__tests__/__util__/RuleTester.js index a41dea76..f77bde66 100644 --- a/__tests__/__util__/RuleTester.js +++ b/__tests__/__util__/RuleTester.js @@ -1,29 +1,36 @@ import test from 'tape'; -import mockProperty from 'mock-property'; import { RuleTester } from 'eslint'; -const orig = RuleTester.prototype.run; -RuleTester.prototype.run = function (name, rule, tests) { - test(`RuleTester: ${name}`, (t) => { - t.teardown(mockProperty(RuleTester.describe, 't', { value: t })); - orig.call(this, name, rule, tests); +// Use only the documented RuleTester.describe and RuleTester.it hooks +// to integrate with tape. This avoids overriding RuleTester.prototype.run, +// which relies on undocumented internal stack-frame assumptions that +// changed in ESLint 10. - t.end(); - }); -}; +let currentTest = null; -RuleTester.describe = function (text, method) { - RuleTester.it.title = text; - const self = this; - RuleTester.describe.t.test(text, (t) => { - t.teardown(mockProperty(RuleTester.it, 't', { value: t })); - method.call(self); - t.end(); - }); +RuleTester.describe = function describe(text, method) { + if (currentTest) { + // Nested describe (e.g. 'valid', 'invalid') — create a subtest + const parent = currentTest; + currentTest.test(text, (t) => { + currentTest = t; + method.call(this); + t.end(); + currentTest = parent; + }); + } else { + // Top-level describe (rule name) — create a new tape test + test(text, (t) => { + currentTest = t; + method.call(this); + t.end(); + currentTest = null; + }); + } }; -RuleTester.it = function (text, method) { - RuleTester.it.t.doesNotThrow(method, `${RuleTester.it.title}: ${text}`); +RuleTester.it = function it(text, method) { + currentTest.doesNotThrow(method, text); }; export default RuleTester; diff --git a/__tests__/__util__/parserOptionsMapper.js b/__tests__/__util__/parserOptionsMapper.js index 790e5462..73c7aa61 100644 --- a/__tests__/__util__/parserOptionsMapper.js +++ b/__tests__/__util__/parserOptionsMapper.js @@ -1,7 +1,11 @@ import { version as eslintVersion } from 'eslint/package.json'; import semver from 'semver'; +import fromEntries from 'object.fromentries'; +import entries from 'object.entries'; -const usingLegacy = semver.major(eslintVersion) < 9; +const eslintMajor = semver.major(eslintVersion); +const usingLegacy = eslintMajor < 9; +const hasTestCaseErrorType = eslintMajor < 10; const defaultParserOptions = { ecmaFeatures: { @@ -22,6 +26,15 @@ const defaultLanguageOptions = { }, }; +/** + * Strip the `type` property from error objects. + * ESLint v10 removed `TestCaseError#type`; including it causes test failures. + */ +function stripErrorType(err) { + if (typeof err === 'string') return err; + return fromEntries(entries(err).filter(([key]) => key !== 'type')); +} + export default function parserOptionsMapper({ code, errors, @@ -29,10 +42,14 @@ export default function parserOptionsMapper({ languageOptions = {}, settings = {}, }) { + const mappedErrors = !hasTestCaseErrorType && Array.isArray(errors) + ? errors.map(stripErrorType) + : errors; + return usingLegacy ? { code, - errors, + errors: mappedErrors, options, parserOptions: { ...defaultLegacyParserOptions, @@ -42,7 +59,7 @@ export default function parserOptionsMapper({ } : { code, - errors, + errors: mappedErrors, options, languageOptions: { ...defaultLanguageOptions, diff --git a/__tests__/eslint-compat-test.js b/__tests__/eslint-compat-test.js new file mode 100644 index 00000000..b8316389 --- /dev/null +++ b/__tests__/eslint-compat-test.js @@ -0,0 +1,111 @@ +/** + * @fileoverview ESLint compatibility smoke tests. + * + * Verifies that the test infrastructure works with the currently-installed + * ESLint version. Catches regressions in the RuleTester/tape integration + * and version-conditional test-case behavior (e.g. error type stripping). + */ + +import test from 'tape'; +import { version as eslintVersion } from 'eslint/package.json'; +import semver from 'semver'; +import plugin from '../src'; +import RuleTester from './__util__/RuleTester'; +import parserOptionsMapper from './__util__/parserOptionsMapper'; + +const eslintMajor = semver.major(eslintVersion); + +// --------------------------------------------------------------------------- +// RuleTester/tape integration smoke test +// --------------------------------------------------------------------------- +// Exercises the describe/it hooks that integrate RuleTester with tape. +// A failure here means the wrapper in __util__/RuleTester.js needs updating +// for the installed ESLint version. + +const reportingRule = { + meta: { + type: 'suggestion', + schema: [{ type: 'object' }], + }, + create(context) { + return { + JSXAttribute(node) { + if (node.name.name === 'accessKey') { + context.report({ node, message: 'accessKey is not allowed' }); + } + }, + }; + }, +}; + +const tester = new RuleTester(); + +// Run 1: valid + invalid — exercises full describe/it nesting +// (describe('rule') → describe('valid') → it, describe('invalid') → it) +tester.run('eslint-compat-smoke', reportingRule, { + valid: [ + parserOptionsMapper({ code: '
;' }), + ], + invalid: [ + parserOptionsMapper({ + code: '', + errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }], + }), + ], +}); + +// Run 2: sequential run — verifies that the wrapper's currentTest +// state is properly reset between runs. If state leaks from run 1, +// this will crash or produce wrong results. +tester.run('eslint-compat-smoke-sequential', reportingRule, { + valid: [ + parserOptionsMapper({ code: ';' }), + parserOptionsMapper({ code: ';' }), + ], + invalid: [ + parserOptionsMapper({ + code: '', + errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }], + }), + ], +}); + +// Run 3: same tester instance, third sequential run — exercises repeated +// reset of describe/it nesting to catch state accumulation bugs. +tester.run('eslint-compat-smoke-third', reportingRule, { + valid: [ + parserOptionsMapper({ code: ';' }), + parserOptionsMapper({ code: ';' }), + parserOptionsMapper({ code: ';' }), + ], + invalid: [ + parserOptionsMapper({ + code: '', + errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }], + }), + parserOptionsMapper({ + code: '', + errors: [{ message: 'accessKey is not allowed', type: 'JSXAttribute' }], + }), + ], +}); + +// --------------------------------------------------------------------------- +// Config export shape +// --------------------------------------------------------------------------- + +test(`ESLint ${eslintMajor}: flat config shape`, (t) => { + const { recommended: rec, strict } = plugin.flatConfigs; + + t.ok(rec.plugins, 'recommended flat config has plugins'); + t.ok(rec.rules, 'recommended flat config has rules'); + t.ok(rec.languageOptions, 'recommended flat config has languageOptions'); + t.equal(typeof rec.languageOptions.parserOptions, 'object', 'has parserOptions in languageOptions'); + t.ok(rec.languageOptions.parserOptions.ecmaFeatures.jsx, 'jsx is enabled'); + + t.ok(strict.plugins, 'strict flat config has plugins'); + t.ok(strict.rules, 'strict flat config has rules'); + t.ok(strict.languageOptions, 'strict flat config has languageOptions'); + + t.end(); +}); diff --git a/__tests__/src/rules/aria-unsupported-elements-test.js b/__tests__/src/rules/aria-unsupported-elements-test.js index 0d51bb2d..a58f1319 100644 --- a/__tests__/src/rules/aria-unsupported-elements-test.js +++ b/__tests__/src/rules/aria-unsupported-elements-test.js @@ -46,7 +46,6 @@ const ariaValidityTests = domElements.map((element) => { }; }).concat({ code: '