Skip to content

Commit 2568190

Browse files
authored
Improve mapping of Clang diagnostics into Carbon diagnostics (#5894)
Instead of taking the complete text of the Clang diagnostic and using it as the message portion of a Carbon diagnostic, generate the individual pieces separately and pass them into the Carbon diagnostic infrastructure. * Clang's context lines are generated by running a custom "diagnostic renderer" and tracking which lines it wants to print as context for a given source location. When mapping from a C++ source location back to a Carbon location, the Carbon `Loc` structure is now fully populated, including filling in the context line and the column number. * Clang's snippet is generated by running a custom diagnostic renderer that is a cut-down version of the full text diagnostic renderer that only prints a snippet. This is then attached to the Carbon diagnostic manually as an override for the snippet we'd usually create. We no longer repeat the file location twice on each diagnostic, and no longer produce a bogus "in import" line for all locations coming from clang that point arbitrarily to the first C++ import in the Carbon file. The `[diagnostic kind]` marker is now displayed at the end of the diagnostic message, not on a line of its own after the snippet.
1 parent b320ea7 commit 2568190

24 files changed

+560
-301
lines changed

toolchain/check/diagnostic_emitter.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "common/raw_string_ostream.h"
1212
#include "toolchain/check/diagnostic_helpers.h"
1313
#include "toolchain/sem_ir/absolute_node_id.h"
14+
#include "toolchain/sem_ir/diagnostic_loc_converter.h"
1415
#include "toolchain/sem_ir/ids.h"
1516
#include "toolchain/sem_ir/stringify.h"
1617

@@ -23,7 +24,28 @@ auto DiagnosticEmitter::ConvertLoc(LocIdForDiagnostics loc_id,
2324
loc_id.loc_id(), loc_id.is_token_only());
2425
for (const auto& import : imports) {
2526
CARBON_DIAGNOSTIC(InImport, LocationInfo, "in import");
26-
context_fn(import.loc, InImport);
27+
CARBON_DIAGNOSTIC(InCppInclude, LocationInfo, "in file included here");
28+
CARBON_DIAGNOSTIC(InCppModule, LocationInfo, "in module imported here");
29+
CARBON_DIAGNOSTIC(InCppMacroExpansion, LocationInfo,
30+
"in expansion of macro defined here");
31+
switch (import.kind) {
32+
case Carbon::SemIR::DiagnosticLocConverter::ImportLoc::Import:
33+
// TODO: Include the library name in the note.
34+
context_fn(import.loc, InImport);
35+
break;
36+
case Carbon::SemIR::DiagnosticLocConverter::ImportLoc::CppInclude:
37+
// TODO: Include the file name in the note.
38+
context_fn(import.loc, InCppInclude);
39+
break;
40+
case Carbon::SemIR::DiagnosticLocConverter::ImportLoc::CppModuleImport:
41+
// TODO: Include the module name in the note.
42+
context_fn(import.loc, InCppModule);
43+
break;
44+
case Carbon::SemIR::DiagnosticLocConverter::ImportLoc::CppMacroExpansion:
45+
// TODO: Include the macro name in the note.
46+
context_fn(import.loc, InCppMacroExpansion);
47+
break;
48+
}
2749
}
2850

2951
// Use the token when possible, but -1 is the default value.

toolchain/check/import_cpp.cpp

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -141,32 +141,26 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
141141
llvm::SmallString<256> message;
142142
info.FormatDiagnostic(message);
143143

144+
// Render a code snippet including any highlighted ranges and fixit hints.
145+
// TODO: Also include the #include stack and macro expansion stack in the
146+
// diagnostic output in some way.
147+
RawStringOstream snippet_stream;
144148
if (!info.hasSourceManager()) {
145-
// If we don't have a source manager, we haven't actually started
146-
// compiling yet, and this is an error from the driver or early in the
147-
// frontend. Pass it on directly.
149+
// If we don't have a source manager, this is an error from early in the
150+
// frontend. Don't produce a snippet.
148151
CARBON_CHECK(info.getLocation().isInvalid());
149-
diagnostic_infos_.push_back({.level = diag_level,
150-
.import_ir_inst_id = clang_import_ir_inst_id,
151-
.message = message.str().str()});
152-
return;
152+
} else {
153+
CodeContextRenderer(snippet_stream, invocation_->getLangOpts(),
154+
invocation_->getDiagnosticOpts())
155+
.emitDiagnostic(
156+
clang::FullSourceLoc(info.getLocation(), info.getSourceManager()),
157+
diag_level, message, info.getRanges(), info.getFixItHints());
153158
}
154159

155-
// TODO: This includes the full clang diagnostic, including the source
156-
// location, resulting in the location appearing twice in the output.
157-
RawStringOstream diagnostics_stream;
158-
clang::TextDiagnostic text_diagnostic(diagnostics_stream,
159-
invocation_->getLangOpts(),
160-
invocation_->getDiagnosticOpts());
161-
text_diagnostic.emitDiagnostic(
162-
clang::FullSourceLoc(info.getLocation(), info.getSourceManager()),
163-
diag_level, message, info.getRanges(), info.getFixItHints());
164-
165-
std::string diagnostics_str = diagnostics_stream.TakeStr();
166-
167160
diagnostic_infos_.push_back({.level = diag_level,
168161
.import_ir_inst_id = clang_import_ir_inst_id,
169-
.message = diagnostics_str});
162+
.message = message.str().str(),
163+
.snippet = snippet_stream.TakeStr()});
170164
}
171165

