Skip to content

Commit

Permalink
Optimize path mapping lookups (#59048)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielRosenwasser authored Aug 29, 2024
1 parent 497316f commit e6f3baf
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 22 deletions.
1 change: 0 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3071,7 +3071,6 @@ function parseJsonConfigFileContentWorker(
validatedFilesSpecBeforeSubstitution,
validatedIncludeSpecsBeforeSubstitution,
validatedExcludeSpecsBeforeSubstitution,
pathPatterns: undefined, // Initialized on first use
isDefaultIncludeSpec,
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2414,7 +2414,7 @@ export function findBestPatternMatch<T>(values: readonly T[], getPattern: (value
for (let i = 0; i < values.length; i++) {
const v = values[i];
const pattern = getPattern(v);
if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) {
if (pattern.prefix.length > longestMatchPrefixLength && isPatternMatch(pattern, candidate)) {
longestMatchPrefixLength = pattern.prefix.length;
matchedValue = v;
}
Expand Down
15 changes: 8 additions & 7 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ import {
normalizeSlashes,
PackageId,
packageIdToString,
ParsedPatterns,
Path,
pathIsRelative,
Pattern,
patternText,
readJson,
removeExtension,
Expand Down Expand Up @@ -1558,7 +1558,7 @@ function tryLoadModuleUsingOptionalResolutionSettings(extensions: Extensions, mo
}

function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) {
const { baseUrl, paths, configFile } = state.compilerOptions;
const { baseUrl, paths } = state.compilerOptions;
if (paths && !pathIsRelative(moduleName)) {
if (state.traceEnabled) {
if (baseUrl) {
Expand All @@ -1567,7 +1567,7 @@ function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: s
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
const pathPatterns = configFile?.configFileSpecs ? configFile.configFileSpecs.pathPatterns ||= tryParsePatterns(paths) : undefined;
const pathPatterns = tryParsePatterns(paths);
return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, pathPatterns, loader, /*onlyRecordFailures*/ false, state);
}
}
Expand Down Expand Up @@ -2518,7 +2518,8 @@ function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: st
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, /*pathPatterns*/ undefined, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state);
const pathPatterns = tryParsePatterns(versionPaths.paths);
const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, pathPatterns, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state);
if (result) {
return removeIgnoredPackageId(result.value);
}
Expand Down Expand Up @@ -3112,16 +3113,16 @@ function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, modu
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, rest);
}
const packageDirectoryExists = nodeModulesDirectoryExists && directoryProbablyExists(packageDirectory, state.host);
const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, versionPaths.paths, /*pathPatterns*/ undefined, loader, !packageDirectoryExists, state);
const pathPatterns = tryParsePatterns(versionPaths.paths);
const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, versionPaths.paths, pathPatterns, loader, !packageDirectoryExists, state);
if (fromPaths) {
return fromPaths.value;
}
}
return loader(extensions, candidate, !nodeModulesDirectoryExists, state);
}

function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike<string[]>, pathPatterns: readonly (string | Pattern)[] | undefined, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult<Resolved> {
pathPatterns ||= tryParsePatterns(paths);
function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike<string[]>, pathPatterns: ParsedPatterns, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult<Resolved> {
const matchedPattern = matchPatternOrExact(pathPatterns, moduleName);
if (matchedPattern) {
const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName);
Expand Down
1 change: 0 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7628,7 +7628,6 @@ export interface ConfigFileSpecs {
validatedFilesSpecBeforeSubstitution: readonly string[] | undefined;
validatedIncludeSpecsBeforeSubstitution: readonly string[] | undefined;
validatedExcludeSpecsBeforeSubstitution: readonly string[] | undefined;
pathPatterns: readonly (string | Pattern)[] | undefined;
isDefaultIncludeSpec: boolean;
}

Expand Down
66 changes: 54 additions & 12 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9972,8 +9972,51 @@ export function tryParsePattern(pattern: string): string | Pattern | undefined {
}

/** @internal */
export function tryParsePatterns(paths: MapLike<string[]>): (string | Pattern)[] {
return mapDefined(getOwnKeys(paths), path => tryParsePattern(path));
export interface ParsedPatterns {
matchableStringSet: ReadonlySet<string> | undefined;
patterns: (readonly Pattern[]) | undefined;
}

const parsedPatternsCache = new WeakMap<MapLike<string[]>, ParsedPatterns>();

/**
* Divides patterns into a set of exact specifiers and patterns.
* NOTE that this function caches the result based on object identity.
*
* @internal
*/
export function tryParsePatterns(paths: MapLike<string[]>): ParsedPatterns {
let result = parsedPatternsCache.get(paths);
if (result !== undefined) {
return result;
}

let matchableStringSet: Set<string> | undefined;
let patterns: Pattern[] | undefined;

const pathList = getOwnKeys(paths);
for (const path of pathList) {
const patternOrStr = tryParsePattern(path);
if (patternOrStr === undefined) {
continue;
}
else if (typeof patternOrStr === "string") {
(matchableStringSet ??= new Set()).add(patternOrStr);
}
else {
(patterns ??= []).push(patternOrStr);
}
}

parsedPatternsCache.set(
paths,
result = {
matchableStringSet,
patterns,
},
);

return result;
}

/** @internal */
Expand Down Expand Up @@ -10030,22 +10073,21 @@ export const emptyFileSystemEntries: FileSystemEntries = {
};

/**
* patternOrStrings contains both patterns (containing "*") and regular strings.
* `parsedPatterns` 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.)
*
* @internal
*/
export function matchPatternOrExact(patternOrStrings: readonly (string | Pattern)[], candidate: string): string | Pattern | undefined {
const patterns: Pattern[] = [];
for (const patternOrString of patternOrStrings) {
if (patternOrString === candidate) {
return candidate;
}
export function matchPatternOrExact(parsedPatterns: ParsedPatterns, candidate: string): string | Pattern | undefined {
const { matchableStringSet, patterns } = parsedPatterns;

if (!isString(patternOrString)) {
patterns.push(patternOrString);
}
if (matchableStringSet?.has(candidate)) {
return candidate;
}

if (patterns === undefined || patterns.length === 0) {
return undefined;
}

return findBestPatternMatch(patterns, _ => _, candidate);
Expand Down

0 comments on commit e6f3baf

Please sign in to comment.