From af41b7fa7b9b8f3023fdabd40846598d5d4d4f61 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 16 Nov 2022 00:48:59 -0500 Subject: [PATCH] feat(typescript-estree): allow providing code as a ts.SourceFile (#5892) --- packages/parser/src/parser.ts | 8 ++-- .../typescript-estree/src/ast-converter.ts | 2 +- .../create-program/createDefaultProgram.ts | 2 +- .../create-program/createIsolatedProgram.ts | 2 +- .../src/create-program/createSourceFile.ts | 17 +++++---- .../src/create-program/createWatchProgram.ts | 10 +++-- .../src/parseSettings/createParseSettings.ts | 20 ++++++---- .../src/parseSettings/index.ts | 9 ++++- packages/typescript-estree/src/parser.ts | 4 +- .../typescript-estree/src/source-files.ts | 17 +++++++++ .../tests/lib/source-files.test.ts | 37 +++++++++++++++++++ .../website/src/components/linter/config.ts | 1 + 12 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 packages/typescript-estree/src/source-files.ts create mode 100644 packages/typescript-estree/tests/lib/source-files.test.ts diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index e7223e93332..f619f5e17c1 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -14,7 +14,7 @@ import { visitorKeys, } from '@typescript-eslint/typescript-estree'; import debug from 'debug'; -import type { CompilerOptions } from 'typescript'; +import type * as ts from 'typescript'; import { ScriptTarget } from 'typescript'; const log = debug('typescript-eslint:parser:parser'); @@ -41,7 +41,7 @@ function validateBoolean( } const LIB_FILENAME_REGEX = /lib\.(.+)\.d\.[cm]?ts$/; -function getLib(compilerOptions: CompilerOptions): Lib[] { +function getLib(compilerOptions: ts.CompilerOptions): Lib[] { if (compilerOptions.lib) { return compilerOptions.lib.reduce((acc, lib) => { const match = LIB_FILENAME_REGEX.exec(lib.toLowerCase()); @@ -76,14 +76,14 @@ function getLib(compilerOptions: CompilerOptions): Lib[] { } function parse( - code: string, + code: string | ts.SourceFile, options?: ParserOptions, ): ParseForESLintResult['ast'] { return parseForESLint(code, options).ast; } function parseForESLint( - code: string, + code: string | ts.SourceFile, options?: ParserOptions | null, ): ParseForESLintResult { if (!options || typeof options !== 'object') { diff --git a/packages/typescript-estree/src/ast-converter.ts b/packages/typescript-estree/src/ast-converter.ts index b9be864f529..0e55541969d 100644 --- a/packages/typescript-estree/src/ast-converter.ts +++ b/packages/typescript-estree/src/ast-converter.ts @@ -63,7 +63,7 @@ export function astConverter( * Optionally convert and include all comments in the AST */ if (parseSettings.comment) { - estree.comments = convertComments(ast, parseSettings.code); + estree.comments = convertComments(ast, parseSettings.codeFullText); } const astMaps = instance.getASTMaps(); diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index bfcc3066b17..174783f43db 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -56,7 +56,7 @@ function createDefaultProgram( const oldReadFile = compilerHost.readFile; compilerHost.readFile = (fileName: string): string | undefined => path.normalize(fileName) === path.normalize(parseSettings.filePath) - ? parseSettings.code + ? parseSettings.codeFullText : oldReadFile(fileName); const program = ts.createProgram( diff --git a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts index 5ec1c8e0fe7..58d7ec8a61a 100644 --- a/packages/typescript-estree/src/create-program/createIsolatedProgram.ts +++ b/packages/typescript-estree/src/create-program/createIsolatedProgram.ts @@ -43,7 +43,7 @@ function createIsolatedProgram(parseSettings: ParseSettings): ASTAndProgram { getSourceFile(filename: string) { return ts.createSourceFile( filename, - parseSettings.code, + parseSettings.codeFullText, ts.ScriptTarget.Latest, /* setParentNodes */ true, getScriptKind(parseSettings.filePath, parseSettings.jsx), diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index 806e503f0e4..0214802f73f 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -2,6 +2,7 @@ import debug from 'debug'; import * as ts from 'typescript'; import type { ParseSettings } from '../parseSettings'; +import { isSourceFile } from '../source-files'; import { getScriptKind } from './getScriptKind'; const log = debug('typescript-eslint:typescript-estree:createSourceFile'); @@ -13,13 +14,15 @@ function createSourceFile(parseSettings: ParseSettings): ts.SourceFile { parseSettings.filePath, ); - return ts.createSourceFile( - parseSettings.filePath, - parseSettings.code, - ts.ScriptTarget.Latest, - /* setParentNodes */ true, - getScriptKind(parseSettings.filePath, parseSettings.jsx), - ); + return isSourceFile(parseSettings.code) + ? parseSettings.code + : ts.createSourceFile( + parseSettings.filePath, + parseSettings.codeFullText, + ts.ScriptTarget.Latest, + /* setParentNodes */ true, + getScriptKind(parseSettings.filePath, parseSettings.jsx), + ); } export { createSourceFile }; diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 96687206f7f..d2d63676fca 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import * as ts from 'typescript'; import type { ParseSettings } from '../parseSettings'; +import { getCodeText } from '../source-files'; import type { CanonicalPath } from './shared'; import { canonicalDirname, @@ -89,7 +90,10 @@ function saveWatchCallback( /** * Holds information about the file currently being linted */ -const currentLintOperationState: { code: string; filePath: CanonicalPath } = { +const currentLintOperationState: { + code: string | ts.SourceFile; + filePath: CanonicalPath; +} = { code: '', filePath: '' as CanonicalPath, }; @@ -147,7 +151,7 @@ function getProgramsForProjects(parseSettings: ParseSettings): ts.Program[] { // Update file version if necessary const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(filePath); - const codeHash = createHash(parseSettings.code); + const codeHash = createHash(getCodeText(parseSettings.code)); if ( parsedFilesSeenHash.get(filePath) !== codeHash && fileWatchCallbacks && @@ -286,7 +290,7 @@ function createWatchProgram( const filePath = getCanonicalFileName(filePathIn); const fileContent = filePath === currentLintOperationState.filePath - ? currentLintOperationState.code + ? getCodeText(currentLintOperationState.code) : oldReadFile(filePath, encoding); if (fileContent !== undefined) { parsedFilesSeenHash.set(filePath, createHash(fileContent)); diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 7a0965ae51c..767e4af8e97 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,6 +1,7 @@ import debug from 'debug'; import { sync as globSync } from 'globby'; import isGlob from 'is-glob'; +import type * as ts from 'typescript'; import type { CanonicalPath } from '../create-program/shared'; import { @@ -8,6 +9,7 @@ import { getCanonicalFileName, } from '../create-program/shared'; import type { TSESTreeOptions } from '../parser-options'; +import { isSourceFile } from '../source-files'; import type { MutableParseSettings } from './index'; import { inferSingleRun } from './inferSingleRun'; import { warnAboutTSVersion } from './warnAboutTSVersion'; @@ -17,15 +19,17 @@ const log = debug( ); export function createParseSettings( - code: string, + code: string | ts.SourceFile, options: Partial = {}, ): MutableParseSettings { + const codeFullText = enforceCodeString(code); const tsconfigRootDir = typeof options.tsconfigRootDir === 'string' ? options.tsconfigRootDir : process.cwd(); const parseSettings: MutableParseSettings = { - code: enforceString(code), + code, + codeFullText, comment: options.comment === true, comments: [], DEPRECATED__createDefaultProgram: @@ -127,12 +131,12 @@ export function createParseSettings( /** * Ensures source code is a string. */ -function enforceString(code: unknown): string { - if (typeof code !== 'string') { - return String(code); - } - - return code; +function enforceCodeString(code: unknown): string { + return isSourceFile(code) + ? code.getFullText(code) + : typeof code === 'string' + ? code + : String(code); } /** diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 02479d5bd2b..07e818ae988 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -10,9 +10,14 @@ type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; */ export interface MutableParseSettings { /** - * Code of the file being parsed. + * Code of the file being parsed, or raw source file containing it. */ - code: string; + code: string | ts.SourceFile; + + /** + * Full text of the file being parsed. + */ + codeFullText: string; /** * Whether the `comment` parse option is enabled. diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 478cfa490c9..6e8c01a974c 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -77,7 +77,7 @@ function parse( } function parseWithNodeMapsInternal( - code: string, + code: string | ts.SourceFile, options: T | undefined, shouldPreserveNodeMaps: boolean, ): ParseWithNodeMapsResult { @@ -130,7 +130,7 @@ function clearParseAndGenerateServicesCalls(): void { } function parseAndGenerateServices( - code: string, + code: string | ts.SourceFile, options: T, ): ParseAndGenerateServicesResult { /** diff --git a/packages/typescript-estree/src/source-files.ts b/packages/typescript-estree/src/source-files.ts new file mode 100644 index 00000000000..18cd4567090 --- /dev/null +++ b/packages/typescript-estree/src/source-files.ts @@ -0,0 +1,17 @@ +import * as ts from 'typescript'; + +export function isSourceFile(code: unknown): code is ts.SourceFile { + if (typeof code !== 'object' || code == null) { + return false; + } + + const maybeSourceFile = code as Partial; + return ( + maybeSourceFile.kind === ts.SyntaxKind.SourceFile && + typeof maybeSourceFile.getFullText === 'function' + ); +} + +export function getCodeText(code: string | ts.SourceFile): string { + return isSourceFile(code) ? code.getFullText(code) : code; +} diff --git a/packages/typescript-estree/tests/lib/source-files.test.ts b/packages/typescript-estree/tests/lib/source-files.test.ts new file mode 100644 index 00000000000..e6edb1c9c65 --- /dev/null +++ b/packages/typescript-estree/tests/lib/source-files.test.ts @@ -0,0 +1,37 @@ +import * as ts from 'typescript'; + +import { getCodeText, isSourceFile } from '../../src/source-files'; + +describe('isSourceFile', () => { + it.each([null, undefined, {}, { getFullText: (): string => '', text: '' }])( + `returns false when given %j`, + input => { + expect(isSourceFile(input)).toBe(false); + }, + ); + + it('returns true when given a real source file', () => { + const input = ts.createSourceFile('test.ts', '', ts.ScriptTarget.ESNext); + + expect(isSourceFile(input)).toBe(true); + }); +}); + +describe('getCodeText', () => { + it('returns the code when code is provided as a string', () => { + const code = '// Hello world'; + + expect(getCodeText(code)).toBe(code); + }); + + it('returns the code when code is provided as a source file', () => { + const code = '// Hello world'; + const sourceFile = ts.createSourceFile( + 'test.ts', + code, + ts.ScriptTarget.ESNext, + ); + + expect(getCodeText(sourceFile)).toBe(code); + }); +}); diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index c3e95b321c3..2dc3eaa0d7c 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -2,6 +2,7 @@ import type { ParseSettings } from '@typescript-eslint/typescript-estree/dist/pa export const parseSettings: ParseSettings = { code: '', + codeFullText: '', comment: true, comments: [], DEPRECATED__createDefaultProgram: false,