172166
// Outputs Carbon diagnostics based on the collected Clang diagnostics. Must
@@ -200,15 +194,22 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
200194
? CppInteropParseWarning
201195
: CppInteropParseError,
202196
info.message);
197+
builder.OverrideSnippet(info.snippet);
203198
for (;
204199
i + 1 < diagnostic_infos_.size() &&
205200
diagnostic_infos_[i + 1].level == clang::DiagnosticsEngine::Note;
206201
++i) {
207202
const ClangDiagnosticInfo& note_info = diagnostic_infos_[i + 1];
208203
CARBON_DIAGNOSTIC(CppInteropParseNote, Note, "{0}", std::string);
209-
builder.Note(SemIR::LocId(note_info.import_ir_inst_id),
210-
CppInteropParseNote, note_info.message);
204+
builder
205+
.Note(SemIR::LocId(note_info.import_ir_inst_id),
206+
CppInteropParseNote, note_info.message)
207+
.OverrideSnippet(note_info.snippet);
211208
}
209+
// TODO: This will apply all current Carbon annotation functions. We
210+
// should instead track how Clang's context notes and Carbon's
211+
// annotation functions are interleaved, and interleave the notes in
212+
// the same order.
212213
builder.Emit();
213214
break;
214215
}
@@ -218,11 +219,36 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
218219
}
219220

