Skip to content

Commit 9ffd00d

Browse files
author
Andy
authored
Merge pull request microsoft#8939 from Microsoft/pattern_ambient_modules
Allow wildcard ("*") patterns in ambient module declarations
2 parents 302cea8 + 559b49b commit 9ffd00d

11 files changed

Lines changed: 325 additions & 39 deletions

src/compiler/binder.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,9 +1183,9 @@ namespace ts {
11831183
lastContainer = next;
11841184
}
11851185

1186-
function declareSymbolAndAddToSymbolTable(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): void {
1186+
function declareSymbolAndAddToSymbolTable(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): Symbol {
11871187
// Just call this directly so that the return type of this function stays "void".
1188-
declareSymbolAndAddToSymbolTableWorker(node, symbolFlags, symbolExcludes);
1188+
return declareSymbolAndAddToSymbolTableWorker(node, symbolFlags, symbolExcludes);
11891189
}
11901190

11911191
function declareSymbolAndAddToSymbolTableWorker(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): Symbol {
@@ -1289,7 +1289,22 @@ namespace ts {
12891289
declareSymbolAndAddToSymbolTable(node, SymbolFlags.NamespaceModule, SymbolFlags.NamespaceModuleExcludes);
12901290
}
12911291
else {
1292-
declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes);
1292+
let pattern: Pattern | undefined;
1293+
if (node.name.kind === SyntaxKind.StringLiteral) {
1294+
const text = (<StringLiteral>node.name).text;
1295+
if (hasZeroOrOneAsteriskCharacter(text)) {
1296+
pattern = tryParsePattern(text);
1297+
}
1298+
else {
1299+
errorOnFirstToken(node.name, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text);
1300+
}
1301+
}
1302+
1303+
const symbol = declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes);
1304+
1305+
if (pattern) {
1306+
(file.patternAmbientModules || (file.patternAmbientModules = [])).push({ pattern, symbol });
1307+
}
12931308
}
12941309
}
12951310
else {
@@ -2067,10 +2082,10 @@ namespace ts {
20672082
checkStrictModeFunctionName(<FunctionDeclaration>node);
20682083
if (inStrictMode) {
20692084
checkStrictModeFunctionDeclaration(node);
2070-
return bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
2085+
bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
20712086
}
20722087
else {
2073-
return declareSymbolAndAddToSymbolTable(<Declaration>node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
2088+
declareSymbolAndAddToSymbolTable(<Declaration>node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
20742089
}
20752090
}
20762091

src/compiler/checker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ namespace ts {
140140
const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true);
141141

142142
const globals: SymbolTable = {};
143+
/**
144+
* List of every ambient module with a "*" wildcard.
145+
* Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches.
146+
* This is only used if there is no exact match.
147+
*/
148+
let patternAmbientModules: PatternAmbientModule[];
143149

144150
let getGlobalESSymbolConstructorSymbol: () => Symbol;
145151

@@ -1285,6 +1291,14 @@ namespace ts {
12851291
}
12861292
return undefined;
12871293
}
1294+
1295+
if (patternAmbientModules) {
1296+
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleName);
1297+
if (pattern) {
1298+
return getMergedSymbol(pattern.symbol);
1299+
}
1300+
}
1301+
12881302
if (moduleNotFoundError) {
12891303
// report errors only if it was requested
12901304
error(moduleReferenceLiteral, moduleNotFoundError, moduleName);
@@ -17646,6 +17660,10 @@ namespace ts {
1764617660
if (!isExternalOrCommonJsModule(file)) {
1764717661
mergeSymbolTable(globals, file.locals);
1764817662
}
17663+
if (file.patternAmbientModules && file.patternAmbientModules.length) {
17664+
patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules);
17665+
}
17666+
1764917667
if (file.moduleAugmentations.length) {
1765017668
(augmentations || (augmentations = [])).push(file.moduleAugmentations);
1765117669
}

src/compiler/program.ts

Lines changed: 79 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ namespace ts {
9595
return compilerOptions.traceResolution && host.trace !== undefined;
9696
}
9797

98-
function hasZeroOrOneAsteriskCharacter(str: string): boolean {
98+
/* @internal */
99+
export function hasZeroOrOneAsteriskCharacter(str: string): boolean {
99100
let seenAsterisk = false;
100101
for (let i = 0; i < str.length; i++) {
101102
if (str.charCodeAt(i) === CharacterCodes.asterisk) {
@@ -496,48 +497,23 @@ namespace ts {
496497
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, state.compilerOptions.baseUrl, moduleName);
497498
}
498499

499-
let longestMatchPrefixLength = -1;
500-
let matchedPattern: string;
501-
let matchedStar: string;
502-
500+
// string is for exact match
501+
let matchedPattern: Pattern | string | undefined = undefined;
503502
if (state.compilerOptions.paths) {
504503
if (state.traceEnabled) {
505504
trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName);
506505
}
507-
508-
for (const key in state.compilerOptions.paths) {
509-
const pattern: string = key;
510-
const indexOfStar = pattern.indexOf("*");
511-
if (indexOfStar !== -1) {
512-
const prefix = pattern.substr(0, indexOfStar);
513-
const suffix = pattern.substr(indexOfStar + 1);
514-
if (moduleName.length >= prefix.length + suffix.length &&
515-
startsWith(moduleName, prefix) &&
516-
endsWith(moduleName, suffix)) {
517-
518-
// use length of prefix as betterness criteria
519-
if (prefix.length > longestMatchPrefixLength) {
520-
longestMatchPrefixLength = prefix.length;
521-
matchedPattern = pattern;
522-
matchedStar = moduleName.substr(prefix.length, moduleName.length - suffix.length);
523-
}
524-
}
525-
}
526-
else if (pattern === moduleName) {
527-
// pattern was matched as is - no need to search further
528-
matchedPattern = pattern;
529-
matchedStar = undefined;
530-
break;
531-
}
532-
}
506+
matchedPattern = matchPatternOrExact(getKeys(state.compilerOptions.paths), moduleName);
533507
}
534508

535509
if (matchedPattern) {
510+
const matchedStar = typeof matchedPattern === "string" ? undefined : matchedText(matchedPattern, moduleName);
511+
const matchedPatternText = typeof matchedPattern === "string" ? matchedPattern : patternText(matchedPattern);
536512
if (state.traceEnabled) {
537-
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPattern);
513+
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText);
538514
}
539-
for (const subst of state.compilerOptions.paths[matchedPattern]) {
540-
const path = matchedStar ? subst.replace("\*", matchedStar) : subst;
515+
for (const subst of state.compilerOptions.paths[matchedPatternText]) {
516+
const path = matchedStar ? subst.replace("*", matchedStar) : subst;
541517
const candidate = normalizePath(combinePaths(state.compilerOptions.baseUrl, path));
542518
if (state.traceEnabled) {
543519
trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path);
@@ -560,6 +536,75 @@ namespace ts {
560536
}
561537
}
562538

539+
/**
540+
* patternStrings contains both pattern strings (containing "*") and regular strings.
541+
* Return an exact match if possible, or a pattern match, or undefined.
542+
* (These are verified by verifyCompilerOptions to have 0 or 1 "*" characters.)
543+
*/
544+
function matchPatternOrExact(patternStrings: string[], candidate: string): string | Pattern | undefined {
545+
const patterns: Pattern[] = [];
546+
for (const patternString of patternStrings) {
547+
const pattern = tryParsePattern(patternString);
548+
if (pattern) {
549+
patterns.push(pattern);
550+
}
551+
else if (patternString === candidate) {
552+
// pattern was matched as is - no need to search further
553+
return patternString;
554+
}
555+
}
556+
557+
return findBestPatternMatch(patterns, _ => _, candidate);
558+
}
559+
560+
function patternText({prefix, suffix}: Pattern): string {
561+
return `${prefix}*${suffix}`;
562+
}
563+
564+
/**
565+
* Given that candidate matches pattern, returns the text matching the '*'.
566+
* E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar"
567+
*/
568+
function matchedText(pattern: Pattern, candidate: string): string {
569+
Debug.assert(isPatternMatch(pattern, candidate));
570+
return candidate.substr(pattern.prefix.length, candidate.length - pattern.suffix.length);
571+
}
572+
573+
/** Return the object corresponding to the best pattern to match `candidate`. */
574+
/* @internal */
575+
export function findBestPatternMatch<T>(values: T[], getPattern: (value: T) => Pattern, candidate: string): T | undefined {
576+
let matchedValue: T | undefined = undefined;
577+
// use length of prefix as betterness criteria
578+
let longestMatchPrefixLength = -1;
579+
580+
for (const v of values) {
581+
const pattern = getPattern(v);
582+
if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) {
583+
longestMatchPrefixLength = pattern.prefix.length;
584+
matchedValue = v;
585+
}
586+
}
587+
588+
return matchedValue;
589+
}
590+
591+
function isPatternMatch({prefix, suffix}: Pattern, candidate: string) {
592+
return candidate.length >= prefix.length + suffix.length &&
593+
startsWith(candidate, prefix) &&
594+
endsWith(candidate, suffix);
595+
}
596+
597+
/* @internal */
598+
export function tryParsePattern(pattern: string): Pattern | undefined {
599+
// This should be verified outside of here and a proper error thrown.
600+
Debug.assert(hasZeroOrOneAsteriskCharacter(pattern));
601+
const indexOfStar = pattern.indexOf("*");
602+
return indexOfStar === -1 ? undefined : {
603+
prefix: pattern.substr(0, indexOfStar),
604+
suffix: pattern.substr(indexOfStar + 1)
605+
};
606+
}
607+
563608
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
564609
const containingDirectory = getDirectoryPath(containingFile);
565610
const supportedExtensions = getSupportedExtensions(compilerOptions);

src/compiler/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,6 +1658,7 @@ namespace ts {
16581658
/* @internal */ resolvedTypeReferenceDirectiveNames: Map<ResolvedTypeReferenceDirective>;
16591659
/* @internal */ imports: LiteralExpression[];
16601660
/* @internal */ moduleAugmentations: LiteralExpression[];
1661+
/* @internal */ patternAmbientModules?: PatternAmbientModule[];
16611662
}
16621663

16631664
export interface ScriptReferenceHost {
@@ -2135,6 +2136,20 @@ namespace ts {
21352136
[index: string]: Symbol;
21362137
}
21372138

2139+
/** Represents a "prefix*suffix" pattern. */
2140+
/* @internal */
2141+
export interface Pattern {
2142+
prefix: string;
2143+
suffix: string;
2144+
}
2145+
2146+
/** Used to track a `declare module "foo*"`-like declaration. */
2147+
/* @internal */
2148+
export interface PatternAmbientModule {
2149+
pattern: Pattern;
2150+
symbol: Symbol;
2151+
}
2152+
21382153
/* @internal */
21392154
export const enum NodeCheckFlags {
21402155
TypeChecked = 0x00000001, // Node has been type checked
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//// [tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts] ////
2+
3+
//// [declarations.d.ts]
4+
declare module "foo*baz" {
5+
export function foo(s: string): void;
6+
}
7+
// Augmentations still work
8+
declare module "foo*baz" {
9+
export const baz: string;
10+
}
11+
12+
// Longest prefix wins
13+
declare module "foos*" {
14+
export const foos: string;
15+
}
16+
17+
declare module "*!text" {
18+
const x: string;
19+
export default x;
20+
}
21+
22+
//// [user.ts]
23+
///<reference path="declarations.d.ts" />
24+
import {foo, baz} from "foobarbaz";
25+
foo(baz);
26+
27+
import {foos} from "foosball";
28+
foo(foos);
29+
30+
// Works with relative file name
31+
import fileText from "./file!text";
32+
foo(fileText);
33+
34+
35+
//// [user.js]
36+
"use strict";
37+
///<reference path="declarations.d.ts" />
38+
var foobarbaz_1 = require("foobarbaz");
39+
foobarbaz_1.foo(foobarbaz_1.baz);
40+
var foosball_1 = require("foosball");
41+
foobarbaz_1.foo(foosball_1.foos);
42+
// Works with relative file name
43+
var file_text_1 = require("./file!text");
44+
foobarbaz_1.foo(file_text_1["default"]);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
=== tests/cases/conformance/ambient/user.ts ===
2+
///<reference path="declarations.d.ts" />
3+
import {foo, baz} from "foobarbaz";
4+
>foo : Symbol(foo, Decl(user.ts, 1, 8))
5+
>baz : Symbol(baz, Decl(user.ts, 1, 12))
6+
7+
foo(baz);
8+
>foo : Symbol(foo, Decl(user.ts, 1, 8))
9+
>baz : Symbol(baz, Decl(user.ts, 1, 12))
10+
11+
import {foos} from "foosball";
12+
>foos : Symbol(foos, Decl(user.ts, 4, 8))
13+
14+
foo(foos);
15+
>foo : Symbol(foo, Decl(user.ts, 1, 8))
16+
>foos : Symbol(foos, Decl(user.ts, 4, 8))
17+
18+
// Works with relative file name
19+
import fileText from "./file!text";
20+
>fileText : Symbol(fileText, Decl(user.ts, 8, 6))
21+
22+
foo(fileText);
23+
>foo : Symbol(foo, Decl(user.ts, 1, 8))
24+
>fileText : Symbol(fileText, Decl(user.ts, 8, 6))
25+
26+
=== tests/cases/conformance/ambient/declarations.d.ts ===
27+
declare module "foo*baz" {
28+
export function foo(s: string): void;
29+
>foo : Symbol(foo, Decl(declarations.d.ts, 0, 26))
30+
>s : Symbol(s, Decl(declarations.d.ts, 1, 24))
31+
}
32+
// Augmentations still work
33+
declare module "foo*baz" {
34+
export const baz: string;
35+
>baz : Symbol(baz, Decl(declarations.d.ts, 5, 16))
36+
}
37+
38+
// Longest prefix wins
39+
declare module "foos*" {
40+
export const foos: string;
41+
>foos : Symbol(foos, Decl(declarations.d.ts, 10, 16))
42+
}
43+
44+
declare module "*!text" {
45+
const x: string;
46+
>x : Symbol(x, Decl(declarations.d.ts, 14, 9))
47+
48+
export default x;
49+
>x : Symbol(x, Decl(declarations.d.ts, 14, 9))
50+
}
51+

0 commit comments

Comments
 (0)