[clangd] Add fixes for clang "include <foo.h>" diagnostics

Clang doesn't offer these fixes I guess for a couple of reasons:
 - where to insert includes is a formatting concern, and clang shouldn't
   depend on clang-format
 - the way clang prints diagnostics, we'd show a bunch of basically irrelevant
   context of "this is where we'd want to insert the include"

Maybe it's possible to hack around 1, but 2 is still a concern.
Meanwhile, bolting this onto include-fixer gets the job done.

Fixes https://github.com/clangd/clangd/issues/355
Fixes https://github.com/clangd/clangd/issues/937

Differential Revision: https://reviews.llvm.org/D114667
This commit is contained in:
Sam McCall 2021-11-28 00:33:11 +01:00
parent b73cf6207e
commit 2676759bf2
5 changed files with 148 additions and 54 deletions

View File

@ -823,6 +823,18 @@ void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
if (LastDiag->Severity == DiagnosticsEngine::Ignored)
return;
// Give include-fixer a chance to replace a note with a fix.
if (Fixer) {
auto ReplacementFixes = Fixer(LastDiag->Severity, Info);
if (!ReplacementFixes.empty()) {
assert(Info.getNumFixItHints() == 0 &&
"Include-fixer replaced a note with clang fix-its attached!");
LastDiag->Fixes.insert(LastDiag->Fixes.end(), ReplacementFixes.begin(),
ReplacementFixes.end());
return;
}
}
if (!Info.getFixItHints().empty()) {
// A clang note with fix-it is not a separate diagnostic in clangd. We
// attach it as a Fix to the main diagnostic instead.

View File

@ -144,6 +144,8 @@ public:
void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
const clang::Diagnostic &Info) override;
/// When passed a main diagnostic, returns fixes to add to it.
/// When passed a note diagnostic, returns fixes to replace it with.
using DiagFixer = std::function<std::vector<Fix>(DiagnosticsEngine::Level,
const clang::Diagnostic &)>;
using LevelAdjuster = std::function<DiagnosticsEngine::Level(

View File

@ -31,7 +31,6 @@
#include "clang/Sema/Scope.h"
#include "clang/Sema/Sema.h"
#include "clang/Sema/TypoCorrection.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/None.h"
#include "llvm/ADT/Optional.h"
@ -47,6 +46,27 @@
namespace clang {
namespace clangd {
namespace {
llvm::Optional<llvm::StringRef> getArgStr(const clang::Diagnostic &Info,
unsigned Index) {
switch (Info.getArgKind(Index)) {
case DiagnosticsEngine::ak_c_string:
return llvm::StringRef(Info.getArgCStr(Index));
case DiagnosticsEngine::ak_std_string:
return llvm::StringRef(Info.getArgStdStr(Index));
default:
return llvm::None;
}
}
std::vector<Fix> only(llvm::Optional<Fix> F) {
if (F)
return {std::move(*F)};
return {};
}
} // namespace
std::vector<Fix> IncludeFixer::fix(DiagnosticsEngine::Level DiagLevel,
const clang::Diagnostic &Info) const {
@ -102,10 +122,53 @@ std::vector<Fix> IncludeFixer::fix(DiagnosticsEngine::Level DiagLevel,
LastUnresolvedName->Loc == Info.getLocation())
return fixUnresolvedName();
}
break;
// Cases where clang explicitly knows which header to include.
// (There's no fix provided for boring formatting reasons).
case diag::err_implied_std_initializer_list_not_found:
return only(insertHeader("<initializer_list>"));
case diag::err_need_header_before_typeid:
return only(insertHeader("<typeid>"));
case diag::err_need_header_before_ms_uuidof:
return only(insertHeader("<guiddef.h>"));
case diag::err_need_header_before_placement_new:
case diag::err_implicit_coroutine_std_nothrow_type_not_found:
return only(insertHeader("<new>"));
case diag::err_omp_implied_type_not_found:
case diag::err_omp_interop_type_not_found:
return only(insertHeader("<omp.h>"));
case diag::err_implied_coroutine_type_not_found:
return only(insertHeader("<coroutine>"));
case diag::err_implied_comparison_category_type_not_found:
return only(insertHeader("<compare>"));
case diag::note_include_header_or_declare:
if (Info.getNumArgs() > 0)
if (auto Header = getArgStr(Info, 0))
return only(insertHeader(("<" + *Header + ">").str(),
getArgStr(Info, 1).getValueOr("")));
break;
}
return {};
}
llvm::Optional<Fix> IncludeFixer::insertHeader(llvm::StringRef Spelled,
llvm::StringRef Symbol) const {
Fix F;
if (auto Edit = Inserter->insert(Spelled))
F.Edits.push_back(std::move(*Edit));
else
return llvm::None;
if (Symbol.empty())
F.Message = llvm::formatv("Include {0}", Spelled);
else
F.Message = llvm::formatv("Include {0} for symbol {1}", Spelled, Symbol);
return F;
}
std::vector<Fix> IncludeFixer::fixIncompleteType(const Type &T) const {
// Only handle incomplete TagDecl type.
const TagDecl *TD = T.getAsTagDecl();
@ -160,14 +223,11 @@ std::vector<Fix> IncludeFixer::fixesForSymbols(const SymbolSlab &Syms) const {
for (const auto &Inc : getRankedIncludes(Sym)) {
if (auto ToInclude = Inserted(Sym, Inc)) {
if (ToInclude->second) {
auto I = InsertedHeaders.try_emplace(ToInclude->first);
if (!I.second)
if (!InsertedHeaders.try_emplace(ToInclude->first).second)
continue;
if (auto Edit = Inserter->insert(ToInclude->first))
Fixes.push_back(Fix{std::string(llvm::formatv(
"Add include {0} for symbol {1}{2}",
ToInclude->first, Sym.Scope, Sym.Name)),
{std::move(*Edit)}});
if (auto Fix =
insertHeader(ToInclude->first, (Sym.Scope + Sym.Name).str()))
Fixes.push_back(std::move(*Fix));
}
} else {
vlog("Failed to calculate include insertion for {0} into {1}: {2}", Inc,

View File

@ -40,6 +40,7 @@ public:
IndexRequestLimit(IndexRequestLimit) {}
/// Returns include insertions that can potentially recover the diagnostic.
/// If Info is a note and fixes are returned, they should *replace* the note.
std::vector<Fix> fix(DiagnosticsEngine::Level DiagLevel,
const clang::Diagnostic &Info) const;
@ -55,6 +56,9 @@ private:
/// Generates header insertion fixes for all symbols. Fixes are deduplicated.
std::vector<Fix> fixesForSymbols(const SymbolSlab &Syms) const;
llvm::Optional<Fix> insertHeader(llvm::StringRef Name,
llvm::StringRef Symbol = "") const;
struct UnresolvedName {
std::string Name; // E.g. "X" in foo::X.
SourceLocation Loc; // Start location of the unresolved name.

View File

@ -867,58 +867,58 @@ class T {
"incomplete type 'ns::X' named in nested name specifier"),
DiagName("incomplete_nested_name_spec"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("base"), "base class has incomplete type"),
DiagName("incomplete_base_class"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("access"),
"member access into incomplete type 'ns::X'"),
DiagName("incomplete_member_access"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(
Diag(
Test.range("type"),
"incomplete type 'ns::X' where a complete type is required"),
DiagName("incomplete_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("incomplete"),
"variable has incomplete type 'ns::X'"),
DiagName("typecheck_decl_incomplete_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(
Diag(Test.range("tag"), "incomplete definition of type 'ns::X'"),
DiagName("typecheck_incomplete_tag"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("use"),
"invalid use of incomplete type 'ns::X'"),
DiagName("invalid_incomplete_type_use"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("sizeof"),
"invalid application of 'sizeof' to "
"an incomplete type 'ns::X'"),
DiagName("sizeof_alignof_incomplete_or_sizeless_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("for"),
"cannot use incomplete type 'ns::X' as a range"),
DiagName("for_range_incomplete_type"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("return"),
"incomplete result type 'ns::X' in function definition"),
DiagName("func_def_incomplete_result"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("field"), "field has incomplete type 'ns::X'"),
DiagName("field_incomplete_or_sizeless"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X")))}))
"Include \"x.h\" for symbol ns::X")))}))
<< Test.code();
}
@ -984,28 +984,28 @@ using Type = ns::$template[[Foo]]<int>;
AllOf(Diag(Test.range("unqualified1"), "unknown type name 'X'"),
DiagName("unknown_typename"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
Diag(Test.range("unqualified2"), "use of undeclared identifier 'X'"),
AllOf(Diag(Test.range("qualified1"),
"no type named 'X' in namespace 'ns'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("qualified2"),
"no member named 'X' in namespace 'ns'"),
DiagName("no_member"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::X"))),
"Include \"x.h\" for symbol ns::X"))),
AllOf(Diag(Test.range("global"),
"no type named 'Global' in the global namespace"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"global.h\"\n",
"Add include \"global.h\" for symbol Global"))),
"Include \"global.h\" for symbol Global"))),
AllOf(Diag(Test.range("template"),
"no template named 'Foo' in namespace 'ns'"),
DiagName("no_member_template"),
WithFix(Fix(Test.range("insert"), "#include \"foo.h\"\n",
"Add include \"foo.h\" for symbol ns::Foo")))));
"Include \"foo.h\" for symbol ns::Foo")))));
}
TEST(IncludeFixerTest, MultipleMatchedSymbols) {
@ -1029,12 +1029,12 @@ void foo() {
Diag(Test.range("unqualified"), "unknown type name 'X'"),
DiagName("unknown_typename"),
WithFix(Fix(Test.range("insert"), "#include \"a.h\"\n",
"Add include \"a.h\" for symbol na::X"),
"Include \"a.h\" for symbol na::X"),
Fix(Test.range("insert"), "#include \"b.h\"\n",
"Add include \"b.h\" for symbol na::nb::X")))));
"Include \"b.h\" for symbol na::nb::X")))));
}
TEST(IncludeFixerTest, NoCrashMemebrAccess) {
TEST(IncludeFixerTest, NoCrashMemberAccess) {
Annotations Test(R"cpp(// error-ok
struct X { int xyz; };
void g() { X x; x.$[[xy]]; }
@ -1085,8 +1085,7 @@ void bar(X *x) {
ADD_FAILURE() << "D.Fixes.size() != 1";
continue;
}
EXPECT_EQ(D.Fixes[0].Message,
std::string("Add include \"a.h\" for symbol X"));
EXPECT_EQ(D.Fixes[0].Message, std::string("Include \"a.h\" for symbol X"));
}
}
@ -1106,11 +1105,11 @@ void g() { ns::$[[scope]]::X_Y(); }
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(AllOf(
Diag(Test.range(), "no member named 'scope' in namespace 'ns'"),
DiagName("no_member"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol ns::scope::X_Y")))));
UnorderedElementsAre(
AllOf(Diag(Test.range(), "no member named 'scope' in namespace 'ns'"),
DiagName("no_member"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Include \"x.h\" for symbol ns::scope::X_Y")))));
}
TEST(IncludeFixerTest, UnresolvedSpecifierWithSemaCorrection) {
@ -1135,32 +1134,29 @@ void f() {
EXPECT_THAT(
*TU.build().getDiagnostics(),
UnorderedElementsAre(
AllOf(
Diag(Test.range("q1"), "use of undeclared identifier 'clangd'; "
"did you mean 'clang'?"),
DiagName("undeclared_var_use_suggest"),
WithFix(_, // change clangd to clang
Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol clang::clangd::X"))),
AllOf(
Diag(Test.range("x"), "no type named 'X' in namespace 'clang'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol clang::clangd::X"))),
AllOf(Diag(Test.range("q1"), "use of undeclared identifier 'clangd'; "
"did you mean 'clang'?"),
DiagName("undeclared_var_use_suggest"),
WithFix(_, // change clangd to clang
Fix(Test.range("insert"), "#include \"x.h\"\n",
"Include \"x.h\" for symbol clang::clangd::X"))),
AllOf(Diag(Test.range("x"), "no type named 'X' in namespace 'clang'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Include \"x.h\" for symbol clang::clangd::X"))),
AllOf(
Diag(Test.range("q2"), "use of undeclared identifier 'clangd'; "
"did you mean 'clang'?"),
DiagName("undeclared_var_use_suggest"),
WithFix(
_, // change clangd to clang
Fix(Test.range("insert"), "#include \"y.h\"\n",
"Add include \"y.h\" for symbol clang::clangd::ns::Y"))),
WithFix(_, // change clangd to clang
Fix(Test.range("insert"), "#include \"y.h\"\n",
"Include \"y.h\" for symbol clang::clangd::ns::Y"))),
AllOf(Diag(Test.range("ns"),
"no member named 'ns' in namespace 'clang'"),
DiagName("no_member"),
WithFix(Fix(
Test.range("insert"), "#include \"y.h\"\n",
"Add include \"y.h\" for symbol clang::clangd::ns::Y")))));
WithFix(
Fix(Test.range("insert"), "#include \"y.h\"\n",
"Include \"y.h\" for symbol clang::clangd::ns::Y")))));
}
TEST(IncludeFixerTest, SpecifiedScopeIsNamespaceAlias) {
@ -1181,7 +1177,7 @@ namespace c {
Diag(Test.range(), "no type named 'X' in namespace 'a'"),
DiagName("typename_nested_not_found"),
WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n",
"Add include \"x.h\" for symbol a::X")))));
"Include \"x.h\" for symbol a::X")))));
}
TEST(IncludeFixerTest, NoCrashOnTemplateInstantiations) {
@ -1206,6 +1202,26 @@ TEST(IncludeFixerTest, NoCrashOnTemplateInstantiations) {
ElementsAre(Diag(Test.range(), "use of undeclared identifier 'a'")));
}
TEST(IncludeFixerTest, HeaderNamedInDiag) {
Annotations Test(R"cpp(
$insert[[]]int main() {
[[printf]]("");
}
)cpp");
auto TU = TestTU::withCode(Test.code());
TU.ExtraArgs = {"-xc"};
auto Index = buildIndexWithSymbol({});
TU.ExternalIndex = Index.get();
EXPECT_THAT(
*TU.build().getDiagnostics(),
ElementsAre(AllOf(
Diag(Test.range(), "implicitly declaring library function 'printf' "
"with type 'int (const char *, ...)'"),
WithFix(Fix(Test.range("insert"), "#include <stdio.h>\n",
"Include <stdio.h> for symbol printf")))));
}
TEST(DiagsInHeaders, DiagInsideHeader) {
Annotations Main(R"cpp(
#include [["a.h"]]