220221
private:
221-
// The type-checking context in which we're running Clang.
222-
Context* context_;
223-
224-
// The compiler invocation that is producing the diagnostics.
225-
std::shared_ptr<clang::CompilerInvocation> invocation_;
222+
// A diagnostics renderer based on clang's TextDiagnostic that captures just
223+
// the code context (the snippet).
224+
class CodeContextRenderer : public clang::TextDiagnostic {
225+
public:
226+
using TextDiagnostic::TextDiagnostic;
227+
228+
void emitDiagnosticMessage(
229+
clang::FullSourceLoc /*loc*/, clang::PresumedLoc /*ploc*/,
230+
clang::DiagnosticsEngine::Level /*level*/, llvm::StringRef /*message*/,
231+
llvm::ArrayRef<clang::CharSourceRange> /*ranges*/,
232+
clang::DiagOrStoredDiag /*info*/) override {}
233+
void emitDiagnosticLoc(
234+
clang::FullSourceLoc /*loc*/, clang::PresumedLoc /*ploc*/,
235+
clang::DiagnosticsEngine::Level /*level*/,
236+
llvm::ArrayRef<clang::CharSourceRange> /*ranges*/) override {}
237+
238+
// emitCodeContext is inherited from clang::TextDiagnostic.
239+
240+
void emitIncludeLocation(clang::FullSourceLoc /*loc*/,
241+
clang::PresumedLoc /*ploc*/) override {}
242+
void emitImportLocation(clang::FullSourceLoc /*loc*/,
243+
clang::PresumedLoc /*ploc*/,
244+
llvm::StringRef /*module_name*/) override {}
245+
void emitBuildingModuleLocation(clang::FullSourceLoc /*loc*/,
246+
clang::PresumedLoc /*ploc*/,
247+
llvm::StringRef /*module_name*/) override {}
248+
249+
// beginDiagnostic and endDiagnostic are inherited from
250+
// clang::TextDiagnostic in case it wants to do any setup / teardown work.
251+
};
226252

227253
// Information on a Clang diagnostic that can be converted to a Carbon
228254
// diagnostic.
@@ -236,25 +262,56 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
236262

237263
// The Clang diagnostic textual message.
238264
std::string message;
265+
266+
// The code snippet produced by clang.
267+
std::string snippet;
239268
};
240269

270+
// The type-checking context in which we're running Clang.
271+
Context* context_;
272+
273+
// The compiler invocation that is producing the diagnostics.
274+
std::shared_ptr<clang::CompilerInvocation> invocation_;
275+
241276
// Collects the information for all Clang diagnostics to be converted to
242277
// Carbon diagnostics after the context has been initialized with the Clang
243278
// AST.
244279
llvm::SmallVector<ClangDiagnosticInfo> diagnostic_infos_;
245280
};
246281

282+
// A wrapper around a clang::CompilerInvocation that allows us to make a shallow
283+
// copy of most of the invocation and only make a deep copy of the parts that we
284+
// want to change.
285+
//
286+
// clang::CowCompilerInvocation almost allows this, but doesn't derive from
287+
// CompilerInvocation or support shallow copies from a CompilerInvocation, so is
288+
// not useful to us as we can't build an ASTUnit from it.
289+
class ShallowCopyCompilerInvocation : public clang::CompilerInvocation {
290+
public:
291+
explicit ShallowCopyCompilerInvocation(
292+
const clang::CompilerInvocation& invocation) {
293+
shallow_copy_assign(invocation);
294+
295+
// The preprocessor options are modified to hold a replacement includes
296+
// buffer, so make our own version of those options.
297+
PPOpts = std::make_shared<clang::PreprocessorOptions>(*PPOpts);
298+
}
299+
};
300+
247301
} // namespace
248302

