Skip to content

Commit

Permalink
feat(typescript-estree): allow providing code as a ts.SourceFile (#5892)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg authored and bradzacher committed Nov 23, 2022
1 parent bda806d commit af41b7f
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 29 deletions.
8 changes: 4 additions & 4 deletions packages/parser/src/parser.ts
Expand Up @@ -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');
Expand All @@ -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());
Expand Down Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript-estree/src/ast-converter.ts
Expand Up @@ -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();
Expand Down
Expand Up @@ -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(
Expand Down
Expand Up @@ -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),
Expand Down
17 changes: 10 additions & 7 deletions packages/typescript-estree/src/create-program/createSourceFile.ts
Expand Up @@ -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');
Expand All @@ -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 };
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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));
Expand Down
@@ -1,13 +1,15 @@
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 {
ensureAbsolutePath,
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';
Expand All @@ -17,15 +19,17 @@ const log = debug(
);

export function createParseSettings(
code: string,
code: string | ts.SourceFile,
options: Partial<TSESTreeOptions> = {},
): 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:
Expand Down Expand Up @@ -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);
}

/**
Expand Down
9 changes: 7 additions & 2 deletions packages/typescript-estree/src/parseSettings/index.ts
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript-estree/src/parser.ts
Expand Up @@ -77,7 +77,7 @@ function parse<T extends TSESTreeOptions = TSESTreeOptions>(
}

function parseWithNodeMapsInternal<T extends TSESTreeOptions = TSESTreeOptions>(
code: string,
code: string | ts.SourceFile,
options: T | undefined,
shouldPreserveNodeMaps: boolean,
): ParseWithNodeMapsResult<T> {
Expand Down Expand Up @@ -130,7 +130,7 @@ function clearParseAndGenerateServicesCalls(): void {
}

function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
code: string,
code: string | ts.SourceFile,
options: T,
): ParseAndGenerateServicesResult<T> {
/**
Expand Down
17 changes: 17 additions & 0 deletions 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<ts.SourceFile>;
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;
}
37 changes: 37 additions & 0 deletions 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);
});
});
1 change: 1 addition & 0 deletions packages/website/src/components/linter/config.ts
Expand Up @@ -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,
Expand Down

0 comments on commit af41b7f

Please sign in to comment.