diff --git a/docs/packages/Parser.mdx b/docs/packages/Parser.mdx index 3fc4fed317b..efaa66924ce 100644 --- a/docs/packages/Parser.mdx +++ b/docs/packages/Parser.mdx @@ -40,10 +40,11 @@ interface ParserOptions { ecmaVersion?: number | 'latest'; emitDecoratorMetadata?: boolean; extraFileExtensions?: string[]; + jsDocParsingMode?: 'all' | 'none' | 'type-info'; jsxFragmentName?: string | null; jsxPragma?: string | null; lib?: string[]; - program?: import('typescript').Program; + programs?: import('typescript').Program; project?: string | string[] | true; projectFolderIgnoreList?: string[]; tsconfigRootDir?: string; @@ -118,6 +119,21 @@ This option allows you to provide one or more additional file extensions which s The default extensions are `['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx']`. Add extensions starting with `.`, followed by the file extension. E.g. for a `.vue` file use `"extraFileExtensions": [".vue"]`. +### `jsDocParsingMode` + +> Default if `parserOptions.project` is set, then `'all'`, otherwise `'none'` + +When TS parses a file it will also parse JSDoc comments into the AST - which can then be consumed by lint rules. +If you are using TypeScript version >=5.3 then this option can be used as a performance optimization. + +The valid values for this rule are: + +- `'all'` - parse all JSDoc comments, always. +- `'none'` - parse no JSDoc comments, ever. +- `'type-info'` - parse just JSDoc comments that are required to provide correct type-info. TS will always parse JSDoc in non-TS files, but never in TS files. + +If you do not use lint rules like `eslint-plugin-deprecation` that rely on TS's JSDoc tag representation, then you can set this to `'none'` to improve parser performance. + ### `jsxFragmentName` > Default `null` @@ -149,7 +165,7 @@ Specifies the TypeScript `lib`s that are available. This is used by the scope an If you provide `parserOptions.project`, you do not need to set this, as it will automatically detected from the compiler. -### `program` +### `programs` > Default `undefined`. diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index 6354565e828..610f641d384 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -70,6 +70,18 @@ interface ParseOptions { */ filePath?: string; + /** + * If you are using TypeScript version >=5.3 then this option can be used as a performance optimization. + * + * The valid values for this rule are: + * - `'all'` - parse all JSDoc comments, always. + * - `'none'` - parse no JSDoc comments, ever. + * - `'type-info'` - parse just JSDoc comments that are required to provide correct type-info. TS will always parse JSDoc in non-TS files, but never in TS files. + * + * If you do not rely on JSDoc tags from the TypeScript AST, then you can safely set this to `'none'` to improve performance. + */ + jsDocParsingMode?: JSDocParsingMode; + /** * Enable parsing of JSX. * For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html @@ -111,6 +123,7 @@ interface ParseOptions { const PARSE_DEFAULT_OPTIONS: ParseOptions = { comment: false, filePath: 'estree.ts', // or 'estree.tsx', if you pass jsx: true + jsDocParsingMode: 'all', jsx: false, loc: false, loggerFn: undefined, diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index bf1655125dc..885645f4bba 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -32,6 +32,9 @@ type EcmaVersion = type SourceTypeClassic = 'module' | 'script'; type SourceType = SourceTypeClassic | 'commonjs'; +type JSDocParsingMode = 'all' | 'none' | 'type-info'; + +// If you add publicly visible options here, make sure they're also documented in `docs/packages/Parser.mdx` interface ParserOptions { ecmaFeatures?: { globalReturn?: boolean; @@ -57,8 +60,9 @@ interface ParserOptions { EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; // purposely undocumented for now extraFileExtensions?: string[]; filePath?: string; + jsDocParsingMode?: JSDocParsingMode; loc?: boolean; - program?: Program | null; + programs?: Program | null; project?: string[] | string | true | null; projectFolderIgnoreList?: (RegExp | string)[]; range?: boolean; @@ -77,6 +81,7 @@ export { CacheDurationSeconds, DebugLevel, EcmaVersion, + JSDocParsingMode, ParserOptions, SourceType, }; diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index e9660337f93..b42ec76c678 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -59,7 +59,7 @@ function createDefaultProgram( [parseSettings.filePath], { ...commandLine.options, - jsDocParsingMode: ts.JSDocParsingMode?.ParseForTypeInfo, + jsDocParsingMode: parseSettings.jsDocParsingMode, }, compilerHost, ); diff --git a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts index ae646e6ad7b..fbe92867dcc 100644 --- a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts +++ b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts @@ -65,7 +65,7 @@ function createIsolatedProgram( const program = ts.createProgram( [parseSettings.filePath], { - jsDocParsingMode: ts.JSDocParsingMode?.ParseForTypeInfo, + jsDocParsingMode: parseSettings.jsDocParsingMode, noResolve: true, target: ts.ScriptTarget.Latest, jsx: parseSettings.jsx ? ts.JsxEmit.Preserve : undefined, diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 0bd4cefd40b..380dec18f4c 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -9,7 +9,9 @@ const createStubFileWatcher = (): ts.FileWatcher => ({ export type TypeScriptProjectService = ts.server.ProjectService; -export function createProjectService(): TypeScriptProjectService { +export function createProjectService( + jsDocParsingMode?: ts.JSDocParsingMode, +): TypeScriptProjectService { // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; @@ -45,6 +47,6 @@ export function createProjectService(): TypeScriptProjectService { startGroup: doNothing, }, session: undefined, - jsDocParsingMode: tsserver.JSDocParsingMode?.ParseForTypeInfo, + jsDocParsingMode, }); } diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index cd021bfced3..bb5bc9d7b8f 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -22,7 +22,7 @@ function createSourceFile(parseSettings: ParseSettings): ts.SourceFile { parseSettings.codeFullText, { languageVersion: ts.ScriptTarget.Latest, - jsDocParsingMode: ts.JSDocParsingMode?.ParseNone, + jsDocParsingMode: parseSettings.jsDocParsingMode, }, /* setParentNodes */ true, getScriptKind(parseSettings.filePath, parseSettings.jsx), diff --git a/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts b/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts index c0175245f8e..21178000ee5 100644 --- a/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts +++ b/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts @@ -267,7 +267,7 @@ function createWatchProgram( // eslint-disable-next-line @typescript-eslint/no-empty-function /*reportWatchStatus*/ () => {}, ) as WatchCompilerHostOfConfigFile; - watchCompilerHost.jsDocParsingMode = ts.JSDocParsingMode?.ParseForTypeInfo; + watchCompilerHost.jsDocParsingMode = parseSettings.jsDocParsingMode; // ensure readFile reads the code being linted instead of the copy on disk const oldReadFile = watchCompilerHost.readFile; diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 537ad85baad..50c39898f49 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,5 +1,5 @@ import debug from 'debug'; -import type * as ts from 'typescript'; +import * as ts from 'typescript'; import type { TypeScriptProjectService } from '../create-program/createProjectService'; import { createProjectService } from '../create-program/createProjectService'; @@ -23,6 +23,15 @@ const log = debug( let TSCONFIG_MATCH_CACHE: ExpiringCache | null; let TSSERVER_PROJECT_SERVICE: TypeScriptProjectService | null = null; +// NOTE - we intentionally use "unnecessary" `?.` here because in TS<5.3 this enum doesn't exist +// This object exists so we can centralize these for tracking and so we don't proliferate these across the file +const JSDocParsingMode = { + ParseAll: ts.JSDocParsingMode?.ParseAll, + ParseNone: ts.JSDocParsingMode?.ParseNone, + ParseForTypeErrors: ts.JSDocParsingMode?.ParseForTypeErrors, + ParseForTypeInfo: ts.JSDocParsingMode?.ParseForTypeInfo, +} as const; + export function createParseSettings( code: ts.SourceFile | string, options: Partial = {}, @@ -34,6 +43,22 @@ export function createParseSettings( ? options.tsconfigRootDir : process.cwd(); const passedLoggerFn = typeof options.loggerFn === 'function'; + const jsDocParsingMode = ((): ts.JSDocParsingMode => { + switch (options.jsDocParsingMode) { + case 'all': + return JSDocParsingMode.ParseAll; + + case 'none': + return JSDocParsingMode.ParseNone; + + case 'type-info': + return JSDocParsingMode.ParseForTypeInfo; + + default: + return JSDocParsingMode.ParseAll; + } + })(); + const parseSettings: MutableParseSettings = { allowInvalidAST: options.allowInvalidAST === true, code, @@ -56,7 +81,7 @@ export function createParseSettings( process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'false') || (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' && options.EXPERIMENTAL_useProjectService !== false) - ? (TSSERVER_PROJECT_SERVICE ??= createProjectService()) + ? (TSSERVER_PROJECT_SERVICE ??= createProjectService(jsDocParsingMode)) : undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, @@ -71,6 +96,7 @@ export function createParseSettings( : getFileName(options.jsx), tsconfigRootDir, ), + jsDocParsingMode, jsx: options.jsx === true, loc: options.loc === true, log: @@ -136,6 +162,17 @@ export function createParseSettings( }); } + // No type-aware linting which means that cross-file (or even same-file) JSDoc is useless + // So in this specific case we default to 'none' if no value was provided + if ( + options.jsDocParsingMode == null && + parseSettings.projects.length === 0 && + parseSettings.programs == null && + parseSettings.EXPERIMENTAL_projectService == null + ) { + parseSettings.jsDocParsingMode = JSDocParsingMode.ParseNone; + } + warnAboutTSVersion(parseSettings, passedLoggerFn); return parseSettings; diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index da093dedfed..352798be158 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -7,6 +7,12 @@ import type { CacheLike } from './ExpiringCache'; type DebugModule = 'eslint' | 'typescript-eslint' | 'typescript'; +// Workaround to support new TS version features for consumers on old TS versions +declare module 'typescript' { + // Added in TypeScript 5.3 + enum JSDocParsingMode {} +} + /** * Internal settings used by the parser to run on a file. */ @@ -84,6 +90,11 @@ export interface MutableParseSettings { */ filePath: string; + /** + * JSDoc parsing style to pass through to TypeScript + */ + jsDocParsingMode: ts.JSDocParsingMode; + /** * Whether parsing of JSX is enabled. * diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 711ef4bca34..badd40ae8cf 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -1,6 +1,7 @@ import type { CacheDurationSeconds, DebugLevel, + JSDocParsingMode, } from '@typescript-eslint/types'; import type * as ts from 'typescript'; @@ -45,6 +46,18 @@ interface ParseOptions { */ filePath?: string; + /** + * If you are using TypeScript version >=5.3 then this option can be used as a performance optimization. + * + * The valid values for this rule are: + * - `'all'` - parse all JSDoc comments, always. + * - `'none'` - parse no JSDoc comments, ever. + * - `'type-info'` - parse just JSDoc comments that are required to provide correct type-info. TS will always parse JSDoc in non-TS files, but never in TS files. + * + * If you do not rely on JSDoc tags from the TypeScript AST, then you can safely set this to `'none'` to improve performance. + */ + jsDocParsingMode?: JSDocParsingMode; + /** * Enable parsing of JSX. * For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html diff --git a/packages/website-eslint/build.ts b/packages/website-eslint/build.ts index 85fb695499b..ef316770155 100644 --- a/packages/website-eslint/build.ts +++ b/packages/website-eslint/build.ts @@ -80,6 +80,9 @@ async function buildPackage(name: string, file: string): Promise { assert: requireResolved('./src/mock/assert.js'), path: requireResolved('./src/mock/path.js'), typescript: requireResolved('./src/mock/typescript.js'), + 'typescript/lib/tsserverlibrary': requireResolved( + './src/mock/typescript.js', + ), 'lru-cache': requireResolved('./src/mock/lru-cache.js'), }, plugins: [ diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index d3c7319a8aa..4ae0a5987b6 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -17,6 +17,7 @@ export const defaultParseSettings: ParseSettings = { EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, extraFileExtensions: [], filePath: '', + jsDocParsingMode: window.ts?.JSDocParsingMode?.ParseAll, jsx: true, loc: true, log: console.log,