diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a619238c90fdc..4bc985e02d950 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -120,6 +120,7 @@ import { createModuleNotFoundChain, createMultiMap, createNameResolver, + createPrefixSuffixTrie, createPrinterWithDefaults, createPrinterWithRemoveComments, createPrinterWithRemoveCommentsNeverAsciiEscape, @@ -17669,13 +17670,55 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function removeStringLiteralsMatchedByTemplateLiterals(types: Type[]) { - const templates = filter(types, isPatternLiteralType) as (TemplateLiteralType | StringMappingType)[]; - if (templates.length) { + let patterns = filter(types, isPatternLiteralType) as (TemplateLiteralType | StringMappingType)[]; + const templateLiterals = filter(patterns, t => !!(t.flags & TypeFlags.TemplateLiteral)) as TemplateLiteralType[]; + + const estimatedCount = templateLiterals.length * countWhere(types, t => !!(t.flags & TypeFlags.StringLiteral)); + // TODO(jakebailey): set higher limit after testing + if (estimatedCount > 0) { + // To remove string literals already covered by template literals, we may potentially + // check every string literal against every template literal, leading to a combinatoric + // explosion. This is made even worse if the strings all share common prefixes or suffixes, + // making the "fast path" of a prefix check in inferFromLiteralPartsToTemplateLiteral not actually + // very fast as we'll repeatedly scan the strings much farther than just a few characters. + // + // To reduce the amount of work we need to do, we can build a two-way trie out of the + // template literals, only checking those which can be satisfied by a given string. + + const trie = createPrefixSuffixTrie(); + + forEach(templateLiterals, t => { + const prefix = t.texts[0]; + const suffix = t.texts[t.texts.length - 1]; + trie.set(prefix, suffix, templates => append(templates, t)); + }); + + let i = types.length; + outer: while (i > 0) { + i--; + const t = types[i]; + if (!(t.flags & TypeFlags.StringLiteral)) continue; + const text = (t as StringLiteralType).value; + + for (const templates of trie.iterateAllMatches(text)) { + if (some(templates, template => isTypeMatchedByTemplateLiteralOrStringMapping(t, template))) { + orderedRemoveItemAt(types, i); + continue outer; + } + } + } + + // Fall through into the general case with just the string mappings. + patterns = filter(patterns, t => !!(t.flags & TypeFlags.StringMapping)) as StringMappingType[]; + } + + if (patterns.length) { let i = types.length; while (i > 0) { i--; const t = types[i]; - if (t.flags & TypeFlags.StringLiteral && some(templates, template => isTypeMatchedByTemplateLiteralOrStringMapping(t, template))) { + if (!(t.flags & TypeFlags.StringLiteral)) continue; + if (some(patterns, template => isTypeMatchedByTemplateLiteralOrStringMapping(t, template))) { orderedRemoveItemAt(types, i); } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index fc692f1a65d2b..96a61c1066cd3 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2590,3 +2590,96 @@ export function isNodeLikeSystem(): boolean { && !(process as any).browser && typeof require !== "undefined"; } + +/** @internal */ +export interface PrefixSuffixTrie { + iterateAllMatches(input: string): Iterable; + set(prefix: string, suffix: string, fn: (value: T | undefined) => T | undefined): void; + hasAnyMatch(input: string): boolean; +} + +/** @internal */ +export function createPrefixSuffixTrie(): PrefixSuffixTrie { + interface Trie { + children: Record> | undefined; + value: T | undefined; + } + + function createTrie(): Trie { + return { + children: undefined, + value: undefined, + }; + } + + const root = createTrie>(); + + function* iterateAllMatches(input: string) { + let node = root; + + if (node.value) { + yield* iterateSuffix(node.value, 0); + } + + for (let i = 0; i < input.length; i++) { + const child = node.children?.[input[i]]; + if (!child) break; + if (child.value) { + yield* iterateSuffix(child.value, i + 1); + } + node = child; + } + + return; + + function* iterateSuffix(node: Trie, start: number) { + if (node.value) { + yield node.value; + } + + for (let i = input.length - 1; i >= start; i--) { + const child = node.children?.[input[i]]; + if (!child) break; + if (child.value) { + yield child.value; + } + node = child; + } + } + } + + function set(prefix: string, suffix: string, fn: (value: T | undefined) => T | undefined) { + let prefixNode = root; + + for (let i = 0; i < prefix.length; i++) { + const char = prefix[i]; + const children = prefixNode.children ??= {}; + const child = children[char] ??= createTrie(); + prefixNode = child; + } + + let suffixNode = prefixNode.value ??= createTrie(); + + for (let i = suffix.length - 1; i >= 0; i--) { + const char = suffix[i]; + const children = suffixNode.children ??= {}; + const child = children[char] ??= createTrie(); + suffixNode = child; + } + + suffixNode.value = fn(suffixNode.value); + } + + function hasAnyMatch(input: string) { + for (const _ of iterateAllMatches(input)) { + return true; + } + return false; + } + + return { + iterateAllMatches, + set, + hasAnyMatch, + }; +}