Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Caching results of parsing Config file and extended file #31101

Merged
merged 4 commits into from Apr 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
114 changes: 74 additions & 40 deletions src/compiler/commandLineParser.ts
Expand Up @@ -1345,7 +1345,12 @@ namespace ts {
/**
* Reads the config file, reports errors if any and exits if the config file cannot be found
*/
export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost): ParsedCommandLine | undefined {
export function getParsedCommandLineOfConfigFile(
configFileName: string,
optionsToExtend: CompilerOptions,
host: ParseConfigFileHost,
extendedConfigCache?: Map<ExtendedConfigCacheEntry>
): ParsedCommandLine | undefined {
let configFileText: string | undefined;
try {
configFileText = host.readFile(configFileName);
Expand All @@ -1366,7 +1371,16 @@ namespace ts {
result.path = toPath(configFileName, cwd, createGetCanonicalFileName(host.useCaseSensitiveFileNames));
result.resolvedPath = result.path;
result.originalFileName = result.fileName;
return parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd));
return parseJsonSourceFileConfigFileContent(
result,
host,
getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd),
optionsToExtend,
getNormalizedAbsolutePath(configFileName, cwd),
/*resolutionStack*/ undefined,
/*extraFileExtension*/ undefined,
extendedConfigCache
);
}

/**
Expand Down Expand Up @@ -1980,8 +1994,8 @@ namespace ts {
* @param basePath A root directory to resolve relative path entries in the config
* file to. e.g. outDir
*/
export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray<FileExtensionInfo>): ParsedCommandLine {
return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions);
export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray<FileExtensionInfo>, /*@internal*/ extendedConfigCache?: Map<ExtendedConfigCacheEntry>): ParsedCommandLine {
return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache);
}

/*@internal*/
Expand Down Expand Up @@ -2020,11 +2034,12 @@ namespace ts {
configFileName?: string,
resolutionStack: Path[] = [],
extraFileExtensions: ReadonlyArray<FileExtensionInfo> = [],
extendedConfigCache?: Map<ExtendedConfigCacheEntry>
): ParsedCommandLine {
Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined));
const errors: Diagnostic[] = [];

const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors);
const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache);
const { raw } = parsedConfig;
const options = extend(existingOptions, parsedConfig.options || {});
options.configFilePath = configFileName && normalizeSlashes(configFileName);
Expand Down Expand Up @@ -2172,7 +2187,7 @@ namespace ts {
return existingErrors !== configParseDiagnostics.length;
}

