Skip to content

Commit ff70666

Browse files
author
Andy Hanson
committed
Allow wildcard ("*") patterns in ambient module declarations
1 parent 166f399 commit ff70666

File tree

7 files changed

+205
-35
lines changed

7 files changed

+205
-35
lines changed

src/compiler/binder.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1287,7 +1287,26 @@ namespace ts {
12871287
declareSymbolAndAddToSymbolTable(node, SymbolFlags.NamespaceModule, SymbolFlags.NamespaceModuleExcludes);
12881288
}
12891289
else {
1290-
declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes);
1290+
let pattern: Pattern | undefined;
1291+
if (node.name.kind === SyntaxKind.StringLiteral) {
1292+
const text = (<StringLiteral>node.name).text;
1293+
if (hasZeroOrOneAsteriskCharacter(text)) {
1294+
pattern = tryParsePattern(text);
1295+
}
1296+
else {
1297+
errorOnFirstToken(node.name, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text);
1298+
}
1299+
}
1300+
1301+
if (pattern) {
1302+
// TODO: don't really need such a symbol in container.locals...
1303+
const symbol = declareSymbol(container.locals, undefined, node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes);
1304+
file.patternAmbientModules = file.patternAmbientModules || [];
1305+
file.patternAmbientModules.push({ pattern, symbol });
1306+
}
1307+
else {
1308+
declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes);
1309+
}
12911310
}
12921311
}
12931312
else {

src/compiler/checker.ts

Lines changed: 26 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,13 +1291,29 @@ namespace ts {
12851291
}
12861292
return undefined;
12871293
}
1294+
1295+
const patternModuleSymbol = getPatternAmbientModule(moduleName);
1296+
if (patternModuleSymbol) {
1297+
return getMergedSymbol(patternModuleSymbol);
1298+
}
1299+
12881300
if (moduleNotFoundError) {
12891301
// report errors only if it was requested
12901302
error(moduleReferenceLiteral, moduleNotFoundError, moduleName);
12911303
}
12921304
return undefined;
12931305
}
12941306

