Skip to content
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

Triple-slash reference type directives can override the import mode used for their resolution #47732

Merged
merged 6 commits into from Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/compiler/checker.ts
Expand Up @@ -42120,19 +42120,19 @@ namespace ts {
// this variable and functions that use it are deliberately moved here from the outer scope
// to avoid scope pollution
const resolvedTypeReferenceDirectives = host.getResolvedTypeReferenceDirectives();
let fileToDirective: ESMap<string, string>;
let fileToDirective: ESMap<string, [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined]>;
if (resolvedTypeReferenceDirectives) {
// populate reverse mapping: file path -> type reference directive that was resolved to this file
fileToDirective = new Map<string, string>();
resolvedTypeReferenceDirectives.forEach((resolvedDirective, key) => {
fileToDirective = new Map<string, [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined]>();
resolvedTypeReferenceDirectives.forEach((resolvedDirective, key, mode) => {
if (!resolvedDirective || !resolvedDirective.resolvedFileName) {
return;
}
const file = host.getSourceFile(resolvedDirective.resolvedFileName);
if (file) {
// Add the transitive closure of path references loaded by this file (as long as they are not)
// part of an existing type reference.
addReferencedFilesToTypeDirective(file, key);
addReferencedFilesToTypeDirective(file, key, mode);
}
});
}
Expand Down Expand Up @@ -42255,7 +42255,7 @@ namespace ts {
}

// defined here to avoid outer scope pollution
function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): string[] | undefined {
function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined {
// program does not have any files with type reference directives - bail out
if (!fileToDirective) {
return undefined;
Expand All @@ -42273,13 +42273,13 @@ namespace ts {
}

// defined here to avoid outer scope pollution
function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[] | undefined {
function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined {
// program does not have any files with type reference directives - bail out
if (!fileToDirective || !isSymbolFromTypeDeclarationFile(symbol)) {
return undefined;
}
// check what declarations in the symbol can contribute to the target meaning
let typeReferenceDirectives: string[] | undefined;
let typeReferenceDirectives: [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined;
for (const decl of symbol.declarations!) {
// check meaning of the local symbol to see if declaration needs to be analyzed further
if (decl.symbol && decl.symbol.flags & meaning!) {
Expand Down Expand Up @@ -42330,14 +42330,14 @@ namespace ts {
return false;
}

function addReferencedFilesToTypeDirective(file: SourceFile, key: string) {
function addReferencedFilesToTypeDirective(file: SourceFile, key: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
if (fileToDirective.has(file.path)) return;
fileToDirective.set(file.path, key);
for (const { fileName } of file.referencedFiles) {
fileToDirective.set(file.path, [key, mode]);
for (const { fileName, resolutionMode } of file.referencedFiles) {
const resolvedFile = resolveTripleslashReference(fileName, file.fileName);
const referencedFile = host.getSourceFile(resolvedFile);
if (referencedFile) {
addReferencedFilesToTypeDirective(referencedFile, key);
addReferencedFilesToTypeDirective(referencedFile, key, resolutionMode || file.impliedNodeFormat);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -1400,6 +1400,14 @@
"category": "Error",
"code": 1451
},
"Resolution modes are only supported when `moduleResolution` is `node12` or `nodenext`.": {
"category": "Error",
"code": 1452
},
"`resolution-mode` should be either `require` or `import`.": {
"category": "Error",
"code": 1453
},

"The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.": {
"category": "Error",
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/emitter.ts
Expand Up @@ -3993,8 +3993,11 @@ namespace ts {
}
for (const directive of types) {
const pos = writer.getTextPos();
writeComment(`/// <reference types="${directive.fileName}" />`);
if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Type, data: directive.fileName });
const resolutionMode = directive.resolutionMode && directive.resolutionMode !== currentSourceFile?.impliedNodeFormat
? `resolution-mode="${directive.resolutionMode === ModuleKind.ESNext ? "import" : "require"}"`
: "";
writeComment(`/// <reference types="${directive.fileName}" ${resolutionMode}/>`);
if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: !directive.resolutionMode ? BundleFileSectionKind.Type : directive.resolutionMode === ModuleKind.ESNext ? BundleFileSectionKind.TypeResolutionModeImport : BundleFileSectionKind.TypeResolutionModeRequire, data: directive.fileName });
writeLine();
}
for (const directive of libs) {
Expand Down
12 changes: 10 additions & 2 deletions src/compiler/factory/nodeFactory.ts
Expand Up @@ -6414,7 +6414,7 @@ namespace ts {
let prologues: UnparsedPrologue[] | undefined;
let helpers: UnscopedEmitHelper[] | undefined;
let referencedFiles: FileReference[] | undefined;
let typeReferenceDirectives: string[] | undefined;
let typeReferenceDirectives: FileReference[] | undefined;
let libReferenceDirectives: FileReference[] | undefined;
let prependChildren: UnparsedTextLike[] | undefined;
let texts: UnparsedSourceText[] | undefined;
Expand All @@ -6435,7 +6435,13 @@ namespace ts {
referencedFiles = append(referencedFiles, { pos: -1, end: -1, fileName: section.data });
break;
case BundleFileSectionKind.Type:
typeReferenceDirectives = append(typeReferenceDirectives, section.data);
typeReferenceDirectives = append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data });
break;
case BundleFileSectionKind.TypeResolutionModeImport:
typeReferenceDirectives = append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ModuleKind.ESNext });
break;
case BundleFileSectionKind.TypeResolutionModeRequire:
typeReferenceDirectives = append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ModuleKind.CommonJS });
break;
case BundleFileSectionKind.Lib:
libReferenceDirectives = append(libReferenceDirectives, { pos: -1, end: -1, fileName: section.data });
Expand Down Expand Up @@ -6496,6 +6502,8 @@ namespace ts {
case BundleFileSectionKind.NoDefaultLib:
case BundleFileSectionKind.Reference:
case BundleFileSectionKind.Type:
case BundleFileSectionKind.TypeResolutionModeImport:
case BundleFileSectionKind.TypeResolutionModeRequire:
case BundleFileSectionKind.Lib:
syntheticReferences = append(syntheticReferences, setTextRange(factory.createUnparsedSyntheticReference(section), section));
break;
Expand Down
29 changes: 22 additions & 7 deletions src/compiler/moduleNameResolver.ts
Expand Up @@ -297,15 +297,15 @@ namespace ts {
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
* is assumed to be the same as root directory of the project.
*/
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache, resolutionMode?: SourceFile["impliedNodeFormat"]): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(options, host);
if (redirectedReference) {
options = redirectedReference.commandLine.options;
}

const containingDirectory = containingFile ? getDirectoryPath(containingFile) : undefined;
const perFolderCache = containingDirectory ? cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference) : undefined;
let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName, /*mode*/ undefined);
let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName, /*mode*/ resolutionMode);
if (result) {
if (traceEnabled) {
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1, typeReferenceDirectiveName, containingFile);
Expand Down Expand Up @@ -340,8 +340,19 @@ namespace ts {
}

const failedLookupLocations: string[] = [];
const features = getDefaultNodeResolutionFeatures(options);
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions: ["node", "require", "types"] };
let features = getDefaultNodeResolutionFeatures(options);
// Unlike `import` statements, whose mode-calculating APIs are all guaranteed to return `undefined` if we're in an un-mode-ed module resolution
// setting, type references will return their target mode regardless of options because of how the parser works, so we guard against the mode being
// set in a non-modal module resolution setting here. Do note that our behavior is not particularly well defined when these mode-overriding imports
// are present in a non-modal project; while in theory we'd like to either ignore the mode or provide faithful modern resolution, depending on what we feel is best,
// in practice, not every cache has the options available to intelligently make the choice to ignore the mode request, and it's unclear how modern "faithful modern
// resolution" should be (`node12`? `nodenext`?). As such, witnessing a mode-overriding triple-slash reference in a non-modal module resolution
// context should _probably_ be an error - and that should likely be handled by the `Program` (which is what we do).
if (resolutionMode === ModuleKind.ESNext && (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext)) {
features |= NodeResolutionFeatures.EsmMode;
}
const conditions = features & NodeResolutionFeatures.Exports ? features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] : [];
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions };
let resolved = primaryLookup();
let primary = true;
if (!resolved) {
Expand All @@ -362,7 +373,7 @@ namespace ts {
};
}
result = { resolvedTypeReferenceDirective, failedLookupLocations };
perFolderCache?.set(typeReferenceDirectiveName, /*mode*/ undefined, result);
perFolderCache?.set(typeReferenceDirectiveName, /*mode*/ resolutionMode, result);
if (traceEnabled) traceResult(result);
return result;

Expand Down Expand Up @@ -733,11 +744,15 @@ namespace ts {
}

/* @internal */
export function zipToModeAwareCache<V>(file: SourceFile, keys: readonly string[], values: readonly V[]): ModeAwareCache<V> {
export function zipToModeAwareCache<V>(file: SourceFile, keys: readonly string[] | readonly FileReference[], values: readonly V[]): ModeAwareCache<V> {
Debug.assert(keys.length === values.length);
const map = createModeAwareCache<V>();
for (let i = 0; i < keys.length; ++i) {
map.set(keys[i], getModeForResolutionAtIndex(file, i), values[i]);
const entry = keys[i];
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.
const name = !isString(entry) ? entry.fileName.toLowerCase() : entry;
const mode = !isString(entry) ? entry.resolutionMode || file.impliedNodeFormat : getModeForResolutionAtIndex(file, i);
map.set(name, mode, values[i]);
}
return map;
}
Expand Down
19 changes: 17 additions & 2 deletions src/compiler/parser.ts
Expand Up @@ -9305,6 +9305,20 @@ namespace ts {
moduleName?: string;
}

function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ModuleKind.ESNext | ModuleKind.CommonJS | undefined {
if (!mode) {
return undefined;
}
if (mode === "import") {
return ModuleKind.ESNext;
}
if (mode === "require") {
return ModuleKind.CommonJS;
}
reportDiagnostic(pos, end - pos, Diagnostics.resolution_mode_should_be_either_require_or_import);
return undefined;
}

/*@internal*/
export function processCommentPragmas(context: PragmaContext, sourceText: string): void {
const pragmas: PragmaPseudoMapEntry[] = [];
Expand Down Expand Up @@ -9350,12 +9364,13 @@ namespace ts {
const typeReferenceDirectives = context.typeReferenceDirectives;
const libReferenceDirectives = context.libReferenceDirectives;
forEach(toArray(entryOrList) as PragmaPseudoMap["reference"][], arg => {
const { types, lib, path } = arg.arguments;
const { types, lib, path, ["resolution-mode"]: res } = arg.arguments;
if (arg.arguments["no-default-lib"]) {
context.hasNoDefaultLib = true;
}
else if (types) {
typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value });
const parsed = parseResolutionMode(res, types.pos, types.end, reportDiagnostic);
typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value, ...(parsed ? { resolutionMode: parsed } : {}) });
}
else if (lib) {
libReferenceDirectives.push({ pos: lib.pos, end: lib.end, fileName: lib.value });
Expand Down