Skip to content

Commit

Permalink
fix: default to parse all JSDoc and provide options to configure it (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Nov 28, 2023
1 parent 39c437a commit 779e13e
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 11 deletions.
20 changes: 18 additions & 2 deletions docs/packages/Parser.mdx
Expand Up @@ -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;
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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`.
Expand Down
13 changes: 13 additions & 0 deletions docs/packages/TypeScript_ESTree.mdx
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/parser-options.ts
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -77,6 +81,7 @@ export {
CacheDurationSeconds,
DebugLevel,
EcmaVersion,
JSDocParsingMode,
ParserOptions,
SourceType,
};
Expand Up @@ -59,7 +59,7 @@ function createDefaultProgram(
[parseSettings.filePath],
{
...commandLine.options,
jsDocParsingMode: ts.JSDocParsingMode?.ParseForTypeInfo,
jsDocParsingMode: parseSettings.jsDocParsingMode,
},
compilerHost,
);
Expand Down
Expand Up @@ -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,
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -45,6 +47,6 @@ export function createProjectService(): TypeScriptProjectService {
startGroup: doNothing,
},
session: undefined,
jsDocParsingMode: tsserver.JSDocParsingMode?.ParseForTypeInfo,
jsDocParsingMode,
});
}
Expand Up @@ -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),
Expand Down
Expand Up @@ -267,7 +267,7 @@ function createWatchProgram(
// eslint-disable-next-line @typescript-eslint/no-empty-function
/*reportWatchStatus*/ () => {},
) as WatchCompilerHostOfConfigFile<ts.BuilderProgram>;
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;
Expand Down
@@ -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';
Expand All @@ -23,6 +23,15 @@ const log = debug(
let TSCONFIG_MATCH_CACHE: ExpiringCache<string, string> | 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<TSESTreeOptions> = {},
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -71,6 +96,7 @@ export function createParseSettings(
: getFileName(options.jsx),
tsconfigRootDir,
),
jsDocParsingMode,
jsx: options.jsx === true,
loc: options.loc === true,
log:
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions packages/typescript-estree/src/parseSettings/index.ts
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
13 changes: 13 additions & 0 deletions 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';

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/website-eslint/build.ts
Expand Up @@ -80,6 +80,9 @@ async function buildPackage(name: string, file: string): Promise<void> {
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: [
Expand Down
1 change: 1 addition & 0 deletions packages/website/src/components/linter/config.ts
Expand Up @@ -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,
Expand Down

0 comments on commit 779e13e

Please sign in to comment.