Skip to content

Commit 4937d9c

Browse files
author
Andy Hanson
committed
Allow untyped imports
1 parent b5ba315 commit 4937d9c

24 files changed

+402
-25
lines changed

src/compiler/checker.ts

+29-6
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ namespace ts {
10691069
const moduleSymbol = resolveExternalModuleName(node, (<ImportDeclaration>node.parent).moduleSpecifier);
10701070

10711071
if (moduleSymbol) {
1072-
const exportDefaultSymbol = isShorthandAmbientModuleSymbol(moduleSymbol) ?
1072+
const exportDefaultSymbol = isUntypedModuleSymbol(moduleSymbol) ?
10731073
moduleSymbol :
10741074
moduleSymbol.exports["export="] ?
10751075
getPropertyOfType(getTypeOfSymbol(moduleSymbol.exports["export="]), "default") :
@@ -1145,7 +1145,7 @@ namespace ts {
11451145
if (targetSymbol) {
11461146
const name = specifier.propertyName || specifier.name;
11471147
if (name.text) {
1148-
if (isShorthandAmbientModuleSymbol(moduleSymbol)) {
1148+
if (isUntypedModuleSymbol(moduleSymbol)) {
11491149
return moduleSymbol;
11501150
}
11511151

@@ -1365,8 +1365,9 @@ namespace ts {
13651365
}
13661366

13671367
const isRelative = isExternalModuleNameRelative(moduleName);
1368+
const quotedName = '"' + moduleName + '"';
13681369
if (!isRelative) {
1369-
const symbol = getSymbol(globals, '"' + moduleName + '"', SymbolFlags.ValueModule);
1370+
const symbol = getSymbol(globals, quotedName, SymbolFlags.ValueModule);
13701371
if (symbol) {
13711372
// merged symbol is module declaration symbol combined with all augmentations
13721373
return getMergedSymbol(symbol);
@@ -1395,6 +1396,28 @@ namespace ts {
13951396
}
13961397
}
13971398

1399+
// May be an untyped module. If so, ignore resolutionDiagnostic.
1400+
if (!isRelative && resolvedModule && !extensionIsTypeScript(resolvedModule.extension)) {
1401+
if (compilerOptions.noImplicitAny) {
1402+
if (moduleNotFoundError) {
1403+
error(errorNode,
1404+
Diagnostics.A_package_for_0_was_found_at_1_but_is_untyped_Because_noImplicitAny_is_enabled_this_package_must_have_a_declaration,
1405+
moduleReference,
1406+
resolvedModule.resolvedFileName);
1407+
}
1408+
return undefined;
1409+
}
1410+
1411+
// Create a new symbol to represent the untyped module and store it in globals.
1412+
// This provides a name to the module. See the test tests/cases/fourslash/untypedModuleImport.ts
1413+
const newSymbol = createSymbol(SymbolFlags.ValueModule, quotedName);
1414+
// Module symbols are expected to have 'exports', although since this is an untyped module it can be empty.
1415+
newSymbol.exports = createMap<Symbol>();
1416+
// Cache it so subsequent accesses will return the same module.
1417+
globals[quotedName] = newSymbol;
1418+
return newSymbol;
1419+
}
1420+
13981421
if (moduleNotFoundError) {
13991422
// report errors only if it was requested
14001423
if (resolutionDiagnostic) {
@@ -3462,7 +3485,7 @@ namespace ts {
34623485
function getTypeOfFuncClassEnumModule(symbol: Symbol): Type {
34633486
const links = getSymbolLinks(symbol);
34643487
if (!links.type) {
3465-
if (symbol.valueDeclaration.kind === SyntaxKind.ModuleDeclaration && isShorthandAmbientModuleSymbol(symbol)) {
3488+
if (symbol.flags & SymbolFlags.Module && isUntypedModuleSymbol(symbol)) {
34663489
links.type = anyType;
34673490
}
34683491
else {
@@ -19011,7 +19034,7 @@ namespace ts {
1901119034

1901219035
function moduleExportsSomeValue(moduleReferenceExpression: Expression): boolean {
1901319036
let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression);
19014-
if (!moduleSymbol || isShorthandAmbientModuleSymbol(moduleSymbol)) {
19037+
if (!moduleSymbol || isUntypedModuleSymbol(moduleSymbol)) {
1901519038
// If the module is not found or is shorthand, assume that it may export a value.
1901619039
return true;
1901719040
}
@@ -19512,7 +19535,7 @@ namespace ts {
1951219535
(typeReferenceDirectives || (typeReferenceDirectives = [])).push(typeReferenceDirective);
1951319536
}
1951419537
else {
19515-
// found at least one entry that does not originate from type reference directive
19538+
// found at least one entry that does not originate from type reference directive
1951619539
return undefined;
1951719540
}
1951819541
}

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -2869,6 +2869,10 @@
28692869
"category": "Error",
28702870
"code": 6143
28712871
},
2872+
"A package for '{0}' was found at '{1}', but is untyped. Because '--noImplicitAny' is enabled, this package must have a declaration.": {
2873+
"category": "Error",
2874+
"code": 6144
2875+
},
28722876
"Variable '{0}' implicitly has an '{1}' type.": {
28732877
"category": "Error",
28742878
"code": 7005

src/compiler/program.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,7 @@ namespace ts {
13241324
// - it's not a top level JavaScript module that exceeded the search max
13251325
const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModuleJsDepth;
13261326
// Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs')
1327+
// This may still end up being an untyped module -- the file won't be included but imports will be allowed.
13271328
const shouldAddFile = resolvedFileName && !getResolutionDiagnostic(options, resolution) && !options.noResolve && i < file.imports.length && !elideImport;
13281329

13291330
if (elideImport) {
@@ -1571,8 +1572,9 @@ namespace ts {
15711572

15721573
/* @internal */
15731574
/**
1574-
* Returns a DiagnosticMessage if we can't use a resolved module due to its extension.
1575+
* Returns a DiagnosticMessage if we won't include a resolved module due to its extension.
15751576
* The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to.
1577+
* This returns a diagnostic even if the module will be an untyped module.
15761578
*/
15771579
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModule): DiagnosticMessage | undefined {
15781580
switch (extension) {

src/compiler/utilities.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,9 @@ namespace ts {
406406
((<ModuleDeclaration>node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(<ModuleDeclaration>node));
407407
}
408408

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

413414
function isShorthandAmbientModule(node: Node): boolean {

src/harness/harness.ts

+22-16
Original file line numberDiff line numberDiff line change
@@ -1108,22 +1108,7 @@ namespace Harness {
11081108
const option = getCommandLineOption(name);
11091109
if (option) {
11101110
const errors: ts.Diagnostic[] = [];
1111-
switch (option.type) {
1112-
case "boolean":
1113-
options[option.name] = value.toLowerCase() === "true";
1114-
break;
1115-
case "string":
1116-
options[option.name] = value;
1117-
break;
1118-
// If not a primitive, the possible types are specified in what is effectively a map of options.
1119-
case "list":
1120-
options[option.name] = ts.parseListTypeOption(<ts.CommandLineOptionOfListType>option, value, errors);
1121-
break;
1122-
default:
1123-
options[option.name] = ts.parseCustomTypeOption(<ts.CommandLineOptionOfCustomType>option, value, errors);
1124-
break;
1125-
}
1126-
1111+
options[option.name] = optionValue(option, value, errors);
11271112
if (errors.length > 0) {
11281113
throw new Error(`Unknown value '${value}' for compiler option '${name}'.`);
11291114
}
@@ -1135,6 +1120,27 @@ namespace Harness {
11351120
}
11361121
}
11371122

1123+
function optionValue(option: ts.CommandLineOption, value: string, errors: ts.Diagnostic[]): any {
1124+
switch (option.type) {
1125+
case "boolean":
1126+
return value.toLowerCase() === "true";
1127+
case "string":
1128+
return value;
1129+
case "number": {
1130+
const number = parseInt(value, 10);
1131+
if (isNaN(number)) {
1132+
throw new Error(`Value must be a number, got: ${JSON.stringify(value)}`);
1133+
}
1134+
return number;
1135+
}
1136+
// If not a primitive, the possible types are specified in what is effectively a map of options.
1137+
case "list":
1138+
return ts.parseListTypeOption(<ts.CommandLineOptionOfListType>option, value, errors);
1139+
default:
1140+
return ts.parseCustomTypeOption(<ts.CommandLineOptionOfCustomType>option, value, errors);
1141+
}
1142+
}
1143+
11381144
export interface TestFile {
11391145
unitName: string;
11401146
content: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//// [tests/cases/conformance/moduleResolution/untypedModuleImport.ts] ////
2+
3+
//// [index.js]
4+
// This tests that importing from a JS file globally works in an untyped way.
5+
// (Assuming we don't have `--noImplicitAny` or `--allowJs`.)
6+
7+
This file is not processed.
8+
9+
//// [a.ts]
10+
import * as foo from "foo";
11+
foo.bar();
12+
13+
//// [b.ts]
14+
import foo = require("foo");
15+
foo();
16+
17+
//// [c.ts]
18+
import foo, { bar } from "foo";
19+
import "./a";
20+
import "./b";
21+
foo(bar());
22+
23+
24+
//// [a.js]
25+
"use strict";
26+
var foo = require("foo");
27+
foo.bar();
28+
//// [b.js]
29+
"use strict";
30+
var foo = require("foo");
31+
foo();
32+
//// [c.js]
33+
"use strict";
34+
var foo_1 = require("foo");
35+
require("./a");
36+
require("./b");
37+
foo_1["default"](foo_1.bar());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
=== /c.ts ===
2+
import foo, { bar } from "foo";
3+
>foo : Symbol(foo, Decl(c.ts, 0, 6))
4+
>bar : Symbol(bar, Decl(c.ts, 0, 13))
5+
6+
import "./a";
7+
import "./b";
8+
foo(bar());
9+
>foo : Symbol(foo, Decl(c.ts, 0, 6))
10+
>bar : Symbol(bar, Decl(c.ts, 0, 13))
11+
12+
=== /a.ts ===
13+
import * as foo from "foo";
14+
>foo : Symbol(foo, Decl(a.ts, 0, 6))
15+
16+
foo.bar();
17+
>foo : Symbol(foo, Decl(a.ts, 0, 6))
18+
19+
=== /b.ts ===
20+
import foo = require("foo");
21+
>foo : Symbol(foo, Decl(b.ts, 0, 0))
22+
23+
foo();
24+
>foo : Symbol(foo, Decl(b.ts, 0, 0))
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
=== /c.ts ===
2+
import foo, { bar } from "foo";
3+
>foo : any
4+
>bar : any
5+
6+
import "./a";
7+
import "./b";
8+
foo(bar());
9+
>foo(bar()) : any
10+
>foo : any
11+
>bar() : any
12+
>bar : any
13+
14+
=== /a.ts ===
15+
import * as foo from "foo";
16+
>foo : any
17+
18+
foo.bar();
19+
>foo.bar() : any
20+
>foo.bar : any
21+
>foo : any
22+
>bar : any
23+
24+
=== /b.ts ===
25+
import foo = require("foo");
26+
>foo : any
27+
28+
foo();
29+
>foo() : any
30+
>foo : any
31+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//// [tests/cases/conformance/moduleResolution/untypedModuleImport_allowJs.ts] ////
2+
3+
//// [index.js]
4+
// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed.
5+
6+
exports.default = { bar() { return 0; } }
7+
8+
//// [a.ts]
9+
import foo from "foo";
10+
foo.bar();
11+
12+
13+
//// [a.js]
14+
"use strict";
15+
var foo_1 = require("foo");
16+
foo_1["default"].bar();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
=== /a.ts ===
2+
import foo from "foo";
3+
>foo : Symbol(foo, Decl(a.ts, 0, 6))
4+
5+
foo.bar();
6+
>foo.bar : Symbol(bar, Decl(index.js, 2, 19))
7+
>foo : Symbol(foo, Decl(a.ts, 0, 6))
8+
>bar : Symbol(bar, Decl(index.js, 2, 19))
9+
10+
=== /node_modules/foo/index.js ===
11+
// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed.
12+
13+
exports.default = { bar() { return 0; } }
14+
>exports : Symbol(default, Decl(index.js, 0, 0))
15+
>default : Symbol(default, Decl(index.js, 0, 0))
16+
>bar : Symbol(bar, Decl(index.js, 2, 19))
17+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
=== /a.ts ===
2+
import foo from "foo";
3+
>foo : { bar(): number; }
4+
5+
foo.bar();
6+
>foo.bar() : number
7+
>foo.bar : () => number
8+
>foo : { bar(): number; }
9+
>bar : () => number
10+
11+
=== /node_modules/foo/index.js ===
12+
// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed.
13+
14+
exports.default = { bar() { return 0; } }
15+
>exports.default = { bar() { return 0; } } : { bar(): number; }
16+
>exports.default : any
17+
>exports : any
18+
>default : any
19+
>{ bar() { return 0; } } : { bar(): number; }
20+
>bar : () => number
21+
>0 : 0
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/a.ts(1,22): error TS6144: A package for 'foo' was found at '/node_modules/foo/index.js', but is untyped. Because '--noImplicitAny' is enabled, this package must have a declaration.
2+
3+
4+
==== /a.ts (1 errors) ====
5+
import * as foo from "foo";
6+
~~~~~
7+
!!! error TS6144: A package for 'foo' was found at '/node_modules/foo/index.js', but is untyped. Because '--noImplicitAny' is enabled, this package must have a declaration.
8+
9+
==== /node_modules/foo/index.js (0 errors) ====
10+
// This tests that `--noImplicitAny` disables untyped modules.
11+
12+
This file is not processed.
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//// [tests/cases/conformance/moduleResolution/untypedModuleImport_noImplicitAny.ts] ////
2+
3+
//// [index.js]
4+
// This tests that `--noImplicitAny` disables untyped modules.
5+
6+
This file is not processed.
7+
8+
//// [a.ts]
9+
import * as foo from "foo";
10+
11+
12+
//// [a.js]
13+
"use strict";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/a.ts(1,22): error TS6143: Module './foo' was resolved to '/foo.js', but '--allowJs' is not set.
2+
3+
4+
==== /a.ts (1 errors) ====
5+
import * as foo from "./foo";
6+
~~~~~~~
7+
!!! error TS6143: Module './foo' was resolved to '/foo.js', but '--allowJs' is not set.
8+
9+
==== /foo.js (0 errors) ====
10+
// This tests that untyped module imports don't happen with local imports.
11+
12+
This file is not processed.
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//// [tests/cases/conformance/moduleResolution/untypedModuleImport_noLocalImports.ts] ////
2+
3+
//// [foo.js]
4+
// This tests that untyped module imports don't happen with local imports.
5+
6+
This file is not processed.
7+
8+
//// [a.ts]
9+
import * as foo from "./foo";
10+
11+
12+
//// [a.js]
13+
"use strict";

0 commit comments

Comments
 (0)