interface ParsedTsconfig {
export interface ParsedTsconfig {
raw: any;
options?: CompilerOptions;
typeAcquisition?: TypeAcquisition;
Expand All @@ -2191,13 +2206,14 @@ namespace ts {
* It does *not* resolve the included files.
*/
function parseConfig(
json: any,
sourceFile: TsConfigSourceFile | undefined,
host: ParseConfigHost,
basePath: string,
configFileName: string | undefined,
resolutionStack: string[],
errors: Push<Diagnostic>,
json: any,
sourceFile: TsConfigSourceFile | undefined,
host: ParseConfigHost,
basePath: string,
configFileName: string | undefined,
resolutionStack: string[],
errors: Push<Diagnostic>,
extendedConfigCache?: Map<ExtendedConfigCacheEntry>
): ParsedTsconfig {
basePath = normalizeSlashes(basePath);
const resolvedPath = getNormalizedAbsolutePath(configFileName || "", basePath);
Expand All @@ -2214,7 +2230,7 @@ namespace ts {
if (ownConfig.extendedConfigPath) {
// copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios.
resolutionStack = resolutionStack.concat([resolvedPath]);
const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, basePath, resolutionStack, errors);
const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, basePath, resolutionStack, errors, extendedConfigCache);
if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) {
const baseRaw = extendedConfig.raw;
const raw = ownConfig.raw;
Expand Down Expand Up @@ -2358,47 +2374,65 @@ namespace ts {
return undefined;
}

export interface ExtendedConfigCacheEntry {
extendedResult: TsConfigSourceFile;
extendedConfig: ParsedTsconfig | undefined;
}

function getExtendedConfig(
sourceFile: TsConfigSourceFile | undefined,
extendedConfigPath: string,
host: ParseConfigHost,
basePath: string,
resolutionStack: string[],
errors: Push<Diagnostic>,
extendedConfigCache?: Map<ExtendedConfigCacheEntry>
): ParsedTsconfig | undefined {
const extendedResult = readJsonConfigFile(extendedConfigPath, path => host.readFile(path));
const path = host.useCaseSensitiveFileNames ? extendedConfigPath : toLowerCase(extendedConfigPath);
let value: ExtendedConfigCacheEntry | undefined;
let extendedResult: TsConfigSourceFile;
let extendedConfig: ParsedTsconfig | undefined;
if (extendedConfigCache && (value = extendedConfigCache.get(path))) {
({ extendedResult, extendedConfig } = value);
}
else {
extendedResult = readJsonConfigFile(extendedConfigPath, path => host.readFile(path));
if (!extendedResult.parseDiagnostics.length) {
const extendedDirname = getDirectoryPath(extendedConfigPath);
extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname,
getBaseFileName(extendedConfigPath), resolutionStack, errors, extendedConfigCache);

if (isSuccessfulParsedTsconfig(extendedConfig)) {
// Update the paths to reflect base path
const relativeDifference = convertToRelativePath(extendedDirname, basePath, identity);
const updatePath = (path: string) => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
const mapPropertiesInRawIfNotUndefined = (propertyName: string) => {
if (raw[propertyName]) {
raw[propertyName] = map(raw[propertyName], updatePath);
}
};

const { raw } = extendedConfig;
mapPropertiesInRawIfNotUndefined("include");
mapPropertiesInRawIfNotUndefined("exclude");
mapPropertiesInRawIfNotUndefined("files");
}
}
if (extendedConfigCache) {
extendedConfigCache.set(path, { extendedResult, extendedConfig });
}
}
if (sourceFile) {
sourceFile.extendedSourceFiles = [extendedResult.fileName];
if (extendedResult.extendedSourceFiles) {
sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles);
}
}
if (extendedResult.parseDiagnostics.length) {
errors.push(...extendedResult.parseDiagnostics);
return undefined;
}

const extendedDirname = getDirectoryPath(extendedConfigPath);
const extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname,
getBaseFileName(extendedConfigPath), resolutionStack, errors);
if (sourceFile && extendedResult.extendedSourceFiles) {
sourceFile.extendedSourceFiles!.push(...extendedResult.extendedSourceFiles);
}

if (isSuccessfulParsedTsconfig(extendedConfig)) {
// Update the paths to reflect base path
const relativeDifference = convertToRelativePath(extendedDirname, basePath, identity);
const updatePath = (path: string) => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path);
const mapPropertiesInRawIfNotUndefined = (propertyName: string) => {
if (raw[propertyName]) {
raw[propertyName] = map(raw[propertyName], updatePath);
}
};

const { raw } = extendedConfig;
mapPropertiesInRawIfNotUndefined("include");
mapPropertiesInRawIfNotUndefined("exclude");
mapPropertiesInRawIfNotUndefined("files");
}

return extendedConfig;
return extendedConfig!;
}

