diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 5fdfc7f02be42..753e02c7a09d4 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1181,9 +1181,9 @@ namespace ts { lastContainer = next; } - function declareSymbolAndAddToSymbolTable(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): void { + function declareSymbolAndAddToSymbolTable(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): Symbol { // Just call this directly so that the return type of this function stays "void". - declareSymbolAndAddToSymbolTableWorker(node, symbolFlags, symbolExcludes); + return declareSymbolAndAddToSymbolTableWorker(node, symbolFlags, symbolExcludes); } function declareSymbolAndAddToSymbolTableWorker(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): Symbol { @@ -1287,7 +1287,22 @@ 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); + } + } + + const symbol = declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes); + + if (pattern) { + (file.patternAmbientModules || (file.patternAmbientModules = [])).push({ pattern, symbol }); + } } } else { @@ -2065,10 +2080,10 @@ namespace ts { checkStrictModeFunctionName(node); if (inStrictMode) { checkStrictModeFunctionDeclaration(node); - return bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); + bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); } else { - return declareSymbolAndAddToSymbolTable(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); + declareSymbolAndAddToSymbolTable(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); } } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 06ef246340252..8e5d5e61b752b 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,14 @@ namespace ts { } return undefined; } + + if (patternAmbientModules) { + const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleName); + if (pattern) { + return getMergedSymbol(pattern.symbol); + } + } + if (moduleNotFoundError) { // report errors only if it was requested error(moduleReferenceLiteral, moduleNotFoundError, moduleName); @@ -11277,7 +11291,7 @@ namespace ts { const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(declaration.parent.symbol); const declaringClass = getDeclaredTypeOfSymbol(declaration.parent.symbol); - // A private or protected constructor can only be instantiated within it's own class + // A private or protected constructor can only be instantiated within it's own class if (!isNodeWithinClass(node, declaringClassDeclaration)) { if (flags & NodeFlags.Private) { error(node, Diagnostics.Constructor_of_class_0_is_private_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); @@ -16140,12 +16154,12 @@ namespace ts { const symbol = getSymbolOfNode(node); const target = resolveAlias(symbol); if (target !== unknownSymbol) { - // For external modules symbol represent local symbol for an alias. + // For external modules symbol represent local symbol for an alias. // This local symbol will merge any other local declarations (excluding other aliases) // and symbol.flags will contains combined representation for all merged declaration. // Based on symbol.flags we can compute a set of excluded meanings (meaning that resolved alias should not have, - // otherwise it will conflict with some local declaration). Note that in addition to normal flags we include matching SymbolFlags.Export* - // in order to prevent collisions with declarations that were exported from the current module (they still contribute to local names). + // otherwise it will conflict with some local declaration). Note that in addition to normal flags we include matching SymbolFlags.Export* + // in order to prevent collisions with declarations that were exported from the current module (they still contribute to local names). const excludedMeanings = (symbol.flags & (SymbolFlags.Value | SymbolFlags.ExportValue) ? SymbolFlags.Value : 0) | (symbol.flags & SymbolFlags.Type ? SymbolFlags.Type : 0) | @@ -16344,7 +16358,7 @@ namespace ts { continue; } const { declarations, flags } = exports[id]; - // ECMA262: 15.2.1.1 It is a Syntax Error if the ExportedNames of ModuleItemList contains any duplicate entries. + // ECMA262: 15.2.1.1 It is a Syntax Error if the ExportedNames of ModuleItemList contains any duplicate entries. // (TS Exceptions: namespaces, function overloads, enums, and interfaces) if (flags & (SymbolFlags.Namespace | SymbolFlags.Interface | SymbolFlags.Enum)) { continue; @@ -17049,10 +17063,10 @@ namespace ts { } // Gets the type of object literal or array literal of destructuring assignment. - // { a } from + // { a } from // for ( { a } of elems) { // } - // [ a ] from + // [ a ] from // [a] = [ some array ...] function getTypeOfArrayLiteralOrObjectLiteralDestructuringAssignment(expr: Expression): Type { Debug.assert(expr.kind === SyntaxKind.ObjectLiteralExpression || expr.kind === SyntaxKind.ArrayLiteralExpression); @@ -17085,10 +17099,10 @@ namespace ts { } // Gets the property symbol corresponding to the property in destructuring assignment - // 'property1' from + // 'property1' from // for ( { property1: a } of elems) { // } - // 'property1' at location 'a' from: + // 'property1' at location 'a' from: // [a] = [ property1, property2 ] function getPropertySymbolOfDestructuringAssignment(location: Identifier) { // Get the type of the object or array literal and then look for property of given name in the type @@ -17627,6 +17641,10 @@ namespace ts { if (!isExternalOrCommonJsModule(file)) { mergeSymbolTable(globals, file.locals); } + if (file.patternAmbientModules && file.patternAmbientModules.length) { + patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules); + } + if (file.moduleAugmentations.length) { (augmentations || (augmentations = [])).push(file.moduleAugmentations); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index bab554927e726..3267737a3e5a8 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -95,7 +95,8 @@ namespace ts { return compilerOptions.traceResolution && host.trace !== undefined; } - function hasZeroOrOneAsteriskCharacter(str: string): boolean { + /* @internal */ + 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 +497,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(getKeys(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 +536,75 @@ 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`. */ + /* @internal */ + 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); + } + + /* @internal */ + 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 c7e14a1003108..bcc4bbcf6965d 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,20 @@ namespace ts { [index: string]: Symbol; } + /** Represents a "prefix*suffix" pattern. */ + /* @internal */ + export interface Pattern { + prefix: string; + suffix: string; + } + + /** Used to track a `declare module "foo*"`-like declaration. */ + /* @internal */ + 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.js b/tests/baselines/reference/ambientDeclarationsPatterns.js new file mode 100644 index 0000000000000..143f26550e997 --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns.js @@ -0,0 +1,44 @@ +//// [tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts] //// + +//// [declarations.d.ts] +declare module "foo*baz" { + export function foo(s: string): void; +} +// Augmentations still work +declare module "foo*baz" { + export const baz: string; +} + +// Longest prefix wins +declare module "foos*" { + export const foos: string; +} + +declare module "*!text" { + const x: string; + export default x; +} + +//// [user.ts] +/// +import {foo, baz} from "foobarbaz"; +foo(baz); + +import {foos} from "foosball"; +foo(foos); + +// Works with relative file name +import fileText from "./file!text"; +foo(fileText); + + +//// [user.js] +"use strict"; +/// +var foobarbaz_1 = require("foobarbaz"); +foobarbaz_1.foo(foobarbaz_1.baz); +var foosball_1 = require("foosball"); +foobarbaz_1.foo(foosball_1.foos); +// Works with relative file name +var file_text_1 = require("./file!text"); +foobarbaz_1.foo(file_text_1["default"]); diff --git a/tests/baselines/reference/ambientDeclarationsPatterns.symbols b/tests/baselines/reference/ambientDeclarationsPatterns.symbols new file mode 100644 index 0000000000000..4c0acc93f8f93 --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns.symbols @@ -0,0 +1,51 @@ +=== tests/cases/conformance/ambient/user.ts === +/// +import {foo, baz} from "foobarbaz"; +>foo : Symbol(foo, Decl(user.ts, 1, 8)) +>baz : Symbol(baz, Decl(user.ts, 1, 12)) + +foo(baz); +>foo : Symbol(foo, Decl(user.ts, 1, 8)) +>baz : Symbol(baz, Decl(user.ts, 1, 12)) + +import {foos} from "foosball"; +>foos : Symbol(foos, Decl(user.ts, 4, 8)) + +foo(foos); +>foo : Symbol(foo, Decl(user.ts, 1, 8)) +>foos : Symbol(foos, Decl(user.ts, 4, 8)) + +// Works with relative file name +import fileText from "./file!text"; +>fileText : Symbol(fileText, Decl(user.ts, 8, 6)) + +foo(fileText); +>foo : Symbol(foo, Decl(user.ts, 1, 8)) +>fileText : Symbol(fileText, Decl(user.ts, 8, 6)) + +=== tests/cases/conformance/ambient/declarations.d.ts === +declare module "foo*baz" { + export function foo(s: string): void; +>foo : Symbol(foo, Decl(declarations.d.ts, 0, 26)) +>s : Symbol(s, Decl(declarations.d.ts, 1, 24)) +} +// Augmentations still work +declare module "foo*baz" { + export const baz: string; +>baz : Symbol(baz, Decl(declarations.d.ts, 5, 16)) +} + +// Longest prefix wins +declare module "foos*" { + export const foos: string; +>foos : Symbol(foos, Decl(declarations.d.ts, 10, 16)) +} + +declare module "*!text" { + const x: string; +>x : Symbol(x, Decl(declarations.d.ts, 14, 9)) + + export default x; +>x : Symbol(x, Decl(declarations.d.ts, 14, 9)) +} + diff --git a/tests/baselines/reference/ambientDeclarationsPatterns.types b/tests/baselines/reference/ambientDeclarationsPatterns.types new file mode 100644 index 0000000000000..adf8ae1ab3b20 --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns.types @@ -0,0 +1,54 @@ +=== tests/cases/conformance/ambient/user.ts === +/// +import {foo, baz} from "foobarbaz"; +>foo : (s: string) => void +>baz : string + +foo(baz); +>foo(baz) : void +>foo : (s: string) => void +>baz : string + +import {foos} from "foosball"; +>foos : string + +foo(foos); +>foo(foos) : void +>foo : (s: string) => void +>foos : string + +// Works with relative file name +import fileText from "./file!text"; +>fileText : string + +foo(fileText); +>foo(fileText) : void +>foo : (s: string) => void +>fileText : string + +=== tests/cases/conformance/ambient/declarations.d.ts === +declare module "foo*baz" { + export function foo(s: string): void; +>foo : (s: string) => void +>s : string +} +// Augmentations still work +declare module "foo*baz" { + export const baz: string; +>baz : string +} + +// Longest prefix wins +declare module "foos*" { + export const foos: string; +>foos : string +} + +declare module "*!text" { + const x: string; +>x : string + + export default x; +>x : string +} + diff --git a/tests/baselines/reference/ambientDeclarationsPatterns_tooManyAsterisks.errors.txt b/tests/baselines/reference/ambientDeclarationsPatterns_tooManyAsterisks.errors.txt new file mode 100644 index 0000000000000..7a3ff02aa5e67 --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns_tooManyAsterisks.errors.txt @@ -0,0 +1,8 @@ +tests/cases/conformance/ambient/ambientDeclarationsPatterns_tooManyAsterisks.ts(1,16): error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character + + +==== tests/cases/conformance/ambient/ambientDeclarationsPatterns_tooManyAsterisks.ts (1 errors) ==== + declare module "too*many*asterisks" { } + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character + \ No newline at end of file diff --git a/tests/baselines/reference/ambientDeclarationsPatterns_tooManyAsterisks.js b/tests/baselines/reference/ambientDeclarationsPatterns_tooManyAsterisks.js new file mode 100644 index 0000000000000..a664eb84dc5aa --- /dev/null +++ b/tests/baselines/reference/ambientDeclarationsPatterns_tooManyAsterisks.js @@ -0,0 +1,5 @@ +//// [ambientDeclarationsPatterns_tooManyAsterisks.ts] +declare module "too*many*asterisks" { } + + +//// [ambientDeclarationsPatterns_tooManyAsterisks.js] diff --git a/tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts b/tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts new file mode 100644 index 0000000000000..d48f50bfa5059 --- /dev/null +++ b/tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts @@ -0,0 +1,30 @@ +// @Filename: declarations.d.ts +declare module "foo*baz" { + export function foo(s: string): void; +} +// Augmentations still work +declare module "foo*baz" { + export const baz: string; +} + +// Longest prefix wins +declare module "foos*" { + export const foos: string; +} + +declare module "*!text" { + const x: string; + export default x; +} + +// @Filename: user.ts +/// +import {foo, baz} from "foobarbaz"; +foo(baz); + +import {foos} from "foosball"; +foo(foos); + +// Works with relative file name +import fileText from "./file!text"; +foo(fileText); diff --git a/tests/cases/conformance/ambient/ambientDeclarationsPatterns_tooManyAsterisks.ts b/tests/cases/conformance/ambient/ambientDeclarationsPatterns_tooManyAsterisks.ts new file mode 100644 index 0000000000000..76f9081906ca8 --- /dev/null +++ b/tests/cases/conformance/ambient/ambientDeclarationsPatterns_tooManyAsterisks.ts @@ -0,0 +1 @@ +declare module "too*many*asterisks" { }