diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 4aee170f195d4..4f8f9b3f68c7c 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1069,7 +1069,7 @@ namespace ts { const moduleSymbol = resolveExternalModuleName(node, (node.parent).moduleSpecifier); if (moduleSymbol) { - const exportDefaultSymbol = isShorthandAmbientModuleSymbol(moduleSymbol) ? + const exportDefaultSymbol = isUntypedModuleSymbol(moduleSymbol) ? moduleSymbol : moduleSymbol.exports["export="] ? getPropertyOfType(getTypeOfSymbol(moduleSymbol.exports["export="]), "default") : @@ -1145,7 +1145,7 @@ namespace ts { if (targetSymbol) { const name = specifier.propertyName || specifier.name; if (name.text) { - if (isShorthandAmbientModuleSymbol(moduleSymbol)) { + if (isUntypedModuleSymbol(moduleSymbol)) { return moduleSymbol; } @@ -1365,8 +1365,9 @@ namespace ts { } const isRelative = isExternalModuleNameRelative(moduleName); + const quotedName = '"' + moduleName + '"'; if (!isRelative) { - const symbol = getSymbol(globals, '"' + moduleName + '"', SymbolFlags.ValueModule); + const symbol = getSymbol(globals, quotedName, SymbolFlags.ValueModule); if (symbol) { // merged symbol is module declaration symbol combined with all augmentations return getMergedSymbol(symbol); @@ -1395,6 +1396,28 @@ namespace ts { } } + // May be an untyped module. If so, ignore resolutionDiagnostic. + if (!isRelative && resolvedModule && !extensionIsTypeScript(resolvedModule.extension)) { + if (compilerOptions.noImplicitAny) { + if (moduleNotFoundError) { + error(errorNode, + Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type, + moduleReference, + resolvedModule.resolvedFileName); + } + return undefined; + } + + // Create a new symbol to represent the untyped module and store it in globals. + // This provides a name to the module. See the test tests/cases/fourslash/untypedModuleImport.ts + const newSymbol = createSymbol(SymbolFlags.ValueModule, quotedName); + // Module symbols are expected to have 'exports', although since this is an untyped module it can be empty. + newSymbol.exports = createMap(); + // Cache it so subsequent accesses will return the same module. + globals[quotedName] = newSymbol; + return newSymbol; + } + if (moduleNotFoundError) { // report errors only if it was requested if (resolutionDiagnostic) { @@ -3462,7 +3485,7 @@ namespace ts { function getTypeOfFuncClassEnumModule(symbol: Symbol): Type { const links = getSymbolLinks(symbol); if (!links.type) { - if (symbol.valueDeclaration.kind === SyntaxKind.ModuleDeclaration && isShorthandAmbientModuleSymbol(symbol)) { + if (symbol.flags & SymbolFlags.Module && isUntypedModuleSymbol(symbol)) { links.type = anyType; } else { @@ -19011,7 +19034,7 @@ namespace ts { function moduleExportsSomeValue(moduleReferenceExpression: Expression): boolean { let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression); - if (!moduleSymbol || isShorthandAmbientModuleSymbol(moduleSymbol)) { + if (!moduleSymbol || isUntypedModuleSymbol(moduleSymbol)) { // If the module is not found or is shorthand, assume that it may export a value. return true; } @@ -19512,7 +19535,7 @@ namespace ts { (typeReferenceDirectives || (typeReferenceDirectives = [])).push(typeReferenceDirective); } else { - // found at least one entry that does not originate from type reference directive + // found at least one entry that does not originate from type reference directive return undefined; } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0580f7db541e3..e2a2a4b9a0cf4 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2901,6 +2901,10 @@ "category": "Error", "code": 7015 }, + "Could not find a declaration file for module '{0}'. '{1}' implicitly has an 'any' type.": { + "category": "Error", + "code": 7016 + }, "Index signature of object type implicitly has an 'any' type.": { "category": "Error", "code": 7017 diff --git a/src/compiler/program.ts b/src/compiler/program.ts index fab35141ba4af..9e89644f4f18e 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1324,6 +1324,7 @@ namespace ts { // - it's not a top level JavaScript module that exceeded the search max const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModuleJsDepth; // Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs') + // This may still end up being an untyped module -- the file won't be included but imports will be allowed. const shouldAddFile = resolvedFileName && !getResolutionDiagnostic(options, resolution) && !options.noResolve && i < file.imports.length && !elideImport; if (elideImport) { @@ -1571,8 +1572,9 @@ namespace ts { /* @internal */ /** - * Returns a DiagnosticMessage if we can't use a resolved module due to its extension. + * Returns a DiagnosticMessage if we won't include a resolved module due to its extension. * The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to. + * This returns a diagnostic even if the module will be an untyped module. */ export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModule): DiagnosticMessage | undefined { switch (extension) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3da6cf7aa3713..bbffdd470d790 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -406,8 +406,9 @@ namespace ts { ((node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(node)); } - export function isShorthandAmbientModuleSymbol(moduleSymbol: Symbol): boolean { - return isShorthandAmbientModule(moduleSymbol.valueDeclaration); + /** Given a symbol for a module, checks that it is either an untyped import or a shorthand ambient module. */ + export function isUntypedModuleSymbol(moduleSymbol: Symbol): boolean { + return !moduleSymbol.valueDeclaration || isShorthandAmbientModule(moduleSymbol.valueDeclaration); } function isShorthandAmbientModule(node: Node): boolean { diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 47b76793af6af..17e67b053a3ee 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1108,22 +1108,7 @@ namespace Harness { const option = getCommandLineOption(name); if (option) { const errors: ts.Diagnostic[] = []; - switch (option.type) { - case "boolean": - options[option.name] = value.toLowerCase() === "true"; - break; - case "string": - options[option.name] = value; - break; - // If not a primitive, the possible types are specified in what is effectively a map of options. - case "list": - options[option.name] = ts.parseListTypeOption(option, value, errors); - break; - default: - options[option.name] = ts.parseCustomTypeOption(option, value, errors); - break; - } - + options[option.name] = optionValue(option, value, errors); if (errors.length > 0) { throw new Error(`Unknown value '${value}' for compiler option '${name}'.`); } @@ -1135,6 +1120,27 @@ namespace Harness { } } + function optionValue(option: ts.CommandLineOption, value: string, errors: ts.Diagnostic[]): any { + switch (option.type) { + case "boolean": + return value.toLowerCase() === "true"; + case "string": + return value; + case "number": { + const number = parseInt(value, 10); + if (isNaN(number)) { + throw new Error(`Value must be a number, got: ${JSON.stringify(value)}`); + } + return number; + } + // If not a primitive, the possible types are specified in what is effectively a map of options. + case "list": + return ts.parseListTypeOption(option, value, errors); + default: + return ts.parseCustomTypeOption(option, value, errors); + } + } + export interface TestFile { unitName: string; content: string; diff --git a/tests/baselines/reference/untypedModuleImport.js b/tests/baselines/reference/untypedModuleImport.js new file mode 100644 index 0000000000000..e3548856411ee --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport.js @@ -0,0 +1,37 @@ +//// [tests/cases/conformance/moduleResolution/untypedModuleImport.ts] //// + +//// [index.js] +// This tests that importing from a JS file globally works in an untyped way. +// (Assuming we don't have `--noImplicitAny` or `--allowJs`.) + +This file is not processed. + +//// [a.ts] +import * as foo from "foo"; +foo.bar(); + +//// [b.ts] +import foo = require("foo"); +foo(); + +//// [c.ts] +import foo, { bar } from "foo"; +import "./a"; +import "./b"; +foo(bar()); + + +//// [a.js] +"use strict"; +var foo = require("foo"); +foo.bar(); +//// [b.js] +"use strict"; +var foo = require("foo"); +foo(); +//// [c.js] +"use strict"; +var foo_1 = require("foo"); +require("./a"); +require("./b"); +foo_1["default"](foo_1.bar()); diff --git a/tests/baselines/reference/untypedModuleImport.symbols b/tests/baselines/reference/untypedModuleImport.symbols new file mode 100644 index 0000000000000..2cc04ed6efab2 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport.symbols @@ -0,0 +1,25 @@ +=== /c.ts === +import foo, { bar } from "foo"; +>foo : Symbol(foo, Decl(c.ts, 0, 6)) +>bar : Symbol(bar, Decl(c.ts, 0, 13)) + +import "./a"; +import "./b"; +foo(bar()); +>foo : Symbol(foo, Decl(c.ts, 0, 6)) +>bar : Symbol(bar, Decl(c.ts, 0, 13)) + +=== /a.ts === +import * as foo from "foo"; +>foo : Symbol(foo, Decl(a.ts, 0, 6)) + +foo.bar(); +>foo : Symbol(foo, Decl(a.ts, 0, 6)) + +=== /b.ts === +import foo = require("foo"); +>foo : Symbol(foo, Decl(b.ts, 0, 0)) + +foo(); +>foo : Symbol(foo, Decl(b.ts, 0, 0)) + diff --git a/tests/baselines/reference/untypedModuleImport.types b/tests/baselines/reference/untypedModuleImport.types new file mode 100644 index 0000000000000..8a0c7e97ede11 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport.types @@ -0,0 +1,31 @@ +=== /c.ts === +import foo, { bar } from "foo"; +>foo : any +>bar : any + +import "./a"; +import "./b"; +foo(bar()); +>foo(bar()) : any +>foo : any +>bar() : any +>bar : any + +=== /a.ts === +import * as foo from "foo"; +>foo : any + +foo.bar(); +>foo.bar() : any +>foo.bar : any +>foo : any +>bar : any + +=== /b.ts === +import foo = require("foo"); +>foo : any + +foo(); +>foo() : any +>foo : any + diff --git a/tests/baselines/reference/untypedModuleImport_allowJs.js b/tests/baselines/reference/untypedModuleImport_allowJs.js new file mode 100644 index 0000000000000..8ca5115a386e2 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_allowJs.js @@ -0,0 +1,16 @@ +//// [tests/cases/conformance/moduleResolution/untypedModuleImport_allowJs.ts] //// + +//// [index.js] +// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed. + +exports.default = { bar() { return 0; } } + +//// [a.ts] +import foo from "foo"; +foo.bar(); + + +//// [a.js] +"use strict"; +var foo_1 = require("foo"); +foo_1["default"].bar(); diff --git a/tests/baselines/reference/untypedModuleImport_allowJs.symbols b/tests/baselines/reference/untypedModuleImport_allowJs.symbols new file mode 100644 index 0000000000000..d660e8116309d --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_allowJs.symbols @@ -0,0 +1,17 @@ +=== /a.ts === +import foo from "foo"; +>foo : Symbol(foo, Decl(a.ts, 0, 6)) + +foo.bar(); +>foo.bar : Symbol(bar, Decl(index.js, 2, 19)) +>foo : Symbol(foo, Decl(a.ts, 0, 6)) +>bar : Symbol(bar, Decl(index.js, 2, 19)) + +=== /node_modules/foo/index.js === +// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed. + +exports.default = { bar() { return 0; } } +>exports : Symbol(default, Decl(index.js, 0, 0)) +>default : Symbol(default, Decl(index.js, 0, 0)) +>bar : Symbol(bar, Decl(index.js, 2, 19)) + diff --git a/tests/baselines/reference/untypedModuleImport_allowJs.types b/tests/baselines/reference/untypedModuleImport_allowJs.types new file mode 100644 index 0000000000000..5a34fdcb646ff --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_allowJs.types @@ -0,0 +1,22 @@ +=== /a.ts === +import foo from "foo"; +>foo : { bar(): number; } + +foo.bar(); +>foo.bar() : number +>foo.bar : () => number +>foo : { bar(): number; } +>bar : () => number + +=== /node_modules/foo/index.js === +// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed. + +exports.default = { bar() { return 0; } } +>exports.default = { bar() { return 0; } } : { bar(): number; } +>exports.default : any +>exports : any +>default : any +>{ bar() { return 0; } } : { bar(): number; } +>bar : () => number +>0 : 0 + diff --git a/tests/baselines/reference/untypedModuleImport_noImplicitAny.errors.txt b/tests/baselines/reference/untypedModuleImport_noImplicitAny.errors.txt new file mode 100644 index 0000000000000..349a944deebb2 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_noImplicitAny.errors.txt @@ -0,0 +1,13 @@ +/a.ts(1,22): error TS7016: Could not find a declaration file for module 'foo'. '/node_modules/foo/index.js' implicitly has an 'any' type. + + +==== /a.ts (1 errors) ==== + import * as foo from "foo"; + ~~~~~ +!!! error TS7016: Could not find a declaration file for module 'foo'. '/node_modules/foo/index.js' implicitly has an 'any' type. + +==== /node_modules/foo/index.js (0 errors) ==== + // This tests that `--noImplicitAny` disables untyped modules. + + This file is not processed. + \ No newline at end of file diff --git a/tests/baselines/reference/untypedModuleImport_noImplicitAny.js b/tests/baselines/reference/untypedModuleImport_noImplicitAny.js new file mode 100644 index 0000000000000..ab1e1940e3625 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_noImplicitAny.js @@ -0,0 +1,13 @@ +//// [tests/cases/conformance/moduleResolution/untypedModuleImport_noImplicitAny.ts] //// + +//// [index.js] +// This tests that `--noImplicitAny` disables untyped modules. + +This file is not processed. + +//// [a.ts] +import * as foo from "foo"; + + +//// [a.js] +"use strict"; diff --git a/tests/baselines/reference/untypedModuleImport_noLocalImports.errors.txt b/tests/baselines/reference/untypedModuleImport_noLocalImports.errors.txt new file mode 100644 index 0000000000000..0f376ea61ec20 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_noLocalImports.errors.txt @@ -0,0 +1,13 @@ +/a.ts(1,22): error TS6143: Module './foo' was resolved to '/foo.js', but '--allowJs' is not set. + + +==== /a.ts (1 errors) ==== + import * as foo from "./foo"; + ~~~~~~~ +!!! error TS6143: Module './foo' was resolved to '/foo.js', but '--allowJs' is not set. + +==== /foo.js (0 errors) ==== + // This tests that untyped module imports don't happen with local imports. + + This file is not processed. + \ No newline at end of file diff --git a/tests/baselines/reference/untypedModuleImport_noLocalImports.js b/tests/baselines/reference/untypedModuleImport_noLocalImports.js new file mode 100644 index 0000000000000..8fa67a0857f21 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_noLocalImports.js @@ -0,0 +1,13 @@ +//// [tests/cases/conformance/moduleResolution/untypedModuleImport_noLocalImports.ts] //// + +//// [foo.js] +// This tests that untyped module imports don't happen with local imports. + +This file is not processed. + +//// [a.ts] +import * as foo from "./foo"; + + +//// [a.js] +"use strict"; diff --git a/tests/baselines/reference/untypedModuleImport_vsAmbient.js b/tests/baselines/reference/untypedModuleImport_vsAmbient.js new file mode 100644 index 0000000000000..080cd4433754b --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_vsAmbient.js @@ -0,0 +1,23 @@ +//// [tests/cases/conformance/moduleResolution/untypedModuleImport_vsAmbient.ts] //// + +//// [index.js] +// This tests that an ambient module declaration overrides an untyped import. + +This file is not processed. + +//// [declarations.d.ts] +declare module "foo" { + export const x: number; +} + +//// [a.ts] +/// +import { x } from "foo"; +x; + + +//// [a.js] +"use strict"; +/// +var foo_1 = require("foo"); +foo_1.x; diff --git a/tests/baselines/reference/untypedModuleImport_vsAmbient.symbols b/tests/baselines/reference/untypedModuleImport_vsAmbient.symbols new file mode 100644 index 0000000000000..8def3c6a09b6c --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_vsAmbient.symbols @@ -0,0 +1,14 @@ +=== /a.ts === +/// +import { x } from "foo"; +>x : Symbol(x, Decl(a.ts, 1, 8)) + +x; +>x : Symbol(x, Decl(a.ts, 1, 8)) + +=== /declarations.d.ts === +declare module "foo" { + export const x: number; +>x : Symbol(x, Decl(declarations.d.ts, 1, 16)) +} + diff --git a/tests/baselines/reference/untypedModuleImport_vsAmbient.types b/tests/baselines/reference/untypedModuleImport_vsAmbient.types new file mode 100644 index 0000000000000..58b0eabefdfe3 --- /dev/null +++ b/tests/baselines/reference/untypedModuleImport_vsAmbient.types @@ -0,0 +1,14 @@ +=== /a.ts === +/// +import { x } from "foo"; +>x : number + +x; +>x : number + +=== /declarations.d.ts === +declare module "foo" { + export const x: number; +>x : number +} + diff --git a/tests/cases/conformance/moduleResolution/untypedModuleImport.ts b/tests/cases/conformance/moduleResolution/untypedModuleImport.ts new file mode 100644 index 0000000000000..2ea07db3ee496 --- /dev/null +++ b/tests/cases/conformance/moduleResolution/untypedModuleImport.ts @@ -0,0 +1,21 @@ +// @noImplicitReferences: true +// @currentDirectory: / +// This tests that importing from a JS file globally works in an untyped way. +// (Assuming we don't have `--noImplicitAny` or `--allowJs`.) + +// @filename: /node_modules/foo/index.js +This file is not processed. + +// @filename: /a.ts +import * as foo from "foo"; +foo.bar(); + +// @filename: /b.ts +import foo = require("foo"); +foo(); + +// @filename: /c.ts +import foo, { bar } from "foo"; +import "./a"; +import "./b"; +foo(bar()); diff --git a/tests/cases/conformance/moduleResolution/untypedModuleImport_allowJs.ts b/tests/cases/conformance/moduleResolution/untypedModuleImport_allowJs.ts new file mode 100644 index 0000000000000..fe509189d9667 --- /dev/null +++ b/tests/cases/conformance/moduleResolution/untypedModuleImport_allowJs.ts @@ -0,0 +1,12 @@ +// @noImplicitReferences: true +// @currentDirectory: / +// @allowJs: true +// @maxNodeModuleJsDepth: 1 +// Same as untypedModuleImport.ts but with --allowJs, so the package will actually be typed. + +// @filename: /node_modules/foo/index.js +exports.default = { bar() { return 0; } } + +// @filename: /a.ts +import foo from "foo"; +foo.bar(); diff --git a/tests/cases/conformance/moduleResolution/untypedModuleImport_noImplicitAny.ts b/tests/cases/conformance/moduleResolution/untypedModuleImport_noImplicitAny.ts new file mode 100644 index 0000000000000..1aee7c069dedf --- /dev/null +++ b/tests/cases/conformance/moduleResolution/untypedModuleImport_noImplicitAny.ts @@ -0,0 +1,10 @@ +// @noImplicitReferences: true +// @currentDirectory: / +// @noImplicitAny: true +// This tests that `--noImplicitAny` disables untyped modules. + +// @filename: /node_modules/foo/index.js +This file is not processed. + +// @filename: /a.ts +import * as foo from "foo"; diff --git a/tests/cases/conformance/moduleResolution/untypedModuleImport_noLocalImports.ts b/tests/cases/conformance/moduleResolution/untypedModuleImport_noLocalImports.ts new file mode 100644 index 0000000000000..8313628e6d7ee --- /dev/null +++ b/tests/cases/conformance/moduleResolution/untypedModuleImport_noLocalImports.ts @@ -0,0 +1,9 @@ +// @noImplicitReferences: true +// @currentDirectory: / +// This tests that untyped module imports don't happen with local imports. + +// @filename: /foo.js +This file is not processed. + +// @filename: /a.ts +import * as foo from "./foo"; diff --git a/tests/cases/conformance/moduleResolution/untypedModuleImport_vsAmbient.ts b/tests/cases/conformance/moduleResolution/untypedModuleImport_vsAmbient.ts new file mode 100644 index 0000000000000..e157772a5edad --- /dev/null +++ b/tests/cases/conformance/moduleResolution/untypedModuleImport_vsAmbient.ts @@ -0,0 +1,16 @@ +// @noImplicitReferences: true +// @currentDirectory: / +// This tests that an ambient module declaration overrides an untyped import. + +// @filename: /node_modules/foo/index.js +This file is not processed. + +// @filename: /declarations.d.ts +declare module "foo" { + export const x: number; +} + +// @filename: /a.ts +/// +import { x } from "foo"; +x; diff --git a/tests/cases/fourslash/untypedModuleImport.ts b/tests/cases/fourslash/untypedModuleImport.ts new file mode 100644 index 0000000000000..3fd209d480d31 --- /dev/null +++ b/tests/cases/fourslash/untypedModuleImport.ts @@ -0,0 +1,22 @@ +/// + +// @Filename: node_modules/foo/index.js +////{} + +// @Filename: a.ts +////import /*foo*/[|foo|] from /*fooModule*/"foo"; +////[|foo|](); + +goTo.file("a.ts"); +debug.printErrorList(); +verify.numberOfErrorsInCurrentFile(0); + +goTo.marker("fooModule"); +verify.goToDefinitionIs([]); +verify.quickInfoIs('module "foo"'); +verify.referencesAre([]) + +goTo.marker("foo"); +verify.goToDefinitionIs([]); +verify.quickInfoIs("import foo"); +verify.rangesReferenceEachOther();