1307+
/** Get an ambient module with a wildcard ("*") in it. */
1308+
function getPatternAmbientModule(name: string): Symbol | undefined {
1309+
if (patternAmbientModules) {
1310+
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, name);
1311+
if (pattern) {
1312+
return pattern.symbol;
1313+
}
1314+
}
1315+
}
1316+
12951317
// An external module with an 'export =' declaration resolves to the target of the 'export =' declaration,
12961318
// and an external module with no 'export =' declaration resolves to the module itself.
12971319
function resolveExternalModuleSymbol(moduleSymbol: Symbol): Symbol {
@@ -17627,6 +17649,10 @@ namespace ts {
1762717649
if (!isExternalOrCommonJsModule(file)) {
1762817650
mergeSymbolTable(globals, file.locals);
1762917651
}
17652+
if (file.patternAmbientModules && file.patternAmbientModules.length) {
17653+
(patternAmbientModules || (patternAmbientModules = [])).push(...file.patternAmbientModules);
17654+
}
17655+
1763017656
if (file.moduleAugmentations.length) {
1763117657
(augmentations || (augmentations = [])).push(file.moduleAugmentations);
1763217658
}

src/compiler/program.ts

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

98-
function hasZeroOrOneAsteriskCharacter(str: string): boolean {
98+
export function hasZeroOrOneAsteriskCharacter(str: string): boolean {
9999
let seenAsterisk = false;
100100
for (let i = 0; i < str.length; i++) {
101101
if (str.charCodeAt(i) === CharacterCodes.asterisk) {
@@ -496,48 +496,23 @@ namespace ts {
496496
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, state.compilerOptions.baseUrl, moduleName);
497497
}
498498

499-
let longestMatchPrefixLength = -1;
500-
let matchedPattern: string;
501-
let matchedStar: string;
502-
499+
// string is for exact match
500+
let matchedPattern: Pattern | string | undefined = undefined;
503501
if (state.compilerOptions.paths) {
504502
if (state.traceEnabled) {
505503
trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName);
506504
}
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-
}
505+
matchedPattern = matchPatternOrExact(Object.keys(state.compilerOptions.paths), moduleName);
533506
}
534507

535508
if (matchedPattern) {
509+
const matchedStar = typeof matchedPattern === "string" ? undefined : matchedText(matchedPattern, moduleName);
510+
const matchedPatternText = typeof matchedPattern === "string" ? matchedPattern : patternText(matchedPattern);
536511
if (state.traceEnabled) {
537-
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPattern);
512+
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText);
538513
}
539-
for (const subst of state.compilerOptions.paths[matchedPattern]) {
540-
const path = matchedStar ? subst.replace("\*", matchedStar) : subst;
514+
for (const subst of state.compilerOptions.paths[matchedPatternText]) {
515+
const path = matchedStar ? subst.replace("*", matchedStar) : subst;
541516
const candidate = normalizePath(combinePaths(state.compilerOptions.baseUrl, path));
542517
if (state.traceEnabled) {
543518
trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path);
@@ -560,6 +535,73 @@ namespace ts {
560535
}
561536
}
562537

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

src/compiler/types.ts

Lines changed: 11 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,16 @@ namespace ts {
21352136
[index: string]: Symbol;
21362137
}
21372138

2139+
export interface Pattern {
2140+
prefix: string;
2141+
suffix: string;
2142+
}
2143+
/** Used to track a `declare module "foo*"`-like declaration. */
2144+
export interface PatternAmbientModule {
2145+
pattern: Pattern;
2146+
symbol: Symbol;
2147+
}
2148+
21382149
/* @internal */
21392150
export const enum NodeCheckFlags {
21402151
TypeChecked = 0x00000001, // Node has been type checked
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
tests/cases/conformance/ambient/declarations.d.ts(6,16): error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character
2+
3+
4+
==== tests/cases/conformance/ambient/user.ts (0 errors) ====
5+
///<reference path="declarations.d.ts" />
6+
import {foo} from "foobarbaz";
7+
foo(0);
8+
9+
import {foos} from "foosball";
10+
11+
==== tests/cases/conformance/ambient/declarations.d.ts (1 errors) ====
12+
declare module "foo*baz" {
13+
export function foo(n: number): void;
14+
}
15+
16+
// Should be an error
17+
declare module "too*many*asterisks" { }
18+
~~~~~~~~~~~~~~~~~~~~
19+
!!! error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character
20+
21+
// Longest prefix wins
22+
declare module "foos*" {
23+
export const foos: number;
24+
}
25+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//// [tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts] ////
2+
3+
//// [declarations.d.ts]
4+
declare module "foo*baz" {
5+
export function foo(n: number): void;
6+
}
7+
8+
// Should be an error
9+
declare module "too*many*asterisks" { }
10+
11+
// Longest prefix wins
12+
declare module "foos*" {
13+
export const foos: number;
14+
}
15+
16+
//// [user.ts]
17+
///<reference path="declarations.d.ts" />
18+
import {foo} from "foobarbaz";
19+
foo(0);
20+
21+
import {foos} from "foosball";
22+
23+
24+
//// [user.js]
25+
"use strict";
26+
///<reference path="declarations.d.ts" />
27+
var foobarbaz_1 = require("foobarbaz");
28+
foobarbaz_1.foo(0);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// @Filename: declarations.d.ts
2+
declare module "foo*baz" {
3+
export function foo(n: number): void;
4+
}
5+
6+
// Should be an error
7+
declare module "too*many*asterisks" { }
8+
9+
// Longest prefix wins
10+
declare module "foos*" {
11+
export const foos: number;
12+
}
13+
14+
// @Filename: user.ts
15+
///<reference path="declarations.d.ts" />
16+
import {foo} from "foobarbaz";
17+
foo(0);
18+
19+
import {foos} from "foosball";

0 commit comments

Comments
 (0)