Properly set up C++ include paths and similar environment settings when parsing imported C++. (#5767)

Stop using the clang tooling library to build an ASTUnit; that library
is set up to process clang frontend arguments, assuming that something
has already built frontend arguments from the compiler arguments. It is
also too encapsulated and doesn't let us inspect and modify the compiler
invocation before it's executed.

Instead, build the AST unit directly in two phases:

* FIrst, take a list of clang driver arguments and convert them into a
list of compiler arguments, using `clang::createInvocation`. Internally,
this uses the clang driver to build a frontend invocation, including
building system-specific include paths as needed.
* Then, directly build an ASTUnit from this compiler invocation.

I've factored this so that we can split out the `createInvocation` step,
with the intention that we may want to move it out of check and into the
carbon driver with the rest of the driver-level argument handling, and
we may want to customize some of the clang options before we invoke the
clang frontend with that set of options.

In order to make the invocation reusable, it no longer depends on the
name of the carbon file importing the C++ code. In place of synthesizing
a header file name as `<foo.carbon>.generated.cpp_imports.h`, we now
insert line marker directives into the generated header so that errors
in that header cause Clang to point a diagnostic back at the Carbon
source file itself. This results in a minor improvement in the
diagnostic output: we no longer refer to a nonexistent generated file.
But the snippet still contains text that doesn't match the source code,
so it remains imperfect.

---------

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
This commit is contained in:
Richard Smith
2025-07-09 03:09:43 +00:00
committed by GitHub
co-authored by Chandler Carruth
parent da99b940f5
commit 3776e464e0
26 changed files with 410 additions and 114 deletions
+164 -37
View File
@@ -10,9 +10,13 @@
#include <tuple>
#include <utility>
#include "clang/Basic/FileManager.h"
#include "clang/Frontend/ASTUnit.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/CompilerInvocation.h"
#include "clang/Frontend/TextDiagnostic.h"
#include "clang/Lex/PreprocessorOptions.h"
#include "clang/Sema/Lookup.h"
#include "clang/Tooling/Tooling.h"
#include "common/ostream.h"
#include "common/raw_string_ostream.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h"
@@ -30,6 +34,7 @@
#include "toolchain/check/pattern_match.h"
#include "toolchain/check/type.h"
#include "toolchain/diagnostics/diagnostic.h"
#include "toolchain/diagnostics/diagnostic_emitter.h"
#include "toolchain/diagnostics/format_providers.h"
#include "toolchain/parse/node_ids.h"
#include "toolchain/sem_ir/ids.h"
@@ -38,6 +43,9 @@
namespace Carbon::Check {
// The fake file name to use for the synthesized includes file.
static constexpr const char IncludesFileName[] = "<carbon Cpp imports>";
// Generates C++ file contents to #include all requested imports.
static auto GenerateCppIncludesHeaderCode(
Context& context, llvm::ArrayRef<Parse::Tree::PackagingNames> imports)
@@ -91,13 +99,107 @@ static auto AddImportIRInst(Context& context,
namespace {
// Used to convert diagnostics from the Clang driver to Carbon diagnostics.
class CarbonClangDriverDiagnosticConsumer : public clang::DiagnosticConsumer {
public:
// Creates an instance with the location that triggers calling Clang.
// `context` must not be null.
explicit CarbonClangDriverDiagnosticConsumer(
Diagnostics::NoLocEmitter* emitter)
: emitter_(emitter) {}
// Generates a Carbon warning for each Clang warning and a Carbon error for
// each Clang error or fatal.
auto HandleDiagnostic(clang::DiagnosticsEngine::Level diag_level,
const clang::Diagnostic& info) -> void override {
DiagnosticConsumer::HandleDiagnostic(diag_level, info);
llvm::SmallString<256> message;
info.FormatDiagnostic(message);
switch (diag_level) {
case clang::DiagnosticsEngine::Ignored:
case clang::DiagnosticsEngine::Note:
case clang::DiagnosticsEngine::Remark: {
// TODO: Emit notes and remarks.
break;
}
case clang::DiagnosticsEngine::Warning:
case clang::DiagnosticsEngine::Error:
case clang::DiagnosticsEngine::Fatal: {
CARBON_DIAGNOSTIC(CppInteropDriverWarning, Warning, "{0}", std::string);
CARBON_DIAGNOSTIC(CppInteropDriverError, Error, "{0}", std::string);
emitter_->Emit(diag_level == clang::DiagnosticsEngine::Warning
? CppInteropDriverWarning
: CppInteropDriverError,
message.str().str());
break;
}
}
}
private:
// Diagnostic emitter. Note that driver diagnostics don't have meaningful
// locations attached.
Diagnostics::NoLocEmitter* emitter_;
};
} // namespace
// Builds a clang `CompilerInvocation` describing the options to use to build an
// imported C++ AST.
// TODO: Cache the compiler invocation created here and reuse it if building
// multiple AST units. Consider building the `CompilerInvocation` from the
// driver and passing it into check. This would also allow us to have a shared
// set of defaults between the Clang invocation we use for imports and the
// invocation we use for `carbon clang`.
static auto BuildCompilerInvocation(
Context& context, llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs,
const std::string& clang_path, const std::string& target)
-> std::unique_ptr<clang::CompilerInvocation> {
Diagnostics::NoLocEmitter emitter(context.emitter());
CarbonClangDriverDiagnosticConsumer diagnostics_consumer(&emitter);
const char* driver_args[] = {
clang_path.c_str(),
// Propagate the target to Clang.
"-target",
target.c_str(),
// Require PIE. Note its default is configurable in Clang.
"-fPIE",
// Parse as a C++ (not C) header.
"-x",
"c++",
IncludesFileName,
};
// Build a diagnostics engine. Note that we don't have any diagnostic options
// yet; they're produced by running the driver.
clang::DiagnosticOptions driver_diag_opts;
llvm::IntrusiveRefCntPtr<clang::DiagnosticsEngine> driver_diags(
clang::CompilerInstance::createDiagnostics(*fs, driver_diag_opts,
&diagnostics_consumer,
/*ShouldOwnClient=*/false));
// Ask the driver to process the arguments and build a corresponding clang
// frontend invocation.
auto invocation =
clang::createInvocation(driver_args, {.Diags = driver_diags, .VFS = fs});
// Emit any queued diagnostics from parsing our driver arguments.
return invocation;
}
namespace {
// Used to convert Clang diagnostics to Carbon diagnostics.
class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
public:
// Creates an instance with the location that triggers calling Clang.
// `context` must not be null.
explicit CarbonClangDiagnosticConsumer(Context* context)
: context_(context) {}
explicit CarbonClangDiagnosticConsumer(Context* context,
clang::CompilerInvocation* invocation)
: context_(context), invocation_(invocation) {}
// Generates a Carbon warning for each Clang warning and a Carbon error for
// each Clang error or fatal.
@@ -111,16 +213,21 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
llvm::SmallString<256> message;
info.FormatDiagnostic(message);
if (!info.hasSourceManager()) {
// If we don't have a source manager, we haven't actually started
// compiling yet, and this is an error from the driver or early in the
// frontend. Pass it on directly.
CARBON_CHECK(info.getLocation().isInvalid());
diagnostic_infos_.push_back({.level = diag_level,
.import_ir_inst_id = clang_import_ir_inst_id,
.message = message.str().str()});
return;
}
RawStringOstream diagnostics_stream;
// TODO: Consider allowing setting `LangOptions` or use
// `ASTContext::getLangOptions()`.
clang::LangOptions lang_options;
// TODO: Consider allowing setting `DiagnosticOptions` or use
// `ASTUnit::getDiagnostics().getLangOptions().getDiagnosticOptions()`.
clang::DiagnosticOptions diagnostic_options;
diagnostic_options.ShowPresumedLoc = true;
clang::TextDiagnostic text_diagnostic(diagnostics_stream, lang_options,
diagnostic_options);
clang::TextDiagnostic text_diagnostic(diagnostics_stream,
invocation_->getLangOpts(),
invocation_->getDiagnosticOpts());
text_diagnostic.emitDiagnostic(
clang::FullSourceLoc(info.getLocation(), info.getSourceManager()),
diag_level, message, info.getRanges(), info.getFixItHints());
@@ -169,6 +276,9 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
// The type-checking context in which we're running Clang.
Context* context_;
// The compiler invocation that is producing the diagnostics.
clang::CompilerInvocation* invocation_;
// Information on a Clang diagnostic that can be converted to a Carbon
// diagnostic.
struct ClangDiagnosticInfo {
@@ -195,35 +305,51 @@ class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
// compilation errors where encountered or the generated AST is null due to an
// error. Sets the AST in the context's `sem_ir`.
// TODO: Consider to always have a (non-null) AST.
static auto GenerateAst(Context& context, llvm::StringRef importing_file_path,
static auto GenerateAst(Context& context,
llvm::ArrayRef<Parse::Tree::PackagingNames> imports,
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs,
llvm::StringRef target)
const std::string& clang_path,
const std::string& target)
-> std::pair<std::unique_ptr<clang::ASTUnit>, bool> {
CarbonClangDiagnosticConsumer diagnostics_consumer(&context);
// Build the options to use to invoke the Clang frontend.
std::shared_ptr<clang::CompilerInvocation> invocation =
BuildCompilerInvocation(context, fs, clang_path, target);
if (!invocation) {
return {nullptr, true};
}
// TODO: Share compilation flags with ClangRunner.
auto ast = clang::tooling::buildASTFromCodeWithArgs(
GenerateCppIncludesHeaderCode(context, imports),
// Parse C++ (and not C).
{
"-x",
"c++",
// Propagate the target to Clang.
"-target",
target.str(),
// Require PIE. Note its default is configurable in Clang.
"-fPIE",
},
(importing_file_path + ".generated.cpp_imports.h").str(), "clang-tool",
std::make_shared<clang::PCHContainerOperations>(),
clang::tooling::getClangStripDependencyFileAdjuster(),
clang::tooling::FileContentMappings(), &diagnostics_consumer, fs);
// Remove link to the diagnostics consumer before its deletion.
// Build a diagnostics engine.
CarbonClangDiagnosticConsumer diagnostics_consumer(&context,
invocation.get());
llvm::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
clang::CompilerInstance::createDiagnostics(
*fs, invocation->getDiagnosticOpts(), &diagnostics_consumer,
/*ShouldOwnClient=*/false));
// Remap the imports file name to the corresponding `#include`s.
std::string includes = GenerateCppIncludesHeaderCode(context, imports);
auto includes_buffer =
llvm::MemoryBuffer::getMemBuffer(includes, IncludesFileName);
invocation->getPreprocessorOpts().addRemappedFile(IncludesFileName,
includes_buffer.get());
// Create the AST unit.
auto ast = clang::ASTUnit::LoadFromCompilerInvocation(
invocation, std::make_shared<clang::PCHContainerOperations>(), nullptr,
diags, new clang::FileManager(invocation->getFileSystemOpts(), fs));
// Remove link to the diagnostics consumer before its destruction.
ast->getDiagnostics().setClient(nullptr);
// In order to emit diagnostics, we need the AST.
// Remove remapped file before its underlying storage is destroyed.
invocation->getPreprocessorOpts().clearRemappedFiles();
// Attach the AST to SemIR. This needs to be done before we can emit any
// diagnostics, so their locations can be properly interpreted by our
// diagnostics machinery.
context.sem_ir().set_cpp_ast(ast.get());
// Emit any diagnostics we queued up while building the AST.
diagnostics_consumer.EmitDiagnostics();
return {std::move(ast), !ast || diagnostics_consumer.getNumErrors() > 0};
@@ -257,10 +383,11 @@ static auto AddNamespace(Context& context, PackageNameId cpp_package_id,
.add_result.name_scope_id;
}
auto ImportCppFiles(Context& context, llvm::StringRef importing_file_path,
auto ImportCppFiles(Context& context,
llvm::ArrayRef<Parse::Tree::PackagingNames> imports,
llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs,
llvm::StringRef target) -> std::unique_ptr<clang::ASTUnit> {
llvm::StringRef clang_path, llvm::StringRef target)
-> std::unique_ptr<clang::ASTUnit> {
if (imports.empty()) {
return nullptr;
}
@@ -275,7 +402,7 @@ auto ImportCppFiles(Context& context, llvm::StringRef importing_file_path,
auto name_scope_id = AddNamespace(context, package_id, imports);
auto [generated_ast, ast_has_error] =
GenerateAst(context, importing_file_path, imports, fs, target);
GenerateAst(context, imports, fs, clang_path.str(), target.str());
SemIR::NameScope& name_scope = context.name_scopes().Get(name_scope_id);
name_scope.set_is_closed_import(true);