Skip to content

Commit ecb7b00

Browse files
Andymhegazy
Andy
authored andcommitted
Forbid augmentation of untyped module (#11962)
* Forbid augmentation of untyped module * Just use `undefined` for untyped modules
1 parent 49be2e2 commit ecb7b00

7 files changed

+77
-29
lines changed

src/compiler/checker.ts

+21-26
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ namespace ts {
498498
const moduleNotFoundError = !isInAmbientContext(moduleName.parent.parent)
499499
? Diagnostics.Invalid_module_name_in_augmentation_module_0_cannot_be_found
500500
: undefined;
501-
let mainModule = resolveExternalModuleNameWorker(moduleName, moduleName, moduleNotFoundError);
501+
let mainModule = resolveExternalModuleNameWorker(moduleName, moduleName, moduleNotFoundError, /*isForAugmentation*/ true);
502502
if (!mainModule) {
503503
return;
504504
}
@@ -1074,7 +1074,7 @@ namespace ts {
10741074
const moduleSymbol = resolveExternalModuleName(node, (<ImportDeclaration>node.parent).moduleSpecifier);
10751075

10761076
if (moduleSymbol) {
1077-
const exportDefaultSymbol = isUntypedModuleSymbol(moduleSymbol) ?
1077+
const exportDefaultSymbol = isShorthandAmbientModuleSymbol(moduleSymbol) ?
10781078
moduleSymbol :
10791079
moduleSymbol.exports["export="] ?
10801080
getPropertyOfType(getTypeOfSymbol(moduleSymbol.exports["export="]), "default") :
@@ -1150,7 +1150,7 @@ namespace ts {
11501150
if (targetSymbol) {
11511151
const name = specifier.propertyName || specifier.name;
11521152
if (name.text) {
1153-
if (isUntypedModuleSymbol(moduleSymbol)) {
1153+
if (isShorthandAmbientModuleSymbol(moduleSymbol)) {
11541154
return moduleSymbol;
11551155
}
11561156

@@ -1351,16 +1351,16 @@ namespace ts {
13511351
return resolveExternalModuleNameWorker(location, moduleReferenceExpression, Diagnostics.Cannot_find_module_0);
13521352
}
13531353

1354-
function resolveExternalModuleNameWorker(location: Node, moduleReferenceExpression: Expression, moduleNotFoundError: DiagnosticMessage): Symbol {
1354+
function resolveExternalModuleNameWorker(location: Node, moduleReferenceExpression: Expression, moduleNotFoundError: DiagnosticMessage, isForAugmentation = false): Symbol {
13551355
if (moduleReferenceExpression.kind !== SyntaxKind.StringLiteral) {
13561356
return;
13571357
}
13581358

13591359
const moduleReferenceLiteral = <LiteralExpression>moduleReferenceExpression;
1360-
return resolveExternalModule(location, moduleReferenceLiteral.text, moduleNotFoundError, moduleReferenceLiteral);
1360+
return resolveExternalModule(location, moduleReferenceLiteral.text, moduleNotFoundError, moduleReferenceLiteral, isForAugmentation);
13611361
}
13621362

1363-
function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage, errorNode: Node): Symbol {
1363+
function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage, errorNode: Node, isForAugmentation = false): Symbol {
13641364
// Module names are escaped in our symbol table. However, string literal values aren't.
13651365
// Escape the name in the "require(...)" clause to ensure we find the right symbol.
13661366
const moduleName = escapeIdentifier(moduleReference);
@@ -1403,24 +1403,19 @@ namespace ts {
14031403

14041404
// May be an untyped module. If so, ignore resolutionDiagnostic.
14051405
if (!isRelative && resolvedModule && !extensionIsTypeScript(resolvedModule.extension)) {
1406-
if (compilerOptions.noImplicitAny) {
1407-
if (moduleNotFoundError) {
1408-
error(errorNode,
1409-
Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type,
1410-
moduleReference,
1411-
resolvedModule.resolvedFileName);
1412-
}
1413-
return undefined;
1414-
}
1415-
1416-
// Create a new symbol to represent the untyped module and store it in globals.
1417-
// This provides a name to the module. See the test tests/cases/fourslash/untypedModuleImport.ts
1418-
const newSymbol = createSymbol(SymbolFlags.ValueModule, quotedName);
1419-
// Module symbols are expected to have 'exports', although since this is an untyped module it can be empty.
1420-
newSymbol.exports = createMap<Symbol>();
1421-
// Cache it so subsequent accesses will return the same module.
1422-
globals[quotedName] = newSymbol;
1423-
return newSymbol;
1406+
if (isForAugmentation) {
1407+
Debug.assert(!!moduleNotFoundError);
1408+
const diag = Diagnostics.Invalid_module_name_in_augmentation_Module_0_resolves_to_an_untyped_module_at_1_which_cannot_be_augmented;
1409+
error(errorNode, diag, moduleName, resolvedModule.resolvedFileName);
1410+
}
1411+
else if (compilerOptions.noImplicitAny && moduleNotFoundError) {
1412+
error(errorNode,
1413+
Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type,
1414+
moduleReference,
1415+
resolvedModule.resolvedFileName);
1416+
}
1417+
// Failed imports and untyped modules are both treated in an untyped manner; only difference is whether we give a diagnostic first.
1418+
return undefined;
14241419
}
14251420

14261421
if (moduleNotFoundError) {
@@ -3490,7 +3485,7 @@ namespace ts {
34903485
function getTypeOfFuncClassEnumModule(symbol: Symbol): Type {
34913486
const links = getSymbolLinks(symbol);
34923487
if (!links.type) {
3493-
if (symbol.flags & SymbolFlags.Module && isUntypedModuleSymbol(symbol)) {
3488+
if (symbol.flags & SymbolFlags.Module && isShorthandAmbientModuleSymbol(symbol)) {
34943489
links.type = anyType;
34953490
}
34963491
else {
@@ -19063,7 +19058,7 @@ namespace ts {
1906319058

1906419059
function moduleExportsSomeValue(moduleReferenceExpression: Expression): boolean {
1906519060
let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression);
19066-
if (!moduleSymbol || isUntypedModuleSymbol(moduleSymbol)) {
19061+
if (!moduleSymbol || isShorthandAmbientModuleSymbol(moduleSymbol)) {
1906719062
// If the module is not found or is shorthand, assume that it may export a value.
1906819063
return true;
1906919064
}

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,10 @@
18391839
"category": "Error",
18401840
"code": 2664
18411841
},
1842+
"Invalid module name in augmentation. Module '{0}' resolves to an untyped module at '{1}', which cannot be augmented.": {
1843+
"category": "Error",
1844+
"code": 2665
1845+
},
18421846
"Exports and export assignments are not permitted in module augmentations.": {
18431847
"category": "Error",
18441848
"code": 2666

src/compiler/utilities.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,8 @@ namespace ts {
403403
}
404404

405405
/** Given a symbol for a module, checks that it is either an untyped import or a shorthand ambient module. */
406-
export function isUntypedModuleSymbol(moduleSymbol: Symbol): boolean {
407-
return !moduleSymbol.valueDeclaration || isShorthandAmbientModule(moduleSymbol.valueDeclaration);
406+
export function isShorthandAmbientModuleSymbol(moduleSymbol: Symbol): boolean {
407+
return isShorthandAmbientModule(moduleSymbol.valueDeclaration);
408408
}
409409

410410
function isShorthandAmbientModule(node: Node): boolean {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/a.ts(1,16): error TS2665: Invalid module name in augmentation. Module 'foo' resolves to an untyped module at '/node_modules/foo/index.js', which cannot be augmented.
2+
3+
4+
==== /a.ts (1 errors) ====
5+
declare module "foo" {
6+
~~~~~
7+
!!! error TS2665: Invalid module name in augmentation. Module 'foo' resolves to an untyped module at '/node_modules/foo/index.js', which cannot be augmented.
8+
export const x: number;
9+
}
10+
import { x } from "foo";
11+
x;
12+
13+
==== /node_modules/foo/index.js (0 errors) ====
14+
// This tests that augmenting an untyped module is forbidden.
15+
16+
This file is not processed.
17+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//// [tests/cases/conformance/moduleResolution/untypedModuleImport_withAugmentation.ts] ////
2+
3+
//// [index.js]
4+
// This tests that augmenting an untyped module is forbidden.
5+
6+
This file is not processed.
7+
8+
//// [a.ts]
9+
declare module "foo" {
10+
export const x: number;
11+
}
12+
import { x } from "foo";
13+
x;
14+
15+
16+
//// [a.js]
17+
"use strict";
18+
var foo_1 = require("foo");
19+
foo_1.x;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// @noImplicitReferences: true
2+
// @currentDirectory: /
3+
// This tests that augmenting an untyped module is forbidden.
4+
5+
// @filename: /node_modules/foo/index.js
6+
This file is not processed.
7+
8+
// @filename: /a.ts
9+
declare module "foo" {
10+
export const x: number;
11+
}
12+
import { x } from "foo";
13+
x;

tests/cases/fourslash/untypedModuleImport.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ verify.numberOfErrorsInCurrentFile(0);
1313

1414
goTo.marker("fooModule");
1515
verify.goToDefinitionIs([]);
16-
verify.quickInfoIs('module "foo"');
16+
verify.quickInfoIs("");
1717
verify.referencesAre([])
1818

1919
goTo.marker("foo");

0 commit comments

Comments
 (0)