diff --git a/.cspell.json b/.cspell.json index 86a9b7af4f9..2d939d1b4a3 100644 --- a/.cspell.json +++ b/.cspell.json @@ -40,6 +40,7 @@ "ASTs", "autofix", "autofixers", + "autofixes", "backticks", "bigint", "bivariant", @@ -70,6 +71,7 @@ "performant", "pluggable", "postprocess", + "postprocessor", "Premade", "prettier's", "recurse", diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index de3cca75343..a1ee754f234 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -147,7 +147,7 @@ export default util.createRule({ } const nextToken = sourceCode.getTokenAfter(prevToken); - if (nextToken && sourceCode.isSpaceBetweenTokens(prevToken, nextToken)) { + if (nextToken && sourceCode.isSpaceBetween(prevToken, nextToken)) { return false; } diff --git a/packages/eslint-plugin/src/rules/indent-new-do-not-use/OffsetStorage.ts b/packages/eslint-plugin/src/rules/indent-new-do-not-use/OffsetStorage.ts index 082ca05129d..d1933467263 100644 --- a/packages/eslint-plugin/src/rules/indent-new-do-not-use/OffsetStorage.ts +++ b/packages/eslint-plugin/src/rules/indent-new-do-not-use/OffsetStorage.ts @@ -273,9 +273,11 @@ export class OffsetStorage { * Gets the first token that the given token's indentation is dependent on * @returns The token that the given token depends on, or `null` if the given token is at the top level */ + getFirstDependency( + token: Exclude, + ): Exclude | null; getFirstDependency(token: TSESTree.Token): TSESTree.Token | null; - getFirstDependency(token: TokenOrComment): TokenOrComment | null; - getFirstDependency(token: TokenOrComment): TokenOrComment | null { + getFirstDependency(token: TSESTree.Token): TSESTree.Token | null { return this.getOffsetDescriptor(token).from; } } diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts index 9aa41a61231..9977f10c140 100644 --- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -2,7 +2,10 @@ import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import baseRule from 'eslint/lib/rules/no-unused-expressions'; import * as util from '../util'; -export default util.createRule({ +type MessageIds = util.InferMessageIdsTypeFromRule; +type Options = util.InferOptionsTypeFromRule; + +export default util.createRule({ name: 'no-unused-expressions', meta: { type: 'suggestion', diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index b939f8214a9..2c3595f3ba2 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -5,7 +5,10 @@ import { import baseRule from 'eslint/lib/rules/no-unused-vars'; import * as util from '../util'; -export default util.createRule({ +type MessageIds = util.InferMessageIdsTypeFromRule; +type Options = util.InferOptionsTypeFromRule; + +export default util.createRule({ name: 'no-unused-vars', meta: { type: 'problem', diff --git a/packages/eslint-plugin/src/rules/space-before-function-paren.ts b/packages/eslint-plugin/src/rules/space-before-function-paren.ts index 41a817938da..5b7f8fd6e04 100644 --- a/packages/eslint-plugin/src/rules/space-before-function-paren.ts +++ b/packages/eslint-plugin/src/rules/space-before-function-paren.ts @@ -145,7 +145,7 @@ export default util.createRule({ rightToken = sourceCode.getFirstToken(node, util.isOpeningParenToken)!; leftToken = sourceCode.getTokenBefore(rightToken)!; } - const hasSpacing = sourceCode.isSpaceBetweenTokens(leftToken, rightToken); + const hasSpacing = sourceCode.isSpaceBetween(leftToken, rightToken); if (hasSpacing && functionConfig === 'never') { context.report({ diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 2a0d891f5f1..82828f13893 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -15,4 +15,16 @@ const { isObjectNotArray, getParserServices, } = ESLintUtils; -export { applyDefault, deepMerge, isObjectNotArray, getParserServices }; +type InferMessageIdsTypeFromRule = ESLintUtils.InferMessageIdsTypeFromRule< + T +>; +type InferOptionsTypeFromRule = ESLintUtils.InferOptionsTypeFromRule; + +export { + applyDefault, + deepMerge, + isObjectNotArray, + getParserServices, + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, +}; diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index e9f50fd2107..2e6c4711cb0 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -22,32 +22,6 @@ function upperCaseFirst(str: string): string { return str[0].toUpperCase() + str.slice(1); } -type InferOptionsTypeFromRuleNever = T extends TSESLint.RuleModule< - never, - infer TOptions -> - ? TOptions - : unknown; -/** - * Uses type inference to fetch the TOptions type from the given RuleModule - */ -type InferOptionsTypeFromRule = T extends TSESLint.RuleModule< - string, - infer TOptions -> - ? TOptions - : InferOptionsTypeFromRuleNever; - -/** - * Uses type inference to fetch the TMessageIds type from the given RuleModule - */ -type InferMessageIdsTypeFromRule = T extends TSESLint.RuleModule< - infer TMessageIds, - unknown[] -> - ? TMessageIds - : unknown; - /** Return true if both parameters are equal. */ type Equal = (a: T, b: T) => boolean; @@ -136,8 +110,6 @@ export { getEnumNames, getNameFromIndexSignature, getNameFromMember, - InferMessageIdsTypeFromRule, - InferOptionsTypeFromRule, isDefinitionFile, RequireKeys, upperCaseFirst, diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts index 172dfdd793a..19e36292f03 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts @@ -23,12 +23,15 @@ const hasExport = /^export/m; function makeExternalModule< T extends ValidTestCase | InvalidTestCase >(tests: T[]): T[] { - tests.forEach(t => { + return tests.map(t => { if (!hasExport.test(t.code)) { - t.code = `${t.code}\nexport const __externalModule = 1;`; + return { + ...t, + code: `${t.code}\nexport const __externalModule = 1;`, + }; } + return t; }); - return tests; } const DEFAULT_IGNORED_REGEX = new RegExp( diff --git a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts new file mode 100644 index 00000000000..66e5b1153c3 --- /dev/null +++ b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts @@ -0,0 +1,26 @@ +import { RuleModule } from '../ts-eslint'; + +type InferOptionsTypeFromRuleNever = T extends RuleModule< + never, + infer TOptions +> + ? TOptions + : unknown; +/** + * Uses type inference to fetch the TOptions type from the given RuleModule + */ +type InferOptionsTypeFromRule = T extends RuleModule + ? TOptions + : InferOptionsTypeFromRuleNever; + +/** + * Uses type inference to fetch the TMessageIds type from the given RuleModule + */ +type InferMessageIdsTypeFromRule = T extends RuleModule< + infer TMessageIds, + unknown[] +> + ? TMessageIds + : unknown; + +export { InferOptionsTypeFromRule, InferMessageIdsTypeFromRule }; diff --git a/packages/experimental-utils/src/eslint-utils/RuleCreator.ts b/packages/experimental-utils/src/eslint-utils/RuleCreator.ts index 4dede52bd7c..ca1cfb3302e 100644 --- a/packages/experimental-utils/src/eslint-utils/RuleCreator.ts +++ b/packages/experimental-utils/src/eslint-utils/RuleCreator.ts @@ -7,7 +7,7 @@ import { } from '../ts-eslint/Rule'; import { applyDefault } from './applyDefault'; -// we'll automatically add the url + tslint description for people. +// we automatically add the url type CreateRuleMetaDocs = Omit; type CreateRuleMeta = { docs: CreateRuleMetaDocs; @@ -25,15 +25,15 @@ function RuleCreator(urlCreator: (ruleName: string) => string) { meta, defaultOptions, create, - }: { + }: Readonly<{ name: string; meta: CreateRuleMeta; - defaultOptions: TOptions; + defaultOptions: Readonly; create: ( - context: RuleContext, - optionsWithDefault: TOptions, + context: Readonly>, + optionsWithDefault: Readonly, ) => TRuleListener; - }): RuleModule { + }>): RuleModule { return { meta: { ...meta, @@ -42,7 +42,9 @@ function RuleCreator(urlCreator: (ruleName: string) => string) { url: urlCreator(name), }, }, - create(context): TRuleListener { + create( + context: Readonly>, + ): TRuleListener { const optionsWithDefault = applyDefault( defaultOptions, context.options, diff --git a/packages/experimental-utils/src/eslint-utils/RuleTester.ts b/packages/experimental-utils/src/eslint-utils/RuleTester.ts index ed642c035ba..ce5674d9c65 100644 --- a/packages/experimental-utils/src/eslint-utils/RuleTester.ts +++ b/packages/experimental-utils/src/eslint-utils/RuleTester.ts @@ -1,5 +1,5 @@ -import * as TSESLint from '../ts-eslint'; import * as path from 'path'; +import * as TSESLint from '../ts-eslint'; const parser = '@typescript-eslint/parser'; @@ -8,10 +8,12 @@ type RuleTesterConfig = Omit & { }; class RuleTester extends TSESLint.RuleTester { + readonly #options: RuleTesterConfig; + // as of eslint 6 you have to provide an absolute path to the parser // but that's not as clean to type, this saves us trying to manually enforce // that contributors require.resolve everything - constructor(private readonly options: RuleTesterConfig) { + constructor(options: RuleTesterConfig) { super({ ...options, parserOptions: { @@ -22,6 +24,8 @@ class RuleTester extends TSESLint.RuleTester { parser: require.resolve(options.parser), }); + this.#options = options; + // make sure that the parser doesn't hold onto file handles between tests // on linux (i.e. our CI env), there can be very a limited number of watch handles available afterAll(() => { @@ -49,8 +53,8 @@ class RuleTester extends TSESLint.RuleTester { } return filename; - } else if (this.options.parserOptions) { - return this.getFilename(this.options.parserOptions); + } else if (this.#options.parserOptions) { + return this.getFilename(this.#options.parserOptions); } return 'file.ts'; @@ -62,10 +66,12 @@ class RuleTester extends TSESLint.RuleTester { run>( name: string, rule: TSESLint.RuleModule, - tests: TSESLint.RunTests, + testsReadonly: TSESLint.RunTests, ): void { const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`; + const tests = { ...testsReadonly }; + // standardize the valid tests as objects tests.valid = tests.valid.map(test => { if (typeof test === 'string') { @@ -76,23 +82,31 @@ class RuleTester extends TSESLint.RuleTester { return test; }); - tests.valid.forEach(test => { + tests.valid = tests.valid.map(test => { if (typeof test !== 'string') { if (test.parser === parser) { throw new Error(errorMessage); } if (!test.filename) { - test.filename = this.getFilename(test.parserOptions); + return { + ...test, + filename: this.getFilename(test.parserOptions), + }; } } + return test; }); - tests.invalid.forEach(test => { + tests.invalid = tests.invalid.map(test => { if (test.parser === parser) { throw new Error(errorMessage); } if (!test.filename) { - test.filename = this.getFilename(test.parserOptions); + return { + ...test, + filename: this.getFilename(test.parserOptions), + }; } + return test; }); super.run(name, rule, tests); diff --git a/packages/experimental-utils/src/eslint-utils/index.ts b/packages/experimental-utils/src/eslint-utils/index.ts index 60452e9aca4..bbbe8df2709 100644 --- a/packages/experimental-utils/src/eslint-utils/index.ts +++ b/packages/experimental-utils/src/eslint-utils/index.ts @@ -1,6 +1,7 @@ export * from './applyDefault'; export * from './batchedSingleLineTests'; export * from './getParserServices'; +export * from './InferTypesFromRule'; export * from './RuleCreator'; export * from './RuleTester'; export * from './deepMerge'; diff --git a/packages/experimental-utils/src/ts-eslint/CLIEngine.ts b/packages/experimental-utils/src/ts-eslint/CLIEngine.ts index 4c640d35c91..fec56c17b41 100644 --- a/packages/experimental-utils/src/ts-eslint/CLIEngine.ts +++ b/packages/experimental-utils/src/ts-eslint/CLIEngine.ts @@ -2,22 +2,70 @@ import { CLIEngine as ESLintCLIEngine } from 'eslint'; import { Linter } from './Linter'; -import { RuleMetaData, RuleModule, RuleListener } from './Rule'; - -interface CLIEngine { +import { RuleListener, RuleMetaData, RuleModule } from './Rule'; + +declare class CLIEngineBase { + /** + * Creates a new instance of the core CLI engine. + * @param providedOptions The options for this instance. + */ + constructor(options: CLIEngine.Options); + + /** + * Add a plugin by passing its configuration + * @param name Name of the plugin. + * @param pluginObject Plugin configuration object. + */ + addPlugin(name: string, pluginObject: Linter.Plugin): void; + + /** + * Executes the current configuration on an array of file and directory names. + * @param patterns An array of file and directory names. + * @returns The results for all files that were linted. + */ executeOnFiles(patterns: string[]): CLIEngine.LintReport; - resolveFileGlobPatterns(patterns: string[]): string[]; - + /** + * Executes the current configuration on text. + * @param text A string of JavaScript code to lint. + * @param filename An optional string representing the texts filename. + * @param warnIgnored Always warn when a file is ignored + * @returns The results for the linting. + */ + executeOnText( + text: string, + filename?: string, + warnIgnored?: boolean, + ): CLIEngine.LintReport; + + /** + * Returns a configuration object for the given file based on the CLI options. + * This is the same logic used by the ESLint CLI executable to determine configuration for each file it processes. + * @param filePath The path of the file to retrieve a config object for. + * @returns A configuration object for the file. + */ getConfigForFile(filePath: string): Linter.Config; - executeOnText(text: string, filename?: string): CLIEngine.LintReport; - - addPlugin(name: string, pluginObject: unknown): void; + /** + * Returns the formatter representing the given format. + * @param format The name of the format to load or the path to a custom formatter. + * @returns The formatter function. + */ + getFormatter(format?: string): CLIEngine.Formatter; + /** + * Checks if a given path is ignored by ESLint. + * @param filePath The path of the file to check. + * @returns Whether or not the given path is ignored. + */ isPathIgnored(filePath: string): boolean; - getFormatter(format?: string): CLIEngine.Formatter; + /** + * Resolves the patterns passed into `executeOnFiles()` into glob-based patterns for easier handling. + * @param patterns The file patterns passed on the command line. + * @returns The equivalent glob patterns. + */ + resolveFileGlobPatterns(patterns: string[]): string[]; getRules< TMessageIds extends string = string, @@ -25,6 +73,34 @@ interface CLIEngine { // for extending base rules TRuleListener extends RuleListener = RuleListener >(): Map>; + + //////////////////// + // static members // + //////////////////// + + /** + * Returns results that only contains errors. + * @param results The results to filter. + * @returns The filtered results. + */ + static getErrorResults( + results: CLIEngine.LintResult[], + ): CLIEngine.LintResult[]; + + /** + * Returns the formatter representing the given format or null if the `format` is not a string. + * @param format The name of the format to load or the path to a custom formatter. + * @returns The formatter function. + */ + static getFormatter(format?: string): CLIEngine.Formatter; + + /** + * Outputs fixes from the given results to files. + * @param report The report object created by CLIEngine. + */ + static outputFixes(report: CLIEngine.LintReport): void; + + static version: string; } namespace CLIEngine { @@ -93,14 +169,12 @@ namespace CLIEngine { ) => string; } -const CLIEngine = ESLintCLIEngine as { - new (options: CLIEngine.Options): CLIEngine; - - // static methods - getErrorResults(results: CLIEngine.LintResult[]): CLIEngine.LintResult[]; - getFormatter(format?: string): CLIEngine.Formatter; - outputFixes(report: CLIEngine.LintReport): void; - version: string; -}; +/** + * The underlying utility that runs the ESLint command line interface. This object will read the filesystem for + * configuration and file information but will not output any results. Instead, it allows you direct access to the + * important information so you can deal with the output yourself. + * @deprecated use the ESLint class instead + */ +class CLIEngine extends (ESLintCLIEngine as typeof CLIEngineBase) {} export { CLIEngine }; diff --git a/packages/experimental-utils/src/ts-eslint/ESLint.ts b/packages/experimental-utils/src/ts-eslint/ESLint.ts new file mode 100644 index 00000000000..9ced6ed6470 --- /dev/null +++ b/packages/experimental-utils/src/ts-eslint/ESLint.ts @@ -0,0 +1,352 @@ +/* eslint-disable @typescript-eslint/no-namespace */ + +import { ESLint as ESLintESLint } from 'eslint'; +import { Linter } from './Linter'; + +declare class ESLintBase { + /** + * Creates a new instance of the main ESLint API. + * @param options The options for this instance. + */ + constructor(options?: ESLint.ESLintOptions); + + /** + * This method calculates the configuration for a given file, which can be useful for debugging purposes. + * - It resolves and merges extends and overrides settings into the top level configuration. + * - It resolves the parser setting to absolute paths. + * - It normalizes the plugins setting to align short names. (e.g., eslint-plugin-foo → foo) + * - It adds the processor setting if a legacy file extension processor is matched. + * - It doesn't interpret the env setting to the globals and parserOptions settings, so the result object contains + * the env setting as is. + * @param filePath The path to the file whose configuration you would like to calculate. Directory paths are forbidden + * because ESLint cannot handle the overrides setting. + * @returns The promise that will be fulfilled with a configuration object. + */ + 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. + * @returns The promise that will be fulfilled with whether the file is ignored or not. If the file is ignored, then + * it will return true. + */ + isPathIgnored(filePath: string): Promise; + /** + * This method lints the files that match the glob patterns and then returns the results. + * @param patterns The lint target files. This can contain any of file paths, directory paths, and glob patterns. + * @returns The promise that will be fulfilled with an array of LintResult objects. + */ + lintFiles(patterns: string | string[]): Promise; + /** + * This method lints the given source code text and then returns the results. + * + * By default, this method uses the configuration that applies to files in the current working directory (the cwd + * constructor option). If you want to use a different configuration, pass options.filePath, and ESLint will load the + * same configuration that eslint.lintFiles() would use for a file at options.filePath. + * + * If the options.filePath value is configured to be ignored, this method returns an empty array. If the + * options.warnIgnored option is set along with the options.filePath option, this method returns a LintResult object. + * In that case, the result may contain a warning that indicates the file was ignored. + * @param code The source code text to check. + * @param options The options. + * @returns The promise that will be fulfilled with an array of LintResult objects. This is an array (despite there + * being only one lint result) in order to keep the interfaces between this and the eslint.lintFiles() + * method similar. + */ + lintText( + code: string, + options?: ESLint.LintTextOptions, + ): Promise; + /** + * This method loads a formatter. Formatters convert lint results to a human- or machine-readable string. + * @param name TThe path to the file you want to check. + * The following values are allowed: + * - undefined. In this case, loads the "stylish" built-in formatter. + * - A name of built-in formatters. + * - A name of third-party formatters. For examples: + * -- `foo` will load eslint-formatter-foo. + * -- `@foo` will load `@foo/eslint-formatter`. + * -- `@foo/bar` will load `@foo/eslint-formatter-bar`. + * - A path to the file that defines a formatter. The path must contain one or more path separators (/) in order to distinguish if it's a path or not. For example, start with ./. + * @returns The promise that will be fulfilled with a Formatter object. + */ + loadFormatter(name?: string): Promise; + + //////////////////// + // static members // + //////////////////// + + /** + * This method copies the given results and removes warnings. The returned value contains only errors. + * @param results The LintResult objects to filter. + * @returns The filtered LintResult objects. + */ + static getErrorResults(results: ESLint.LintResult): ESLint.LintResult; + /** + * This method writes code modified by ESLint's autofix feature into its respective file. If any of the modified + * files don't exist, this method does nothing. + * @param results The LintResult objects to write. + * @returns The promise that will be fulfilled after all files are written. + */ + static outputFixes(results: ESLint.LintResult): Promise; + /** + * The version text. + */ + static readonly version: string; +} + +namespace ESLint { + export interface ESLintOptions { + /** + * If false is present, ESLint suppresses directive comments in source code. + * If this option is false, it overrides the noInlineConfig setting in your configurations. + */ + allowInlineConfig?: boolean; + /** + * Configuration object, extended by all configurations used with this instance. + * 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; + /** + * 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 + * to remove the cache file manually. The eslint.lintText() method doesn't use caches even if you pass the + * options.filePath to the method. + */ + cache?: boolean; + /** + * The eslint.lintFiles() method writes caches into this file. + */ + cacheLocation?: string; + /** + * The working directory. This must be an absolute path. + */ + cwd?: string; + /** + * Unless set to false, the eslint.lintFiles() method will throw an error when no target files are found. + */ + errorOnUnmatchedPattern?: boolean; + /** + * If you pass directory paths to the eslint.lintFiles() method, ESLint checks the files in those directories that + * have the given extensions. For example, when passing the src/ directory and extensions is [".js", ".ts"], ESLint + * will lint *.js and *.ts files in src/. If extensions is null, ESLint checks *.js files and files that match + * overrides[].files patterns in your configuration. + * Note: This option only applies when you pass directory paths to the eslint.lintFiles() method. + * If you pass glob patterns, ESLint will lint all files matching the glob pattern regardless of extension. + */ + extensions?: string[] | null; + /** + * If true is present, the eslint.lintFiles() and eslint.lintText() methods work in autofix mode. + * If a predicate function is present, the methods pass each lint message to the function, then use only the + * lint messages for which the function returned true. + */ + fix?: boolean | ((message: LintMessage) => boolean); + /** + * The types of the rules that the eslint.lintFiles() and eslint.lintText() methods use for autofix. + */ + fixTypes?: string[]; + /** + * If false is present, the eslint.lintFiles() method doesn't interpret glob patterns. + */ + globInputPaths?: boolean; + /** + * If false is present, the eslint.lintFiles() method doesn't respect `.eslintignore` files or ignorePatterns in + * your configuration. + */ + ignore?: boolean; + /** + * The path to a file ESLint uses instead of `$CWD/.eslintignore`. + * If a path is present and the file doesn't exist, this constructor will throw an error. + */ + ignorePath?: string; + /** + * 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.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. + */ + overrideConfigFile?: string | null; + /** + * The plugin implementations that ESLint uses for the plugins setting of your configuration. + * This is a map-like object. Those keys are plugin IDs and each value is implementation. + */ + plugins?: Record | null; + /** + * The severity to report unused eslint-disable directives. + * If this option is a severity, it overrides the reportUnusedDisableDirectives setting in your configurations. + */ + reportUnusedDisableDirectives?: Linter.SeverityString | null; + /** + * The path to a directory where plugins should be resolved from. + * If null is present, ESLint loads plugins from the location of the configuration file that contains the plugin + * setting. + * If a path is present, ESLint loads all plugins from there. + */ + resolvePluginsRelativeTo?: string | null; + /** + * An array of paths to directories to load custom rules from. + */ + rulePaths?: string[]; + /** + * If false is present, ESLint doesn't load configuration files (.eslintrc.* files). + * Only the configuration of the constructor options is valid. + */ + useEslintrc?: boolean; + } + + export interface DeprecatedRuleInfo { + /** + * The rule ID. + */ + ruleId: string; + /** + * The rule IDs that replace this deprecated rule. + */ + replacedBy: string[]; + } + + /** + * The LintResult value is the information of the linting result of each file. + */ + export interface LintResult { + /** + * The number of errors. This includes fixable errors. + */ + errorCount: number; + /** + * The absolute path to the file of this result. This is the string "" if the file path is unknown (when you + * didn't pass the options.filePath option to the eslint.lintText() method). + */ + filePath: string; + /** + * The number of errors that can be fixed automatically by the fix constructor option. + */ + fixableErrorCount: number; + /** + * The number of warnings that can be fixed automatically by the fix constructor option. + */ + fixableWarningCount: number; + /** + * The array of LintMessage objects. + */ + messages: Linter.LintMessage[]; + /** + * The source code of the file that was linted, with as many fixes applied as possible. + */ + output?: string; + /** + * The original source code text. This property is undefined if any messages didn't exist or the output + * property exists. + */ + source?: string; + /** + * The information about the deprecated rules that were used to check this file. + */ + usedDeprecatedRules: DeprecatedRuleInfo[]; + /** + * The number of warnings. This includes fixable warnings. + */ + warningCount: number; + } + + export interface LintTextOptions { + /** + * The path to the file of the source code text. If omitted, the result.filePath becomes the string "". + */ + filePath?: string; + /** + * If true is present and the options.filePath is a file ESLint should ignore, this method returns a lint result + * contains a warning message. + */ + warnIgnored?: boolean; + } + + /** + * The LintMessage value is the information of each linting error. + */ + export interface LintMessage { + /** + * The 1-based column number of the begin point of this message. + */ + column: number; + /** + * The 1-based column number of the end point of this message. This property is undefined if this message + * is not a range. + */ + endColumn: number | undefined; + /** + * The 1-based line number of the end point of this message. This property is undefined if this + * message is not a range. + */ + endLine: number | undefined; + /** + * The EditInfo object of autofix. This property is undefined if this message is not fixable. + */ + fix: EditInfo | undefined; + /** + * The 1-based line number of the begin point of this message. + */ + line: number; + /** + * The error message + */ + message: string; + /** + * The rule name that generates this lint message. If this message is generated by the ESLint core rather than + * rules, this is null. + */ + ruleId: string | null; + /** + * The severity of this message. 1 means warning and 2 means error. + */ + severity: 1 | 2; + /** + * The list of suggestions. Each suggestion is the pair of a description and an EditInfo object to fix code. API + * users such as editor integrations can choose one of them to fix the problem of this message. This property is + * undefined if this message doesn't have any suggestions. + */ + suggestions: { desc: string; fix: EditInfo }[] | undefined; + } + + /** + * The EditInfo value is information to edit text. + * + * This edit information means replacing the range of the range property by the text property value. It's like + * sourceCodeText.slice(0, edit.range[0]) + edit.text + sourceCodeText.slice(edit.range[1]). Therefore, it's an add + * if the range[0] and range[1] property values are the same value, and it's removal if the text property value is + * empty string. + */ + export interface EditInfo { + /** + * The pair of 0-based indices in source code text to remove. + */ + range: [number, number]; + /** + * The text to add. + */ + text: string; + } + + /** + * The Formatter value is the object to convert the LintResult objects to text. + */ + export interface Formatter { + /** + * The method to convert the LintResult objects to text + */ + format(results: LintResult[]): string; + } +} + +/** + * The ESLint class is the primary class to use in Node.js applications. + * This class depends on the Node.js fs module and the file system, so you cannot use it in browsers. + * + * If you want to lint code on browsers, use the Linter class instead. + */ +class ESLint extends (ESLintESLint as typeof ESLintBase) {} + +export { ESLint }; diff --git a/packages/experimental-utils/src/ts-eslint/Linter.ts b/packages/experimental-utils/src/ts-eslint/Linter.ts index 57b1d23ea6c..9abefbabe7d 100644 --- a/packages/experimental-utils/src/ts-eslint/Linter.ts +++ b/packages/experimental-utils/src/ts-eslint/Linter.ts @@ -3,60 +3,111 @@ import { Linter as ESLintLinter } from 'eslint'; import { TSESTree, ParserServices } from '../ts-estree'; import { ParserOptions as TSParserOptions } from './ParserOptions'; -import { RuleModule, RuleFix } from './Rule'; +import { RuleCreateFunction, RuleFix, RuleModule } from './Rule'; import { Scope } from './Scope'; import { SourceCode } from './SourceCode'; -interface Linter { - version: string; +declare class LinterBase { + /** + * Initialize the Linter. + * @param config the config object + */ + constructor(config?: Linter.LinterOptions); + /** + * Define a new parser module + * @param parserId Name of the parser + * @param parserModule The parser object + */ + defineParser(parserId: string, parserModule: Linter.ParserModule): void; + + /** + * Defines a new linting rule. + * @param ruleId A unique rule identifier + * @param ruleModule Function from context to object mapping AST node types to event handlers + */ + defineRule( + ruleId: string, + ruleModule: RuleModule | RuleCreateFunction, + ): void; + + /** + * Defines many new linting rules. + * @param rulesToDefine map from unique rule identifier to rule + */ + defineRules( + rulesToDefine: Record< + string, + RuleModule | RuleCreateFunction + >, + ): void; + + /** + * Gets an object with all loaded rules. + * @returns All loaded rules + */ + getRules(): Map>; + + /** + * Gets the `SourceCode` object representing the parsed source. + * @returns The `SourceCode` object. + */ + getSourceCode(): SourceCode; + + /** + * Verifies the text against the rules specified by the second argument. + * @param textOrSourceCode The text to parse or a SourceCode object. + * @param config An ESLintConfig instance to configure everything. + * @param filenameOrOptions The optional filename of the file being checked. + * If this is not set, the filename will default to '' in the rule context. + * If this is an object, then it has "filename", "allowInlineConfig", and some properties. + * @returns The results as an array of messages or an empty array if no messages. + */ verify( - code: SourceCode | string, - config: Linter.Config, - filename?: string, - ): Linter.LintMessage[]; - verify( - code: SourceCode | string, + textOrSourceCode: SourceCode | string, config: Linter.Config, - options: Linter.LintOptions, + filenameOrOptions?: string | Linter.VerifyOptions, ): Linter.LintMessage[]; - verifyAndFix( - code: string, - config: Linter.Config, - filename?: string, - ): Linter.FixReport; + /** + * Performs multiple autofix passes over the text until as many fixes as possible have been applied. + * @param text The source text to apply fixes to. + * @param config The ESLint config object to use. + * @param options The ESLint options object to use. + * @returns The result of the fix operation as returned from the SourceCodeFixer. + */ verifyAndFix( code: string, config: Linter.Config, options: Linter.FixOptions, ): Linter.FixReport; - getSourceCode(): SourceCode; + /** + * The version from package.json. + */ + readonly version: string; - defineRule( - name: string, - rule: { - meta?: RuleModule['meta']; - create: RuleModule['create']; - }, - ): void; + //////////////////// + // static members // + //////////////////// - defineRules( - rules: Record>, - ): void; - - getRules< - TMessageIds extends string, - TOptions extends readonly unknown[] - >(): Map>; - - defineParser(name: string, parser: Linter.ParserModule): void; + /** + * The version from package.json. + */ + static readonly version: string; } namespace Linter { + export interface LinterOptions { + /** + * path to a directory that should be considered as the current working directory. + */ + cwd?: string; + } + export type Severity = 0 | 1 | 2; - export type RuleLevel = Severity | 'off' | 'warn' | 'error'; + export type SeverityString = 'off' | 'warn' | 'error'; + export type RuleLevel = Severity | SeverityString; export type RuleLevelAndOptions = [RuleLevel, ...unknown[]]; @@ -66,39 +117,116 @@ namespace Linter { // https://github.com/eslint/eslint/blob/v6.8.0/conf/config-schema.js interface BaseConfig { $schema?: string; + /** + * The environment settings. + */ env?: { [name: string]: boolean }; + /** + * The path to other config files or the package name of shareable configs. + */ extends?: string | string[]; + /** + * The global variable settings. + */ globals?: { [name: string]: boolean }; + /** + * 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; - settings?: { [name: string]: unknown }; + /** + * The rule settings. + */ rules?: RulesRecord; + /** + * The shared settings. + */ + settings?: { [name: string]: unknown }; } export interface ConfigOverride extends BaseConfig { excludedFiles?: string | string[]; files: string | string[]; } - export type RuleOverride = ConfigOverride; // TODO - delete this next major 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 interface LintOptions { - filename?: string; - preprocess?: (code: string) => string[]; - postprocess?: (problemLists: LintMessage[][]) => LintMessage[]; + export interface VerifyOptions { + /** + * Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied. + * Useful if you want to validate JS without comments overriding rules. + */ allowInlineConfig?: boolean; - reportUnusedDisableDirectives?: boolean; + /** + * if `true` then the linter doesn't make `fix` properties into the lint result. + */ + disableFixes?: boolean; + /** + * the filename of the source code. + */ + filename?: string; + /** + * the predicate function that selects adopt code blocks. + */ + filterCodeBlock?: (filename: string, text: string) => boolean; + /** + * postprocessor for report messages. + * If provided, this should accept an array of the message lists + * for each code block returned from the preprocessor, apply a mapping to + * the messages as appropriate, and return a one-dimensional array of + * messages. + */ + postprocess?: Processor['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']; + /** + * Adds reported errors for unused `eslint-disable` directives. + */ + reportUnusedDisableDirectives?: boolean | SeverityString; + } + + export interface FixOptions extends VerifyOptions { + /** + * Determines whether fixes should be applied. + */ + fix?: boolean; } export interface LintSuggestion { @@ -108,37 +236,75 @@ namespace Linter { } export interface LintMessage { + /** + * The 1-based column number. + */ column: number; - line: number; + /** + * The 1-based column number of the end location. + */ endColumn?: number; + /** + * The 1-based line number of the end location. + */ endLine?: number; - ruleId: string | null; + /** + * If `true` then this is a fatal error. + */ + fatal?: true; + /** + * Information for autofix. + */ + fix?: RuleFix; + /** + * The 1-based line number. + */ + line: number; + /** + * The error message. + */ message: string; messageId?: string; nodeType: string; - fatal?: true; + /** + * The ID of the rule which makes this message. + */ + ruleId: string | null; + /** + * The severity of this message. + */ severity: Severity; - fix?: RuleFix; source: string | null; + /** + * Information for suggestions + */ suggestions?: LintSuggestion[]; } - export interface FixOptions extends LintOptions { - fix?: boolean; - } - export interface FixReport { + /** + * True, if the code was fixed + */ fixed: boolean; + /** + * Fixed code text (might be the same as input if no fixes were applied). + */ output: string; + /** + * Collection of all messages for the given code + */ messages: LintMessage[]; } export type ParserModule = | { - parse(text: string, options?: unknown): TSESTree.Program; + parse(text: string, options?: ParserOptions): TSESTree.Program; } | { - parseForESLint(text: string, options?: unknown): ESLintParseResult; + parseForESLint( + text: string, + options?: ParserOptions, + ): ESLintParseResult; }; export interface ESLintParseResult { @@ -147,10 +313,64 @@ namespace Linter { scopeManager?: Scope.ScopeManager; visitorKeys?: SourceCode.VisitorKeys; } + + export interface Processor { + /** + * The function to extract code blocks. + */ + preprocess?: ( + text: string, + filename: string, + ) => Array; + /** + * The function to merge messages. + */ + postprocess?: ( + messagesList: Linter.LintMessage[][], + filename: string, + ) => Linter.LintMessage[]; + /** + * If `true` then it means the processor supports autofix. + */ + supportsAutofix?: boolean; + } + + export interface Environment { + /** + * The definition of global variables. + */ + globals?: Record; + /** + * The parser options that will be enabled under this environment. + */ + parserOptions?: ParserOptions; + } + + export interface Plugin { + /** + * The definition of plugin configs. + */ + configs?: Record; + /** + * The definition of plugin environments. + */ + environments?: Record; + /** + * The definition of plugin processors. + */ + processors?: Record; + /** + * The definition of plugin rules. + */ + rules?: Record>; + } } -const Linter = ESLintLinter as { - new (): Linter; -}; +/** + * The Linter object does the actual evaluation of the JavaScript code. It doesn't do any filesystem operations, it + * simply parses and reports on the code. In particular, the Linter object does not process configuration objects + * or files. + */ +class Linter extends (ESLintLinter as typeof LinterBase) {} export { Linter }; diff --git a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts index 4a85f2acab5..c3678eb939d 100644 --- a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts +++ b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts @@ -23,14 +23,13 @@ interface ParserOptions { jsx?: boolean; }; ecmaVersion?: EcmaVersion; + // ts-estree specific + debugLevel?: TSESTreeOptions['debugLevel']; errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; errorOnUnknownASTType?: boolean; extraFileExtensions?: string[]; - // ts-estree specific - debugLevel?: TSESTreeOptions['debugLevel']; filePath?: string; loc?: boolean; - noWatch?: boolean; project?: string | string[]; projectFolderIgnoreList?: (string | RegExp)[]; range?: boolean; diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index 12a3bed1f75..27d3a1d63ba 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -123,42 +123,44 @@ interface ReportDescriptorBase { /** * The parameters for the message string associated with `messageId`. */ - data?: Record; + readonly data?: Readonly>; /** * The fixer function. */ - fix?: ReportFixFunction | null; + readonly fix?: ReportFixFunction | null; /** * The messageId which is being reported. */ - messageId: TMessageIds; + readonly messageId: TMessageIds; // we disallow this because it's much better to use messageIds for reusable errors that are easily testable - // desc?: string; + // readonly desc?: string; } interface ReportDescriptorWithSuggestion extends ReportDescriptorBase { /** * 6.7's Suggestions API */ - suggest?: Readonly> | null; + readonly suggest?: Readonly> | null; } interface ReportDescriptorNodeOptionalLoc { /** * The Node or AST Token which the report is being attached to */ - node: TSESTree.Node | TSESTree.Comment | TSESTree.Token; + readonly node: TSESTree.Node | TSESTree.Comment | TSESTree.Token; /** * An override of the location of the report */ - loc?: TSESTree.SourceLocation | TSESTree.LineAndColumnData; + readonly loc?: + | Readonly + | Readonly; } interface ReportDescriptorLocOnly { /** * An override of the location of the report */ - loc: TSESTree.SourceLocation | TSESTree.LineAndColumnData; + loc: Readonly | Readonly; } type ReportDescriptor< TMessageIds extends string @@ -178,11 +180,6 @@ interface RuleContext< * This array does not include the rule severity. */ options: TOptions; - /** - * The shared settings from configuration. - * We do not have any shared settings in this plugin. - */ - settings: Record; /** * The name of the parser from configuration. */ @@ -195,6 +192,11 @@ interface RuleContext< * An object containing parser-provided services for rules */ parserServices?: ParserServices; + /** + * The shared settings from configuration. + * We do not have any shared settings in this plugin. + */ + settings: Record; /** * Returns an array of the ancestors of the currently-traversed node, starting at @@ -224,7 +226,7 @@ interface RuleContext< * Returns a SourceCode object that you can use to work with the source that * was passed to ESLint. */ - getSourceCode(): SourceCode; + getSourceCode(): Readonly; /** * Marks a variable with the given name in the current scope as used. @@ -436,14 +438,19 @@ interface RuleModule< * Function which returns an object with methods that ESLint calls to “visit” * nodes while traversing the abstract syntax tree. */ - create(context: RuleContext): TRuleListener; + create(context: Readonly>): TRuleListener; } +type RuleCreateFunction = ( + context: Readonly>, +) => RuleListener; + export { ReportDescriptor, ReportFixFunction, ReportSuggestionArray, RuleContext, + RuleCreateFunction, RuleFix, RuleFixer, RuleFunction, diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index 41dadcb6363..d1e11e4ad77 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -4,49 +4,109 @@ import { ParserOptions } from './ParserOptions'; import { RuleModule } from './Rule'; interface ValidTestCase> { - code: string; - options?: TOptions; - filename?: string; - parserOptions?: ParserOptions; - settings?: Record; - parser?: string; - globals?: Record; - env?: { - browser?: boolean; - }; + /** + * Code for the test case. + */ + readonly code: string; + /** + * Environments for the test case. + */ + readonly env?: Readonly>; + /** + * The fake filename for the test case. Useful for rules that make assertion about filenames. + */ + readonly filename?: string; + /** + * The additional global variables. + */ + readonly globals?: Record; + /** + * Options for the test case. + */ + readonly options?: Readonly; + /** + * The absolute path for the parser. + */ + readonly parser?: string; + /** + * Options for the parser. + */ + readonly parserOptions?: Readonly; + /** + * Settings for the test case. + */ + readonly settings?: Readonly>; } interface SuggestionOutput { - messageId: TMessageIds; - data?: Record; + /** + * Reported message ID. + */ + readonly messageId: TMessageIds; + /** + * The data used to fill the message template. + */ + readonly data?: Readonly>; /** * NOTE: Suggestions will be applied as a stand-alone change, without triggering multi-pass fixes. * Each individual error has its own suggestion, so you have to show the correct, _isolated_ output for each suggestion. */ - output: string; + readonly output: string; + // we disallow this because it's much better to use messageIds for reusable errors that are easily testable - // desc?: string; + // readonly desc?: string; } interface InvalidTestCase< TMessageIds extends string, TOptions extends Readonly > extends ValidTestCase { - errors: TestCaseError[]; - output?: string | null; + /** + * Expected errors. + */ + readonly errors: TestCaseError[]; + /** + * The expected code after autofixes are applied. If set to `null`, the test runner will assert that no autofix is suggested. + */ + readonly output?: string | null; } interface TestCaseError { - messageId: TMessageIds; + /** + * The 1-based column number of the reported start location. + */ + readonly column?: number; + /** + * The data used to fill the message template. + */ + readonly data?: Readonly>; + /** + * The 1-based column number of the reported end location. + */ + readonly endColumn?: number; + /** + * The 1-based line number of the reported end location. + */ + readonly endLine?: number; + /** + * The 1-based line number of the reported start location. + */ + readonly line?: number; + /** + * Reported message ID. + */ + readonly messageId: TMessageIds; + /** + * Reported suggestions. + */ + readonly suggestions?: SuggestionOutput[] | null; + /** + * The type of the reported AST node. + */ + readonly type?: AST_NODE_TYPES | AST_TOKEN_TYPES; + // we disallow this because it's much better to use messageIds for reusable errors that are easily testable - // message?: string; - data?: Record; - type?: AST_NODE_TYPES | AST_TOKEN_TYPES; - line?: number; - column?: number; - endLine?: number; - endColumn?: number; - suggestions?: SuggestionOutput[] | null; + // readonly message?: string | RegExp; } interface RunTests< @@ -54,41 +114,56 @@ interface RunTests< TOptions extends Readonly > { // RuleTester.run also accepts strings for valid cases - valid: (ValidTestCase | string)[]; - invalid: InvalidTestCase[]; + readonly valid: (ValidTestCase | string)[]; + readonly invalid: InvalidTestCase[]; } interface RuleTesterConfig { // should be require.resolve(parserPackageName) - parser: string; - parserOptions?: ParserOptions; + readonly parser: string; + readonly parserOptions?: Readonly; } -// the cast on the extends is so that we don't want to have the built type defs to attempt to import eslint -class RuleTester extends (ESLintRuleTester as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (...args: unknown[]): any; -}) { - constructor(config?: RuleTesterConfig) { - super(config); - - // nobody will ever need watching in tests - // so we can give everyone a perf win by disabling watching - if (config?.parserOptions?.project) { - config.parserOptions.noWatch = - typeof config.parserOptions.noWatch === 'boolean' || true; - } - } +declare class RuleTesterBase { + /** + * Creates a new instance of RuleTester. + * @param testerConfig extra configuration for the tester + */ + constructor(testerConfig?: RuleTesterConfig); + /** + * Adds a new rule test to execute. + * @param ruleName The name of the rule to run. + * @param rule The rule to test. + * @param test The collection of tests to run. + */ run>( - name: string, + ruleName: string, rule: RuleModule, tests: RunTests, - ): void { - // this method is only defined here because we lazily type the eslint import with `any` - super.run(name, rule, tests); - } + ): void; + + /** + * If you supply a value to this property, the rule tester will call this instead of using the version defined on + * the global namespace. + * @param text a string describing the rule + * @param callback the test callback + */ + static describe?: (text: string, callback: () => void) => void; + + /** + * If you supply a value to this property, the rule tester will call this instead of using the version defined on + * the global namespace. + * @param text a string describing the test case + * @param callback the test callback + */ + static it?: (text: string, callback: () => void) => void; } +/** + * @deprecated - use RuleTesterSafe instead + */ +class RuleTester extends (ESLintRuleTester as typeof RuleTesterBase) {} + export { InvalidTestCase, SuggestionOutput, diff --git a/packages/experimental-utils/src/ts-eslint/SourceCode.ts b/packages/experimental-utils/src/ts-eslint/SourceCode.ts index 0ea39973a25..82593bbfc6a 100644 --- a/packages/experimental-utils/src/ts-eslint/SourceCode.ts +++ b/packages/experimental-utils/src/ts-eslint/SourceCode.ts @@ -4,150 +4,346 @@ import { SourceCode as ESLintSourceCode } from 'eslint'; import { ParserServices, TSESTree } from '../ts-estree'; import { Scope } from './Scope'; -interface SourceCode { - text: string; - ast: SourceCode.Program; - lines: string[]; - hasBOM: boolean; - parserServices: ParserServices; - scopeManager: Scope.ScopeManager; - visitorKeys: SourceCode.VisitorKeys; - tokensAndComments: (TSESTree.Comment | TSESTree.Token)[]; - - getText( - node?: TSESTree.Node, - beforeCount?: number, - afterCount?: number, - ): string; - - getLines(): string[]; - - getAllComments(): TSESTree.Comment[]; - - getComments( - node: TSESTree.Node, - ): { leading: TSESTree.Comment[]; trailing: TSESTree.Comment[] }; - - getJSDocComment(node: TSESTree.Node): TSESTree.Node | TSESTree.Token | null; - - getNodeByRangeIndex(index: number): TSESTree.Node | null; - - isSpaceBetween( - first: TSESTree.Token | TSESTree.Comment | TSESTree.Node, - second: TSESTree.Token | TSESTree.Comment | TSESTree.Node, +declare class TokenStore { + /** + * Checks whether any comments exist or not between the given 2 nodes. + * @param left The node to check. + * @param right The node to check. + * @returns `true` if one or more comments exist. + */ + commentsExistBetween( + left: TSESTree.Node | TSESTree.Token, + right: TSESTree.Node | TSESTree.Token, ): boolean; - /** - * @deprecated in favor of isSpaceBetween() + * Gets all comment tokens directly after the given node or token. + * @param nodeOrToken The AST node or token to check for adjacent comment tokens. + * @returns An array of comments in occurrence order. + */ + getCommentsAfter( + nodeOrToken: TSESTree.Node | TSESTree.Token, + ): TSESTree.Comment[]; + /** + * Gets all comment tokens directly before the given node or token. + * @param nodeOrToken The AST node or token to check for adjacent comment tokens. + * @returns An array of comments in occurrence order. + */ + getCommentsBefore( + nodeOrToken: TSESTree.Node | TSESTree.Token, + ): TSESTree.Comment[]; + /** + * Gets all comment tokens inside the given node. + * @param node The AST node to get the comments for. + * @returns An array of comments in occurrence order. + */ + getCommentsInside(node: TSESTree.Node): TSESTree.Comment[]; + /** + * Gets the first token of the given node. + * @param node The AST node. + * @param option The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. + * @returns An object representing the token. */ - isSpaceBetweenTokens(first: TSESTree.Token, second: TSESTree.Token): boolean; - - getLocFromIndex(index: number): TSESTree.LineAndColumnData; - - getIndexFromLoc(location: TSESTree.LineAndColumnData): number; - - // Inherited methods from TokenStore - // --------------------------------- - - getTokenByRangeStart( - offset: number, - options?: T, - ): SourceCode.ReturnTypeFromOptions | null; - getFirstToken( node: TSESTree.Node, options?: T, ): SourceCode.ReturnTypeFromOptions | null; - + /** + * Gets the first token between two non-overlapping nodes. + * @param left Node before the desired token range. + * @param right Node after the desired token range. + * @param option The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. + * @returns An object representing the token. + */ + getFirstTokenBetween( + left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + options?: T, + ): SourceCode.ReturnTypeFromOptions | null; + /** + * Gets the first `count` tokens of the given node. + * @param node The AST node. + * @param options The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. + * @returns Tokens. + */ getFirstTokens( node: TSESTree.Node, options?: T, ): SourceCode.ReturnTypeFromOptions[]; - + /** + * Gets the first `count` tokens between two non-overlapping nodes. + * @param left Node before the desired token range. + * @param right Node after the desired token range. + * @param options The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. + * @returns Tokens between left and right. + */ + getFirstTokensBetween( + left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + options?: T, + ): SourceCode.ReturnTypeFromOptions[]; + /** + * Gets the last token of the given node. + * @param node The AST node. + * @param option The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. + * @returns An object representing the token. + */ getLastToken( node: TSESTree.Node, options?: T, ): SourceCode.ReturnTypeFromOptions | null; - + /** + * Gets the last token between two non-overlapping nodes. + * @param left Node before the desired token range. + * @param right Node after the desired token range. + * @param option The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. + * @returns An object representing the token. + */ + getLastTokenBetween( + left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + options?: T, + ): SourceCode.ReturnTypeFromOptions | null; + /** + * Gets the last `count` tokens of the given node. + * @param node The AST node. + * @param options The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. + * @returns Tokens. + */ getLastTokens( node: TSESTree.Node, options?: T, ): SourceCode.ReturnTypeFromOptions[]; - - getTokenBefore( - node: TSESTree.Node | TSESTree.Token | TSESTree.Comment, - options?: T, - ): SourceCode.ReturnTypeFromOptions | null; - - getTokensBefore( - node: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + /** + * Gets the last `count` tokens between two non-overlapping nodes. + * @param left Node before the desired token range. + * @param right Node after the desired token range. + * @param options The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. + * @returns Tokens between left and right. + */ + getLastTokensBetween( + left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, options?: T, ): SourceCode.ReturnTypeFromOptions[]; - + /** + * Gets the token that follows a given node or token. + * @param node The AST node or token. + * @param option The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. + * @returns An object representing the token. + */ getTokenAfter( node: TSESTree.Node | TSESTree.Token | TSESTree.Comment, options?: T, ): SourceCode.ReturnTypeFromOptions | null; - - getTokensAfter( + /** + * Gets the token that precedes a given node or token. + * @param node The AST node or token. + * @param options The option object + * @returns An object representing the token. + */ + getTokenBefore( node: TSESTree.Node | TSESTree.Token | TSESTree.Comment, options?: T, - ): SourceCode.ReturnTypeFromOptions[]; - - getFirstTokenBetween( - left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, - right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + ): SourceCode.ReturnTypeFromOptions | null; + /** + * Gets the token starting at the specified index. + * @param offset Index of the start of the token's range. + * @param option The option object. If this is a number then it's `options.skip`. If this is a function then it's `options.filter`. + * @returns The token starting at index, or null if no such token. + */ + getTokenByRangeStart( + offset: number, options?: T, ): SourceCode.ReturnTypeFromOptions | null; - - getFirstTokensBetween( - left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, - right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + /** + * Gets all tokens that are related to the given node. + * @param node The AST node. + * @param beforeCount The number of tokens before the node to retrieve. + * @param afterCount The number of tokens after the node to retrieve. + * @returns Array of objects representing tokens. + */ + getTokens( + node: TSESTree.Node, + beforeCount?: number, + afterCount?: number, + ): TSESTree.Token[]; + /** + * Gets all tokens that are related to the given node. + * @param node The AST node. + * @param options The option object. If this is a function then it's `options.filter`. + * @returns Array of objects representing tokens. + */ + getTokens( + node: TSESTree.Node, + options: T, + ): SourceCode.ReturnTypeFromOptions[]; + /** + * Gets the `count` tokens that follows a given node or token. + * @param node The AST node. + * @param options The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. + * @returns Tokens. + */ + getTokensAfter( + node: TSESTree.Node | TSESTree.Token | TSESTree.Comment, options?: T, ): SourceCode.ReturnTypeFromOptions[]; - - getLastTokenBetween( - left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, - right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, + /** + * Gets the `count` tokens that precedes a given node or token. + * @param node The AST node. + * @param options The option object. If this is a number then it's `options.count`. If this is a function then it's `options.filter`. + * @returns Tokens. + */ + getTokensBefore( + node: TSESTree.Node | TSESTree.Token | TSESTree.Comment, options?: T, - ): SourceCode.ReturnTypeFromOptions | null; - - getLastTokensBetween( + ): SourceCode.ReturnTypeFromOptions[]; + /** + * Gets all of the tokens between two non-overlapping nodes. + * @param left Node before the desired token range. + * @param right Node after the desired token range. + * @param options The option object. If this is a function then it's `options.filter`. + * @returns Tokens between left and right. + */ + getTokensBetween( left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, - options?: T, + padding?: T, ): SourceCode.ReturnTypeFromOptions[]; - + /** + * Gets all of the tokens between two non-overlapping nodes. + * @param left Node before the desired token range. + * @param right Node after the desired token range. + * @param padding Number of extra tokens on either side of center. + * @returns Tokens between left and right. + */ getTokensBetween( left: TSESTree.Node | TSESTree.Token | TSESTree.Comment, right: TSESTree.Node | TSESTree.Token | TSESTree.Comment, - padding?: T, + padding?: number, ): SourceCode.ReturnTypeFromOptions[]; +} - getTokens( +declare class SourceCodeBase extends TokenStore { + /** + * Represents parsed source code. + * @param text The source code text. + * @param ast The Program node of the AST representing the code. This AST should be created from the text that BOM was stripped. + */ + constructor(text: string, ast: SourceCode.Program); + /** + * Represents parsed source code. + * @param config The config object. + */ + constructor(config: SourceCode.SourceCodeConfig); + + /** + * The parsed AST for the source code. + */ + ast: SourceCode.Program; + /** + * Retrieves an array containing all comments in the source code. + * @returns An array of comment nodes. + */ + getAllComments(): TSESTree.Comment[]; + /** + * Gets all comments for the given node. + * @param node The AST node to get the comments for. + * @returns An object containing a leading and trailing array of comments indexed by their position. + */ + getComments( node: TSESTree.Node, + ): { leading: TSESTree.Comment[]; trailing: TSESTree.Comment[] }; + /** + * Converts a (line, column) pair into a range index. + * @param loc A line/column location + * @returns The range index of the location in the file. + */ + getIndexFromLoc(location: TSESTree.LineAndColumnData): number; + /** + * Gets the entire source text split into an array of lines. + * @returns The source text as an array of lines. + */ + getLines(): string[]; + /** + * Converts a source text index into a (line, column) pair. + * @param index The index of a character in a file + * @returns A {line, column} location object with a 0-indexed column + */ + getLocFromIndex(index: number): TSESTree.LineAndColumnData; + /** + * Gets the deepest node containing a range index. + * @param index Range index of the desired node. + * @returns The node if found or `null` if not found. + */ + getNodeByRangeIndex(index: number): TSESTree.Node | null; + /** + * Gets the source code for the given node. + * @param node The AST node to get the text for. + * @param beforeCount The number of characters before the node to retrieve. + * @param afterCount The number of characters after the node to retrieve. + * @returns The text representing the AST node. + */ + getText( + node?: TSESTree.Node, beforeCount?: number, afterCount?: number, - ): TSESTree.Token[]; - getTokens( - node: TSESTree.Node, - options: T, - ): SourceCode.ReturnTypeFromOptions[]; - - commentsExistBetween( - left: TSESTree.Node | TSESTree.Token, - right: TSESTree.Node | TSESTree.Token, + ): string; + /** + * The flag to indicate that the source code has Unicode BOM. + */ + hasBOM: boolean; + /** + * Determines if two nodes or tokens have at least one whitespace character + * between them. Order does not matter. Returns false if the given nodes or + * tokens overlap. + * @param first The first node or token to check between. + * @param second The second node or token to check between. + * @returns True if there is a whitespace character between any of the tokens found between the two given nodes or tokens. + */ + isSpaceBetween( + first: TSESTree.Token | TSESTree.Comment | TSESTree.Node, + second: TSESTree.Token | TSESTree.Comment | TSESTree.Node, ): boolean; + /** + * The source code split into lines according to ECMA-262 specification. + * This is done to avoid each rule needing to do so separately. + */ + lines: string[]; + /** + * The indexes in `text` that each line starts + */ + lineStartIndices: number[]; + /** + * The parser services of this source code. + */ + parserServices: ParserServices; + /** + * The scope of this source code. + */ + scopeManager: Scope.ScopeManager | null; + /** + * The original text source code. BOM was stripped from this text. + */ + text: string; + /** + * All of the tokens and comments in the AST. + */ + tokensAndComments: (TSESTree.Comment | TSESTree.Token)[]; + /** + * The visitor keys to traverse AST. + */ + visitorKeys: SourceCode.VisitorKeys; - getCommentsBefore( - nodeOrToken: TSESTree.Node | TSESTree.Token, - ): TSESTree.Comment[]; - - getCommentsAfter( - nodeOrToken: TSESTree.Node | TSESTree.Token, - ): TSESTree.Comment[]; + //////////////////// + // static members // + //////////////////// - getCommentsInside(node: TSESTree.Node): TSESTree.Comment[]; + /** + * Split the source code into multiple lines based on the line delimiters. + * @param text Source code as a string. + * @returns Array of source code lines. + */ + static splitLines(text: string): string[]; } namespace SourceCode { @@ -156,12 +352,27 @@ namespace SourceCode { tokens: TSESTree.Token[]; } - export interface Config { - text: string; + export interface SourceCodeConfig { + /** + * The Program node of the AST representing the code. This AST should be created from the text that BOM was stripped. + */ ast: Program; - parserServices?: ParserServices; - scopeManager?: Scope.ScopeManager; - visitorKeys?: VisitorKeys; + /** + * The parser services. + */ + parserServices: ParserServices | null; + /** + * The scope of this source code. + */ + scopeManager: Scope.ScopeManager | null; + /** + * The source code text. + */ + text: string; + /** + * The visitor keys to traverse AST. + */ + visitorKeys: VisitorKeys | null; } export interface VisitorKeys { @@ -173,15 +384,24 @@ namespace SourceCode { ) => boolean; export type ReturnTypeFromOptions = T extends { includeComments: true } - ? TSESTree.Token | TSESTree.Comment - : TSESTree.Token; + ? TSESTree.Token + : Exclude; export type CursorWithSkipOptions = | number | FilterPredicate | { - includeComments?: boolean; + /** + * The predicate function to choose tokens. + */ filter?: FilterPredicate; + /** + * The flag to iterate comments as well. + */ + includeComments?: boolean; + /** + * The count of tokens the cursor skips. + */ skip?: number; }; @@ -189,18 +409,21 @@ namespace SourceCode { | number | FilterPredicate | { - includeComments?: boolean; + /** + * The predicate function to choose tokens. + */ filter?: FilterPredicate; + /** + * The flag to iterate comments as well. + */ + includeComments?: boolean; + /** + * The maximum count of tokens the cursor iterates. + */ count?: number; }; } -const SourceCode = ESLintSourceCode as { - new (text: string, ast: SourceCode.Program): SourceCode; - new (config: SourceCode.Config): SourceCode; - - // static methods - splitLines(text: string): string[]; -}; +class SourceCode extends (ESLintSourceCode as typeof SourceCodeBase) {} export { SourceCode }; diff --git a/packages/experimental-utils/typings/eslint.d.ts b/packages/experimental-utils/typings/eslint.d.ts index a32b469a977..71978423a4b 100644 --- a/packages/experimental-utils/typings/eslint.d.ts +++ b/packages/experimental-utils/typings/eslint.d.ts @@ -10,6 +10,7 @@ declare module 'eslint' { const RuleTester: unknown; const SourceCode: unknown; const CLIEngine: unknown; + const ESLint: unknown; - export { Linter, RuleTester, SourceCode, CLIEngine }; + export { Linter, RuleTester, SourceCode, CLIEngine, ESLint }; } diff --git a/packages/parser/tests/lib/basics.ts b/packages/parser/tests/lib/basics.ts index b1ab1d9a96a..0db6e698249 100644 --- a/packages/parser/tests/lib/basics.ts +++ b/packages/parser/tests/lib/basics.ts @@ -39,20 +39,18 @@ export const Price: React.SFC = function Price(props) {} }; linter.defineParser('@typescript-eslint/parser', parser); - linter.defineRule('test', { - create(context) { - return { - TSTypeReference(node): void { - const name = context.getSourceCode().getText(node.typeName); - context.report({ - node, - message: 'called on {{name}}', - data: { name }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - }, - }; - }, + linter.defineRule('test', function create(context) { + return { + TSTypeReference(node): void { + const name = context.getSourceCode().getText(node.typeName); + context.report({ + node, + message: 'called on {{name}}', + data: { name }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + }, + }; }); const messages = linter.verify(code, config, { filename: 'issue.ts' });