From 3b1982589007e0419c025ad716ec47e7ff2566cf Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 2 Jun 2016 05:43:40 -0700 Subject: [PATCH] Allow wildcard ("*") patterns in ambient module declarations --- src/compiler/binder.ts | 21 +++- src/compiler/checker.ts | 26 +++++ src/compiler/program.ts | 110 ++++++++++++------ src/compiler/types.ts | 13 +++ .../ambientDeclarationsPatterns.errors.txt | 25 ++++ .../reference/ambientDeclarationsPatterns.js | 28 +++++ .../ambient/ambientDeclarationsPatterns.ts | 19 +++ 7 files changed, 207 insertions(+), 35 deletions(-) create mode 100644 tests/baselines/reference/ambientDeclarationsPatterns.errors.txt create mode 100644 tests/baselines/reference/ambientDeclarationsPatterns.js create mode 100644 tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 5fdfc7f02be..d3363285afc 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1287,7 +1287,26 @@ namespace ts { declareSymbolAndAddToSymbolTable(node, SymbolFlags.NamespaceModule, SymbolFlags.NamespaceModuleExcludes); } else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes); + let pattern: Pattern | undefined; + if (node.name.kind === SyntaxKind.StringLiteral) { + const text = (node.name).text; + if (hasZeroOrOneAsteriskCharacter(text)) { + pattern = tryParsePattern(text); + } + else { + errorOnFirstToken(node.name, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text); + } + } + + if (pattern) { + // TODO: don't really need such a symbol in container.locals... + const symbol = declareSymbol(container.locals, undefined, node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes); + file.patternAmbientModules = file.patternAmbientModules || []; + file.patternAmbientModules.push({ pattern, symbol }); + } + else { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes); + } } } else { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 06ef2463402..3e66cf1661f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -140,6 +140,12 @@ namespace ts { const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true); const globals: SymbolTable = {}; + /** + * List of every ambient module with a "*" wildcard. + * Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches. + * This is only used if there is no exact match. + */ + let patternAmbientModules: PatternAmbientModule[]; let getGlobalESSymbolConstructorSymbol: () => Symbol; @@ -1285,6 +1291,12 @@ namespace ts { } return undefined; } + + const patternModuleSymbol = getPatternAmbientModule(moduleName); + if (patternModuleSymbol) { + return getMergedSymbol(patternModuleSymbol); + } + if (moduleNotFoundError) { // report errors only if it was requested error(moduleReferenceLiteral, moduleNotFoundError, moduleName); @@ -1292,6 +1304,16 @@ namespace ts { return undefined; } + /** Get an ambient module with a wildcard ("*") in it. */ + function getPatternAmbientModule(name: string): Symbol | undefined { + if (patternAmbientModules) { + const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, name); + if (pattern) { + return pattern.symbol; + } + } + } + // An external module with an 'export =' declaration resolves to the target of the 'export =' declaration, // and an external module with no 'export =' declaration resolves to the module itself. function resolveExternalModuleSymbol(moduleSymbol: Symbol): Symbol { @@ -17627,6 +17649,10 @@ namespace ts { if (!isExternalOrCommonJsModule(file)) { mergeSymbolTable(globals, file.locals); } + if (file.patternAmbientModules && file.patternAmbientModules.length) { + (patternAmbientModules || (patternAmbientModules = [])).push(...file.patternAmbientModules); + } + if (file.moduleAugmentations.length) { (augmentations || (augmentations = [])).push(file.moduleAugmentations); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index bab554927e7..1677f61c15d 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -95,7 +95,7 @@ namespace ts { return compilerOptions.traceResolution && host.trace !== undefined; } - function hasZeroOrOneAsteriskCharacter(str: string): boolean { + export function hasZeroOrOneAsteriskCharacter(str: string): boolean { let seenAsterisk = false; for (let i = 0; i < str.length; i++) { if (str.charCodeAt(i) === CharacterCodes.asterisk) { @@ -496,48 +496,23 @@ namespace ts { trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, state.compilerOptions.baseUrl, moduleName); } - let longestMatchPrefixLength = -1; - let matchedPattern: string; - let matchedStar: string; - + // string is for exact match + let matchedPattern: Pattern | string | undefined = undefined; if (state.compilerOptions.paths) { if (state.traceEnabled) { trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName); } - - for (const key in state.compilerOptions.paths) { - const pattern: string = key; - const indexOfStar = pattern.indexOf("*"); - if (indexOfStar !== -1) { - const prefix = pattern.substr(0, indexOfStar); - const suffix = pattern.substr(indexOfStar + 1); - if (moduleName.length >= prefix.length + suffix.length && - startsWith(moduleName, prefix) && - endsWith(moduleName, suffix)) { - - // use length of prefix as betterness criteria - if (prefix.length > longestMatchPrefixLength) { - longestMatchPrefixLength = prefix.length; - matchedPattern = pattern; - matchedStar = moduleName.substr(prefix.length, moduleName.length - suffix.length); - } - } - } - else if (pattern === moduleName) { - // pattern was matched as is - no need to search further - matchedPattern = pattern; - matchedStar = undefined; - break; - } - } + matchedPattern = matchPatternOrExact(Object.keys(state.compilerOptions.paths), moduleName); } if (matchedPattern) { + const matchedStar = typeof matchedPattern === "string" ? undefined : matchedText(matchedPattern, moduleName); + const matchedPatternText = typeof matchedPattern === "string" ? matchedPattern : patternText(matchedPattern); if (state.traceEnabled) { - trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPattern); + trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText); } - for (const subst of state.compilerOptions.paths[matchedPattern]) { - const path = matchedStar ? subst.replace("\*", matchedStar) : subst; + for (const subst of state.compilerOptions.paths[matchedPatternText]) { + const path = matchedStar ? subst.replace("*", matchedStar) : subst; const candidate = normalizePath(combinePaths(state.compilerOptions.baseUrl, path)); if (state.traceEnabled) { trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path); @@ -560,6 +535,73 @@ namespace ts { } } + /** + * patternStrings contains both pattern strings (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.) + */ + function matchPatternOrExact(patternStrings: string[], candidate: string): string | Pattern | undefined { + const patterns: Pattern[] = []; + for (const patternString of patternStrings) { + const pattern = tryParsePattern(patternString); + if (pattern) { + patterns.push(pattern); + } + else if (patternString === candidate) { + // pattern was matched as is - no need to search further + return patternString; + } + } + + return findBestPatternMatch(patterns, _ => _, candidate); + } + + function patternText({prefix, suffix}: Pattern): string { + return `${prefix}*${suffix}`; + } + + /** + * Given that candidate matches pattern, returns the text matching the '*'. + * E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar" + */ + function matchedText(pattern: Pattern, candidate: string): string { + Debug.assert(isPatternMatch(pattern, candidate)); + return candidate.substr(pattern.prefix.length, candidate.length - pattern.suffix.length); + } + + /** Return the object corresponding to the best pattern to match `candidate`. */ + export function findBestPatternMatch(values: T[], getPattern: (value: T) => Pattern, candidate: string): T | undefined { + let matchedValue: T | undefined = undefined; + // use length of prefix as betterness criteria + let longestMatchPrefixLength = -1; + + for (const v of values) { + const pattern = getPattern(v); + if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) { + longestMatchPrefixLength = pattern.prefix.length; + matchedValue = v; + } + } + + return matchedValue; + } + + function isPatternMatch({prefix, suffix}: Pattern, candidate: string) { + return candidate.length >= prefix.length + suffix.length && + startsWith(candidate, prefix) && + endsWith(candidate, suffix); + } + + export function tryParsePattern(pattern: string): Pattern | undefined { + // This should be verified outside of here and a proper error thrown. + Debug.assert(hasZeroOrOneAsteriskCharacter(pattern)); + const indexOfStar = pattern.indexOf("*"); + return indexOfStar === -1 ? undefined : { + prefix: pattern.substr(0, indexOfStar), + suffix: pattern.substr(indexOfStar + 1) + }; + } + export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { const containingDirectory = getDirectoryPath(containingFile); const supportedExtensions = getSupportedExtensions(compilerOptions); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c7e14a10031..a6f19f121cf 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1658,6 +1658,7 @@ namespace ts { /* @internal */ resolvedTypeReferenceDirectiveNames: Map; /* @internal */ imports: LiteralExpression[]; /* @internal */ moduleAugmentations: LiteralExpression[]; + /* @internal */ patternAmbientModules?: PatternAmbientModule[]; } export interface ScriptReferenceHost { @@ -2135,6 +2136,18 @@ namespace ts { [index: string]: Symbol; } + /** Represents a "prefix*suffix" pattern. */ + export interface Pattern { + prefix: string; + suffix: string; + } + + /** Used to track a `declare module "foo*"`-like declaration. */ + export interface PatternAmbientModule { + pattern: Pattern; + symbol: Symbol; + } + /* @internal */ export const enum NodeCheckFlags { TypeChecked = 0x00000001, // Node has been type checked diff --git a/tests/baselines/reference/ambientDeclarationsPatterns.errors.txt b/tests/baselines/reference/ambientDeclarationsPatterns.errors.txt new file mode 100644 index 00000000000..c197fe54f8d --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns.errors.txt @@ -0,0 +1,25 @@ +tests/cases/conformance/ambient/declarations.d.ts(6,16): error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character + + +==== tests/cases/conformance/ambient/user.ts (0 errors) ==== + /// + import {foo} from "foobarbaz"; + foo(0); + + import {foos} from "foosball"; + +==== tests/cases/conformance/ambient/declarations.d.ts (1 errors) ==== + declare module "foo*baz" { + export function foo(n: number): void; + } + + // Should be an error + declare module "too*many*asterisks" { } + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character + + // Longest prefix wins + declare module "foos*" { + export const foos: number; + } + \ No newline at end of file diff --git a/tests/baselines/reference/ambientDeclarationsPatterns.js b/tests/baselines/reference/ambientDeclarationsPatterns.js new file mode 100644 index 00000000000..056416f1086 --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns.js @@ -0,0 +1,28 @@ +//// [tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts] //// + +//// [declarations.d.ts] +declare module "foo*baz" { + export function foo(n: number): void; +} + +// Should be an error +declare module "too*many*asterisks" { } + +// Longest prefix wins +declare module "foos*" { + export const foos: number; +} + +//// [user.ts] +/// +import {foo} from "foobarbaz"; +foo(0); + +import {foos} from "foosball"; + + +//// [user.js] +"use strict"; +/// +var foobarbaz_1 = require("foobarbaz"); +foobarbaz_1.foo(0); diff --git a/tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts b/tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts new file mode 100644 index 00000000000..0d3b2e1c60e --- /dev/null +++ b/tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts @@ -0,0 +1,19 @@ +// @Filename: declarations.d.ts +declare module "foo*baz" { + export function foo(n: number): void; +} + +// Should be an error +declare module "too*many*asterisks" { } + +// Longest prefix wins +declare module "foos*" { + export const foos: number; +} + +// @Filename: user.ts +/// +import {foo} from "foobarbaz"; +foo(0); + +import {foos} from "foosball";