From 66cd0c0535e5de1b46ba337919a9a92748d2b0a6 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 13 Nov 2023 23:36:28 +1030 Subject: [PATCH] feat: add types for flat config files (#7273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add types for flat config files * Update packages/utils/src/ts-eslint/Parser.ts --------- Co-authored-by: Josh Goldberg ✨ --- .../eslint-plugin-tslint/tests/index.spec.ts | 4 +- packages/eslint-plugin/index.d.ts | 4 +- .../eslint-plugin/tools/generate-configs.ts | 53 ++-- packages/rule-tester/src/RuleTester.ts | 3 +- .../rule-tester/src/types/RuleTesterConfig.ts | 7 +- .../rule-tester/src/types/ValidTestCase.ts | 5 +- .../src/utils/validationHelpers.ts | 6 +- packages/scope-manager/src/ScopeManager.ts | 4 +- packages/scope-manager/src/analyze.ts | 4 +- packages/types/src/parser-options.ts | 10 +- packages/utils/src/ts-eslint/Config.ts | 244 ++++++++++++++++++ packages/utils/src/ts-eslint/ESLint.ts | 7 +- packages/utils/src/ts-eslint/Linter.ts | 173 +++---------- packages/utils/src/ts-eslint/Parser.ts | 71 +++++ packages/utils/src/ts-eslint/Processor.ts | 48 ++++ packages/utils/src/ts-eslint/RuleTester.ts | 7 +- packages/utils/src/ts-eslint/SourceCode.ts | 3 +- packages/utils/src/ts-eslint/index.ts | 3 + .../website/src/components/linter/config.ts | 4 +- .../src/components/linter/createLinter.ts | 25 +- .../src/components/linter/createParser.ts | 6 +- .../website/src/components/linter/types.ts | 15 +- 22 files changed, 500 insertions(+), 206 deletions(-) create mode 100644 packages/utils/src/ts-eslint/Config.ts create mode 100644 packages/utils/src/ts-eslint/Parser.ts create mode 100644 packages/utils/src/ts-eslint/Processor.ts diff --git a/packages/eslint-plugin-tslint/tests/index.spec.ts b/packages/eslint-plugin-tslint/tests/index.spec.ts index 238cc318506..59f84be3077 100644 --- a/packages/eslint-plugin-tslint/tests/index.spec.ts +++ b/packages/eslint-plugin-tslint/tests/index.spec.ts @@ -3,6 +3,8 @@ import { TSESLint } from '@typescript-eslint/utils'; import { readFileSync } from 'fs'; import path = require('path'); +import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint'; + import type { Options } from '../src/rules/config'; import rule from '../src/rules/config'; @@ -165,7 +167,7 @@ ruleTester.run('tslint/config', rule, { }); describe('tslint/error', () => { - function testOutput(code: string, config: TSESLint.Linter.Config): void { + function testOutput(code: string, config: ClassicConfig.Config): void { const linter = new TSESLint.Linter(); linter.defineRule('tslint/config', rule); linter.defineParser('@typescript-eslint/parser', parser); diff --git a/packages/eslint-plugin/index.d.ts b/packages/eslint-plugin/index.d.ts index 7b4715f81f7..30f22e9b09e 100644 --- a/packages/eslint-plugin/index.d.ts +++ b/packages/eslint-plugin/index.d.ts @@ -1,9 +1,9 @@ -import type { TSESLint } from '@typescript-eslint/utils'; +import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint'; import type rules from './rules'; declare const cjsExport: { - configs: Record; + configs: Record; rules: typeof rules; }; export = cjsExport; diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index ccce38829ef..44508e14b08 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -1,7 +1,12 @@ -import prettier from '@prettier/sync'; -import type { TSESLint } from '@typescript-eslint/utils'; +import type { + ClassicConfig, + Linter, + RuleModule, + RuleRecommendation, +} from '@typescript-eslint/utils/ts-eslint'; import * as fs from 'fs'; import * as path from 'path'; +import prettier from 'prettier'; import * as url from 'url'; import type RulesFile from '../src/rules'; @@ -45,11 +50,11 @@ async function main(): Promise { ].join('\n'); } - const prettierConfig = prettier.resolveConfig(__dirname); + const prettierConfig = await prettier.resolveConfig(__dirname); - type LinterConfigRules = Record; + type LinterConfigRules = Record; - interface LinterConfig extends TSESLint.Linter.Config { + interface LinterConfig extends ClassicConfig.Config { extends?: string[] | string; plugins?: string[]; } @@ -74,8 +79,7 @@ async function main(): Promise { ); const EXTENDS = ['./configs/base', './configs/eslint-recommended']; - // TODO: migrate to import { RuleModule } from '@typescript-eslint/utils/ts-eslint' - type RuleEntry = [string, TSESLint.RuleModule]; + type RuleEntry = [string, RuleModule]; const allRuleEntries: RuleEntry[] = Object.entries(rules).sort((a, b) => a[0].localeCompare(b[0]), @@ -85,7 +89,7 @@ async function main(): Promise { deprecated?: 'exclude'; typeChecked?: 'exclude' | 'include-only'; baseRuleForExtensionRule?: 'exclude'; - forcedRuleLevel?: TSESLint.Linter.RuleLevel; + forcedRuleLevel?: Linter.RuleLevel; } /** @@ -144,13 +148,16 @@ async function main(): Promise { /** * Helper function writes configuration. */ - function writeConfig(getConfig: () => LinterConfig, name: string): void { + async function writeConfig( + getConfig: () => LinterConfig, + name: string, + ): Promise { const hyphens = '-'.repeat(35 - Math.ceil(name.length / 2)); console.log(chalk.blueBright(`\n${hyphens} ${name}.ts ${hyphens}`)); // note: we use `export =` because ESLint will import these configs via a commonjs import const code = `export = ${JSON.stringify(getConfig())};`; - const configStr = prettier.format(addAutoGeneratedComment(code), { + const configStr = await prettier.format(addAutoGeneratedComment(code), { parser: 'typescript', ...prettierConfig, }); @@ -167,13 +174,13 @@ async function main(): Promise { ruleEntries: readonly RuleEntry[]; } - function writeExtendedConfig({ + async function writeExtendedConfig({ extraExtends = [], filters: ruleFilter, name, ruleEntries, - }: ExtendedConfigSettings): void { - writeConfig( + }: ExtendedConfigSettings): Promise { + await writeConfig( () => ({ extends: [...EXTENDS, ...extraExtends], rules: ruleEntries.reduce( @@ -186,14 +193,14 @@ async function main(): Promise { } function filterRuleEntriesTo( - ...recommendations: (TSESLint.RuleRecommendation | undefined)[] + ...recommendations: (RuleRecommendation | undefined)[] ): RuleEntry[] { return allRuleEntries.filter(([, rule]) => recommendations.includes(rule.meta.docs?.recommended), ); } - writeConfig((): LinterConfig => { + await writeConfig((): LinterConfig => { const baseConfig: LinterConfig = { parser: '@typescript-eslint/parser', parserOptions: { @@ -215,7 +222,7 @@ async function main(): Promise { return baseConfig; }, 'base'); - writeExtendedConfig({ + await writeExtendedConfig({ name: 'all', filters: { deprecated: 'exclude', @@ -223,7 +230,7 @@ async function main(): Promise { ruleEntries: allRuleEntries, }); - writeExtendedConfig({ + await writeExtendedConfig({ filters: { typeChecked: 'exclude', }, @@ -231,12 +238,12 @@ async function main(): Promise { ruleEntries: filterRuleEntriesTo('recommended'), }); - writeExtendedConfig({ + await writeExtendedConfig({ name: 'recommended-type-checked', ruleEntries: filterRuleEntriesTo('recommended'), }); - writeExtendedConfig({ + await writeExtendedConfig({ filters: { typeChecked: 'exclude', }, @@ -244,12 +251,12 @@ async function main(): Promise { ruleEntries: filterRuleEntriesTo('recommended', 'strict'), }); - writeExtendedConfig({ + await writeExtendedConfig({ name: 'strict-type-checked', ruleEntries: filterRuleEntriesTo('recommended', 'strict'), }); - writeExtendedConfig({ + await writeExtendedConfig({ filters: { typeChecked: 'exclude', }, @@ -257,12 +264,12 @@ async function main(): Promise { ruleEntries: filterRuleEntriesTo('stylistic'), }); - writeExtendedConfig({ + await writeExtendedConfig({ name: 'stylistic-type-checked', ruleEntries: filterRuleEntriesTo('stylistic'), }); - writeConfig( + await writeConfig( () => ({ parserOptions: { project: null, diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 85bd223d860..18137231812 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -10,6 +10,7 @@ import { deepMerge } from '@typescript-eslint/utils/eslint-utils'; import type { AnyRuleCreateFunction, AnyRuleModule, + Parser, ParserOptions, RuleContext, RuleModule, @@ -525,7 +526,7 @@ export class RuleTester extends TestFramework { this.#linter.defineParser( config.parser, - wrapParser(require(config.parser) as Linter.ParserModule), + wrapParser(require(config.parser) as Parser.ParserModule), ); if (schema) { diff --git a/packages/rule-tester/src/types/RuleTesterConfig.ts b/packages/rule-tester/src/types/RuleTesterConfig.ts index c722c5be074..a51f1a4e016 100644 --- a/packages/rule-tester/src/types/RuleTesterConfig.ts +++ b/packages/rule-tester/src/types/RuleTesterConfig.ts @@ -1,8 +1,11 @@ -import type { Linter, ParserOptions } from '@typescript-eslint/utils/ts-eslint'; +import type { + ClassicConfig, + ParserOptions, +} from '@typescript-eslint/utils/ts-eslint'; import type { DependencyConstraint } from './DependencyConstraint'; -export interface RuleTesterConfig extends Linter.Config { +export interface RuleTesterConfig extends ClassicConfig.Config { /** * The default parser to use for tests. * @default '@typescript-eslint/parser' diff --git a/packages/rule-tester/src/types/ValidTestCase.ts b/packages/rule-tester/src/types/ValidTestCase.ts index 74776b58f19..11b6c9e7e4b 100644 --- a/packages/rule-tester/src/types/ValidTestCase.ts +++ b/packages/rule-tester/src/types/ValidTestCase.ts @@ -1,4 +1,5 @@ import type { + Linter, ParserOptions, SharedConfigurationSettings, } from '@typescript-eslint/utils/ts-eslint'; @@ -17,7 +18,7 @@ export interface ValidTestCase> { /** * Environments for the test case. */ - readonly env?: Readonly>; + readonly env?: Readonly; /** * The fake filename for the test case. Useful for rules that make assertion about filenames. */ @@ -25,7 +26,7 @@ export interface ValidTestCase> { /** * The additional global variables. */ - readonly globals?: Record; + readonly globals?: Readonly; /** * Options for the test case. */ diff --git a/packages/rule-tester/src/utils/validationHelpers.ts b/packages/rule-tester/src/utils/validationHelpers.ts index 33fd0c234de..34e0ca3277d 100644 --- a/packages/rule-tester/src/utils/validationHelpers.ts +++ b/packages/rule-tester/src/utils/validationHelpers.ts @@ -1,6 +1,6 @@ import { simpleTraverse } from '@typescript-eslint/typescript-estree'; import type { TSESTree } from '@typescript-eslint/utils'; -import type { Linter, SourceCode } from '@typescript-eslint/utils/ts-eslint'; +import type { Parser, SourceCode } from '@typescript-eslint/utils/ts-eslint'; /* * List every parameters possible on a test case that are not related to eslint @@ -75,7 +75,7 @@ const parserSymbol = Symbol.for('eslint.RuleTester.parser'); * Wraps the given parser in order to intercept and modify return values from the `parse` and `parseForESLint` methods, for test purposes. * In particular, to modify ast nodes, tokens and comments to throw on access to their `start` and `end` properties. */ -export function wrapParser(parser: Linter.ParserModule): Linter.ParserModule { +export function wrapParser(parser: Parser.ParserModule): Parser.ParserModule { /** * Define `start`/`end` properties of all nodes of the given AST as throwing error. */ @@ -121,7 +121,7 @@ export function wrapParser(parser: Linter.ParserModule): Linter.ParserModule { return { // @ts-expect-error -- see above [parserSymbol]: parser, - parseForESLint(...args): Linter.ESLintParseResult { + parseForESLint(...args): Parser.ParseResult { const ret = parser.parseForESLint(...args); defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys); diff --git a/packages/scope-manager/src/ScopeManager.ts b/packages/scope-manager/src/ScopeManager.ts index e6d3ee333c3..66f8bbe500f 100644 --- a/packages/scope-manager/src/ScopeManager.ts +++ b/packages/scope-manager/src/ScopeManager.ts @@ -1,4 +1,4 @@ -import type { TSESTree } from '@typescript-eslint/types'; +import type { SourceType, TSESTree } from '@typescript-eslint/types'; import { assert } from './assert'; import type { Scope } from './scope'; @@ -27,7 +27,7 @@ import type { Variable } from './variable'; interface ScopeManagerOptions { globalReturn?: boolean; - sourceType?: 'module' | 'script'; + sourceType?: SourceType; impliedStrict?: boolean; } diff --git a/packages/scope-manager/src/analyze.ts b/packages/scope-manager/src/analyze.ts index 2ab613325c9..450283553b2 100644 --- a/packages/scope-manager/src/analyze.ts +++ b/packages/scope-manager/src/analyze.ts @@ -1,4 +1,4 @@ -import type { Lib, TSESTree } from '@typescript-eslint/types'; +import type { Lib, SourceType, TSESTree } from '@typescript-eslint/types'; import { visitorKeys } from '@typescript-eslint/visitor-keys'; import type { ReferencerOptions } from './referencer'; @@ -55,7 +55,7 @@ interface AnalyzeOptions { /** * The source type of the script. */ - sourceType?: 'module' | 'script'; + sourceType?: SourceType; /** * Emit design-type metadata for decorated declarations in source. diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index 5389efba75e..bf1655125dc 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -16,6 +16,8 @@ type EcmaVersion = | 11 | 12 | 13 + | 14 + | 15 | 2015 | 2016 | 2017 @@ -23,14 +25,18 @@ type EcmaVersion = | 2019 | 2020 | 2021 - | 2022; + | 2022 + | 2023 + | 2024; -type SourceType = 'module' | 'script'; +type SourceTypeClassic = 'module' | 'script'; +type SourceType = SourceTypeClassic | 'commonjs'; interface ParserOptions { ecmaFeatures?: { globalReturn?: boolean; jsx?: boolean; + [key: string]: unknown; }; ecmaVersion?: EcmaVersion | 'latest'; diff --git a/packages/utils/src/ts-eslint/Config.ts b/packages/utils/src/ts-eslint/Config.ts new file mode 100644 index 00000000000..6a94d765541 --- /dev/null +++ b/packages/utils/src/ts-eslint/Config.ts @@ -0,0 +1,244 @@ +/* eslint-disable @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/no-namespace */ + +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'; + +/** @internal */ +export namespace SharedConfig { + export type Severity = 0 | 1 | 2; + export type SeverityString = 'error' | 'off' | 'warn'; + export type RuleLevel = Severity | SeverityString; + + export type RuleLevelAndOptions = [RuleLevel, ...unknown[]]; + + export type RuleEntry = RuleLevel | RuleLevelAndOptions; + export type RulesRecord = Partial>; + + export type GlobalVariableOptionBase = 'off' | 'readonly' | 'writable'; + export type GlobalVariableOption = GlobalVariableOptionBase | boolean; + + export interface GlobalsConfig { + [name: string]: GlobalVariableOption; + } + export interface EnvironmentConfig { + [name: string]: boolean; + } + + export type ParserOptions = ParserOptionsTypes.ParserOptions; +} + +export namespace ClassicConfig { + export type EnvironmentConfig = SharedConfig.EnvironmentConfig; + export type GlobalsConfig = SharedConfig.GlobalsConfig; + export type GlobalVariableOption = SharedConfig.GlobalVariableOption; + export type GlobalVariableOptionBase = SharedConfig.GlobalVariableOptionBase; + export type ParserOptions = SharedConfig.ParserOptions; + export type RuleEntry = SharedConfig.RuleEntry; + export type RuleLevel = SharedConfig.RuleLevel; + export type RuleLevelAndOptions = SharedConfig.RuleLevelAndOptions; + export type RulesRecord = SharedConfig.RulesRecord; + export type Severity = SharedConfig.Severity; + export type SeverityString = SharedConfig.SeverityString; + + // https://github.com/eslint/eslintrc/blob/v2.1.0/conf/config-schema.js + interface BaseConfig { + $schema?: string; + /** + * The environment settings. + */ + env?: EnvironmentConfig; + /** + * The path to other config files or the package name of shareable configs. + */ + extends?: string[] | string; + /** + * The global variable settings. + */ + globals?: GlobalsConfig; + /** + * The flag that disables directive comments. + */ + noInlineConfig?: boolean; + /** + * The override settings per kind of files. + */ + overrides?: ConfigOverride[]; + /** + * The path to a parser or the package name of a parser. + */ + parser?: string | null; + /** + * The parser options. + */ + parserOptions?: ParserOptions; + /** + * The plugin specifiers. + */ + plugins?: string[]; + /** + * The processor specifier. + */ + processor?: string; + /** + * The flag to report unused `eslint-disable` comments. + */ + reportUnusedDisableDirectives?: boolean; + /** + * The rule settings. + */ + rules?: RulesRecord; + /** + * The shared settings. + */ + settings?: SharedConfigurationSettings; + } + + export interface ConfigOverride extends BaseConfig { + excludedFiles?: string[] | string; + files: string[] | string; + } + + export interface Config extends BaseConfig { + /** + * The glob patterns that ignore to lint. + */ + ignorePatterns?: string[] | string; + /** + * The root flag. + */ + root?: boolean; + } +} + +export namespace FlatConfig { + export type EcmaVersion = ParserOptionsTypes.EcmaVersion; + export type GlobalsConfig = SharedConfig.GlobalsConfig; + export type Parser = ParserType.ParserModule; + export type ParserOptions = SharedConfig.ParserOptions; + export type Processor = ProcessorType.ProcessorModule; + export type RuleEntry = SharedConfig.RuleEntry; + export type RuleLevel = SharedConfig.RuleLevel; + export type RuleLevelAndOptions = SharedConfig.RuleLevelAndOptions; + export type Rules = SharedConfig.RulesRecord; + export type Settings = SharedConfigurationSettings; + export type Severity = SharedConfig.Severity; + export type SeverityString = SharedConfig.SeverityString; + export type SourceType = ParserOptionsTypes.SourceType | 'commonjs'; + + export interface Plugin { + /** + * The definition of plugin processors. + * Can be referenced by string in the config (i.e., `"pluginName/processorName"`). + */ + processors?: Record; + /** + * The definition of plugin rules. + */ + rules?: Record; + } + export interface Plugins { + [pluginAlias: string]: Plugin; + } + + export interface LinterOptions { + /** + * A Boolean value indicating if inline configuration is allowed. + */ + noInlineConfig?: boolean; + /** + * A Boolean value indicating if unused disable directives should be tracked and reported. + */ + reportUnusedDisableDirectives?: boolean; + } + + export interface LanguageOptions { + /** + * The version of ECMAScript to support. + * May be any year (i.e., `2022`) or version (i.e., `5`). + * Set to `"latest"` for the most recent supported version. + * @default "latest" + */ + ecmaVersion?: EcmaVersion; + /** + * An object specifying additional objects that should be added to the global scope during linting. + */ + globals?: GlobalsConfig; + /** + * An object containing a `parse()` method or a `parseForESLint()` method. + * @default + * ``` + * // https://github.com/eslint/espree + * require('espree') + * ``` + */ + parser?: Parser; + /** + * An object specifying additional options that are passed directly to the parser. + * The available options are parser-dependent. + */ + parserOptions?: ParserOptions; + /** + * The type of JavaScript source code. + * Possible values are `"script"` for traditional script files, `"module"` for ECMAScript modules (ESM), and `"commonjs"` for CommonJS files. + * @default + * ``` + * // for `.js` and `.mjs` files + * "module" + * // for `.cjs` files + * "commonjs" + * ``` + */ + sourceType?: SourceType; + } + + // 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 { + /** + * 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[]; + /** + * 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[]; + /** + * An object containing settings related to how JavaScript is configured for linting. + */ + languageOptions?: LanguageOptions; + /** + * An object containing settings related to the linting process. + */ + linterOptions?: LinterOptions; + /** + * An object containing a name-value mapping of plugin names to plugin objects. + * When `files` is specified, these plugins are only available to the matching files. + */ + plugins?: Plugins; + /** + * Either an object containing `preprocess()` and `postprocess()` methods or + * a string indicating the name of a processor inside of a plugin + * (i.e., `"pluginName/processorName"`). + */ + processor?: Processor; + /** + * An object containing the configured rules. + * When `files` or `ignores` are specified, these rule configurations are only available to the matching files. + */ + rules?: Rules; + /** + * An object containing name-value pairs of information that should be available to all rules. + */ + settings?: Settings; + } + export type ConfigArray = Config[]; + export type ConfigFile = ConfigArray | (() => Promise); +} diff --git a/packages/utils/src/ts-eslint/ESLint.ts b/packages/utils/src/ts-eslint/ESLint.ts index eb1aae34de1..104d8f83765 100644 --- a/packages/utils/src/ts-eslint/ESLint.ts +++ b/packages/utils/src/ts-eslint/ESLint.ts @@ -2,6 +2,7 @@ import { ESLint as ESLintESLint } from 'eslint'; +import type { ClassicConfig } from './Config'; import type { Linter } from './Linter'; declare class ESLintBase { @@ -23,7 +24,7 @@ declare class ESLintBase { * because ESLint cannot handle the overrides setting. * @returns The promise that will be fulfilled with a configuration object. */ - calculateConfigForFile(filePath: string): Promise; + calculateConfigForFile(filePath: string): Promise; /** * This method checks if a given file is ignored by your configuration. * @param filePath The path to the file you want to check. @@ -107,7 +108,7 @@ namespace ESLint { * You can use this option to define the default settings that will be used if your configuration files don't * configure it. */ - baseConfig?: Linter.Config | null; + baseConfig?: Linter.ConfigType | null; /** * If true is present, the eslint.lintFiles() method caches lint results and uses it if each target file is not * changed. Please mind that ESLint doesn't clear the cache when you upgrade ESLint plugins. In that case, you have @@ -164,7 +165,7 @@ namespace ESLint { * Configuration object, overrides all configurations used with this instance. * You can use this option to define the settings that will be used even if your configuration files configure it. */ - overrideConfig?: Linter.Config | null; + overrideConfig?: ClassicConfig.ConfigOverride | null; /** * The path to a configuration file, overrides all configurations used with this instance. * The options.overrideConfig option is applied after this option is applied. diff --git a/packages/utils/src/ts-eslint/Linter.ts b/packages/utils/src/ts-eslint/Linter.ts index 62a6645e009..f1f645d82eb 100644 --- a/packages/utils/src/ts-eslint/Linter.ts +++ b/packages/utils/src/ts-eslint/Linter.ts @@ -2,15 +2,10 @@ import { Linter as ESLintLinter } from 'eslint'; -import type { ParserServices, TSESTree } from '../ts-estree'; -import type { ParserOptions as TSParserOptions } from './ParserOptions'; -import type { - RuleCreateFunction, - RuleFix, - RuleModule, - SharedConfigurationSettings, -} from './Rule'; -import type { Scope } from './Scope'; +import type { ClassicConfig, FlatConfig, SharedConfig } from './Config'; +import type { Parser } from './Parser'; +import type { Processor as ProcessorType } from './Processor'; +import type { RuleCreateFunction, RuleFix, RuleModule } from './Rule'; import type { SourceCode } from './SourceCode'; export type MinimalRuleModule< @@ -31,7 +26,7 @@ declare class LinterBase { * @param parserId Name of the parser * @param parserModule The parser object */ - defineParser(parserId: string, parserModule: Linter.ParserModule): void; + defineParser(parserId: string, parserModule: Parser.ParserModule): void; /** * Defines a new linting rule. @@ -78,7 +73,7 @@ declare class LinterBase { */ verify( textOrSourceCode: SourceCode | string, - config: Linter.Config, + config: Linter.ConfigType, filenameOrOptions?: Linter.VerifyOptions | string, ): Linter.LintMessage[]; @@ -91,7 +86,7 @@ declare class LinterBase { */ verifyAndFix( code: string, - config: Linter.Config, + config: Linter.ConfigType, options: Linter.FixOptions, ): Linter.FixReport; @@ -118,91 +113,23 @@ namespace Linter { cwd?: string; } - export type Severity = 0 | 1 | 2; - export type SeverityString = 'error' | 'off' | 'warn'; - export type RuleLevel = Severity | SeverityString; - - export type RuleLevelAndOptions = [RuleLevel, ...unknown[]]; - - export type RuleEntry = RuleLevel | RuleLevelAndOptions; - export type RulesRecord = Partial>; - - export type GlobalVariableOptionBase = 'off' | 'readonly' | 'writable'; - export type GlobalVariableOption = GlobalVariableOptionBase | boolean; - - export type GlobalsConfig = Record; - export type EnvironmentConfig = Record; - - // https://github.com/eslint/eslint/blob/v6.8.0/conf/config-schema.js - interface BaseConfig { - $schema?: string; - /** - * The environment settings. - */ - env?: EnvironmentConfig; - /** - * The path to other config files or the package name of shareable configs. - */ - extends?: string[] | string; - /** - * The global variable settings. - */ - globals?: GlobalsConfig; - /** - * The flag that disables directive comments. - */ - noInlineConfig?: boolean; - /** - * The override settings per kind of files. - */ - overrides?: ConfigOverride[]; - /** - * The path to a parser or the package name of a parser. - */ - parser?: string; - /** - * The parser options. - */ - parserOptions?: ParserOptions; - /** - * The plugin specifiers. - */ - plugins?: string[]; - /** - * The processor specifier. - */ - processor?: string; - /** - * The flag to report unused `eslint-disable` comments. - */ - reportUnusedDisableDirectives?: boolean; - /** - * The rule settings. - */ - rules?: RulesRecord; - /** - * The shared settings. - */ - settings?: SharedConfigurationSettings; - } - - export interface ConfigOverride extends BaseConfig { - excludedFiles?: string[] | string; - files: string[] | string; - } - - export interface Config extends BaseConfig { - /** - * The glob patterns that ignore to lint. - */ - ignorePatterns?: string[] | string; - /** - * The root flag. - */ - root?: boolean; - } - - export type ParserOptions = TSParserOptions; + export type EnvironmentConfig = SharedConfig.EnvironmentConfig; + export type GlobalsConfig = SharedConfig.GlobalsConfig; + export type GlobalVariableOption = SharedConfig.GlobalVariableOption; + export type GlobalVariableOptionBase = SharedConfig.GlobalVariableOptionBase; + export type ParserOptions = SharedConfig.ParserOptions; + export type RuleEntry = SharedConfig.RuleEntry; + export type RuleLevel = SharedConfig.RuleLevel; + export type RuleLevelAndOptions = SharedConfig.RuleLevelAndOptions; + export type RulesRecord = SharedConfig.RulesRecord; + export type Severity = SharedConfig.Severity; + export type SeverityString = SharedConfig.SeverityString; + + /** @deprecated use ClassicConfig.ConfigType instead */ + export type Config = ClassicConfig.Config; + export type ConfigType = ClassicConfig.Config | FlatConfig.ConfigArray; + /** @deprecated use ClassicConfig.ConfigOverride instead */ + export type ConfigOverride = ClassicConfig.ConfigOverride; export interface VerifyOptions { /** @@ -229,12 +156,12 @@ namespace Linter { * the messages as appropriate, and return a one-dimensional array of * messages. */ - postprocess?: Processor['postprocess']; + postprocess?: ProcessorType.PostProcess; /** * preprocessor for source text. * If provided, this should accept a string of source text, and return an array of code blocks to lint. */ - preprocess?: Processor['preprocess']; + preprocess?: ProcessorType.PreProcess; /** * Adds reported errors for unused `eslint-disable` directives. */ @@ -315,50 +242,20 @@ namespace Linter { messages: LintMessage[]; } - export type ParserModule = - | { - parse(text: string, options?: ParserOptions): TSESTree.Program; - } - | { - parseForESLint( - text: string, - options?: ParserOptions, - ): ESLintParseResult; - }; + /** @deprecated use Parser.ParserModule */ + export type ParserModule = Parser.ParserModule; - export interface ESLintParseResult { - ast: TSESTree.Program; - services?: ParserServices; - scopeManager?: Scope.ScopeManager; - visitorKeys?: SourceCode.VisitorKeys; - } + /** @deprecated use Parser.ParseResult */ + export type ESLintParseResult = Parser.ParseResult; - export interface Processor { - /** - * The function to extract code blocks. - */ - preprocess?: ( - text: string, - filename: string, - ) => (string | { text: string; filename: string })[]; - /** - * The function to merge messages. - */ - postprocess?: ( - messagesList: Linter.LintMessage[][], - filename: string, - ) => Linter.LintMessage[]; - /** - * If `true` then it means the processor supports autofix. - */ - supportsAutofix?: boolean; - } + /** @deprecated use Processor.ProcessorModule */ + export type Processor = ProcessorType.ProcessorModule; export interface Environment { /** * The definition of global variables. */ - globals?: Record; + globals?: GlobalsConfig; /** * The parser options that will be enabled under this environment. */ @@ -369,7 +266,7 @@ namespace Linter { /** * The definition of plugin configs. */ - configs?: Record; + configs?: Record; /** * The definition of plugin environments. */ @@ -377,7 +274,7 @@ namespace Linter { /** * The definition of plugin processors. */ - processors?: Record; + processors?: Record; /** * The definition of plugin rules. */ diff --git a/packages/utils/src/ts-eslint/Parser.ts b/packages/utils/src/ts-eslint/Parser.ts new file mode 100644 index 00000000000..e709ef70df6 --- /dev/null +++ b/packages/utils/src/ts-eslint/Parser.ts @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +import type { ParserServices, TSESTree } from '../ts-estree'; +import type { ParserOptions } from './ParserOptions'; +import type { Scope } from './Scope'; + +export namespace Parser { + export interface ParserMeta { + /** + * The unique name of the parser. + */ + name: string; + /** + * The a string identifying the version of the parser. + */ + version?: string; + } + + export type ParserModule = + | { + /** + * Information about the parser to uniquely identify it when serializing. + */ + meta?: ParserMeta; + /** + * Parses the given text into an ESTree AST + */ + parse(text: string, options?: ParserOptions): TSESTree.Program; + } + | { + /** + * Information about the parser to uniquely identify it when serializing. + */ + meta?: ParserMeta; + /** + * Parses the given text into an AST + */ + parseForESLint(text: string, options?: ParserOptions): ParseResult; + }; + + export interface ParseResult { + /** + * The ESTree AST + */ + ast: TSESTree.Program; + /** + * Any parser-dependent services (such as type checkers for nodes). + * The value of the services property is available to rules as `context.sourceCode.parserServices`. + * The default is an empty object. + */ + services?: ParserServices; + /** + * A `ScopeManager` object. + * Custom parsers can use customized scope analysis for experimental/enhancement syntaxes. + * The default is the `ScopeManager` object which is created by `eslint-scope`. + */ + scopeManager?: Scope.ScopeManager; + /** + * An object to customize AST traversal. + * The keys of the object are the type of AST nodes. + * Each value is an array of the property names which should be traversed. + * The default is `KEYS` of `eslint-visitor-keys`. + */ + visitorKeys?: VisitorKeys; + } + + // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style + export interface VisitorKeys { + [nodeType: string]: string[]; + } +} diff --git a/packages/utils/src/ts-eslint/Processor.ts b/packages/utils/src/ts-eslint/Processor.ts new file mode 100644 index 00000000000..57f03ae5def --- /dev/null +++ b/packages/utils/src/ts-eslint/Processor.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +import type { Linter } from './Linter'; + +export namespace Processor { + export interface ProcessorMeta { + /** + * The unique name of the processor. + */ + name: string; + /** + * The a string identifying the version of the processor. + */ + version?: string; + } + + export type PreProcess = ( + text: string, + filename: string, + ) => (string | { text: string; filename: string })[]; + + export type PostProcess = ( + messagesList: Linter.LintMessage[][], + filename: string, + ) => Linter.LintMessage[]; + + export interface ProcessorModule { + /** + * Information about the processor to uniquely identify it when serializing. + */ + meta?: ProcessorMeta; + + /** + * The function to extract code blocks. + */ + preprocess?: PreProcess; + + /** + * The function to merge messages. + */ + postprocess?: PostProcess; + + /** + * If `true` then it means the processor supports autofix. + */ + supportsAutofix?: boolean; + } +} diff --git a/packages/utils/src/ts-eslint/RuleTester.ts b/packages/utils/src/ts-eslint/RuleTester.ts index 5e2af43d59b..51b54fb7b5f 100644 --- a/packages/utils/src/ts-eslint/RuleTester.ts +++ b/packages/utils/src/ts-eslint/RuleTester.ts @@ -1,6 +1,7 @@ import { RuleTester as ESLintRuleTester } from 'eslint'; import type { AST_NODE_TYPES, AST_TOKEN_TYPES } from '../ts-estree'; +import type { ClassicConfig } from './Config'; import type { Linter } from './Linter'; import type { ParserOptions } from './ParserOptions'; import type { @@ -23,7 +24,7 @@ interface ValidTestCase> { /** * Environments for the test case. */ - readonly env?: Readonly>; + readonly env?: Readonly; /** * The fake filename for the test case. Useful for rules that make assertion about filenames. */ @@ -31,7 +32,7 @@ interface ValidTestCase> { /** * The additional global variables. */ - readonly globals?: Record; + readonly globals?: Readonly; /** * Options for the test case. */ @@ -143,7 +144,7 @@ interface RunTests< readonly valid: readonly (ValidTestCase | string)[]; readonly invalid: readonly InvalidTestCase[]; } -interface RuleTesterConfig extends Linter.Config { +interface RuleTesterConfig extends ClassicConfig.Config { // should be require.resolve(parserPackageName) readonly parser: string; readonly parserOptions?: Readonly; diff --git a/packages/utils/src/ts-eslint/SourceCode.ts b/packages/utils/src/ts-eslint/SourceCode.ts index a1410e0928e..50ffd9000a7 100644 --- a/packages/utils/src/ts-eslint/SourceCode.ts +++ b/packages/utils/src/ts-eslint/SourceCode.ts @@ -3,6 +3,7 @@ import { SourceCode as ESLintSourceCode } from 'eslint'; import type { ParserServices, TSESTree } from '../ts-estree'; +import type { Parser } from './Parser'; import type { Scope } from './Scope'; declare class TokenStore { @@ -397,7 +398,7 @@ namespace SourceCode { visitorKeys: VisitorKeys | null; } - export type VisitorKeys = Record; + export type VisitorKeys = Parser.VisitorKeys; export type FilterPredicate = (token: TSESTree.Token) => boolean; export type GetFilterPredicate = diff --git a/packages/utils/src/ts-eslint/index.ts b/packages/utils/src/ts-eslint/index.ts index 48af8aacb95..5f6a1e1b71d 100644 --- a/packages/utils/src/ts-eslint/index.ts +++ b/packages/utils/src/ts-eslint/index.ts @@ -1,8 +1,11 @@ export * from './AST'; export * from './CLIEngine'; +export * from './Config'; export * from './ESLint'; export * from './Linter'; +export * from './Parser'; export * from './ParserOptions'; +export * from './Processor'; export * from './Rule'; export * from './RuleTester'; export * from './Scope'; diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index fc6389b4e1b..d3c7319a8aa 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -1,5 +1,5 @@ import type { ParseSettings } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; -import type { TSESLint } from '@typescript-eslint/utils'; +import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint'; export const PARSER_NAME = '@typescript-eslint/parser'; @@ -31,7 +31,7 @@ export const defaultParseSettings: ParseSettings = { tsconfigRootDir: '/', }; -export const defaultEslintConfig: TSESLint.Linter.Config = { +export const defaultEslintConfig: ClassicConfig.Config = { parser: PARSER_NAME, parserOptions: { ecmaFeatures: { diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts index 2f0c1725329..7f3f8541e78 100644 --- a/packages/website/src/components/linter/createLinter.ts +++ b/packages/website/src/components/linter/createLinter.ts @@ -1,5 +1,10 @@ import type * as tsvfs from '@site/src/vendor/typescript-vfs'; -import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { JSONSchema, TSESTree } from '@typescript-eslint/utils'; +import type { + ClassicConfig, + Linter, + SourceType, +} from '@typescript-eslint/utils/ts-eslint'; import type * as ts from 'typescript'; import { createCompilerOptions } from '../lib/createCompilerOptions'; @@ -25,11 +30,11 @@ export interface CreateLinter { } >; configs: string[]; - triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; + triggerFix(filename: string): Linter.FixReport | undefined; triggerLint(filename: string): void; onLint(cb: LinterOnLint): () => void; onParse(cb: LinterOnParse): () => void; - updateParserOptions(sourceType?: TSESLint.SourceType): void; + updateParserOptions(sourceType?: SourceType): void; } export function createLinter( @@ -40,7 +45,7 @@ export function createLinter( const rules: CreateLinter['rules'] = new Map(); const configs = new Map(Object.entries(webLinterModule.configs)); let compilerOptions: ts.CompilerOptions = {}; - const eslintConfig: TSESLint.Linter.Config = { ...defaultEslintConfig }; + const eslintConfig: ClassicConfig.Config = { ...defaultEslintConfig }; const onLint = createEventsBinder(); const onParse = createEventsBinder(); @@ -76,7 +81,7 @@ export function createLinter( const messages = linter.verify(code, eslintConfig, filename); onLint.trigger(filename, messages); } catch (e) { - const lintMessage: TSESLint.Linter.LintMessage = { + const lintMessage: Linter.LintMessage = { source: 'eslint', ruleId: '', severity: 2, @@ -97,9 +102,7 @@ export function createLinter( } }; - const triggerFix = ( - filename: string, - ): TSESLint.Linter.FixReport | undefined => { + const triggerFix = (filename: string): Linter.FixReport | undefined => { const code = system.readFile(filename); if (code) { return linter.verifyAndFix(code, eslintConfig, { @@ -110,14 +113,14 @@ export function createLinter( return undefined; }; - const updateParserOptions = (sourceType?: TSESLint.SourceType): void => { + const updateParserOptions = (sourceType?: SourceType): void => { eslintConfig.parserOptions ??= {}; eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; }; const resolveEslintConfig = ( - cfg: Partial, - ): TSESLint.Linter.Config => { + cfg: Partial, + ): ClassicConfig.Config => { const config = { rules: {} }; if (cfg.extends) { const cfgExtends = Array.isArray(cfg.extends) diff --git a/packages/website/src/components/linter/createParser.ts b/packages/website/src/components/linter/createParser.ts index 1c0d9258a8f..ee509fed0ac 100644 --- a/packages/website/src/components/linter/createParser.ts +++ b/packages/website/src/components/linter/createParser.ts @@ -1,6 +1,6 @@ import type * as tsvfs from '@site/src/vendor/typescript-vfs'; import type { ParserOptions } from '@typescript-eslint/types'; -import type { TSESLint } from '@typescript-eslint/utils'; +import type { Parser } from '@typescript-eslint/utils/ts-eslint'; import type * as ts from 'typescript'; import { defaultParseSettings } from './config'; @@ -17,7 +17,7 @@ export function createParser( onUpdate: (filename: string, model: UpdateModel) => void, utils: WebLinterModule, vfs: typeof tsvfs, -): TSESLint.Linter.ParserModule & { +): Parser.ParserModule & { updateConfig: (compilerOptions: ts.CompilerOptions) => void; } { const registeredFiles = new Set(); @@ -42,7 +42,7 @@ export function createParser( parseForESLint: ( text: string, options: ParserOptions = {}, - ): TSESLint.Linter.ESLintParseResult => { + ): Parser.ParseResult => { const filePath = options.filePath ?? '/input.ts'; // if text is empty use empty line to avoid error diff --git a/packages/website/src/components/linter/types.ts b/packages/website/src/components/linter/types.ts index 12e4c9654d4..4a30ef634c7 100644 --- a/packages/website/src/components/linter/types.ts +++ b/packages/website/src/components/linter/types.ts @@ -1,6 +1,11 @@ import type { analyze, ScopeManager } from '@typescript-eslint/scope-manager'; import type { astConverter } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { TSESTree } from '@typescript-eslint/utils'; +import type { + ClassicConfig, + Linter, + SourceCode, +} from '@typescript-eslint/utils/ts-eslint'; import type esquery from 'esquery'; import type * as ts from 'typescript'; @@ -14,12 +19,12 @@ export interface UpdateModel { } export interface WebLinterModule { - createLinter: () => TSESLint.Linter; + createLinter: () => Linter; analyze: typeof analyze; - visitorKeys: TSESLint.SourceCode.VisitorKeys; + visitorKeys: SourceCode.VisitorKeys; astConverter: typeof astConverter; esquery: typeof esquery; - configs: Record; + configs: Record; } export type PlaygroundSystem = Required< @@ -31,7 +36,7 @@ export type PlaygroundSystem = Required< export type LinterOnLint = ( fileName: string, - messages: TSESLint.Linter.LintMessage[], + messages: Linter.LintMessage[], ) => void; export type LinterOnParse = (fileName: string, model: UpdateModel) => void;