249303
// Returns an AST for the C++ imports and a bool that represents whether
250304
// compilation errors where encountered or the generated AST is null due to an
251305
// error. Sets the AST in the context's `sem_ir`.
252306
// TODO: Consider to always have a (non-null) AST.
253-
static auto GenerateAst(Context& context,
254-
llvm::ArrayRef<Parse::Tree::PackagingNames> imports,
255-
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs,
256-
std::shared_ptr<clang::CompilerInvocation> invocation)
307+
static auto GenerateAst(
308+
Context& context, llvm::ArrayRef<Parse::Tree::PackagingNames> imports,
309+
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs,
310+
std::shared_ptr<clang::CompilerInvocation> base_invocation)
257311
-> std::pair<std::unique_ptr<clang::ASTUnit>, bool> {
312+
auto invocation =
313+
std::make_shared<ShallowCopyCompilerInvocation>(*base_invocation);
314+
258315
// Build a diagnostics engine.
259316
llvm::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
260317
clang::CompilerInstance::createDiagnostics(
@@ -274,9 +331,10 @@ static auto GenerateAst(Context& context,
274331
// TODO: Modify the frontend options to specify this memory buffer as input
275332
// instead of remapping the file.
276333
std::string includes = GenerateCppIncludesHeaderCode(context, imports);
277-
auto includes_buffer = llvm::MemoryBuffer::getMemBuffer(includes, file_name);
334+
auto includes_buffer =
335+
llvm::MemoryBuffer::getMemBufferCopy(includes, file_name);
278336
invocation->getPreprocessorOpts().addRemappedFile(file_name,
279-
includes_buffer.get());
337+
includes_buffer.release());
280338

281339
clang::DiagnosticErrorTrap trap(*diags);
282340

@@ -285,9 +343,6 @@ static auto GenerateAst(Context& context,
285343
invocation, std::make_shared<clang::PCHContainerOperations>(), nullptr,
286344
diags, new clang::FileManager(invocation->getFileSystemOpts(), fs));
287345

288-
// Remove remapped file before its underlying storage is destroyed.
289-
invocation->getPreprocessorOpts().clearRemappedFiles();
290-
291346
// Attach the AST to SemIR. This needs to be done before we can emit any
292347
// diagnostics, so their locations can be properly interpreted by our
293348
// diagnostics machinery.
@@ -377,6 +432,9 @@ static auto ClangLookup(Context& context, SemIR::NameScopeId scope_id,
377432
CARBON_CHECK(ast);
378433
clang::Sema& sema = ast->getSema();
379434

435+
// TODO: Map the LocId of the lookup to a clang SourceLocation and provide it
436+
// here so that clang's diagnostics can point into the carbon code that uses
437+
// the name.
380438
clang::LookupResult lookup(
381439
sema,
382440
clang::DeclarationNameInfo(

toolchain/check/testdata/interop/cpp/bad_import.carbon

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,8 @@ import Cpp library "";
3434

3535
library "[[@TEST_NAME]]";
3636

37-
// CHECK:STDERR: fail_import_cpp_library_file_with_quotes.carbon:[[@LINE+6]]:1: in import [InImport]
38-
// CHECK:STDERR: fail_import_cpp_library_file_with_quotes.carbon:[[@LINE+5]]: error: fail_import_cpp_library_file_with_quotes.carbon:[[@LINE+5]]:10: fatal error: '\"foo.h\"' file not found
39-
// CHECK:STDERR: 10 | #include "\"foo.h\""
37+
// CHECK:STDERR: fail_import_cpp_library_file_with_quotes.carbon:[[@LINE+4]]:10: error: '\"foo.h\"' file not found [CppInteropParseError]
38+
// CHECK:STDERR: 8 | #include "\"foo.h\""
4039
// CHECK:STDERR: | ^~~~~~~~~~~
41-
// CHECK:STDERR: [CppInteropParseError]
4240
// CHECK:STDERR:
4341
import Cpp library "\"foo.h\"";

toolchain/check/testdata/interop/cpp/class/access.carbon

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ library "[[@TEST_NAME]]";
5050
import Cpp library "non_function_member_protected.h";
5151

5252
fn F(c: Cpp.C) {
53-
// CHECK:STDERR: fail_import_non_function_member_protected.carbon:[[@LINE+6]]:16: error: cannot access protected member `x` of type `Cpp.C` [ClassInvalidMemberAccess]
53+
// CHECK:STDERR: fail_import_non_function_member_protected.carbon:[[@LINE+8]]:16: error: cannot access protected member `x` of type `Cpp.C` [ClassInvalidMemberAccess]
5454
// CHECK:STDERR: let x: i32 = c.x;
5555
// CHECK:STDERR: ^~~
56-
// CHECK:STDERR: fail_import_non_function_member_protected.carbon:[[@LINE-6]]:1: in import [InImport]
57-
// CHECK:STDERR: ./non_function_member_protected.h:2: note: declared here [ClassMemberDeclaration]
56+
// CHECK:STDERR: fail_import_non_function_member_protected.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
57+
// CHECK:STDERR: ./non_function_member_protected.h:2:7: note: declared here [ClassMemberDeclaration]
58+
// CHECK:STDERR: class C {
59+
// CHECK:STDERR: ^
5860
// CHECK:STDERR:
5961
let x: i32 = c.x;
6062
}
@@ -76,11 +78,13 @@ library "[[@TEST_NAME]]";
7678
import Cpp library "non_function_member_private.h";
7779

7880
fn F(c: Cpp.C) {
79-
// CHECK:STDERR: fail_import_non_function_member_private.carbon:[[@LINE+6]]:16: error: cannot access private member `x` of type `Cpp.C` [ClassInvalidMemberAccess]
81+
// CHECK:STDERR: fail_import_non_function_member_private.carbon:[[@LINE+8]]:16: error: cannot access private member `x` of type `Cpp.C` [ClassInvalidMemberAccess]
8082
// CHECK:STDERR: let x: i32 = c.x;
8183
// CHECK:STDERR: ^~~
82-
// CHECK:STDERR: fail_import_non_function_member_private.carbon:[[@LINE-6]]:1: in import [InImport]
83-
// CHECK:STDERR: ./non_function_member_private.h:2: note: declared here [ClassMemberDeclaration]
84+
// CHECK:STDERR: fail_import_non_function_member_private.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
85+
// CHECK:STDERR: ./non_function_member_private.h:2:7: note: declared here [ClassMemberDeclaration]
86+
// CHECK:STDERR: class C {
87+
// CHECK:STDERR: ^
8488
// CHECK:STDERR:
8589
let x: i32 = c.x;
8690
}

toolchain/check/testdata/interop/cpp/class/class.carbon

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ library "[[@TEST_NAME]]";
3535
import Cpp library "declaration.h";
3636

3737
fn MyF() {
38-
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE+6]]:12: error: binding pattern has incomplete type `Bar` in name binding declaration [IncompleteTypeInBindingDecl]
38+
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE+8]]:12: error: binding pattern has incomplete type `Bar` in name binding declaration [IncompleteTypeInBindingDecl]
3939
// CHECK:STDERR: var bar: Cpp.Bar;
4040
// CHECK:STDERR: ^~~~~~~
41-
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE-6]]:1: in import [InImport]
42-
// CHECK:STDERR: ./declaration.h:2: note: class was forward declared here [ClassForwardDeclaredHere]
41+
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
42+
// CHECK:STDERR: ./declaration.h:2:7: note: class was forward declared here [ClassForwardDeclaredHere]
43+
// CHECK:STDERR: class Bar;
44+
// CHECK:STDERR: ^
4345
// CHECK:STDERR:
4446
var bar: Cpp.Bar;
4547
}

toolchain/check/testdata/interop/cpp/class/struct.carbon

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ library "[[@TEST_NAME]]";
3535
import Cpp library "declaration.h";
3636

3737
fn MyF() {
38-
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE+6]]:12: error: binding pattern has incomplete type `Bar` in name binding declaration [IncompleteTypeInBindingDecl]
38+
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE+8]]:12: error: binding pattern has incomplete type `Bar` in name binding declaration [IncompleteTypeInBindingDecl]
3939
// CHECK:STDERR: var bar: Cpp.Bar;
4040
// CHECK:STDERR: ^~~~~~~
41-
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE-6]]:1: in import [InImport]
42-
// CHECK:STDERR: ./declaration.h:2: note: class was forward declared here [ClassForwardDeclaredHere]
41+
// CHECK:STDERR: fail_use_declaration_as_definition.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
42+
// CHECK:STDERR: ./declaration.h:2:8: note: class was forward declared here [ClassForwardDeclaredHere]
43+
// CHECK:STDERR: struct Bar;
44+
// CHECK:STDERR: ^
4345
// CHECK:STDERR:
4446
var bar: Cpp.Bar;
4547
}

0 commit comments

Comments
 (0)