diff --git a/eslint.config.mjs b/eslint.config.mjs index 66399e018ce..4e049e6e257 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -329,8 +329,8 @@ export default tseslint.config( 'packages/*/tests/**/spec.{ts,tsx,cts,mts}', 'packages/*/tests/**/test.{ts,tsx,cts,mts}', 'packages/parser/tests/**/*.{ts,tsx,cts,mts}', - 'packages/integration-tests/tools/integration-test-base.{ts,tsx,cts,mts}', - 'packages/integration-tests/tools/pack-packages.{ts,tsx,cts,mts}', + 'packages/integration-tests/tools/integration-test-base.ts', + 'packages/integration-tests/tools/pack-packages.ts', ], rules: { '@typescript-eslint/no-empty-function': [ diff --git a/packages/integration-tests/fixtures/flat-config-types/dirname.cjs b/packages/integration-tests/fixtures/flat-config-types/dirname.cjs new file mode 100644 index 00000000000..f7cd3060fa9 --- /dev/null +++ b/packages/integration-tests/fixtures/flat-config-types/dirname.cjs @@ -0,0 +1,2 @@ +// a hacky way to allow __dirname within ESM +module.exports = __dirname; diff --git a/packages/integration-tests/fixtures/flat-config-types/eslint.config.js b/packages/integration-tests/fixtures/flat-config-types/eslint.config.js new file mode 100644 index 00000000000..93701448726 --- /dev/null +++ b/packages/integration-tests/fixtures/flat-config-types/eslint.config.js @@ -0,0 +1,57 @@ +// @ts-check + +import { FlatCompat } from '@eslint/eslintrc'; +import eslint from '@eslint/js'; +import stylisticPlugin from '@stylistic/eslint-plugin'; +import deprecationPlugin from 'eslint-plugin-deprecation'; +import jestPlugin from 'eslint-plugin-jest'; +import tseslint from 'typescript-eslint'; + +import __dirname from './dirname.cjs'; + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: {}, + allConfig: {}, +}); + +// this config is run through eslint as part of the integration test +// so it needs to be a correct config +export default tseslint.config( + { + plugins: { + ['@typescript-eslint']: tseslint.plugin, + ['deprecation']: deprecationPlugin, + ['jest']: jestPlugin, + }, + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + stylisticPlugin.configs['recommended-flat'], +); + +// these are just tests for the types and are not seen by eslint so they can be whatever +tseslint.config({ + plugins: { + ['@stylistic']: stylisticPlugin, + ['@typescript-eslint']: tseslint.plugin, + ['deprecation']: deprecationPlugin, + ['jest']: jestPlugin, + }, +}); +tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + stylisticPlugin.configs['recommended-flat'], +); +tseslint.config( + // @ts-expect-error + compat.config(deprecationPlugin.configs.recommended), + ...compat.config(jestPlugin.configs.recommended), +); +tseslint.config( + // @ts-expect-error + deprecationPlugin.configs.recommended, + // this should error but doesn't because there are no types exported from the jest plugin + jestPlugin.configs.recommended, +); diff --git a/packages/integration-tests/fixtures/flat-config-types/package.json b/packages/integration-tests/fixtures/flat-config-types/package.json new file mode 100644 index 00000000000..5f3e28a91d5 --- /dev/null +++ b/packages/integration-tests/fixtures/flat-config-types/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "devDependencies": { + "@types/eslint__eslintrc": "latest", + "@eslint/eslintrc": "latest", + "@types/eslint__js": "latest", + "@eslint/js": "latest", + "@types/eslint": "latest", + "eslint": "latest", + "@stylistic/eslint-plugin": "latest", + "eslint-plugin-deprecation": "latest", + "eslint-plugin-jest": "latest" + } +} diff --git a/packages/integration-tests/tests/__snapshots__/eslint-v8.test.ts.snap b/packages/integration-tests/tests/__snapshots__/eslint-v8.test.ts.snap index 1a77699f990..fc96eb5bd1b 100644 --- a/packages/integration-tests/tests/__snapshots__/eslint-v8.test.ts.snap +++ b/packages/integration-tests/tests/__snapshots__/eslint-v8.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`eslint-v8 should lint successfully 1`] = ` +exports[`eslint-v8 eslint should work successfully 1`] = ` [ { "errorCount": 1, diff --git a/packages/integration-tests/tests/__snapshots__/flat-config-types.test.ts.snap b/packages/integration-tests/tests/__snapshots__/flat-config-types.test.ts.snap new file mode 100644 index 00000000000..4280148fc36 --- /dev/null +++ b/packages/integration-tests/tests/__snapshots__/flat-config-types.test.ts.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`flat-config-types eslint should work successfully 1`] = ` +[ + { + "errorCount": 2, + "fatalErrorCount": 0, + "filePath": "/eslint.config.js", + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "messages": [ + { + "column": 3, + "endColumn": 22, + "endLine": 48, + "line": 48, + "message": "Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer.", + "messageId": "tsDirectiveCommentRequiresDescription", + "nodeType": "Line", + "ruleId": "@typescript-eslint/ban-ts-comment", + "severity": 2, + }, + { + "column": 3, + "endColumn": 22, + "endLine": 53, + "line": 53, + "message": "Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer.", + "messageId": "tsDirectiveCommentRequiresDescription", + "nodeType": "Line", + "ruleId": "@typescript-eslint/ban-ts-comment", + "severity": 2, + }, + ], + "output": "// @ts-check + +import { FlatCompat } from '@eslint/eslintrc' +import eslint from '@eslint/js' +import stylisticPlugin from '@stylistic/eslint-plugin' +import deprecationPlugin from 'eslint-plugin-deprecation' +import jestPlugin from 'eslint-plugin-jest' +import tseslint from 'typescript-eslint' + +import __dirname from './dirname.cjs' + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: {}, + allConfig: {}, +}) + +// this config is run through eslint as part of the integration test +// so it needs to be a correct config +export default tseslint.config( + { + plugins: { + ['@typescript-eslint']: tseslint.plugin, + ['deprecation']: deprecationPlugin, + ['jest']: jestPlugin, + }, + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + stylisticPlugin.configs['recommended-flat'], +) + +// these are just tests for the types and are not seen by eslint so they can be whatever +tseslint.config({ + plugins: { + ['@stylistic']: stylisticPlugin, + ['@typescript-eslint']: tseslint.plugin, + ['deprecation']: deprecationPlugin, + ['jest']: jestPlugin, + }, +}) +tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + stylisticPlugin.configs['recommended-flat'], +) +tseslint.config( + // @ts-expect-error + compat.config(deprecationPlugin.configs.recommended), + ...compat.config(jestPlugin.configs.recommended), +) +tseslint.config( + // @ts-expect-error + deprecationPlugin.configs.recommended, + // this should error but doesn't because there are no types exported from the jest plugin + jestPlugin.configs.recommended, +) +", + "suppressedMessages": [], + "usedDeprecatedRules": [ + { + "replacedBy": [], + "ruleId": "no-extra-semi", + }, + { + "replacedBy": [], + "ruleId": "no-mixed-spaces-and-tabs", + }, + ], + "warningCount": 0, + }, +] +`; diff --git a/packages/integration-tests/tests/__snapshots__/markdown.test.ts.snap b/packages/integration-tests/tests/__snapshots__/markdown.test.ts.snap index 4c0f3f4a78b..b987cad32f7 100644 --- a/packages/integration-tests/tests/__snapshots__/markdown.test.ts.snap +++ b/packages/integration-tests/tests/__snapshots__/markdown.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`markdown should lint successfully 1`] = ` +exports[`markdown eslint should work successfully 1`] = ` [ { "errorCount": 10, diff --git a/packages/integration-tests/tests/__snapshots__/recommended-does-not-require-program.test.ts.snap b/packages/integration-tests/tests/__snapshots__/recommended-does-not-require-program.test.ts.snap index 2b9032643f3..8feccdc5b0e 100644 --- a/packages/integration-tests/tests/__snapshots__/recommended-does-not-require-program.test.ts.snap +++ b/packages/integration-tests/tests/__snapshots__/recommended-does-not-require-program.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`recommended-does-not-require-program should lint successfully 1`] = ` +exports[`recommended-does-not-require-program eslint should work successfully 1`] = ` [ { "errorCount": 1, diff --git a/packages/integration-tests/tests/__snapshots__/typescript-and-tslint-plugins-together.test.ts.snap b/packages/integration-tests/tests/__snapshots__/typescript-and-tslint-plugins-together.test.ts.snap index 06affd98eee..d79b8458b4e 100644 --- a/packages/integration-tests/tests/__snapshots__/typescript-and-tslint-plugins-together.test.ts.snap +++ b/packages/integration-tests/tests/__snapshots__/typescript-and-tslint-plugins-together.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`typescript-and-tslint-plugins-together should lint successfully 1`] = ` +exports[`typescript-and-tslint-plugins-together eslint should work successfully 1`] = ` [ { "errorCount": 1, diff --git a/packages/integration-tests/tests/__snapshots__/vue-jsx.test.ts.snap b/packages/integration-tests/tests/__snapshots__/vue-jsx.test.ts.snap index 7cd8ff12fa3..94cb255c605 100644 --- a/packages/integration-tests/tests/__snapshots__/vue-jsx.test.ts.snap +++ b/packages/integration-tests/tests/__snapshots__/vue-jsx.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`vue-jsx should lint successfully 1`] = ` +exports[`vue-jsx eslint should work successfully 1`] = ` [ { "errorCount": 1, diff --git a/packages/integration-tests/tests/__snapshots__/vue-sfc.test.ts.snap b/packages/integration-tests/tests/__snapshots__/vue-sfc.test.ts.snap index e0fdc6549ef..f012057ae90 100644 --- a/packages/integration-tests/tests/__snapshots__/vue-sfc.test.ts.snap +++ b/packages/integration-tests/tests/__snapshots__/vue-sfc.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`vue-sfc should lint successfully 1`] = ` +exports[`vue-sfc eslint should work successfully 1`] = ` [ { "errorCount": 1, diff --git a/packages/integration-tests/tests/eslint-v8.test.ts b/packages/integration-tests/tests/eslint-v8.test.ts index 8f0d81b8926..fe0b175c3f8 100644 --- a/packages/integration-tests/tests/eslint-v8.test.ts +++ b/packages/integration-tests/tests/eslint-v8.test.ts @@ -1,3 +1,3 @@ -import { integrationTest } from '../tools/integration-test-base'; +import { eslintIntegrationTest } from '../tools/integration-test-base'; -integrationTest(__filename, '*.ts'); +eslintIntegrationTest(__filename, '*.ts'); diff --git a/packages/integration-tests/tests/flat-config-types.test.ts b/packages/integration-tests/tests/flat-config-types.test.ts new file mode 100644 index 00000000000..a1d66e47c36 --- /dev/null +++ b/packages/integration-tests/tests/flat-config-types.test.ts @@ -0,0 +1,37 @@ +import { + eslintIntegrationTest, + typescriptIntegrationTest, +} from '../tools/integration-test-base'; + +typescriptIntegrationTest( + __filename, + ['--allowJs', '--esModuleInterop', 'eslint.config.js'], + out => { + const lines = out + .split('\n') + .filter( + line => + // error TS18028: Private identifiers are only available when targeting ECMAScript 2015 and higher. + // this is fine for us to ignore in this context + !line.includes('error TS18028'), + ) + .join('\n'); + + // The stylistic type errors: https://github.com/eslint-stylistic/eslint-stylistic/issues/276 + expect(lines).toMatchInlineSnapshot(` + "node_modules/@stylistic/eslint-plugin-plus/dts/index.d.ts(7,46): error TS2694: Namespace '"//node_modules/@types/eslint/index".ESLint' has no exported member 'RuleModule'. + node_modules/@stylistic/eslint-plugin/dist/dts/rule-options.d.ts(6,11): error TS2320: Interface 'UnprefixedRuleOptions' cannot simultaneously extend types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions'. + Named property ''comma-dangle'' of types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions' are not identical. + node_modules/@stylistic/eslint-plugin/dist/dts/rule-options.d.ts(6,11): error TS2320: Interface 'UnprefixedRuleOptions' cannot simultaneously extend types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions'. + Named property ''keyword-spacing'' of types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions' are not identical. + node_modules/@stylistic/eslint-plugin/dist/dts/rule-options.d.ts(6,11): error TS2320: Interface 'UnprefixedRuleOptions' cannot simultaneously extend types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions'. + Named property ''lines-around-comment'' of types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions' are not identical. + node_modules/@stylistic/eslint-plugin/dist/dts/rule-options.d.ts(6,11): error TS2320: Interface 'UnprefixedRuleOptions' cannot simultaneously extend types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions'. + Named property ''lines-between-class-members'' of types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions' are not identical. + node_modules/@stylistic/eslint-plugin/dist/dts/rule-options.d.ts(6,11): error TS2320: Interface 'UnprefixedRuleOptions' cannot simultaneously extend types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions'. + Named property ''padding-line-between-statements'' of types 'UnprefixedRuleOptions' and 'UnprefixedRuleOptions' are not identical. + " + `); + }, +); +eslintIntegrationTest(__filename, 'eslint.config.js', true); diff --git a/packages/integration-tests/tests/markdown.test.ts b/packages/integration-tests/tests/markdown.test.ts index 8c006309eff..2cb86728d58 100644 --- a/packages/integration-tests/tests/markdown.test.ts +++ b/packages/integration-tests/tests/markdown.test.ts @@ -1,3 +1,3 @@ -import { integrationTest } from '../tools/integration-test-base'; +import { eslintIntegrationTest } from '../tools/integration-test-base'; -integrationTest(__filename, '*.md'); +eslintIntegrationTest(__filename, '*.md'); diff --git a/packages/integration-tests/tests/recommended-does-not-require-program.test.ts b/packages/integration-tests/tests/recommended-does-not-require-program.test.ts index 8f0d81b8926..fe0b175c3f8 100644 --- a/packages/integration-tests/tests/recommended-does-not-require-program.test.ts +++ b/packages/integration-tests/tests/recommended-does-not-require-program.test.ts @@ -1,3 +1,3 @@ -import { integrationTest } from '../tools/integration-test-base'; +import { eslintIntegrationTest } from '../tools/integration-test-base'; -integrationTest(__filename, '*.ts'); +eslintIntegrationTest(__filename, '*.ts'); diff --git a/packages/integration-tests/tests/typescript-and-tslint-plugins-together.test.ts b/packages/integration-tests/tests/typescript-and-tslint-plugins-together.test.ts index 8f0d81b8926..fe0b175c3f8 100644 --- a/packages/integration-tests/tests/typescript-and-tslint-plugins-together.test.ts +++ b/packages/integration-tests/tests/typescript-and-tslint-plugins-together.test.ts @@ -1,3 +1,3 @@ -import { integrationTest } from '../tools/integration-test-base'; +import { eslintIntegrationTest } from '../tools/integration-test-base'; -integrationTest(__filename, '*.ts'); +eslintIntegrationTest(__filename, '*.ts'); diff --git a/packages/integration-tests/tests/vue-jsx.test.ts b/packages/integration-tests/tests/vue-jsx.test.ts index 20e6b09b797..b1062e099a6 100644 --- a/packages/integration-tests/tests/vue-jsx.test.ts +++ b/packages/integration-tests/tests/vue-jsx.test.ts @@ -1,3 +1,3 @@ -import { integrationTest } from '../tools/integration-test-base'; +import { eslintIntegrationTest } from '../tools/integration-test-base'; -integrationTest(__filename, '*.vue'); +eslintIntegrationTest(__filename, '*.vue'); diff --git a/packages/integration-tests/tests/vue-sfc.test.ts b/packages/integration-tests/tests/vue-sfc.test.ts index 20e6b09b797..b1062e099a6 100644 --- a/packages/integration-tests/tests/vue-sfc.test.ts +++ b/packages/integration-tests/tests/vue-sfc.test.ts @@ -1,3 +1,3 @@ -import { integrationTest } from '../tools/integration-test-base'; +import { eslintIntegrationTest } from '../tools/integration-test-base'; -integrationTest(__filename, '*.vue'); +eslintIntegrationTest(__filename, '*.vue'); diff --git a/packages/integration-tests/tools/integration-test-base.ts b/packages/integration-tests/tools/integration-test-base.ts index 833ec45182a..121d89ebb5e 100644 --- a/packages/integration-tests/tools/integration-test-base.ts +++ b/packages/integration-tests/tools/integration-test-base.ts @@ -1,9 +1,11 @@ -import childProcess from 'child_process'; -import fs from 'fs'; +import childProcess from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { promisify } from 'node:util'; + import ncp from 'ncp'; -import path from 'path'; +import type { DirOptions } from 'tmp'; import tmp from 'tmp'; -import { promisify } from 'util'; interface PackageJSON { name: string; @@ -18,7 +20,7 @@ tmp.setGracefulCleanup(); const copyDir = promisify(ncp.ncp); const execFile = promisify(childProcess.execFile); const readFile = promisify(fs.readFile); -const tmpDir = promisify(tmp.dir); +const tmpDir = promisify(tmp.dir) as (opts?: DirOptions) => Promise; const tmpFile = promisify(tmp.file); const writeFile = promisify(fs.writeFile); @@ -30,129 +32,186 @@ const BASE_DEPENDENCIES: PackageJSON['devDependencies'] = { }; const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures'); +// an env var to persist the temp folder so that it can be inspected for debugging purposes +const KEEP_INTEGRATION_TEST_DIR = + process.env.KEEP_INTEGRATION_TEST_DIR === 'true'; // make sure that jest doesn't timeout the test jest.setTimeout(60000); -export function integrationTest(testFilename: string, filesGlob: string): void { +function integrationTest( + testName: string, + testFilename: string, + executeTest: (testFolder: string) => Promise, +): void { const fixture = path.parse(testFilename).name.replace('.test', ''); describe(fixture, () => { const fixtureDir = path.join(FIXTURES_DIR, fixture); - it('should lint successfully', async () => { - const testFolder = await tmpDir(); + describe(testName, () => { + it('should work successfully', async () => { + const testFolder = await tmpDir({ + keep: KEEP_INTEGRATION_TEST_DIR, + }); + if (KEEP_INTEGRATION_TEST_DIR) { + console.error(testFolder); + } - // copy the fixture files to the temp folder - await copyDir(fixtureDir, testFolder); + // copy the fixture files to the temp folder + await copyDir(fixtureDir, testFolder); - // build and write the package.json for the test - const fixturePackageJson: PackageJSON = await import( - path.join(fixtureDir, 'package.json') - ); - await writeFile( - path.join(testFolder, 'package.json'), - JSON.stringify({ - private: true, - devDependencies: { - ...BASE_DEPENDENCIES, - ...fixturePackageJson.devDependencies, - // install tslint with the base version if required - tslint: fixturePackageJson.devDependencies.tslint - ? rootPackageJson.devDependencies.tslint - : undefined, - }, - // ensure everything uses the locally packed versions instead of the NPM versions - resolutions: { - ...global.tseslintPackages, - }, - }), - ); - // console.log('package.json written.'); + // build and write the package.json for the test + const fixturePackageJson: PackageJSON = await import( + path.join(fixtureDir, 'package.json') + ); + await writeFile( + path.join(testFolder, 'package.json'), + JSON.stringify({ + private: true, + ...fixturePackageJson, + devDependencies: { + ...BASE_DEPENDENCIES, + ...fixturePackageJson.devDependencies, + // install tslint with the base version if required + tslint: fixturePackageJson.devDependencies.tslint + ? rootPackageJson.devDependencies.tslint + : undefined, + }, + // ensure everything uses the locally packed versions instead of the NPM versions + resolutions: { + ...global.tseslintPackages, + }, + }), + ); + // console.log('package.json written.'); - // Ensure yarn uses the node-modules linker and not PnP - await writeFile( - path.join(testFolder, '.yarnrc.yml'), - `nodeLinker: node-modules`, - ); + // Ensure yarn uses the node-modules linker and not PnP + await writeFile( + path.join(testFolder, '.yarnrc.yml'), + `nodeLinker: node-modules`, + ); - await new Promise((resolve, reject) => { - // we use the non-promise version so we can log everything on error - childProcess.execFile( - // we use yarn instead of npm as it will cache the remote packages and - // make installing things faster - 'yarn', - // We call explicitly with --no-immutable to prevent errors related to missing lock files in CI - ['install', '--no-immutable'], - { - cwd: testFolder, - }, - (err, stdout, stderr) => { - if (err) { - if (stdout.length > 0) { - console.warn(stdout); - } - if (stderr.length > 0) { - console.error(stderr); + await new Promise((resolve, reject) => { + // we use the non-promise version so we can log everything on error + childProcess.execFile( + // we use yarn instead of npm as it will cache the remote packages and + // make installing things faster + 'yarn', + // We call explicitly with --no-immutable to prevent errors related to missing lock files in CI + ['install', '--no-immutable'], + { + cwd: testFolder, + }, + (err, stdout, stderr) => { + if (err) { + if (stdout.length > 0) { + console.warn(stdout); + } + if (stderr.length > 0) { + console.error(stderr); + } + // childProcess.ExecFileException is an extension of Error + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err); + } else { + resolve(); } - // childProcess.ExecFileException is an extension of Error - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(err); - } else { - resolve(); - } - }, - ); + }, + ); + }); + // console.log('Install complete.'); + + await executeTest(testFolder); }); - // console.log('Install complete.'); - - // lint, outputting to a JSON file - const outFile = await tmpFile(); - let stderr = ''; - try { - await execFile( - 'yarn', - [ - 'eslint', - '--format', - 'json', - '--output-file', - outFile, - '--config', - './.eslintrc.js', - '--fix-dry-run', - filesGlob, - ], - { - cwd: testFolder, - }, - ); - } catch (ex) { - // we expect eslint will "fail" because we have intentional lint errors - // useful for debugging - if (typeof ex === 'object' && ex != null && 'stderr' in ex) { - stderr = String(ex.stderr); - } - } - // console.log('Lint complete.'); - - // assert the linting state is consistent - const lintOutputRAW = (await readFile(outFile, 'utf8')) - // clean the output to remove any changing facets so tests are stable - .replace( - new RegExp(`"filePath": ?"(/private)?${testFolder}`, 'g'), - '"filePath": "', - ); - try { - const lintOutput = JSON.parse(lintOutputRAW); - expect(lintOutput).toMatchSnapshot(); - } catch { - throw new Error( - `Lint output could not be parsed as JSON: \`${lintOutputRAW}\`. The error logs from eslint were: \`${stderr}\``, - ); - } + afterAll(() => {}); }); + }); +} + +export function eslintIntegrationTest( + testFilename: string, + filesGlob: string, + flatConfig = false, +): void { + integrationTest('eslint', testFilename, async testFolder => { + // lint, outputting to a JSON file + const outFile = await tmpFile(); + let stderr = ''; + try { + await execFile( + 'yarn', + [ + 'eslint', + '--format', + 'json', + '--output-file', + outFile, + '--config', + flatConfig ? './eslint.config.js' : './.eslintrc.js', + '--fix-dry-run', + filesGlob, + ], + { + cwd: testFolder, + }, + ); + } catch (ex) { + // we expect eslint will "fail" because we have intentional lint errors - afterAll(() => {}); + // useful for debugging + if (typeof ex === 'object' && ex != null && 'stderr' in ex) { + stderr = String(ex.stderr); + } + } + // console.log('Lint complete.'); + expect(stderr).toHaveLength(0); + + // assert the linting state is consistent + const lintOutputRAW = (await readFile(outFile, 'utf8')) + // clean the output to remove any changing facets so tests are stable + .replace( + new RegExp(`"filePath": ?"(/private)?${testFolder}`, 'g'), + '"filePath": "', + ); + try { + const lintOutput = JSON.parse(lintOutputRAW); + expect(lintOutput).toMatchSnapshot(); + } catch { + throw new Error( + `Lint output could not be parsed as JSON: \`${lintOutputRAW}\`.`, + ); + } + }); +} + +export function typescriptIntegrationTest( + testFilename: string, + tscArgs: string[], + assertOutput: (out: string) => void, +): void { + integrationTest('typescript', testFilename, async testFolder => { + const [result] = await Promise.allSettled([ + execFile('yarn', ['tsc', '--noEmit', ...tscArgs], { + cwd: testFolder, + }), + ]); + + if (result.status === 'rejected') { + // this looks weird - but it means that we can show the stdout (the errors) + // in the test output when typescript fails which helps with debugging + assertOutput( + (result.reason as { stdout: string }).stdout.replace( + // on macos the tmp path might be shown by TS with `/private/`, but + // the tmp util does not include that prefix folder + new RegExp(`(/private)?${testFolder}`), + '/', + ), + ); + } else { + // TS logs nothing when it succeeds + expect(result.value.stdout).toBe(''); + expect(result.value.stderr).toBe(''); + } }); } diff --git a/packages/scope-manager/package.json b/packages/scope-manager/package.json index b63aefcc23f..160b295c7c2 100644 --- a/packages/scope-manager/package.json +++ b/packages/scope-manager/package.json @@ -16,6 +16,7 @@ }, "./package.json": "./package.json" }, + "types": "./dist/index.d.ts", "engines": { "node": "^16.0.0 || >=18.0.0" }, diff --git a/packages/types/package.json b/packages/types/package.json index 4dbab29b4a8..79e027befcf 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -17,6 +17,7 @@ }, "./package.json": "./package.json" }, + "types": "./dist/index.d.ts", "engines": { "node": "^16.0.0 || >=18.0.0" }, diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index e84e3ab2566..477acb50cac 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -27,7 +27,8 @@ type EcmaVersion = | 2021 | 2022 | 2023 - | 2024; + | 2024 + | 'latest'; type SourceTypeClassic = 'module' | 'script'; type SourceType = SourceTypeClassic | 'commonjs'; @@ -41,7 +42,7 @@ interface ParserOptions { jsx?: boolean; [key: string]: unknown; }; - ecmaVersion?: EcmaVersion | 'latest'; + ecmaVersion?: EcmaVersion; // scope-manager specific jsxPragma?: string | null; diff --git a/packages/typescript-eslint/package.json b/packages/typescript-eslint/package.json index 0d47b91c083..e9796c2b819 100644 --- a/packages/typescript-eslint/package.json +++ b/packages/typescript-eslint/package.json @@ -16,6 +16,7 @@ }, "./package.json": "./package.json" }, + "types": "./dist/index.d.ts", "engines": { "node": "^16.0.0 || >=18.0.0" }, diff --git a/packages/typescript-eslint/src/config-helper.ts b/packages/typescript-eslint/src/config-helper.ts index 51af07ca639..691ffdbab4d 100644 --- a/packages/typescript-eslint/src/config-helper.ts +++ b/packages/typescript-eslint/src/config-helper.ts @@ -1,6 +1,24 @@ -import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint'; +/* +This package is consumed from js config files with @ts-check. Often times these +files are not covered by a tsconfig.json -- meaning they use the default +`node10` module resolution. -interface ConfigWithExtends extends FlatConfig.Config { +In order to support this use-case we need to ensure this package's module +signature is compatible with `node10` resolution. If we use `/utils/ts-eslint` +here then we need to make sure that that import works in `node10` -- which is a +pain because `node10` is "simple" and just maps to the files on disk. + +So to avoid that problem entirely we use the root import which is easy to make +`node10` compatible. + +For more context see: +https://github.com/typescript-eslint/typescript-eslint/pull/8460 + +TODO - convert this to /utils/ts-eslint +*/ +import type { TSESLint } from '@typescript-eslint/utils'; + +interface ConfigWithExtends extends TSESLint.FlatConfig.Config { /** * Allows you to "extend" a set of configs similar to `extends` from the * classic configs. @@ -41,7 +59,7 @@ interface ConfigWithExtends extends FlatConfig.Config { * ] * ``` */ - extends?: FlatConfig.ConfigArray; + extends?: TSESLint.FlatConfig.ConfigArray; } /** @@ -66,7 +84,7 @@ interface ConfigWithExtends extends FlatConfig.Config { */ export function config( ...configs: ConfigWithExtends[] -): FlatConfig.ConfigArray { +): TSESLint.FlatConfig.ConfigArray { return configs.flatMap(configWithExtends => { const { extends: extendsArr, ...config } = configWithExtends; if (extendsArr == null || extendsArr.length === 0) { diff --git a/packages/typescript-eslint/src/index.ts b/packages/typescript-eslint/src/index.ts index ea63e2c9301..ad8487db584 100644 --- a/packages/typescript-eslint/src/index.ts +++ b/packages/typescript-eslint/src/index.ts @@ -1,6 +1,7 @@ import pluginBase from '@typescript-eslint/eslint-plugin'; import * as parserBase from '@typescript-eslint/parser'; -import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint'; +// see the comment in config-helper.ts for why this doesn't use /ts-eslint +import type { TSESLint } from '@typescript-eslint/utils'; import { config } from './config-helper'; import allConfig from './configs/all'; @@ -14,12 +15,12 @@ import strictTypeCheckedConfig from './configs/strict-type-checked'; import stylisticConfig from './configs/stylistic'; import stylisticTypeCheckedConfig from './configs/stylistic-type-checked'; -const parser: FlatConfig.Parser = { +const parser: TSESLint.FlatConfig.Parser = { meta: parserBase.meta, parseForESLint: parserBase.parseForESLint, }; -const plugin: FlatConfig.Plugin = { +const plugin: TSESLint.FlatConfig.Plugin = { meta: pluginBase.meta, rules: pluginBase.rules, }; diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index eb721176c5d..df75c17202c 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -20,6 +20,7 @@ "default": "./dist/use-at-your-own-risk.js" } }, + "types": "./dist/index.d.ts", "engines": { "node": "^16.0.0 || >=18.0.0" }, diff --git a/packages/utils/package.json b/packages/utils/package.json index 4f1eb9906d8..503eaf7e9a4 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -37,6 +37,7 @@ }, "./package.json": "./package.json" }, + "types": "./dist/index.d.ts", "engines": { "node": "^16.0.0 || >=18.0.0" }, diff --git a/packages/utils/src/ts-eslint/Config.ts b/packages/utils/src/ts-eslint/Config.ts index 7d65599bc06..250c291c32d 100644 --- a/packages/utils/src/ts-eslint/Config.ts +++ b/packages/utils/src/ts-eslint/Config.ts @@ -3,11 +3,7 @@ import type { Parser as ParserType } from './Parser'; import type * as ParserOptionsTypes from './ParserOptions'; import type { Processor as ProcessorType } from './Processor'; -import type { - AnyRuleModule, - RuleCreateFunction, - SharedConfigurationSettings, -} from './Rule'; +import type { LooseRuleDefinition, SharedConfigurationSettings } from './Rule'; /** @internal */ export namespace SharedConfig { @@ -133,7 +129,7 @@ export namespace FlatConfig { export type Parser = ParserType.LooseParserModule; export type ParserOptions = SharedConfig.ParserOptions; export type PluginMeta = SharedConfig.PluginMeta; - export type Processor = ProcessorType.ProcessorModule; + export type Processor = ProcessorType.LooseProcessorModule; export type RuleEntry = SharedConfig.RuleEntry; export type RuleLevel = SharedConfig.RuleLevel; export type RuleLevelAndOptions = SharedConfig.RuleLevelAndOptions; @@ -155,7 +151,7 @@ export namespace FlatConfig { /** * Metadata about your plugin for easier debugging and more effective caching of plugins. */ - meta?: PluginMeta; + meta?: Partial; /** * The definition of plugin processors. * Users can stringly reference the processor using the key in their config (i.e., `"pluginName/processorName"`). @@ -167,7 +163,7 @@ export namespace FlatConfig { * Users can stringly reference the rule using the key they registered the plugin under combined with the rule name. * i.e. for the user config `plugins: { foo: pluginReference }` - the reference would be `"foo/ruleName"`. */ - rules?: Record; + rules?: Record; } export interface Plugins { /** @@ -236,6 +232,16 @@ export namespace FlatConfig { sourceType?: SourceType; } + // The function form is undocumented but allowed: + // https://github.com/eslint/eslint/issues/18118 + // + // We have to support it as well because the DefinitelyTyped configs define it + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e26919eb3426f5ba85fed394c90c39efb217037a/types/eslint/index.d.ts#L1208-L1223 + // + // If we don't then users can't use shareable configs defined using the DT types + // https://github.com/typescript-eslint/typescript-eslint/issues/8467 + export type FileSpec = string | ((filePath: string) => boolean); + // it's not a json schema so it's nowhere near as nice to read and convert... // https://github.com/eslint/eslint/blob/v8.45.0/lib/config/flat-config-schema.js export interface Config { @@ -243,12 +249,16 @@ export namespace FlatConfig { * An array of glob patterns indicating the files that the configuration object should apply to. * If not specified, the configuration object applies to all files matched by any other configuration object. */ - files?: string[]; + files?: ( + | FileSpec + // yes, a single layer of array nesting is supported + | FileSpec[] + )[]; /** * An array of glob patterns indicating the files that the configuration object should not apply to. * If not specified, the configuration object applies to all files matched by files. */ - ignores?: string[]; + ignores?: FileSpec[]; /** * An object containing settings related to how JavaScript is configured for linting. */ diff --git a/packages/utils/src/ts-eslint/Parser.ts b/packages/utils/src/ts-eslint/Parser.ts index e5526c58a2e..c07485c0610 100644 --- a/packages/utils/src/ts-eslint/Parser.ts +++ b/packages/utils/src/ts-eslint/Parser.ts @@ -20,13 +20,15 @@ export namespace Parser { * A loose definition of the ParserModule type for use with configs * This type intended to relax validation of configs so that parsers that have * different AST types or scope managers can still be passed to configs + * + * @see {@link LooseRuleDefinition}, {@link LooseProcessorModule} */ export type LooseParserModule = | { /** * Information about the parser to uniquely identify it when serializing. */ - meta?: ParserMeta; + meta?: Partial; /** * Parses the given text into an ESTree AST */ @@ -36,7 +38,7 @@ export namespace Parser { /** * Information about the parser to uniquely identify it when serializing. */ - meta?: ParserMeta; + meta?: Partial; /** * Parses the given text into an AST */ diff --git a/packages/utils/src/ts-eslint/Processor.ts b/packages/utils/src/ts-eslint/Processor.ts index 57f03ae5def..513c6388232 100644 --- a/packages/utils/src/ts-eslint/Processor.ts +++ b/packages/utils/src/ts-eslint/Processor.ts @@ -45,4 +45,43 @@ export namespace Processor { */ supportsAutofix?: boolean; } + + /** + * A loose definition of the ParserModule type for use with configs + * This type intended to relax validation of configs so that parsers that have + * different AST types or scope managers can still be passed to configs + * + * @see {@link LooseRuleDefinition}, {@link LooseParserModule} + */ + export interface LooseProcessorModule { + /** + * Information about the processor to uniquely identify it when serializing. + */ + meta?: Partial; + + /** + * The function to extract code blocks. + */ + /* + eslint-disable-next-line @typescript-eslint/no-explicit-any -- + intentionally using `any` to allow bi-directional assignment (unknown and + never only allow unidirectional) + */ + preprocess?: (text: string, filename: string) => any; + + /** + * The function to merge messages. + */ + /* + eslint-disable-next-line @typescript-eslint/no-explicit-any -- + intentionally using `any` to allow bi-directional assignment (unknown and + never only allow unidirectional) + */ + postprocess?: (messagesList: any, filename: string) => any; + + /** + * If `true` then it means the processor supports autofix. + */ + supportsAutofix?: boolean; + } } diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index 01cfd91a1e8..c1824060d6e 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -8,7 +8,7 @@ import type { SourceCode } from './SourceCode'; export type RuleRecommendation = 'recommended' | 'strict' | 'stylistic'; -interface RuleMetaDataDocs { +export interface RuleMetaDataDocs { /** * Concise description of the rule */ @@ -35,7 +35,7 @@ interface RuleMetaDataDocs { */ extendsBaseRule?: boolean | string; } -interface RuleMetaData { +export interface RuleMetaData { /** * True if the rule is deprecated, false otherwise */ @@ -75,12 +75,12 @@ interface RuleMetaData { schema: JSONSchema4 | readonly JSONSchema4[]; } -interface RuleFix { +export interface RuleFix { range: Readonly; text: string; } -interface RuleFixer { +export interface RuleFixer { insertTextAfter( nodeOrToken: TSESTree.Node | TSESTree.Token, text: string, @@ -107,18 +107,18 @@ interface RuleFixer { replaceTextRange(range: Readonly, text: string): RuleFix; } -interface SuggestionReportDescriptor +export interface SuggestionReportDescriptor extends Omit, 'fix'> { readonly fix: ReportFixFunction; } -type ReportFixFunction = ( +export type ReportFixFunction = ( fixer: RuleFixer, ) => IterableIterator | RuleFix | readonly RuleFix[] | null; -type ReportSuggestionArray = +export type ReportSuggestionArray = SuggestionReportDescriptor[]; -type ReportDescriptorMessageData = Readonly>; +export type ReportDescriptorMessageData = Readonly>; interface ReportDescriptorBase { /** @@ -163,7 +163,7 @@ interface ReportDescriptorLocOnly { */ loc: Readonly | Readonly; } -type ReportDescriptor = +export type ReportDescriptor = ReportDescriptorWithSuggestion & (ReportDescriptorLocOnly | ReportDescriptorNodeOptionalLoc); @@ -171,9 +171,9 @@ type ReportDescriptor = * Plugins can add their settings using declaration * merging against this interface. */ -type SharedConfigurationSettings = Record; +export type SharedConfigurationSettings = Record; -interface RuleContext< +export interface RuleContext< TMessageIds extends string, TOptions extends readonly unknown[], > { @@ -308,7 +308,7 @@ interface RuleContext< * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6993 */ -interface CodePath { +export interface CodePath { /** * A unique string. Respective rules can use `id` to save additional * information for each code path. @@ -349,7 +349,7 @@ interface CodePath { * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6993 */ -interface CodePathSegment { +export interface CodePathSegment { /** * A unique string. Respective rules can use `id` to save additional * information for each segment. @@ -385,7 +385,7 @@ interface CodePathSegment { * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6993 */ -type CodePathFunction = +export type CodePathFunction = | (( fromSegment: CodePathSegment, toSegment: CodePathSegment, @@ -396,7 +396,7 @@ type CodePathFunction = // This isn't the correct signature, but it makes it easier to do custom unions within reusable listeners // never will break someone's code unless they specifically type the function argument -type RuleFunction = ( +export type RuleFunction = ( node: T, ) => void; @@ -565,7 +565,7 @@ type RuleListenerExitSelectors = { type RuleListenerCatchAllBaseCase = Record; // Interface to merge into for anyone that wants to add more selectors // eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RuleListenerExtension { +export interface RuleListenerExtension { // The code path functions below were introduced in ESLint v8.7.0 but are // intentionally commented out because they cause unresolvable compiler // errors: @@ -606,11 +606,11 @@ interface RuleListenerExtension { */ } -type RuleListener = RuleListenerBaseSelectors & +export type RuleListener = RuleListenerBaseSelectors & RuleListenerCatchAllBaseCase & RuleListenerExitSelectors; -interface RuleModule< +export interface RuleModule< TMessageIds extends string, TOptions extends readonly unknown[] = [], // for extending base rules @@ -632,33 +632,48 @@ interface RuleModule< */ create(context: Readonly>): TRuleListener; } -type AnyRuleModule = RuleModule; +export type AnyRuleModule = RuleModule; -type RuleCreateFunction< +/** + * A loose definition of the RuleModule type for use with configs. This type is + * intended to relax validation of types so that we can have basic validation + * without being overly strict about nitty gritty details matching. + * + * For example the plugin might be declared using an old version of our types or + * they might use the DefinitelyTyped eslint types. Ultimately we don't need + * super strict validation in a config - a loose shape match is "good enough" to + * help validate the config is correct. + * + * @see {@link LooseParserModule}, {@link LooseProcessorModule} + */ +export type LooseRuleDefinition = + // TODO - ESLint v9 will remove support for RuleCreateFunction + | LooseRuleCreateFunction + | { + meta?: object; + create: LooseRuleCreateFunction; + }; +/* +eslint-disable-next-line @typescript-eslint/no-explicit-any -- +intentionally using `any` to allow bi-directional assignment (unknown and +never only allow unidirectional) +*/ +export type LooseRuleCreateFunction = (context: any) => Record< + string, + /* + eslint-disable-next-line @typescript-eslint/ban-types -- + intentionally use Function here to give us the basic "is a function" validation + without enforcing specific argument types so that different AST types can still + be passed to configs + */ + Function | undefined +>; + +export type RuleCreateFunction< TMessageIds extends string = never, TOptions extends readonly unknown[] = unknown[], > = (context: Readonly>) => RuleListener; -type AnyRuleCreateFunction = RuleCreateFunction; - -export { - AnyRuleCreateFunction, - AnyRuleModule, - CodePath, - CodePathFunction, - CodePathSegment, - ReportDescriptor, - ReportDescriptorMessageData, - ReportFixFunction, - ReportSuggestionArray, - RuleContext, - RuleCreateFunction, - RuleFix, - RuleFixer, - RuleFunction, - RuleListener, - RuleListenerExtension, - RuleMetaData, - RuleMetaDataDocs, - RuleModule, - SharedConfigurationSettings, -}; +export type AnyRuleCreateFunction = RuleCreateFunction< + string, + readonly unknown[] +>; diff --git a/packages/visitor-keys/package.json b/packages/visitor-keys/package.json index 3937248f3f5..91c2233dfcf 100644 --- a/packages/visitor-keys/package.json +++ b/packages/visitor-keys/package.json @@ -17,6 +17,7 @@ }, "./package.json": "./package.json" }, + "types": "./dist/index.d.ts", "engines": { "node": "^16.0.0 || >=18.0.0" },