-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Allow wildcard ("*") patterns in ambient module declarations #8939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,7 +95,8 @@ namespace ts { | |
return compilerOptions.traceResolution && host.trace !== undefined; | ||
} | ||
|
||
function hasZeroOrOneAsteriskCharacter(str: string): boolean { | ||
/* @internal */ | ||
export function hasZeroOrOneAsteriskCharacter(str: string): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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<T>(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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1658,6 +1658,7 @@ namespace ts { | |
/* @internal */ resolvedTypeReferenceDirectiveNames: Map<ResolvedTypeReferenceDirective>; | ||
/* @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. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
/* @internal */ | ||
export interface Pattern { | ||
prefix: string; | ||
suffix: string; | ||
} | ||
|
||
/** Used to track a `declare module "foo*"`-like declaration. */ | ||
/* @internal */ | ||
export interface PatternAmbientModule { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and this too |
||
pattern: Pattern; | ||
symbol: Symbol; | ||
} | ||
|
||
/* @internal */ | ||
export const enum NodeCheckFlags { | ||
TypeChecked = 0x00000001, // Node has been type checked | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
tests/cases/conformance/ambient/declarations.d.ts(10,16): error TS5061: Pattern 'too*many*asterisks' can have at most one '*' character | ||
|
||
|
||
==== tests/cases/conformance/ambient/user.ts (0 errors) ==== | ||
///<reference path="declarations.d.ts" /> | ||
import {foo, baz} from "foobarbaz"; | ||
foo(baz); | ||
|
||
import {foos} from "foosball"; | ||
|
||
==== tests/cases/conformance/ambient/declarations.d.ts (1 errors) ==== | ||
declare module "foo*baz" { | ||
export function foo(n: number): void; | ||
} | ||
// Augmentations still work | ||
declare module "foo*baz" { | ||
export const baz: number; | ||
} | ||
|
||
// 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; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
//// [tests/cases/conformance/ambient/ambientDeclarationsPatterns.ts] //// | ||
|
||
//// [declarations.d.ts] | ||
declare module "foo*baz" { | ||
export function foo(n: number): void; | ||
} | ||
// Augmentations still work | ||
declare module "foo*baz" { | ||
export const baz: number; | ||
} | ||
|
||
// Should be an error | ||
declare module "too*many*asterisks" { } | ||
|
||
// Longest prefix wins | ||
declare module "foos*" { | ||
export const foos: number; | ||
} | ||
|
||
//// [user.ts] | ||
///<reference path="declarations.d.ts" /> | ||
import {foo, baz} from "foobarbaz"; | ||
foo(baz); | ||
|
||
import {foos} from "foosball"; | ||
|
||
|
||
//// [user.js] | ||
"use strict"; | ||
///<reference path="declarations.d.ts" /> | ||
var foobarbaz_1 = require("foobarbaz"); | ||
foobarbaz_1.foo(foobarbaz_1.baz); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// @Filename: declarations.d.ts | ||
declare module "foo*baz" { | ||
export function foo(n: number): void; | ||
} | ||
// Augmentations still work | ||
declare module "foo*baz" { | ||
export const baz: number; | ||
} | ||
|
||
// Should be an error | ||
declare module "too*many*asterisks" { } | ||
|
||
// Longest prefix wins | ||
declare module "foos*" { | ||
export const foos: number; | ||
} | ||
|
||
// @Filename: user.ts | ||
///<reference path="declarations.d.ts" /> | ||
import {foo, baz} from "foobarbaz"; | ||
foo(baz); | ||
|
||
import {foos} from "foosball"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i remember we had issues with this pattern for diagnostics, and concat fared better there. consider using
patternAmbientModules = concatenate(patternAmbientModules , file.patternAmbientModules)
instead.