function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Push<Diagnostic>): boolean {
Expand Down
36 changes: 25 additions & 11 deletions src/compiler/program.ts
Expand Up @@ -2678,18 +2678,32 @@ namespace ts {
return fromCache || undefined;
}

// An absolute path pointing to the containing directory of the config file
const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory());
const sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined;
addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined);
if (sourceFile === undefined) {
projectReferenceRedirects.set(sourceFilePath, false);
return undefined;
let commandLine: ParsedCommandLine | undefined;
let sourceFile: JsonSourceFile | undefined;
if (host.getParsedCommandLine) {
commandLine = host.getParsedCommandLine(refPath);
if (!commandLine) {
addFileToFilesByName(/*sourceFile*/ undefined, sourceFilePath, /*redirectedPath*/ undefined);
projectReferenceRedirects.set(sourceFilePath, false);
return undefined;
}
sourceFile = Debug.assertDefined(commandLine.options.configFile);
addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined);
}
else {
// An absolute path pointing to the containing directory of the config file
const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory());
sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined;
addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined);
if (sourceFile === undefined) {
projectReferenceRedirects.set(sourceFilePath, false);
return undefined;
}
sourceFile.path = sourceFilePath;
sourceFile.resolvedPath = sourceFilePath;
sourceFile.originalFileName = refPath;
commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath);
}
sourceFile.path = sourceFilePath;
sourceFile.resolvedPath = sourceFilePath;
sourceFile.originalFileName = refPath;
const commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath);
const resolvedRef: ResolvedProjectReference = { commandLine, sourceFile };
projectReferenceRedirects.set(sourceFilePath, resolvedRef);
if (commandLine.projectReferences) {
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/tsbuild.ts
Expand Up @@ -399,12 +399,14 @@ namespace ts {
let projectCompilerOptions = baseCompilerOptions;
const compilerHost = createCompilerHostFromProgramHost(host, () => projectCompilerOptions);
setGetSourceFileAsHashVersioned(compilerHost, host);
compilerHost.getParsedCommandLine = parseConfigFile;

compilerHost.resolveModuleNames = maybeBind(host, host.resolveModuleNames);
compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives);
let moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined;

const buildInfoChecked = createFileMap<true>(toPath);
let extendedConfigCache: Map<ExtendedConfigCacheEntry> | undefined;

// Watch state
const builderPrograms = createFileMap<T>(toPath);
Expand Down Expand Up @@ -481,7 +483,7 @@ namespace ts {

let diagnostic: Diagnostic | undefined;
parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d;
const parsed = getParsedCommandLineOfConfigFile(configFilePath, baseCompilerOptions, parseConfigFileHost);
const parsed = getParsedCommandLineOfConfigFile(configFilePath, baseCompilerOptions, parseConfigFileHost, extendedConfigCache);
parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop;
configFileCache.setValue(configFilePath, parsed || diagnostic!);
return parsed;
Expand Down Expand Up @@ -1395,6 +1397,7 @@ namespace ts {
} = changeCompilerHostLikeToUseCache(host, toPath, (...args) => savedGetSourceFile.call(compilerHost, ...args));
readFileWithCache = newReadFileWithCache;
compilerHost.getSourceFile = getSourceFileWithCache!;
extendedConfigCache = createMap();

const originalResolveModuleNames = compilerHost.resolveModuleNames;
if (!compilerHost.resolveModuleNames) {
Expand Down Expand Up @@ -1463,6 +1466,7 @@ namespace ts {
host.writeFile = originalWriteFile;
compilerHost.getSourceFile = savedGetSourceFile;
readFileWithCache = savedReadFileWithCache;
extendedConfigCache = undefined;
compilerHost.resolveModuleNames = originalResolveModuleNames;
moduleResolutionCache = undefined;
return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success;
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Expand Up @@ -5131,6 +5131,7 @@ namespace ts {
/* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution;
/* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean;
createHash?(data: string): string;
getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;

// TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesnt use compilerHost as base
/*@internal*/createDirectory?(directory: string): void;
Expand Down
18 changes: 16 additions & 2 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Expand Up @@ -2761,6 +2761,7 @@ declare namespace ts {
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[];
getEnvironmentVariable?(name: string): string | undefined;
createHash?(data: string): string;
getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;
}
interface SourceMapRange extends TextRange {
source?: SourceMapSource;
Expand Down Expand Up @@ -3632,7 +3633,7 @@ declare namespace ts {
/**
* Reads the config file, reports errors if any and exits if the config file cannot be found
*/
function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost): ParsedCommandLine | undefined;
function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map<ExtendedConfigCacheEntry>): ParsedCommandLine | undefined;
/**
* Read tsconfig.json file
* @param fileName The path to the config file
Expand Down Expand Up @@ -3674,7 +3675,20 @@ declare namespace ts {
* @param basePath A root directory to resolve relative path entries in the config
* file to. e.g. outDir
*/
function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray<FileExtensionInfo>): ParsedCommandLine;
function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray<FileExtensionInfo>, /*@internal*/ extendedConfigCache?: Map<ExtendedConfigCacheEntry>): ParsedCommandLine;
interface ParsedTsconfig {
raw: any;
options?: CompilerOptions;
typeAcquisition?: TypeAcquisition;
/**
* Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet
*/
extendedConfigPath?: string;
}
interface ExtendedConfigCacheEntry {
extendedResult: TsConfigSourceFile;
extendedConfig: ParsedTsconfig | undefined;
}
function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): {
options: CompilerOptions;
errors: Diagnostic[];
Expand Down