Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix(typescript-estree): adds support for project services using extended config files#9306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
22 commits
Select commitHold shift + click to select a range
69aa3c9
refactor(typescript-estree): refactors default project config and add…
higherorderfunctorJun 8, 2024
d32c1e5
test(typescript-estree): fix original failing unit tests
higherorderfunctorJun 8, 2024
148a9d0
chore(typescript-estree): cleans up PR submission
higherorderfunctorJun 8, 2024
8bc4b40
chore: merge remote-tracking branch 'upstream/v8' into fix/9205-proje…
higherorderfunctorJun 8, 2024
b6718a1
chore(typescript-estree): fixes test names to use the new function name
higherorderfunctorJun 8, 2024
bde1a26
test(typescript-estree): adds additional tests for coverage
higherorderfunctorJun 8, 2024
3315345
test(typescript-estree): fixes test for windows
higherorderfunctorJun 8, 2024
2859ef9
test(typescript-estree): fixes test name typo
higherorderfunctorJun 8, 2024
f01fe0e
chore(typescript-estree): minor cleanup to comments and variables
higherorderfunctorJun 9, 2024
73e2c78
style(typescript-estree): unifies how compiler options are defined in…
higherorderfunctorJun 9, 2024
4b8c010
chore(typescript-estree): updates create-program/getParsedConfigFile.…
higherorderfunctorJun 26, 2024
4f0e2de
chore: merge upstream/v8
higherorderfunctorJul 19, 2024
abd0095
test(typescript-estree): adds initial getParsedConfigFile tests
higherorderfunctorJul 23, 2024
c563b8d
test(typescript-estree): updates createProjectService tests
higherorderfunctorJul 24, 2024
4d043be
chore: merge upstream/v8
higherorderfunctorJul 24, 2024
c7181b9
test(typescript-estree): cleans up PR
higherorderfunctorJul 24, 2024
6d23cb1
Merge remote-tracking branch 'upstream/v8' into fix/9205-project-serv…
higherorderfunctorJul 24, 2024
45551dc
test(typescript-estree): fixes snapshot tests
higherorderfunctorJul 24, 2024
d5f06c5
style(typescript-estree): applies suggestions from code review
higherorderfunctorJul 24, 2024
1dd15bf
style(typescript-estree): applies manual suggestions from code review
higherorderfunctorJul 25, 2024
ac8c9bc
fixes(typescript-estree): fixes linting errors
higherorderfunctorJul 25, 2024
4478aea
Update packages/typescript-estree/src/create-program/createProjectSer…
JoshuaKGoldbergJul 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/
importos from 'node:os';
importpath from 'node:path';

import debug from 'debug';
import type * as ts from 'typescript/lib/tsserverlibrary';

import type { ProjectServiceOptions } from '../parser-options';
import { getParsedConfigFile } from './getParsedConfigFile';
import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForFilesGlob';

const DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8;
Expand DownExpand Up@@ -123,38 +124,26 @@ export function createProjectService(

if (options.defaultProject) {
log('Enabling default project: %s', options.defaultProject);
letconfigRead;
letconfigFile: ts.ParsedCommandLine;

try {
configRead = tsserver.readConfigFile(
configFile = getParsedConfigFile(
tsserver,
options.defaultProject,
system.readFile,
path.dirname(options.defaultProject),
);
} catch (error) {
throw new Error(
`Could not parse default project '${options.defaultProject}': ${(error as Error).message}`,
);
}

if (configRead.error) {
throw new Error(
`Could not read default project '${options.defaultProject}': ${tsserver.formatDiagnostic(
configRead.error,
{
getCurrentDirectory: system.getCurrentDirectory,
getCanonicalFileName: fileName => fileName,
getNewLine: () => os.EOL,
},
)}`,
`Could not read default project '${options.defaultProject}': ${(error as Error).message}`,
);
}

service.setCompilerOptionsForInferredProjects(
(
configRead.config as {
compilerOptions: ts.server.protocol.InferredProjectCompilerOptions;
}
).compilerOptions,
// NOTE: The inferred projects API is not intended for source files when a tsconfig
// exists. There is no API that generates an InferredProjectCompilerOptions suggesting
// it is meant for hard coded options passed in. Hard asserting as a work around.
// See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904
configFile.options as ts.server.protocol.InferredProjectCompilerOptions,
);
}

Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
import * as fs from 'fs';
import * as path from 'path';
import type * as ts from 'typescript/lib/tsserverlibrary';

import { CORE_COMPILER_OPTIONS } from './shared';

/**
* Utility offered by parser to help consumers parse a config file.
*
* @param configFile the path to the tsconfig.json file, relative to `projectDirectory`
* @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()`
*/
function getParsedConfigFile(
tsserver: typeof ts,
configFile: string,
projectDirectory?: string,
): ts.ParsedCommandLine {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (tsserver.sys === undefined) {
throw new Error(
'`getParsedConfigFile` is only supported in a Node-like environment.',
);
}

const parsed = tsserver.getParsedCommandLineOfConfigFile(
configFile,
CORE_COMPILER_OPTIONS,
{
onUnRecoverableConfigFileDiagnostic: diag => {
throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined.
},
fileExists: fs.existsSync,
getCurrentDirectory,
readDirectory: tsserver.sys.readDirectory,
readFile: file => fs.readFileSync(file, 'utf-8'),
useCaseSensitiveFileNames: tsserver.sys.useCaseSensitiveFileNames,
},
);

if (parsed?.errors.length) {
throw new Error(formatDiagnostics(parsed.errors));
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return parsed!;

function getCurrentDirectory(): string {
return projectDirectory ? path.resolve(projectDirectory) : process.cwd();
}

function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined {
return tsserver.formatDiagnostics(diagnostics, {
getCanonicalFileName: f => f,
getCurrentDirectory,
getNewLine: () => '\n',
});
}
}

export { getParsedConfigFile };
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
import debug from 'debug';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';

import type { ParseSettings } from '../parseSettings';
import { getParsedConfigFile } from './getParsedConfigFile';
import type { ASTAndDefiniteProgram } from './shared';
import {CORE_COMPILER_OPTIONS,getAstFromProgram } from './shared';
import { getAstFromProgram } from './shared';

const log = debug('typescript-eslint:typescript-estree:useProvidedProgram');

Expand DownExpand Up@@ -60,44 +60,9 @@ function createProgramFromConfigFile(
configFile: string,
projectDirectory?: string,
): ts.Program {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ts.sys === undefined) {
throw new Error(
'`createProgramFromConfigFile` is only supported in a Node-like environment.',
);
}

const parsed = ts.getParsedCommandLineOfConfigFile(
configFile,
CORE_COMPILER_OPTIONS,
{
onUnRecoverableConfigFileDiagnostic: diag => {
throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined.
},
fileExists: fs.existsSync,
getCurrentDirectory: () =>
(projectDirectory && path.resolve(projectDirectory)) || process.cwd(),
readDirectory: ts.sys.readDirectory,
readFile: file => fs.readFileSync(file, 'utf-8'),
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
},
);
// parsed is not undefined, since we throw on failure.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = parsed!;
if (result.errors.length) {
throw new Error(formatDiagnostics(result.errors));
}
const host = ts.createCompilerHost(result.options, true);
return ts.createProgram(result.fileNames, result.options, host);
}

function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined {
return ts.formatDiagnostics(diagnostics, {
getCanonicalFileName: f => f,
getCurrentDirectory: process.cwd,
getNewLine: () => '\n',
});
const parsed = getParsedConfigFile(ts, configFile, projectDirectory);
const host = ts.createCompilerHost(parsed.options, true);
return ts.createProgram(parsed.fileNames, parsed.options, host);
}

export { useProvidedPrograms, createProgramFromConfigFile };
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
import debug from 'debug';
import * as ts from 'typescript';

import { createProjectService } from '../../src/create-program/createProjectService';

const mockReadConfigFile = jest.fn();
const mockGetParsedConfigFile = jest.fn();
const mockSetCompilerOptionsForInferredProjects = jest.fn();
const mockSetHostConfiguration = jest.fn();

jest.mock('../../src/create-program/getParsedConfigFile', () => ({
getParsedConfigFile: mockGetParsedConfigFile,
}));

jest.mock('typescript/lib/tsserverlibrary', () => ({
...jest.requireActual('typescript/lib/tsserverlibrary'),
readConfigFile: mockReadConfigFile,
server: {
...jest.requireActual('typescript/lib/tsserverlibrary').server,
ProjectService: class {
Expand All@@ -33,6 +34,11 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({
},
}));

const {
createProjectService,
// eslint-disable-next-line @typescript-eslint/no-require-imports
} = require('../../src/create-program/createProjectService');

describe('createProjectService', () => {
afterEach(() => {
jest.resetAllMocks();
Expand All@@ -51,18 +57,9 @@ describe('createProjectService', () => {
expect(settings.allowDefaultProject).toBeUndefined();
});

it('throws an error when options.defaultProject is set and readConfigFile returns an error', () => {
mockReadConfigFile.mockReturnValue({
error: {
category: ts.DiagnosticCategory.Error,
code: 1234,
file: ts.createSourceFile('./tsconfig.json', '', {
languageVersion: ts.ScriptTarget.Latest,
}),
start: 0,
length: 0,
messageText: 'Oh no!',
} satisfies ts.Diagnostic,
it('throws an error when options.defaultProject is set and getParsedConfigFile throws a diagnostic error', () => {
mockGetParsedConfigFile.mockImplementation(() => {
throw new Error('./tsconfig.json(1,1): error TS1234: Oh no!');
});

expect(() =>
Expand All@@ -78,9 +75,11 @@ describe('createProjectService', () => {
);
});

it('throws an error when options.defaultProject is set and readConfigFile throws an error', () => {
mockReadConfigFile.mockImplementation(() => {
throw new Error('Oh no!');
it('throws an error when options.defaultProject is set and getParsedConfigFile throws an environment error', () => {
mockGetParsedConfigFile.mockImplementation(() => {
throw new Error(
'`getParsedConfigFile` is only supported in a Node-like environment.',
);
});

expect(() =>
Expand All@@ -91,12 +90,14 @@ describe('createProjectService', () => {
},
undefined,
),
).toThrow("Could not parse default project './tsconfig.json': Oh no!");
).toThrow(
"Could not read default project './tsconfig.json': `getParsedConfigFile` is only supported in a Node-like environment.",
);
});

it('uses the default projects compiler options when options.defaultProject is set andreadConfigFile succeeds', () => {
it('uses the default projects compiler options when options.defaultProject is set andgetParsedConfigFile succeeds', () => {
const compilerOptions = { strict: true };
mockReadConfigFile.mockReturnValue({config: {compilerOptions } });
mockGetParsedConfigFile.mockReturnValue({options:compilerOptions });

const { service } = createProjectService(
{
Expand Down
112 changes: 112 additions & 0 deletionspackages/typescript-estree/tests/lib/getParsedConfigFile.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
import path from 'node:path';

import * as ts from 'typescript';

import { getParsedConfigFile } from '../../src/create-program/getParsedConfigFile';

const mockGetParsedCommandLineOfConfigFile = jest.fn();

const mockTsserver: typeof ts = {
sys: {} as ts.System,
formatDiagnostics: ts.formatDiagnostics,
getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;

describe('getParsedConfigFile', () => {
afterEach(() => {
jest.resetAllMocks();
});

it('throws an error when tsserver.sys is undefined', () => {
expect(() =>
getParsedConfigFile({} as typeof ts, './tsconfig.json'),
).toThrow(
'`getParsedConfigFile` is only supported in a Node-like environment.',
);
});

it('uses the cwd as the default project directory', () => {
getParsedConfigFile(mockTsserver, './tsconfig.json');
expect(mockGetParsedCommandLineOfConfigFile).toHaveBeenCalledTimes(1);
const [_configFileName, _optionsToExtend, host] =
mockGetParsedCommandLineOfConfigFile.mock.calls[0];
expect(host.getCurrentDirectory()).toBe(process.cwd());
});

it('resolves a relative project directory when passed', () => {
getParsedConfigFile(
mockTsserver,
'./tsconfig.json',
path.relative('./', path.dirname(__filename)),
);
expect(mockGetParsedCommandLineOfConfigFile).toHaveBeenCalledTimes(1);
const [_configFileName, _optionsToExtend, host] =
mockGetParsedCommandLineOfConfigFile.mock.calls[0];
expect(host.getCurrentDirectory()).toBe(path.dirname(__filename));
});

it('resolves an absolute project directory when passed', () => {
getParsedConfigFile(mockTsserver, './tsconfig.json', __dirname);
expect(mockGetParsedCommandLineOfConfigFile).toHaveBeenCalledTimes(1);
const [_configFileName, _optionsToExtend, host] =
mockGetParsedCommandLineOfConfigFile.mock.calls[0];
expect(host.getCurrentDirectory()).toBe(__dirname);
});

it('throws a diagnostic error when getParsedCommandLineOfConfigFile returns an error', () => {
mockGetParsedCommandLineOfConfigFile.mockReturnValue({
errors: [
{
category: ts.DiagnosticCategory.Error,
code: 1234,
file: ts.createSourceFile('./tsconfig.json', '', {
languageVersion: ts.ScriptTarget.Latest,
}),
start: 0,
length: 0,
messageText: 'Oh no!',
},
] satisfies ts.Diagnostic[],
});
expect(() => getParsedConfigFile(mockTsserver, './tsconfig.json')).toThrow(
/.+ error TS1234: Oh no!/,
);
});

it('throws a diagnostic error when getParsedCommandLineOfConfigFile throws an error', () => {
mockGetParsedCommandLineOfConfigFile.mockImplementation(
(
...[_configFileName, _optionsToExtend, host]: Parameters<
typeof ts.getParsedCommandLineOfConfigFile
>
) => {
return host.onUnRecoverableConfigFileDiagnostic({
category: ts.DiagnosticCategory.Error,
code: 1234,
file: ts.createSourceFile('./tsconfig.json', '', {
languageVersion: ts.ScriptTarget.Latest,
}),
start: 0,
length: 0,
messageText: 'Oh no!',
} satisfies ts.Diagnostic);
},
);
expect(() => getParsedConfigFile(mockTsserver, './tsconfig.json')).toThrow(
/.+ error TS1234: Oh no!/,
);
});

it('uses compiler options when parsing a config file succeeds', () => {
const parsedConfigFile = {
options: { strict: true },
raw: { compilerOptions: { strict: true } },
errors: [],
};
mockGetParsedCommandLineOfConfigFile.mockReturnValue(parsedConfigFile);
expect(getParsedConfigFile(mockTsserver, 'tsconfig.json')).toEqual(
expect.objectContaining(parsedConfigFile),
);
});
});

[8]ページ先頭

©2009-2025 Movatter.jp