diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 515789a3fd..3bc2ffe8f1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1857,7 +1857,15 @@ namespace ts { combineValueAndTypeSymbols(symbolFromVariable, symbolFromModule) : symbolFromModule || symbolFromVariable; if (!symbol) { - error(name, Diagnostics.Module_0_has_no_exported_member_1, getFullyQualifiedName(moduleSymbol), declarationNameToString(name)); + const moduleName = getFullyQualifiedName(moduleSymbol); + const declarationName = declarationNameToString(name); + const suggestion = getSuggestionForNonexistentModule(name, targetSymbol); + if (suggestion !== undefined) { + error(name, Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_2, moduleName, declarationName, suggestion); + } + else { + error(name, Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); + } } return symbol; } @@ -16218,6 +16226,11 @@ namespace ts { return result && symbolName(result); } + function getSuggestionForNonexistentModule(name: Identifier, targetModule: Symbol): string | undefined { + const suggestion = targetModule.exports && getSpellingSuggestionForName(idText(name), getExportsOfModuleAsArray(targetModule), SymbolFlags.ModuleMember); + return suggestion && symbolName(suggestion); + } + /** * Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough. * Names less than length 3 only check for case-insensitive equality, not levenshtein distance. diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index ced3699c6d..a76a71fb40 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2317,6 +2317,10 @@ "category": "Error", "code": 2723 }, + "Module '{0}' has no exported member '{1}'. Did you mean '{2}'?": { + "category": "Error", + "code": 2724 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", "code": 4000 diff --git a/tests/baselines/reference/exportSpellingSuggestion.errors.txt b/tests/baselines/reference/exportSpellingSuggestion.errors.txt new file mode 100644 index 0000000000..fefae72351 --- /dev/null +++ b/tests/baselines/reference/exportSpellingSuggestion.errors.txt @@ -0,0 +1,13 @@ +tests/cases/conformance/es6/modules/b.ts(1,10): error TS2724: Module '"tests/cases/conformance/es6/modules/a"' has no exported member 'assertNevar'. Did you mean 'assertNever'? + + +==== tests/cases/conformance/es6/modules/a.ts (0 errors) ==== + export function assertNever(x: never, msg: string) { + throw new Error("Unexpected " + msg); + } + +==== tests/cases/conformance/es6/modules/b.ts (1 errors) ==== + import { assertNevar } from "./a"; + ~~~~~~~~~~~ +!!! error TS2724: Module '"tests/cases/conformance/es6/modules/a"' has no exported member 'assertNevar'. Did you mean 'assertNever'? + \ No newline at end of file diff --git a/tests/baselines/reference/exportSpellingSuggestion.js b/tests/baselines/reference/exportSpellingSuggestion.js new file mode 100644 index 0000000000..66bf80f609 --- /dev/null +++ b/tests/baselines/reference/exportSpellingSuggestion.js @@ -0,0 +1,21 @@ +//// [tests/cases/conformance/es6/modules/exportSpellingSuggestion.ts] //// + +//// [a.ts] +export function assertNever(x: never, msg: string) { + throw new Error("Unexpected " + msg); +} + +//// [b.ts] +import { assertNevar } from "./a"; + + +//// [a.js] +"use strict"; +exports.__esModule = true; +function assertNever(x, msg) { + throw new Error("Unexpected " + msg); +} +exports.assertNever = assertNever; +//// [b.js] +"use strict"; +exports.__esModule = true; diff --git a/tests/baselines/reference/exportSpellingSuggestion.symbols b/tests/baselines/reference/exportSpellingSuggestion.symbols new file mode 100644 index 0000000000..ad22b4bbeb --- /dev/null +++ b/tests/baselines/reference/exportSpellingSuggestion.symbols @@ -0,0 +1,15 @@ +=== tests/cases/conformance/es6/modules/a.ts === +export function assertNever(x: never, msg: string) { +>assertNever : Symbol(assertNever, Decl(a.ts, 0, 0)) +>x : Symbol(x, Decl(a.ts, 0, 28)) +>msg : Symbol(msg, Decl(a.ts, 0, 37)) + + throw new Error("Unexpected " + msg); +>Error : Symbol(Error, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>msg : Symbol(msg, Decl(a.ts, 0, 37)) +} + +=== tests/cases/conformance/es6/modules/b.ts === +import { assertNevar } from "./a"; +>assertNevar : Symbol(assertNevar, Decl(b.ts, 0, 8)) + diff --git a/tests/baselines/reference/exportSpellingSuggestion.types b/tests/baselines/reference/exportSpellingSuggestion.types new file mode 100644 index 0000000000..134da4d700 --- /dev/null +++ b/tests/baselines/reference/exportSpellingSuggestion.types @@ -0,0 +1,18 @@ +=== tests/cases/conformance/es6/modules/a.ts === +export function assertNever(x: never, msg: string) { +>assertNever : (x: never, msg: string) => void +>x : never +>msg : string + + throw new Error("Unexpected " + msg); +>new Error("Unexpected " + msg) : Error +>Error : ErrorConstructor +>"Unexpected " + msg : string +>"Unexpected " : "Unexpected " +>msg : string +} + +=== tests/cases/conformance/es6/modules/b.ts === +import { assertNevar } from "./a"; +>assertNevar : any + diff --git a/tests/cases/conformance/es6/modules/exportSpellingSuggestion.ts b/tests/cases/conformance/es6/modules/exportSpellingSuggestion.ts new file mode 100644 index 0000000000..c637327086 --- /dev/null +++ b/tests/cases/conformance/es6/modules/exportSpellingSuggestion.ts @@ -0,0 +1,7 @@ +// @filename: a.ts +export function assertNever(x: never, msg: string) { + throw new Error("Unexpected " + msg); +} + +// @filename: b.ts +import { assertNevar } from "./a";