diff --git a/package.json b/package.json index 8255719940d..76fed22655a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "cz": "git-cz", "check:docs": "lerna run check:docs", "check:configs": "lerna run check:configs", - "check:spelling": "cspell --config=.cspell.json **/*.{md,ts,js}", + "check:spelling": "cspell --config=.cspell.json \"**/*.{md,ts,js}\"", "generate-contributors": "yarn ts-node --transpile-only ./tools/generate-contributors.ts && yarn all-contributors generate", "format": "prettier --write \"./**/*.{ts,js,json,md}\"", "format-check": "prettier --list-different \"./**/*.{ts,js,json,md}\"", diff --git a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts index 87f91912905..12c121989dc 100644 --- a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts +++ b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts @@ -1,3 +1,5 @@ +import { TSESTreeOptions } from '@typescript-eslint/typescript-estree'; + export interface ParserOptions { comment?: boolean; ecmaFeatures?: { @@ -9,6 +11,7 @@ export interface ParserOptions { errorOnUnknownASTType?: boolean; extraFileExtensions?: string[]; // ts-estree specific + debugLevel?: TSESTreeOptions['debugLevel']; filePath?: string; loc?: boolean; noWatch?: boolean; diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md index bd61d41e59d..1d9a38d0fb3 100644 --- a/packages/typescript-estree/README.md +++ b/packages/typescript-estree/README.md @@ -29,36 +29,78 @@ yarn add -D @typescript-eslint/typescript-estree ## API -### parse(code, options) +### Parsing -Parses the given string of code with the options provided and returns an ESTree-compatible AST. The options object has the following properties: +#### `parse(code, options)` -```js -{ - // attach range information to each node - range: false, +Parses the given string of code with the options provided and returns an ESTree-compatible AST. - // attach line/column location information to each node - loc: false, +```ts +interface ParseOptions { + /** + * create a top-level comments array containing all comments + */ + comment?: boolean; - // create a top-level tokens array containing all tokens - tokens: false, + /** + * An array of modules to turn explicit debugging on for. + * - 'typescript-eslint' is the same as setting the env var `DEBUG=typescript-eslint:*` + * - 'eslint' is the same as setting the env var `DEBUG=eslint:*` + * - 'typescript' is the same as setting `extendedDiagnostics: true` in your tsconfig compilerOptions + * + * For convenience, also supports a boolean: + * - true === ['typescript-eslint'] + * - false === [] + */ + debugLevel?: boolean | ('typescript-eslint' | 'eslint' | 'typescript')[]; - // create a top-level comments array containing all comments - comment: false, + /** + * Cause the parser to error if it encounters an unknown AST node type (useful for testing). + * This case only usually occurs when TypeScript releases new features. + */ + errorOnUnknownASTType?: boolean; - /* - * enable parsing JSX. For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html + /** + * The absolute path to the file being parsed. + */ + filePath?: string; + + /** + * Enable parsing of JSX. + * For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html * * NOTE: this setting does not effect known file types (.js, .jsx, .ts, .tsx, .json) because the * TypeScript compiler has its own internal handling for known file extensions. * - * Exact behaviour: - * - .js, .jsx, .tsx files are parsed as if this is true - * - .ts files are parsed as if this is false - * - unknown extensions (.md, .vue) will respect this setting + * For the exact behavior, see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#parseroptionsecmafeaturesjsx */ - jsx: false, + jsx?: boolean; + + /** + * Controls whether the `loc` information to each node. + * The `loc` property is an object which contains the exact line/column the node starts/ends on. + * This is similar to the `range` property, except it is line/column relative. + */ + loc?: boolean; + + /* + * Allows overriding of function used for logging. + * When value is `false`, no logging will occur. + * When value is not provided, `console.log()` will be used. + */ + loggerFn?: Function | false; + + /** + * Controls whether the `range` property is included on AST nodes. + * The `range` property is a [number, number] which indicates the start/end index of the node in the file contents. + * This is similar to the `loc` property, except this is the absolute index. + */ + range?: boolean; + + /** + * Set to true to create a top-level array containing all tokens from the file. + */ + tokens?: boolean; /* * The JSX AST changed the node type for string literals @@ -66,17 +108,61 @@ Parses the given string of code with the options provided and returns an ESTree- * When value is `true`, these nodes will be parsed as type `JSXText`. * When value is `false`, these nodes will be parsed as type `Literal`. */ - useJSXTextNode: false, + useJSXTextNode?: boolean; +} - // Cause the parser to error if it encounters an unknown AST node type (useful for testing) +const PARSE_DEFAULT_OPTIONS: ParseOptions = { + comment: false, errorOnUnknownASTType: false, + filePath: 'estree.ts', // or 'estree.tsx', if you pass jsx: true + jsx: false, + loc: false, + loggerFn: undefined, + range: false, + tokens: false, + useJSXTextNode: false, +}; - /* - * Allows overriding of function used for logging. - * When value is `false`, no logging will occur. - * When value is not provided, `console.log()` will be used. +declare function parse( + code: string, + options: ParseOptions = PARSE_DEFAULT_OPTIONS, +): TSESTree.Program; +``` + +Example usage: + +```js +import { parse } from '@typescript-eslint/typescript-estree'; + +const code = `const hello: string = 'world';`; +const ast = parse(code, { + loc: true, + range: true, +}); +``` + +#### `parseAndGenerateServices(code, options)` + +Parses the given string of code with the options provided and returns an ESTree-compatible AST. Accepts additional options which can be used to generate type information along with the AST. + +```ts +interface ParseAndGenerateServicesOptions extends ParseOptions { + /** + * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. */ - loggerFn: undefined, + errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + + /** + * When `project` is provided, this controls the non-standard file extensions which will be parsed. + * It accepts an array of file extensions, each preceded by a `.`. + */ + extraFileExtensions?: string[]; + + /** + * The absolute path to the file being parsed. + * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. + */ + filePath?: string; /** * Allows the user to control whether or not two-way AST node maps are preserved @@ -88,42 +174,67 @@ Parses the given string of code with the options provided and returns an ESTree- * NOTE: If `preserveNodeMaps` is explicitly set by the user, it will be respected, * regardless of whether or not `project` is in use. */ - preserveNodeMaps: undefined + preserveNodeMaps?: boolean; + + /** + * Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s). + * If this is provided, type information will be returned. + */ + project?: string | string[]; + + /** + * The absolute path to the root directory for all provided `project`s. + */ + tsconfigRootDir?: string; + + /** + *************************************************************************************** + * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * + *************************************************************************************** + * + * When passed with `project`, this allows the parser to create a catch-all, default program. + * This means that if the parser encounters a file not included in any of the provided `project`s, + * it will not error, but will instead parse the file and its dependencies in a new program. + */ + createDefaultProgram?: boolean; } + +const PARSE_AND_GENERATE_SERVICES_DEFAULT_OPTIONS: ParseOptions = { + ...PARSE_DEFAULT_OPTIONS, + errorOnTypeScriptSyntacticAndSemanticIssues: false, + extraFileExtensions: [], + preserveNodeMaps: false, // or true, if you do not set this, but pass `project` + project: undefined, + tsconfigRootDir: process.cwd(), +}; + +declare function parseAndGenerateServices( + code: string, + options: ParseOptions = PARSE_DEFAULT_OPTIONS, +): TSESTree.Program; ``` Example usage: ```js -const parser = require('@typescript-eslint/typescript-estree'); +import { parseAndGenerateServices } from '@typescript-eslint/typescript-estree'; + const code = `const hello: string = 'world';`; -const ast = parser.parse(code, { - range: true, +const ast = parseAndGenerateServices(code, { + filePath: '/some/path/to/file/foo.ts', loc: true, + project: './tsconfig.json', + range: true, }); ``` -### version +### `TSESTree`, `AST_NODE_TYPES` and `AST_TOKEN_TYPES` -Exposes the current version of `typescript-estree` as specified in `package.json`. +Types for the AST produced by the parse functions. -Example usage: - -```js -const parser = require('@typescript-eslint/typescript-estree'); -const version = parser.version; -``` - -### `AST_NODE_TYPES` - -Exposes an object that contains the AST node types produced by the parser. - -Example usage: - -```js -const parser = require('@typescript-eslint/typescript-estree'); -const astNodeTypes = parser.AST_NODE_TYPES; -``` +- `TSESTree` is a namespace which contains object types representing all of the AST Nodes produced by the parser. +- `AST_NODE_TYPES` is an enum which provides the values for every single AST node's `type` property. +- `AST_TOKEN_TYPES` is an enum which provides the values for every single AST token's `type` property. ## Supported TypeScript Version diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index 0db0d1fc699..c1e69b25335 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -3,9 +3,9 @@ import path from 'path'; import * as ts from 'typescript'; import { Extra } from '../parser-options'; import { - getTsconfigPath, - DEFAULT_COMPILER_OPTIONS, ASTAndProgram, + getTsconfigPath, + createDefaultCompilerOptionsFromExtra, } from './shared'; const log = debug('typescript-eslint:typescript-estree:createDefaultProgram'); @@ -31,7 +31,7 @@ function createDefaultProgram( const commandLine = ts.getParsedCommandLineOfConfigFile( tsconfigPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra(extra), { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }, ); diff --git a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts index c6c74b8c5ab..257577d5eb2 100644 --- a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts +++ b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts @@ -3,7 +3,7 @@ import * as ts from 'typescript'; import { Extra } from '../parser-options'; import { ASTAndProgram, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra, getScriptKind, } from './shared'; @@ -67,7 +67,7 @@ function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram { noResolve: true, target: ts.ScriptTarget.Latest, jsx: extra.jsx ? ts.JsxEmit.Preserve : undefined, - ...DEFAULT_COMPILER_OPTIONS, + ...createDefaultCompilerOptionsFromExtra(extra), }, compilerHost, ); diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index f60905a9879..dd4401af3b6 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -6,9 +6,9 @@ import { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile'; import { canonicalDirname, CanonicalPath, - getTsconfigPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra, getCanonicalFileName, + getTsconfigPath, } from './shared'; const log = debug('typescript-eslint:typescript-estree:createWatchProgram'); @@ -233,7 +233,7 @@ function createWatchProgram( // create compiler host const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra(extra), ts.sys, ts.createSemanticDiagnosticsBuilderProgram, diagnosticReporter, diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index be60f245374..1aa6a4fe3c0 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -20,6 +20,19 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { noUnusedParameters: true, }; +function createDefaultCompilerOptionsFromExtra( + extra: Extra, +): ts.CompilerOptions { + if (extra.debugLevel.has('typescript')) { + return { + ...DEFAULT_COMPILER_OPTIONS, + extendedDiagnostics: true, + }; + } + + return DEFAULT_COMPILER_OPTIONS; +} + // This narrows the type so we can be sure we're passing canonical names in the correct places type CanonicalPath = string & { __brand: unknown }; @@ -85,7 +98,7 @@ export { ASTAndProgram, canonicalDirname, CanonicalPath, - DEFAULT_COMPILER_OPTIONS, + createDefaultCompilerOptionsFromExtra, ensureAbsolutePath, getCanonicalFileName, getScriptKind, diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 4a4bc3eb415..1cd8b4d5475 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -1,11 +1,14 @@ import { Program } from 'typescript'; import { TSESTree, TSNode, TSESTreeToTSNode, TSToken } from './ts-estree'; +type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; + export interface Extra { code: string; comment: boolean; comments: TSESTree.Comment[]; createDefaultProgram: boolean; + debugLevel: Set; errorOnTypeScriptSyntacticAndSemanticIssues: boolean; errorOnUnknownASTType: boolean; extraFileExtensions: string[]; @@ -22,22 +25,131 @@ export interface Extra { useJSXTextNode: boolean; } +//////////////////////////////////////////////////// +// MAKE SURE THIS IS KEPT IN SYNC WITH THE README // +//////////////////////////////////////////////////// + export interface TSESTreeOptions { + /** + * create a top-level comments array containing all comments + */ comment?: boolean; - createDefaultProgram?: boolean; + + /** + * For convenience: + * - true === ['typescript-eslint'] + * - false === [] + * + * An array of modules to turn explicit debugging on for. + * - 'typescript-eslint' is the same as setting the env var `DEBUG=typescript-eslint:*` + * - 'eslint' is the same as setting the env var `DEBUG=eslint:*` + * - 'typescript' is the same as setting `extendedDiagnostics: true` in your tsconfig compilerOptions + */ + debugLevel?: boolean | DebugModule[]; + + /** + * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. + */ errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + + /** + * Cause the parser to error if it encounters an unknown AST node type (useful for testing). + * This case only usually occurs when TypeScript releases new features. + */ errorOnUnknownASTType?: boolean; + + /** + * When `project` is provided, this controls the non-standard file extensions which will be parsed. + * It accepts an array of file extensions, each preceded by a `.`. + */ extraFileExtensions?: string[]; + + /** + * The absolute path to the file being parsed. + * When `project` is provided, this is required, as it is used to fetch the file from the TypeScript compiler's cache. + */ filePath?: string; + + /** + * Enable parsing of JSX. + * For more details, see https://www.typescriptlang.org/docs/handbook/jsx.html + * + * NOTE: this setting does not effect known file types (.js, .jsx, .ts, .tsx, .json) because the + * TypeScript compiler has its own internal handling for known file extensions. + * + * For the exact behavior, see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#parseroptionsecmafeaturesjsx + */ jsx?: boolean; + + /** + * Controls whether the `loc` information to each node. + * The `loc` property is an object which contains the exact line/column the node starts/ends on. + * This is similar to the `range` property, except it is line/column relative. + */ loc?: boolean; + + /* + * Allows overriding of function used for logging. + * When value is `false`, no logging will occur. + * When value is not provided, `console.log()` will be used. + */ loggerFn?: Function | false; + + /** + * Allows the user to control whether or not two-way AST node maps are preserved + * during the AST conversion process. + * + * By default: the AST node maps are NOT preserved, unless `project` has been specified, + * in which case the maps are made available on the returned `parserServices`. + * + * NOTE: If `preserveNodeMaps` is explicitly set by the user, it will be respected, + * regardless of whether or not `project` is in use. + */ preserveNodeMaps?: boolean; + + /** + * Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s). + * If this is provided, type information will be returned. + */ project?: string | string[]; + + /** + * Controls whether the `range` property is included on AST nodes. + * The `range` property is a [number, number] which indicates the start/end index of the node in the file contents. + * This is similar to the `loc` property, except this is the absolute index. + */ range?: boolean; + + /** + * Set to true to create a top-level array containing all tokens from the file. + */ tokens?: boolean; + + /** + * The absolute path to the root directory for all provided `project`s. + */ tsconfigRootDir?: string; + + /* + * The JSX AST changed the node type for string literals + * inside a JSX Element from `Literal` to `JSXText`. + * When value is `true`, these nodes will be parsed as type `JSXText`. + * When value is `false`, these nodes will be parsed as type `Literal`. + */ useJSXTextNode?: boolean; + + /** + *************************************************************************************** + * IT IS RECOMMENDED THAT YOU DO NOT USE THIS OPTION, AS IT CAUSES PERFORMANCE ISSUES. * + *************************************************************************************** + * + * When passed with `project`, this allows the parser to create a catch-all, default program. + * This means that if the parser encounters a file not included in any of the provided `project`s, + * it will not error, but will instead parse the file and its dependencies in a new program. + * + * This + */ + createDefaultProgram?: boolean; } // This lets us use generics to type the return value, and removes the need to diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 0ecd1de3312..6142efaf1af 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,7 +1,8 @@ -import semver from 'semver'; -import * as ts from 'typescript'; +import debug from 'debug'; import { sync as globSync } from 'glob'; import isGlob from 'is-glob'; +import semver from 'semver'; +import * as ts from 'typescript'; import { astConverter } from './ast-converter'; import { convertError } from './convert'; import { createDefaultProgram } from './create-program/createDefaultProgram'; @@ -92,6 +93,7 @@ function resetExtra(): void { comment: false, comments: [], createDefaultProgram: false, + debugLevel: new Set(), errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: false, extraFileExtensions: [], @@ -110,6 +112,31 @@ function resetExtra(): void { } function applyParserOptionsToExtra(options: TSESTreeOptions): void { + /** + * Configure Debug logging + */ + if (options.debugLevel === true) { + extra.debugLevel = new Set(['typescript-eslint']); + } else if (Array.isArray(options.debugLevel)) { + extra.debugLevel = new Set(options.debugLevel); + } + if (extra.debugLevel.size > 0) { + // debug doesn't support multiple `enable` calls, so have to do it all at once + const namespaces = []; + if (extra.debugLevel.has('typescript-eslint')) { + namespaces.push('typescript-eslint:*'); + } + if ( + extra.debugLevel.has('eslint') || + // make sure we don't turn off the eslint debug if it was enabled via --debug + debug.enabled('eslint:*') + ) { + // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25 + namespaces.push('eslint:*,-eslint:code-path'); + } + debug.enable(namespaces.join(',')); + } + /** * Track range information in the AST */ @@ -257,7 +284,6 @@ function warnAboutTSVersion(): void { //------------------------------------------------------------------------------ type AST = TSESTree.Program & - (T['range'] extends true ? { range: [number, number] } : {}) & (T['tokens'] extends true ? { tokens: TSESTree.Token[] } : {}) & (T['comment'] extends true ? { comments: TSESTree.Comment[] } : {}); @@ -417,10 +443,9 @@ export { parse, parseAndGenerateServices, ParseAndGenerateServicesResult, - ParserServices, - TSESTreeOptions, version, }; +export { ParserServices, TSESTreeOptions } from './parser-options'; export { simpleTraverse } from './simple-traverse'; export { visitorKeys } from './visitor-keys'; export * from './ts-estree'; diff --git a/packages/typescript-estree/tests/lib/parse.ts b/packages/typescript-estree/tests/lib/parse.ts index 986bf02573a..1e6a1e97236 100644 --- a/packages/typescript-estree/tests/lib/parse.ts +++ b/packages/typescript-estree/tests/lib/parse.ts @@ -1,7 +1,9 @@ +import debug from 'debug'; import { join, resolve } from 'path'; import * as parser from '../../src/parser'; import * as astConverter from '../../src/ast-converter'; import { TSESTreeOptions } from '../../src/parser-options'; +import * as sharedParserUtils from '../../src/create-program/shared'; import { createSnapshotTestBlock } from '../../tools/test-utils'; const FIXTURES_DIR = './tests/fixtures/simpleProject'; @@ -496,4 +498,63 @@ describe('parse()', () => { }); }); }); + + describe('debug options', () => { + const debugEnable = jest.fn(); + beforeEach(() => { + debugEnable.mockReset(); + debug.enable = debugEnable; + jest.spyOn(debug, 'enabled').mockImplementation(() => false); + }); + + it("shouldn't turn on debugger if no options were provided", () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: [], + }); + expect(debugEnable).not.toHaveBeenCalled(); + }); + + it('should turn on eslint debugger', () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['eslint'], + }); + expect(debugEnable).toHaveBeenCalledTimes(1); + expect(debugEnable).toHaveBeenCalledWith('eslint:*,-eslint:code-path'); + }); + + it('should turn on typescript-eslint debugger', () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript-eslint'], + }); + expect(debugEnable).toHaveBeenCalledTimes(1); + expect(debugEnable).toHaveBeenCalledWith('typescript-eslint:*'); + }); + + it('should turn on both eslint and typescript-eslint debugger', () => { + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript-eslint', 'eslint'], + }); + expect(debugEnable).toHaveBeenCalledTimes(1); + expect(debugEnable).toHaveBeenCalledWith( + 'typescript-eslint:*,eslint:*,-eslint:code-path', + ); + }); + + it('should turn on typescript debugger', () => { + const spy = jest.spyOn( + sharedParserUtils, + 'createDefaultCompilerOptionsFromExtra', + ); + + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript'], + }); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveReturnedWith( + expect.objectContaining({ + extendedDiagnostics: true, + }), + ); + }); + }); });