diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 48083056ff8b0..1ecf30690a0bc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -833,11 +833,12 @@ namespace ts { return emitResolver; } - function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { + function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic { const diagnostic = location ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); diagnostics.add(diagnostic); + return diagnostic; } function addErrorOrSuggestion(isError: boolean, diagnostic: DiagnosticWithLocation) { @@ -10491,18 +10492,21 @@ namespace ts { } } - diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode!, errorInfo)); // TODO: GH#18217 - } - // Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement - if (headMessage && errorNode && !result && source.symbol) { - const links = getSymbolLinks(source.symbol); - if (links.originatingImport && !isImportCall(links.originatingImport)) { - const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target!), target, relation, /*errorNode*/ undefined); - if (helpfulRetry) { - // Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import - diagnostics.add(createDiagnosticForNode(links.originatingImport, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime)); + let relatedInformation: DiagnosticRelatedInformation[] | undefined; + // Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement + if (headMessage && errorNode && !result && source.symbol) { + const links = getSymbolLinks(source.symbol); + if (links.originatingImport && !isImportCall(links.originatingImport)) { + const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target!), target, relation, /*errorNode*/ undefined); + if (helpfulRetry) { + // Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import + const diag = createDiagnosticForNode(links.originatingImport, Diagnostics.Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead); + relatedInformation = append(relatedInformation, diag); // Cause the error to appear with the error that triggered it + } } } + + diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode!, errorInfo, relatedInformation)); // TODO: GH#18217 } return result !== Ternary.False; @@ -18865,14 +18869,13 @@ namespace ts { } function invocationError(node: Node, apparentType: Type, kind: SignatureKind) { - error(node, kind === SignatureKind.Call + invocationErrorRecovery(apparentType, kind, error(node, kind === SignatureKind.Call ? Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures : Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature - , typeToString(apparentType)); - invocationErrorRecovery(apparentType, kind); + , typeToString(apparentType))); } - function invocationErrorRecovery(apparentType: Type, kind: SignatureKind) { + function invocationErrorRecovery(apparentType: Type, kind: SignatureKind, diagnostic: Diagnostic) { if (!apparentType.symbol) { return; } @@ -18882,7 +18885,8 @@ namespace ts { if (importNode && !isImportCall(importNode)) { const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target!), kind); if (!sigs || !sigs.length) return; - error(importNode, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime); + diagnostic.relatedInformation = diagnostic.relatedInformation || []; + diagnostic.relatedInformation.push(createDiagnosticForNode(importNode, Diagnostics.Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead)); } } @@ -18961,8 +18965,9 @@ namespace ts { if (!callSignatures.length) { let errorInfo = chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType)); errorInfo = chainDiagnosticMessages(errorInfo, headMessage); - diagnostics.add(createDiagnosticForNodeFromMessageChain(node, errorInfo)); - invocationErrorRecovery(apparentType, SignatureKind.Call); + const diag = createDiagnosticForNodeFromMessageChain(node, errorInfo); + diagnostics.add(diag); + invocationErrorRecovery(apparentType, SignatureKind.Call, diag); return resolveErrorCall(node); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 0e65011e0e470..3289271e7c6df 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -252,7 +252,9 @@ namespace ts { const gutterSeparator = " "; const resetEscapeSequence = "\u001b[0m"; const ellipsis = "..."; - function getCategoryFormat(category: DiagnosticCategory): string { + const halfIndent = " "; + const indent = " "; + function getCategoryFormat(category: DiagnosticCategory): ForegroundColorEscapeSequences { switch (category) { case DiagnosticCategory.Error: return ForegroundColorEscapeSequences.Red; case DiagnosticCategory.Warning: return ForegroundColorEscapeSequences.Yellow; @@ -273,68 +275,79 @@ namespace ts { return s; } + function formatCodeSpan(file: SourceFile, start: number, length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, host: FormatDiagnosticsHost) { + const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); + const { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start + length); + const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line; + + const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; + let gutterWidth = (lastLine + 1 + "").length; + if (hasMoreThanFiveLines) { + gutterWidth = Math.max(ellipsis.length, gutterWidth); + } + + let context = ""; + for (let i = firstLine; i <= lastLine; i++) { + context += host.getNewLine(); + // If the error spans over 5 lines, we'll only show the first 2 and last 2 lines, + // so we'll skip ahead to the second-to-last line. + if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) { + context += indent + formatColorAndReset(padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); + i = lastLine - 1; + } + + const lineStart = getPositionOfLineAndCharacter(file, i, 0); + const lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; + let lineContent = file.text.slice(lineStart, lineEnd); + lineContent = lineContent.replace(/\s+$/g, ""); // trim from end + lineContent = lineContent.replace("\t", " "); // convert tabs to single spaces + + // Output the gutter and the actual contents of the line. + context += indent + formatColorAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; + context += lineContent + host.getNewLine(); + + // Output the gutter and the error span for the line using tildes. + context += indent + formatColorAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator; + context += squiggleColor; + if (i === firstLine) { + // If we're on the last line, then limit it to the last character of the last line. + // Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position. + const lastCharForLine = i === lastLine ? lastLineChar : undefined; + + context += lineContent.slice(0, firstLineChar).replace(/\S/g, " "); + context += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~"); + } + else if (i === lastLine) { + context += lineContent.slice(0, lastLineChar).replace(/./g, "~"); + } + else { + // Squiggle the entire line. + context += lineContent.replace(/./g, "~"); + } + context += resetEscapeSequence; + } + return context; + } + + function formatLocation(file: SourceFile, start: number, host: FormatDiagnosticsHost) { + const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); // TODO: GH#18217 + const relativeFileName = host ? convertToRelativePath(file.fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)) : file.fileName; + + let output = ""; + output += formatColorAndReset(relativeFileName, ForegroundColorEscapeSequences.Cyan); + output += ":"; + output += formatColorAndReset(`${firstLine + 1}`, ForegroundColorEscapeSequences.Yellow); + output += ":"; + output += formatColorAndReset(`${firstLineChar + 1}`, ForegroundColorEscapeSequences.Yellow); + return output; + } + export function formatDiagnosticsWithColorAndContext(diagnostics: ReadonlyArray, host: FormatDiagnosticsHost): string { let output = ""; for (const diagnostic of diagnostics) { - let context = ""; if (diagnostic.file) { - const { start, length, file } = diagnostic; - const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start!); // TODO: GH#18217 - const { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start! + length!); - const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line; - const relativeFileName = host ? convertToRelativePath(file.fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)) : file.fileName; - - const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; - let gutterWidth = (lastLine + 1 + "").length; - if (hasMoreThanFiveLines) { - gutterWidth = Math.max(ellipsis.length, gutterWidth); - } - - for (let i = firstLine; i <= lastLine; i++) { - context += host.getNewLine(); - // If the error spans over 5 lines, we'll only show the first 2 and last 2 lines, - // so we'll skip ahead to the second-to-last line. - if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) { - context += formatColorAndReset(padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); - i = lastLine - 1; - } - - const lineStart = getPositionOfLineAndCharacter(file, i, 0); - const lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; - let lineContent = file.text.slice(lineStart, lineEnd); - lineContent = lineContent.replace(/\s+$/g, ""); // trim from end - lineContent = lineContent.replace("\t", " "); // convert tabs to single spaces - - // Output the gutter and the actual contents of the line. - context += formatColorAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; - context += lineContent + host.getNewLine(); - - // Output the gutter and the error span for the line using tildes. - context += formatColorAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator; - context += ForegroundColorEscapeSequences.Red; - if (i === firstLine) { - // If we're on the last line, then limit it to the last character of the last line. - // Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position. - const lastCharForLine = i === lastLine ? lastLineChar : undefined; - - context += lineContent.slice(0, firstLineChar).replace(/\S/g, " "); - context += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~"); - } - else if (i === lastLine) { - context += lineContent.slice(0, lastLineChar).replace(/./g, "~"); - } - else { - // Squiggle the entire line. - context += lineContent.replace(/./g, "~"); - } - context += resetEscapeSequence; - } - - output += formatColorAndReset(relativeFileName, ForegroundColorEscapeSequences.Cyan); - output += ":"; - output += formatColorAndReset(`${firstLine + 1}`, ForegroundColorEscapeSequences.Yellow); - output += ":"; - output += formatColorAndReset(`${firstLineChar + 1}`, ForegroundColorEscapeSequences.Yellow); + const { file, start } = diagnostic; + output += formatLocation(file, start!, host); // TODO: GH#18217 output += " - "; } @@ -344,7 +357,19 @@ namespace ts { if (diagnostic.file) { output += host.getNewLine(); - output += context; + output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217 + if (diagnostic.relatedInformation) { + output += host.getNewLine(); + for (const { file, start, length, messageText } of diagnostic.relatedInformation) { + if (file) { + output += host.getNewLine(); + output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217 + output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217 + } + output += host.getNewLine(); + output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine()); + } + } } output += host.getNewLine(); diff --git a/src/parser/diagnosticMessages.json b/src/parser/diagnosticMessages.json index 599d81c499778..24b3031b8771e 100644 --- a/src/parser/diagnosticMessages.json +++ b/src/parser/diagnosticMessages.json @@ -3828,7 +3828,7 @@ "category": "Message", "code": 7037 }, - "A namespace-style import cannot be called or constructed, and will cause a failure at runtime.": { + "Type originates at this import. A namespace-style import cannot be called or constructed, and will cause a failure at runtime. Consider using a default import or import require here instead.": { "category": "Error", "code": 7038 }, diff --git a/src/parser/types.ts b/src/parser/types.ts index 7fb2dc0a94f52..9e520241faec1 100644 --- a/src/parser/types.ts +++ b/src/parser/types.ts @@ -4193,16 +4193,19 @@ namespace ts { next?: DiagnosticMessageChain; } - export interface Diagnostic { - file: SourceFile | undefined; - start: number | undefined; - length: number | undefined; - messageText: string | DiagnosticMessageChain; + export interface Diagnostic extends DiagnosticRelatedInformation { category: DiagnosticCategory; /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ reportsUnnecessary?: {}; code: number; source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; + } + export interface DiagnosticRelatedInformation { + file: SourceFile | undefined; + start: number | undefined; + length: number | undefined; + messageText: string | DiagnosticMessageChain; } export interface DiagnosticWithLocation extends Diagnostic { file: SourceFile; diff --git a/src/parser/utilities.ts b/src/parser/utilities.ts index 5c0b00132c260..68482403377d2 100644 --- a/src/parser/utilities.ts +++ b/src/parser/utilities.ts @@ -799,7 +799,7 @@ namespace ts { return createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2, arg3); } - export function createDiagnosticForNodeFromMessageChain(node: Node, messageChain: DiagnosticMessageChain): DiagnosticWithLocation { + export function createDiagnosticForNodeFromMessageChain(node: Node, messageChain: DiagnosticMessageChain, relatedInformation?: DiagnosticRelatedInformation[]): DiagnosticWithLocation { const sourceFile = getSourceFileOfNode(node); const span = getErrorSpanForNode(sourceFile, node); return { @@ -808,7 +808,8 @@ namespace ts { length: span.length, code: messageChain.code, category: messageChain.category, - messageText: messageChain.next ? messageChain : messageChain.messageText + messageText: messageChain.next ? messageChain : messageChain.messageText, + relatedInformation }; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3082dbae6aa1e..4fed23363ebc9 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -466,6 +466,7 @@ namespace ts.server.protocol { code: number; /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ reportsUnnecessary?: {}; + relatedInformation?: DiagnosticRelatedInformation[]; } /** @@ -2215,6 +2216,11 @@ namespace ts.server.protocol { reportsUnnecessary?: {}; + /** + * Any related spans the diagnostic may have, such as other locations relevant to an error, such as declarartion sites + */ + relatedInformation?: DiagnosticRelatedInformation[]; + /** * The error code of the diagnostic message. */ @@ -2233,6 +2239,23 @@ namespace ts.server.protocol { fileName: string; } + /** + * Represents additional spans returned with a diagnostic which are relevant to it + * Like DiagnosticWithLinePosition, this is provided in two forms: + * - start and length of the span + * - startLocation and endLocation a pair of Location objects storing the start/end line offset of the span + */ + export interface DiagnosticRelatedInformation { + /** + * Text of related or additional information. + */ + message: string; + /** + * Associated location + */ + span?: FileSpan; + } + export interface DiagnosticEventBody { /** * The file for which diagnostic information is reported. diff --git a/src/server/session.ts b/src/server/session.ts index 88980367e8424..74629d97e4877 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -76,7 +76,24 @@ namespace ts.server { code: diag.code, category: diagnosticCategoryName(diag), reportsUnnecessary: diag.reportsUnnecessary, - source: diag.source + source: diag.source, + relatedInformation: map(diag.relatedInformation, formatRelatedInformation), + }; + } + + function formatRelatedInformation(info: DiagnosticRelatedInformation): protocol.DiagnosticRelatedInformation { + if (!info.file) { + return { + message: flattenDiagnosticMessageText(info.messageText, "\n") + }; + } + return { + span: { + start: convertToLocation(getLineAndCharacterOfPosition(info.file, info.start!)), + end: convertToLocation(getLineAndCharacterOfPosition(info.file, info.start! + info.length!)), // TODO: GH#18217 + file: info.file.fileName + }, + message: flattenDiagnosticMessageText(info.messageText, "\n") }; } @@ -92,8 +109,19 @@ namespace ts.server { const text = flattenDiagnosticMessageText(diag.messageText, "\n"); const { code, source } = diag; const category = diagnosticCategoryName(diag); - return includeFileName ? { start, end, text, code, category, source, reportsUnnecessary: diag.reportsUnnecessary, fileName: diag.file && diag.file.fileName } : - { start, end, text, code, category, reportsUnnecessary: diag.reportsUnnecessary, source }; + const common = { + start, + end, + text, + code, + category, + reportsUnnecessary: diag.reportsUnnecessary, + source, + relatedInformation: map(diag.relatedInformation, formatRelatedInformation), + }; + return includeFileName + ? { ...common, fileName: diag.file && diag.file.fileName } + : common; } export interface PendingErrorCheck { @@ -612,7 +640,8 @@ namespace ts.server { category: diagnosticCategoryName(d), code: d.code, startLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start!)))!, // TODO: GH#18217 - endLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start! + d.length!)))! // TODO: GH#18217 + endLocation: (d.file && convertToLocation(getLineAndCharacterOfPosition(d.file, d.start! + d.length!)))!, // TODO: GH#18217 + relatedInformation: map(d.relatedInformation, formatRelatedInformation) })); } @@ -640,7 +669,8 @@ namespace ts.server { source: d.source, startLocation: scriptInfo && scriptInfo.positionToLineOffset(d.start!), // TODO: GH#18217 endLocation: scriptInfo && scriptInfo.positionToLineOffset(d.start! + d.length!), - reportsUnnecessary: d.reportsUnnecessary + reportsUnnecessary: d.reportsUnnecessary, + relatedInformation: map(d.relatedInformation, formatRelatedInformation), }); } diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index cfdc19e257d30..906c6f13cd1e0 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -2,25 +2,6 @@ namespace ts.codefix { const fixName = "invalidImportSyntax"; - registerCodeFix({ - errorCodes: [Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime.code], - getCodeActions: getActionsForInvalidImport - }); - - function getActionsForInvalidImport(context: CodeFixContext): CodeFixAction[] | undefined { - const sourceFile = context.sourceFile; - - // This is the whole import statement, eg: - // import * as Bluebird from 'bluebird'; - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - const node = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false).parent as ImportDeclaration; - if (!isImportDeclaration(node)) { - // No import quick fix for import calls - return []; - } - return getCodeFixesForImportDeclaration(context, node); - } - function getCodeFixesForImportDeclaration(context: CodeFixContext, node: ImportDeclaration): CodeFixAction[] { const sourceFile = getSourceFileOfNode(node); const namespace = getNamespaceDeclarationNode(node) as NamespaceImport; @@ -45,7 +26,7 @@ namespace ts.codefix { function createAction(context: CodeFixContext, sourceFile: SourceFile, node: Node, replacement: Node): CodeFixAction { const changes = textChanges.ChangeTracker.with(context, t => t.replaceNode(sourceFile, node, replacement)); - return createCodeFixActionNoFixId("invalidImportSyntax", changes, [Diagnostics.Replace_import_with_0, changes[0].textChanges[0].newText]); + return createCodeFixActionNoFixId(fixName, changes, [Diagnostics.Replace_import_with_0, changes[0].textChanges[0].newText]); } registerCodeFix({ @@ -64,6 +45,38 @@ namespace ts.codefix { return []; } const expr = node.expression; + return getImportCodeFixesForExpression(context, expr); + } + + registerCodeFix({ + errorCodes: [ + // The following error codes cover pretty much all assignability errors that could involve an expression + Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code, + Diagnostics.Type_0_does_not_satisfy_the_constraint_1.code, + Diagnostics.Type_0_is_not_assignable_to_type_1.code, + Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated.code, + Diagnostics.Type_predicate_0_is_not_assignable_to_1.code, + Diagnostics.Property_0_of_type_1_is_not_assignable_to_string_index_type_2.code, + Diagnostics.Property_0_of_type_1_is_not_assignable_to_numeric_index_type_2.code, + Diagnostics.Numeric_index_type_0_is_not_assignable_to_string_index_type_1.code, + Diagnostics.Property_0_in_type_1_is_not_assignable_to_the_same_property_in_base_type_2.code, + Diagnostics.Property_0_in_type_1_is_not_assignable_to_type_2.code, + Diagnostics.Property_0_of_JSX_spread_attribute_is_not_assignable_to_target_property.code, + Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1.code, + ], + getCodeActions: getActionsForInvalidImportLocation + }); + + function getActionsForInvalidImportLocation(context: CodeFixContext): CodeFixAction[] | undefined { + const sourceFile = context.sourceFile; + const node = findAncestor(getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false), a => a.getStart() === context.span.start && a.getEnd() === (context.span.start + context.span.length)); + if (!node) { + return []; + } + return getImportCodeFixesForExpression(context, node); + } + + function getImportCodeFixesForExpression(context: CodeFixContext, expr: Node): CodeFixAction[] | undefined { const type = context.program.getTypeChecker().getTypeAtLocation(expr)!; // TODO: GH#18217 if (!(type.symbol && (type.symbol as TransientSymbol).originatingImport)) { return []; @@ -73,8 +86,11 @@ namespace ts.codefix { if (!isImportCall(relatedImport)) { addRange(fixes, getCodeFixesForImportDeclaration(context, relatedImport)); } - const changes = textChanges.ChangeTracker.with(context, t => t.replaceNode(sourceFile, expr, createPropertyAccess(expr, "default"), {})); - fixes.push(createCodeFixActionNoFixId(fixName, changes, Diagnostics.Use_synthetic_default_member)); + if (isExpression(expr) && !(isNamedDeclaration(expr.parent) && expr.parent.name === expr)) { + const sourceFile = context.sourceFile; + const changes = textChanges.ChangeTracker.with(context, t => t.replaceNode(sourceFile, expr, createPropertyAccess(expr, "default"), {})); + fixes.push(createCodeFixActionNoFixId(fixName, changes, Diagnostics.Use_synthetic_default_member)); + } return fixes; } } diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index b9966f101f46a..feb7377a86852 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -503,8 +503,8 @@ namespace ts.projectSystem { checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); } - function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}): protocol.Diagnostic { - return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, source: undefined }; + function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { + return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; } function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 45e4a4b98619a..61deb189cc933 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3576,16 +3576,19 @@ declare namespace ts { code: number; next?: DiagnosticMessageChain; } - interface Diagnostic { - file: SourceFile | undefined; - start: number | undefined; - length: number | undefined; - messageText: string | DiagnosticMessageChain; + interface Diagnostic extends DiagnosticRelatedInformation { category: DiagnosticCategory; /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ reportsUnnecessary?: {}; code: number; source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; + } + interface DiagnosticRelatedInformation { + file: SourceFile | undefined; + start: number | undefined; + length: number | undefined; + messageText: string | DiagnosticMessageChain; } interface DiagnosticWithLocation extends Diagnostic { file: SourceFile; @@ -5762,7 +5765,7 @@ declare namespace ts { Try_npm_install_types_Slash_0_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0: DiagnosticMessage; Dynamic_import_s_specifier_must_be_of_type_string_but_here_has_type_0: DiagnosticMessage; Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports: DiagnosticMessage; - A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime: DiagnosticMessage; + Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead: DiagnosticMessage; Mapped_object_type_implicitly_has_an_any_template_type: DiagnosticMessage; You_cannot_rename_this_element: DiagnosticMessage; You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library: DiagnosticMessage; @@ -6126,7 +6129,7 @@ declare namespace ts { function createDiagnosticForNode(node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation; function createDiagnosticForNodeArray(sourceFile: SourceFile, nodes: NodeArray, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic; function createDiagnosticForNodeInSourceFile(sourceFile: SourceFile, node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation; - function createDiagnosticForNodeFromMessageChain(node: Node, messageChain: DiagnosticMessageChain): DiagnosticWithLocation; + function createDiagnosticForNodeFromMessageChain(node: Node, messageChain: DiagnosticMessageChain, relatedInformation?: DiagnosticRelatedInformation[]): DiagnosticWithLocation; function getSpanOfTokenAtPosition(sourceFile: SourceFile, pos: number): TextSpan; function getErrorSpanForNode(sourceFile: SourceFile, node: Node): TextSpan; function isExternalOrCommonJsModule(file: SourceFile): boolean; @@ -12375,6 +12378,7 @@ declare namespace ts.server.protocol { category: string; code: number; reportsUnnecessary?: {}; + relatedInformation?: DiagnosticRelatedInformation[]; } interface ProjectInfoResponse extends Response { body?: ProjectInfo; @@ -12965,12 +12969,17 @@ declare namespace ts.server.protocol { text: string; category: string; reportsUnnecessary?: {}; + relatedInformation?: DiagnosticRelatedInformation[]; code?: number; source?: string; } interface DiagnosticWithFileName extends Diagnostic { fileName: string; } + interface DiagnosticRelatedInformation { + message: string; + span?: FileSpan; + } interface DiagnosticEventBody { file: string; diagnostics: Diagnostic[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 3576a4e8f6ab0..d599179819113 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3576,16 +3576,19 @@ declare namespace ts { code: number; next?: DiagnosticMessageChain; } - interface Diagnostic { - file: SourceFile | undefined; - start: number | undefined; - length: number | undefined; - messageText: string | DiagnosticMessageChain; + interface Diagnostic extends DiagnosticRelatedInformation { category: DiagnosticCategory; /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ reportsUnnecessary?: {}; code: number; source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; + } + interface DiagnosticRelatedInformation { + file: SourceFile | undefined; + start: number | undefined; + length: number | undefined; + messageText: string | DiagnosticMessageChain; } interface DiagnosticWithLocation extends Diagnostic { file: SourceFile; @@ -5762,7 +5765,7 @@ declare namespace ts { Try_npm_install_types_Slash_0_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0: DiagnosticMessage; Dynamic_import_s_specifier_must_be_of_type_string_but_here_has_type_0: DiagnosticMessage; Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports: DiagnosticMessage; - A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime: DiagnosticMessage; + Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead: DiagnosticMessage; Mapped_object_type_implicitly_has_an_any_template_type: DiagnosticMessage; You_cannot_rename_this_element: DiagnosticMessage; You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library: DiagnosticMessage; @@ -6126,7 +6129,7 @@ declare namespace ts { function createDiagnosticForNode(node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation; function createDiagnosticForNodeArray(sourceFile: SourceFile, nodes: NodeArray, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic; function createDiagnosticForNodeInSourceFile(sourceFile: SourceFile, node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation; - function createDiagnosticForNodeFromMessageChain(node: Node, messageChain: DiagnosticMessageChain): DiagnosticWithLocation; + function createDiagnosticForNodeFromMessageChain(node: Node, messageChain: DiagnosticMessageChain, relatedInformation?: DiagnosticRelatedInformation[]): DiagnosticWithLocation; function getSpanOfTokenAtPosition(sourceFile: SourceFile, pos: number): TextSpan; function getErrorSpanForNode(sourceFile: SourceFile, node: Node): TextSpan; function isExternalOrCommonJsModule(file: SourceFile): boolean; diff --git a/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.errors.txt b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.errors.txt new file mode 100644 index 0000000000000..96a31032d4dde --- /dev/null +++ b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.errors.txt @@ -0,0 +1,24 @@ +tests/cases/compiler/index.ts:3:8 - error TS2345: Argument of type '{ default: () => void; }' is not assignable to parameter of type '() => void'. + Type '{ default: () => void; }' provides no match for the signature '(): void'. + +3 invoke(foo); +   ~~~ + + tests/cases/compiler/index.ts:1:1 + 1 import * as foo from "./foo"; +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Type originates at this import. A namespace-style import cannot be called or constructed, and will cause a failure at runtime. Consider using a default import or import require here instead. + + +==== tests/cases/compiler/foo.d.ts (0 errors) ==== + declare function foo(): void; + declare namespace foo {} + export = foo; +==== tests/cases/compiler/index.ts (1 errors) ==== + import * as foo from "./foo"; + function invoke(f: () => void) { f(); } + invoke(foo); + ~~~ +!!! error TS2345: Argument of type '{ default: () => void; }' is not assignable to parameter of type '() => void'. +!!! error TS2345: Type '{ default: () => void; }' provides no match for the signature '(): void'. + \ No newline at end of file diff --git a/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.js b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.js new file mode 100644 index 0000000000000..80809deefc593 --- /dev/null +++ b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.js @@ -0,0 +1,25 @@ +//// [tests/cases/compiler/esModuleInteropPrettyErrorRelatedInformation.ts] //// + +//// [foo.d.ts] +declare function foo(): void; +declare namespace foo {} +export = foo; +//// [index.ts] +import * as foo from "./foo"; +function invoke(f: () => void) { f(); } +invoke(foo); + + +//// [index.js] +"use strict"; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +exports.__esModule = true; +var foo = __importStar(require("./foo")); +function invoke(f) { f(); } +invoke(foo); diff --git a/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.symbols b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.symbols new file mode 100644 index 0000000000000..9c632fa26da99 --- /dev/null +++ b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.symbols @@ -0,0 +1,23 @@ +=== tests/cases/compiler/foo.d.ts === +declare function foo(): void; +>foo : Symbol(foo, Decl(foo.d.ts, 0, 0), Decl(foo.d.ts, 0, 29)) + +declare namespace foo {} +>foo : Symbol(foo, Decl(foo.d.ts, 0, 0), Decl(foo.d.ts, 0, 29)) + +export = foo; +>foo : Symbol(foo, Decl(foo.d.ts, 0, 0), Decl(foo.d.ts, 0, 29)) + +=== tests/cases/compiler/index.ts === +import * as foo from "./foo"; +>foo : Symbol(foo, Decl(index.ts, 0, 6)) + +function invoke(f: () => void) { f(); } +>invoke : Symbol(invoke, Decl(index.ts, 0, 29)) +>f : Symbol(f, Decl(index.ts, 1, 16)) +>f : Symbol(f, Decl(index.ts, 1, 16)) + +invoke(foo); +>invoke : Symbol(invoke, Decl(index.ts, 0, 29)) +>foo : Symbol(foo, Decl(index.ts, 0, 6)) + diff --git a/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.types b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.types new file mode 100644 index 0000000000000..662932d82ed65 --- /dev/null +++ b/tests/baselines/reference/esModuleInteropPrettyErrorRelatedInformation.types @@ -0,0 +1,25 @@ +=== tests/cases/compiler/foo.d.ts === +declare function foo(): void; +>foo : () => void + +declare namespace foo {} +>foo : () => void + +export = foo; +>foo : () => void + +=== tests/cases/compiler/index.ts === +import * as foo from "./foo"; +>foo : { default: () => void; } + +function invoke(f: () => void) { f(); } +>invoke : (f: () => void) => void +>f : () => void +>f() : void +>f : () => void + +invoke(foo); +>invoke(foo) : void +>invoke : (f: () => void) => void +>foo : { default: () => void; } + diff --git a/tests/cases/compiler/esModuleInteropPrettyErrorRelatedInformation.ts b/tests/cases/compiler/esModuleInteropPrettyErrorRelatedInformation.ts new file mode 100644 index 0000000000000..6b8c5c947b20f --- /dev/null +++ b/tests/cases/compiler/esModuleInteropPrettyErrorRelatedInformation.ts @@ -0,0 +1,10 @@ +// @pretty: true +// @esModuleInterop: true +// @filename: foo.d.ts +declare function foo(): void; +declare namespace foo {} +export = foo; +// @filename: index.ts +import * as foo from "./foo"; +function invoke(f: () => void) { f(); } +invoke(foo); diff --git a/tests/cases/fourslash/codeFixCalledES2015Import1.ts b/tests/cases/fourslash/codeFixCalledES2015Import1.ts index 45e88c190478d..af102c1a0cb0d 100644 --- a/tests/cases/fourslash/codeFixCalledES2015Import1.ts +++ b/tests/cases/fourslash/codeFixCalledES2015Import1.ts @@ -6,13 +6,15 @@ ////export = foo; // @Filename: index.ts -////[|import * as foo from "./foo";|] +////import * as foo from "./foo"; ////function invoke(f: () => void) { f(); } -////invoke(foo); +////invoke([|foo|]); goTo.file(1); verify.codeFix({ description: `Replace import with 'import foo = require("./foo");'.`, - newRangeContent: `import foo = require("./foo");`, + newFileContent: `import foo = require("./foo"); +function invoke(f: () => void) { f(); } +invoke(foo);`, index: 1, }); diff --git a/tests/cases/fourslash/codeFixCalledES2015Import12.ts b/tests/cases/fourslash/codeFixCalledES2015Import12.ts index 6c31531d572ef..e869d9d5701d4 100644 --- a/tests/cases/fourslash/codeFixCalledES2015Import12.ts +++ b/tests/cases/fourslash/codeFixCalledES2015Import12.ts @@ -14,5 +14,5 @@ verify.codeFix({ description: `Use synthetic 'default' member.`, newFileContent: `import * as foo from "./foo"; foo.default();`, - index: 4, + index: 2, }); diff --git a/tests/cases/fourslash/codeFixCalledES2015Import2.ts b/tests/cases/fourslash/codeFixCalledES2015Import2.ts index 80040cb419287..1ea71b2fc7706 100644 --- a/tests/cases/fourslash/codeFixCalledES2015Import2.ts +++ b/tests/cases/fourslash/codeFixCalledES2015Import2.ts @@ -6,13 +6,15 @@ ////export = foo; // @Filename: index.ts -////[|import * as foo from "./foo";|] +////import * as foo from "./foo"; ////function invoke(f: () => void) { f(); } -////invoke(foo); +////invoke([|foo|]); goTo.file(1); verify.codeFix({ description: `Replace import with 'import foo from "./foo";'.`, - newRangeContent: `import foo from "./foo";`, + newFileContent: `import foo from "./foo"; +function invoke(f: () => void) { f(); } +invoke(foo);`, index: 0, }); diff --git a/tests/cases/fourslash/codeFixCalledES2015Import3.ts b/tests/cases/fourslash/codeFixCalledES2015Import3.ts index 82f08fbc22f0b..c9eb402c800c5 100644 --- a/tests/cases/fourslash/codeFixCalledES2015Import3.ts +++ b/tests/cases/fourslash/codeFixCalledES2015Import3.ts @@ -7,13 +7,15 @@ ////export = foo; // @Filename: index.ts -////[|import * as foo from "./foo";|] +////import * as foo from "./foo"; ////function invoke(f: () => void) { f(); } -////invoke(foo); +////invoke([|foo|]); goTo.file(1); verify.codeFix({ description: `Replace import with 'import foo from "./foo";'.`, - newRangeContent: `import foo from "./foo";`, + newFileContent: `import foo from "./foo"; +function invoke(f: () => void) { f(); } +invoke(foo);`, index: 0, }); diff --git a/tests/cases/fourslash/codeFixCalledES2015Import4.ts b/tests/cases/fourslash/codeFixCalledES2015Import4.ts index ba6d90d6d411f..68d29db2e9881 100644 --- a/tests/cases/fourslash/codeFixCalledES2015Import4.ts +++ b/tests/cases/fourslash/codeFixCalledES2015Import4.ts @@ -6,12 +6,13 @@ ////export = foo; // @Filename: index.ts -////[|import * as foo from "./foo";|] -////foo(); +////import * as foo from "./foo"; +////[|foo|](); goTo.file(1); verify.codeFix({ description: `Replace import with 'import foo = require("./foo");'.`, - newRangeContent: `import foo = require("./foo");`, + newFileContent: `import foo = require("./foo"); +foo();`, index: 1, });