From 3ffa245f07c4472ed3015fcb59ea0d2d73432102 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Wed, 26 May 2021 09:40:42 -0700 Subject: [PATCH] Cache parsed path mapping patterns (#44078) * Cache parsed path mapping patterns If a project has many of them (e.g. 1800), parsing the patterns repeatedly can take up a lot of time. * Move cache to ConfigFileSpecs * Inline constants * Simplify cache access --- src/compiler/binder.ts | 10 +++---- src/compiler/commandLineParser.ts | 1 + src/compiler/moduleNameResolver.ts | 14 +++++----- src/compiler/types.ts | 1 + src/compiler/utilities.ts | 43 ++++++++++++++++++------------ src/services/stringCompletions.ts | 4 +-- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 02432a24794..78bca28c26b 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1977,19 +1977,17 @@ namespace ts { declareModuleSymbol(node); } else { - let pattern: Pattern | undefined; + let pattern: string | Pattern | undefined; if (node.name.kind === SyntaxKind.StringLiteral) { const { text } = node.name; - if (hasZeroOrOneAsteriskCharacter(text)) { - pattern = tryParsePattern(text); - } - else { + pattern = tryParsePattern(text); + if (pattern === undefined) { errorOnFirstToken(node.name, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text); } } const symbol = declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes)!; - file.patternAmbientModules = append(file.patternAmbientModules, pattern && { pattern, symbol }); + file.patternAmbientModules = append(file.patternAmbientModules, pattern && !isString(pattern) ? { pattern, symbol } : undefined); } } else { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 4fec5cd18ca..eedac508368 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2533,6 +2533,7 @@ namespace ts { validatedFilesSpec: filter(filesSpecs, isString), validatedIncludeSpecs, validatedExcludeSpecs, + pathPatterns: undefined, // Initialized on first use }; } diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 22577901f20..897aafa2611 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -952,7 +952,7 @@ namespace ts { } function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) { - const { baseUrl, paths } = state.compilerOptions; + const { baseUrl, paths, configFile } = state.compilerOptions; if (paths && !pathIsRelative(moduleName)) { if (state.traceEnabled) { if (baseUrl) { @@ -961,7 +961,8 @@ namespace ts { trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName); } const baseDirectory = getPathsBasePath(state.compilerOptions, state.host)!; // Always defined when 'paths' is defined - return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, loader, /*onlyRecordFailures*/ false, state); + const pathPatterns = configFile?.configFileSpecs ? configFile.configFileSpecs.pathPatterns ||= tryParsePatterns(paths) : undefined; + return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, pathPatterns, loader, /*onlyRecordFailures*/ false, state); } } @@ -1400,7 +1401,7 @@ namespace ts { if (state.traceEnabled) { trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.version, version, moduleName); } - const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state); + const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, /*pathPatterns*/ undefined, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state); if (result) { return removeIgnoredPackageId(result.value); } @@ -1536,7 +1537,7 @@ namespace ts { trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, packageInfo.versionPaths.version, version, rest); } const packageDirectoryExists = nodeModulesDirectoryExists && directoryProbablyExists(packageDirectory, state.host); - const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, loader, !packageDirectoryExists, state); + const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, /*pathPatterns*/ undefined, loader, !packageDirectoryExists, state); if (fromPaths) { return fromPaths.value; } @@ -1546,8 +1547,9 @@ namespace ts { return loader(extensions, candidate, !nodeModulesDirectoryExists, state); } - function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult { - const matchedPattern = matchPatternOrExact(getOwnKeys(paths), moduleName); + function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike, pathPatterns: readonly (string | Pattern)[] | undefined, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult { + pathPatterns ||= tryParsePatterns(paths); + const matchedPattern = matchPatternOrExact(pathPatterns, moduleName); if (matchedPattern) { const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName); const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f395014baad..9a5cf354125 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6195,6 +6195,7 @@ namespace ts { validatedFilesSpec: readonly string[] | undefined; validatedIncludeSpecs: readonly string[] | undefined; validatedExcludeSpecs: readonly string[] | undefined; + pathPatterns: readonly (string | Pattern)[] | undefined; } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 8271e6adfc2..7a012114524 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6769,14 +6769,25 @@ namespace ts { return changeAnyExtension(path, newExtension, extensionsToRemove, /*ignoreCase*/ false) as T; } - export function tryParsePattern(pattern: string): Pattern | undefined { - // This should be verified outside of here and a proper error thrown. - Debug.assert(hasZeroOrOneAsteriskCharacter(pattern)); + /** + * Returns the input if there are no stars, a pattern if there is exactly one, + * and undefined if there are more. + */ + export function tryParsePattern(pattern: string): string | Pattern | undefined { const indexOfStar = pattern.indexOf("*"); - return indexOfStar === -1 ? undefined : { - prefix: pattern.substr(0, indexOfStar), - suffix: pattern.substr(indexOfStar + 1) - }; + if (indexOfStar === -1) { + return pattern; + } + return pattern.indexOf("*", indexOfStar + 1) !== -1 + ? undefined + : { + prefix: pattern.substr(0, indexOfStar), + suffix: pattern.substr(indexOfStar + 1) + }; + } + + export function tryParsePatterns(paths: MapLike): (string | Pattern)[] { + return mapDefined(getOwnKeys(paths), path => tryParsePattern(path)); } export function positionIsSynthesized(pos: number): boolean { @@ -6822,21 +6833,19 @@ namespace ts { /** - * patternStrings contains both pattern strings (containing "*") and regular strings. + * patternOrStrings contains both patterns (containing "*") and regular strings. * Return an exact match if possible, or a pattern match, or undefined. * (These are verified by verifyCompilerOptions to have 0 or 1 "*" characters.) */ - export function matchPatternOrExact(patternStrings: readonly string[], candidate: string): string | Pattern | undefined { + export function matchPatternOrExact(patternOrStrings: readonly (string | Pattern)[], candidate: string): string | Pattern | undefined { const patterns: Pattern[] = []; - for (const patternString of patternStrings) { - if (!hasZeroOrOneAsteriskCharacter(patternString)) continue; - const pattern = tryParsePattern(patternString); - if (pattern) { - patterns.push(pattern); + for (const patternOrString of patternOrStrings) { + if (patternOrString === candidate) { + return candidate; } - else if (patternString === candidate) { - // pattern was matched as is - no need to search further - return patternString; + + if (!isString(patternOrString)) { + patterns.push(patternOrString); } } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index 27218f3cfd1..92d205ed8bc 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -553,8 +553,8 @@ namespace ts.Completions.StringCompletions { return undefined; } - const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined; - if (!parsed) { + const parsed = tryParsePattern(pattern); + if (parsed === undefined || isString(parsed)) { return undefined; }