55 changes: 39 additions & 16 deletions clang-tools-extra/clangd/index/SymbolCollector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ bool isPrivateProtoDecl(const NamedDecl &ND) {
// We only collect #include paths for symbols that are suitable for global code
// completion, except for namespaces since #include path for a namespace is hard
// to define.
bool shouldCollectIncludePath(index::SymbolKind Kind) {
Symbol::IncludeDirective shouldCollectIncludePath(index::SymbolKind Kind) {
using SK = index::SymbolKind;
switch (Kind) {
case SK::Macro:
Expand All @@ -90,9 +90,11 @@ bool shouldCollectIncludePath(index::SymbolKind Kind) {
case SK::Variable:
case SK::EnumConstant:
case SK::Concept:
return true;
return Symbol::Include | Symbol::Import;
case SK::Protocol:
return Symbol::Import;
default:
return false;
return Symbol::Invalid;
}
}

Expand Down Expand Up @@ -805,12 +807,12 @@ void SymbolCollector::processRelations(
}

void SymbolCollector::setIncludeLocation(const Symbol &S, SourceLocation Loc) {
if (Opts.CollectIncludePath)
if (shouldCollectIncludePath(S.SymInfo.Kind))
// Use the expansion location to get the #include header since this is
// where the symbol is exposed.
IncludeFiles[S.ID] =
PP->getSourceManager().getDecomposedExpansionLoc(Loc).first;
if (Opts.CollectIncludePath &&
shouldCollectIncludePath(S.SymInfo.Kind) != Symbol::Invalid)
// Use the expansion location to get the #include header since this is
// where the symbol is exposed.
IncludeFiles[S.ID] =
PP->getSourceManager().getDecomposedExpansionLoc(Loc).first;
}

void SymbolCollector::finish() {
Expand All @@ -835,11 +837,12 @@ void SymbolCollector::finish() {
Symbols.erase(ID);
}
}
llvm::DenseMap<FileID, bool> FileToContainsImportsOrObjC;
// Fill in IncludeHeaders.
// We delay this until end of TU so header guards are all resolved.
llvm::SmallString<128> QName;
for (const auto &Entry : IncludeFiles) {
if (const Symbol *S = Symbols.find(Entry.first)) {
for (const auto &[SID, FID] : IncludeFiles) {
if (const Symbol *S = Symbols.find(SID)) {
llvm::StringRef IncludeHeader;
// Look for an overridden include header for this symbol specifically.
if (Opts.Includes) {
Expand All @@ -856,19 +859,36 @@ void SymbolCollector::finish() {
}
// Otherwise find the approprate include header for the defining file.
if (IncludeHeader.empty())
IncludeHeader = HeaderFileURIs->getIncludeHeader(Entry.second);
IncludeHeader = HeaderFileURIs->getIncludeHeader(FID);

// Symbols in slabs aren't mutable, insert() has to walk all the strings
if (!IncludeHeader.empty()) {
Symbol NewSym = *S;
NewSym.IncludeHeaders.push_back({IncludeHeader, 1});
Symbols.insert(NewSym);
Symbol::IncludeDirective Directives = Symbol::Invalid;
auto CollectDirectives = shouldCollectIncludePath(S->SymInfo.Kind);
if ((CollectDirectives & Symbol::Include) != 0)
Directives |= Symbol::Include;
// Only allow #import for symbols from ObjC-like files.
if ((CollectDirectives & Symbol::Import) != 0) {
auto [It, Inserted] = FileToContainsImportsOrObjC.try_emplace(FID);
if (Inserted)
It->second = FilesWithObjCConstructs.contains(FID) ||
tooling::codeContainsImports(
ASTCtx->getSourceManager().getBufferData(FID));
if (It->second)
Directives |= Symbol::Import;
}
if (Directives != Symbol::Invalid) {
Symbol NewSym = *S;
NewSym.IncludeHeaders.push_back({IncludeHeader, 1, Directives});
Symbols.insert(NewSym);
}
}
}
}

ReferencedSymbols.clear();
IncludeFiles.clear();
FilesWithObjCConstructs.clear();
}

const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND, SymbolID ID,
Expand Down Expand Up @@ -896,7 +916,8 @@ const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND, SymbolID ID,
auto Loc = nameLocation(ND, SM);
assert(Loc.isValid() && "Invalid source location for NamedDecl");
// FIXME: use the result to filter out symbols.
shouldIndexFile(SM.getFileID(Loc));
auto FID = SM.getFileID(Loc);
shouldIndexFile(FID);
if (auto DeclLoc = getTokenLocation(Loc))
S.CanonicalDeclaration = *DeclLoc;

Expand Down Expand Up @@ -940,6 +961,8 @@ const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND, SymbolID ID,

Symbols.insert(S);
setIncludeLocation(S, ND.getLocation());
if (S.SymInfo.Lang == index::SymbolLanguage::ObjC)
FilesWithObjCConstructs.insert(FID);
return Symbols.find(S.ID);
}

Expand Down
3 changes: 3 additions & 0 deletions clang-tools-extra/clangd/index/SymbolCollector.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ class SymbolCollector : public index::IndexDataConsumer {
// File IDs for Symbol.IncludeHeaders.
// The final spelling is calculated in finish().
llvm::DenseMap<SymbolID, FileID> IncludeFiles;
// Files which contain ObjC symbols.
// This is finalized and used in finish().
llvm::DenseSet<FileID> FilesWithObjCConstructs;
void setIncludeLocation(const Symbol &S, SourceLocation);
// Indexed macros, to be erased if they turned out to be include guards.
llvm::DenseSet<const IdentifierInfo *> IndexedMacros;
Expand Down
61 changes: 56 additions & 5 deletions clang-tools-extra/clangd/index/YAMLSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@
#include "llvm/Support/raw_ostream.h"
#include <cstdint>

namespace {
struct YIncludeHeaderWithReferences;
}

LLVM_YAML_IS_SEQUENCE_VECTOR(clang::clangd::Symbol::IncludeHeaderWithReferences)
LLVM_YAML_IS_SEQUENCE_VECTOR(clang::clangd::Ref)
LLVM_YAML_IS_SEQUENCE_VECTOR(YIncludeHeaderWithReferences)

namespace {
using RefBundle =
Expand All @@ -48,6 +53,21 @@ struct YPosition {
uint32_t Line;
uint32_t Column;
};
// A class helps YAML to serialize the IncludeHeaderWithReferences as YAMLIO
// can't directly map bitfields.
struct YIncludeHeaderWithReferences {
llvm::StringRef IncludeHeader;
uint32_t References;
clang::clangd::Symbol::IncludeDirective SupportedDirectives;

YIncludeHeaderWithReferences() = default;

YIncludeHeaderWithReferences(
llvm::StringRef IncludeHeader, uint32_t References,
clang::clangd::Symbol::IncludeDirective SupportedDirectives)
: IncludeHeader(IncludeHeader), References(References),
SupportedDirectives(SupportedDirectives) {}
};

// avoid ODR violation of specialization for non-owned CompileCommand
struct CompileCommandYAML : clang::tooling::CompileCommand {};
Expand Down Expand Up @@ -165,20 +185,51 @@ template <> struct MappingTraits<SymbolInfo> {
}
};

template <>
struct MappingTraits<clang::clangd::Symbol::IncludeHeaderWithReferences> {
static void mapping(IO &IO,
clang::clangd::Symbol::IncludeHeaderWithReferences &Inc) {
template <> struct ScalarBitSetTraits<clang::clangd::Symbol::IncludeDirective> {
static void bitset(IO &IO, clang::clangd::Symbol::IncludeDirective &Value) {
IO.bitSetCase(Value, "Include", clang::clangd::Symbol::Include);
IO.bitSetCase(Value, "Import", clang::clangd::Symbol::Import);
}
};

template <> struct MappingTraits<YIncludeHeaderWithReferences> {
static void mapping(IO &IO, YIncludeHeaderWithReferences &Inc) {
IO.mapRequired("Header", Inc.IncludeHeader);
IO.mapRequired("References", Inc.References);
IO.mapOptional("Directives", Inc.SupportedDirectives,
clang::clangd::Symbol::Include);
}
};

struct NormalizedIncludeHeaders {
using IncludeHeader = clang::clangd::Symbol::IncludeHeaderWithReferences;
NormalizedIncludeHeaders(IO &) {}
NormalizedIncludeHeaders(
IO &, const llvm::SmallVector<IncludeHeader, 1> &IncludeHeaders) {
for (auto &I : IncludeHeaders) {
Headers.emplace_back(I.IncludeHeader, I.References,
I.supportedDirectives());
}
}

llvm::SmallVector<IncludeHeader, 1> denormalize(IO &) {
llvm::SmallVector<IncludeHeader, 1> Result;
for (auto &H : Headers)
Result.emplace_back(H.IncludeHeader, H.References, H.SupportedDirectives);
return Result;
}
llvm::SmallVector<YIncludeHeaderWithReferences, 1> Headers;
};

template <> struct MappingTraits<Symbol> {
static void mapping(IO &IO, Symbol &Sym) {
MappingNormalization<NormalizedSymbolID, SymbolID> NSymbolID(IO, Sym.ID);
MappingNormalization<NormalizedSymbolFlag, Symbol::SymbolFlag> NSymbolFlag(
IO, Sym.Flags);
MappingNormalization<
NormalizedIncludeHeaders,
llvm::SmallVector<Symbol::IncludeHeaderWithReferences, 1>>
NIncludeHeaders(IO, Sym.IncludeHeaders);
IO.mapRequired("ID", NSymbolID->HexString);
IO.mapRequired("Name", Sym.Name);
IO.mapRequired("Scope", Sym.Scope);
Expand All @@ -195,7 +246,7 @@ template <> struct MappingTraits<Symbol> {
IO.mapOptional("Documentation", Sym.Documentation);
IO.mapOptional("ReturnType", Sym.ReturnType);
IO.mapOptional("Type", Sym.Type);
IO.mapOptional("IncludeHeaders", Sym.IncludeHeaders);
IO.mapOptional("IncludeHeaders", NIncludeHeaders->Headers);
}
};

Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clangd/index/remote/Index.proto
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ message Position {
message HeaderWithReferences {
optional string header = 1;
optional uint32 references = 2;
optional uint32 supported_directives = 3;
}

message RelationsRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ llvm::Expected<HeaderWithReferences> Marshaller::toProtobuf(
const clangd::Symbol::IncludeHeaderWithReferences &IncludeHeader) {
HeaderWithReferences Result;
Result.set_references(IncludeHeader.References);
Result.set_supported_directives(IncludeHeader.SupportedDirectives);
const std::string Header = IncludeHeader.IncludeHeader.str();
if (isLiteralInclude(Header)) {
Result.set_header(Header);
Expand All @@ -427,8 +428,12 @@ Marshaller::fromProtobuf(const HeaderWithReferences &Message) {
return URIString.takeError();
Header = *URIString;
}
return clangd::Symbol::IncludeHeaderWithReferences{Strings.save(Header),
Message.references()};
auto Directives = clangd::Symbol::IncludeDirective::Include;
if (Message.has_supported_directives())
Directives = static_cast<clangd::Symbol::IncludeDirective>(
Message.supported_directives());
return clangd::Symbol::IncludeHeaderWithReferences{
Strings.save(Header), Message.references(), Directives};
}

} // namespace remote
Expand Down
Binary file not shown.
48 changes: 38 additions & 10 deletions clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ MATCHER_P(insertInclude, IncludeHeader, "") {
return !arg.Includes.empty() && arg.Includes[0].Header == IncludeHeader &&
bool(arg.Includes[0].Insertion);
}
MATCHER_P(insertIncludeText, InsertedText, "") {
return !arg.Includes.empty() && arg.Includes[0].Insertion &&
arg.Includes[0].Insertion->newText == InsertedText;
}
MATCHER(insertInclude, "") {
return !arg.Includes.empty() && bool(arg.Includes[0].Insertion);
}
Expand Down Expand Up @@ -812,7 +816,7 @@ TEST(CompletionTest, IncludeInsertionPreprocessorIntegrationTests) {

Symbol Sym = cls("ns::X");
Sym.CanonicalDeclaration.FileURI = BarURI.c_str();
Sym.IncludeHeaders.emplace_back(BarURI, 1);
Sym.IncludeHeaders.emplace_back(BarURI, 1, Symbol::Include);
// Shorten include path based on search directory and insert.
Annotations Test("int main() { ns::^ }");
TU.Code = Test.code().str();
Expand Down Expand Up @@ -843,8 +847,8 @@ TEST(CompletionTest, NoIncludeInsertionWhenDeclFoundInFile) {
auto BarURI = URI::create(BarHeader).toString();
SymX.CanonicalDeclaration.FileURI = BarURI.c_str();
SymY.CanonicalDeclaration.FileURI = BarURI.c_str();
SymX.IncludeHeaders.emplace_back("<bar>", 1);
SymY.IncludeHeaders.emplace_back("<bar>", 1);
SymX.IncludeHeaders.emplace_back("<bar>", 1, Symbol::Include);
SymY.IncludeHeaders.emplace_back("<bar>", 1, Symbol::Include);
// Shorten include path based on search directory and insert.
auto Results = completions(R"cpp(
namespace ns {
Expand Down Expand Up @@ -1867,7 +1871,7 @@ TEST(CompletionTest, OverloadBundling) {
// Differences in header-to-insert suppress bundling.
std::string DeclFile = URI::create(testPath("foo")).toString();
NoArgsGFunc.CanonicalDeclaration.FileURI = DeclFile.c_str();
NoArgsGFunc.IncludeHeaders.emplace_back("<foo>", 1);
NoArgsGFunc.IncludeHeaders.emplace_back("<foo>", 1, Symbol::Include);
EXPECT_THAT(
completions(Context + "int y = GFunc^", {NoArgsGFunc}, Opts).Completions,
UnorderedElementsAre(AllOf(named("GFuncC"), insertInclude("<foo>")),
Expand Down Expand Up @@ -1901,8 +1905,8 @@ TEST(CompletionTest, OverloadBundlingSameFileDifferentURI) {
SymX.CanonicalDeclaration.FileURI = BarURI.c_str();
SymY.CanonicalDeclaration.FileURI = BarURI.c_str();
// The include header is different, but really it's the same file.
SymX.IncludeHeaders.emplace_back("\"bar.h\"", 1);
SymY.IncludeHeaders.emplace_back(BarURI.c_str(), 1);
SymX.IncludeHeaders.emplace_back("\"bar.h\"", 1, Symbol::Include);
SymY.IncludeHeaders.emplace_back(BarURI.c_str(), 1, Symbol::Include);

auto Results = completions("void f() { ::ns::^ }", {SymX, SymY}, Opts);
// Expect both results are bundled, despite the different-but-same
Expand Down Expand Up @@ -2699,15 +2703,39 @@ TEST(CompletionTest, InsertTheMostPopularHeader) {
std::string DeclFile = URI::create(testPath("foo")).toString();
Symbol Sym = func("Func");
Sym.CanonicalDeclaration.FileURI = DeclFile.c_str();
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 2);
Sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000);
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 2, Symbol::Include);
Sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000, Symbol::Include);

auto Results = completions("Fun^", {Sym}).Completions;
assert(!Results.empty());
EXPECT_THAT(Results[0], AllOf(named("Func"), insertInclude("\"bar.h\"")));
EXPECT_EQ(Results[0].Includes.size(), 2u);
}

TEST(CompletionTest, InsertIncludeOrImport) {
std::string DeclFile = URI::create(testPath("foo")).toString();
Symbol Sym = func("Func");
Sym.CanonicalDeclaration.FileURI = DeclFile.c_str();
Sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000,
Symbol::Include | Symbol::Import);

auto Results = completions("Fun^", {Sym}).Completions;
assert(!Results.empty());
EXPECT_THAT(Results[0],
AllOf(named("Func"), insertIncludeText("#include \"bar.h\"\n")));

Results = completions("Fun^", {Sym}, {}, "Foo.m").Completions;
assert(!Results.empty());
// TODO: Once #import integration support is done this should be #import.
EXPECT_THAT(Results[0],
AllOf(named("Func"), insertIncludeText("#include \"bar.h\"\n")));

Sym.IncludeHeaders[0].SupportedDirectives = Symbol::Import;
Results = completions("Fun^", {Sym}).Completions;
assert(!Results.empty());
EXPECT_THAT(Results[0], AllOf(named("Func"), Not(insertInclude())));
}

TEST(CompletionTest, NoInsertIncludeIfOnePresent) {
Annotations Test(R"cpp(
#include "foo.h"
Expand All @@ -2719,8 +2747,8 @@ TEST(CompletionTest, NoInsertIncludeIfOnePresent) {
std::string DeclFile = URI::create(testPath("foo")).toString();
Symbol Sym = func("Func");
Sym.CanonicalDeclaration.FileURI = DeclFile.c_str();
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 2);
Sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000);
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 2, Symbol::Include);
Sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000, Symbol::Include);

EXPECT_THAT(completions(TU, Test.point(), {Sym}).Completions,
UnorderedElementsAre(AllOf(named("Func"), hasInclude("\"foo.h\""),
Expand Down
8 changes: 4 additions & 4 deletions clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ buildIndexWithSymbol(llvm::ArrayRef<SymbolWithHeader> Syms) {
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.CanonicalDeclaration.FileURI = S.DeclaringFile.c_str();
Sym.Definition.FileURI = S.DeclaringFile.c_str();
Sym.IncludeHeaders.emplace_back(S.IncludeHeader, 1);
Sym.IncludeHeaders.emplace_back(S.IncludeHeader, 1, Symbol::Include);
Slab.insert(Sym);
}
return MemIndex::build(std::move(Slab).build(), RefSlab(), RelationSlab());
Expand Down Expand Up @@ -1129,7 +1129,7 @@ TEST(IncludeFixerTest, IncompleteEnum) {
Symbol Sym = enm("X");
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.CanonicalDeclaration.FileURI = Sym.Definition.FileURI = "unittest:///x.h";
Sym.IncludeHeaders.emplace_back("\"x.h\"", 1);
Sym.IncludeHeaders.emplace_back("\"x.h\"", 1, Symbol::Include);
SymbolSlab::Builder Slab;
Slab.insert(Sym);
auto Index =
Expand Down Expand Up @@ -1172,7 +1172,7 @@ int main() {
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.CanonicalDeclaration.FileURI = "unittest:///x.h";
Sym.Definition.FileURI = "unittest:///x.cc";
Sym.IncludeHeaders.emplace_back("\"x.h\"", 1);
Sym.IncludeHeaders.emplace_back("\"x.h\"", 1, Symbol::Include);

SymbolSlab::Builder Slab;
Slab.insert(Sym);
Expand Down Expand Up @@ -1503,7 +1503,7 @@ TEST(IncludeFixerTest, CImplicitFunctionDecl) {
Symbol Sym = func("foo");
Sym.Flags |= Symbol::IndexedForCodeCompletion;
Sym.CanonicalDeclaration.FileURI = "unittest:///foo.h";
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 1);
Sym.IncludeHeaders.emplace_back("\"foo.h\"", 1, Symbol::Include);

SymbolSlab::Builder Slab;
Slab.insert(Sym);
Expand Down
16 changes: 11 additions & 5 deletions clang-tools-extra/clangd/unittests/HeadersTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/CompilerInvocation.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/Inclusions/HeaderIncludes.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FormatVariadic.h"
Expand Down Expand Up @@ -117,7 +118,8 @@ class HeadersTest : public ::testing::Test {
return Path.value_or("");
}

llvm::Optional<TextEdit> insert(llvm::StringRef VerbatimHeader) {
llvm::Optional<TextEdit> insert(llvm::StringRef VerbatimHeader,
tooling::IncludeDirective Directive) {
Clang = setupClang();
PreprocessOnlyAction Action;
EXPECT_TRUE(
Expand All @@ -126,7 +128,7 @@ class HeadersTest : public ::testing::Test {
IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(),
CDB.getCompileCommand(MainFile)->Directory,
&Clang->getPreprocessor().getHeaderSearchInfo());
auto Edit = Inserter.insert(VerbatimHeader);
auto Edit = Inserter.insert(VerbatimHeader, Directive);
Action.EndSourceFile();
return Edit;
}
Expand Down Expand Up @@ -330,9 +332,13 @@ TEST_F(HeadersTest, DontInsertDuplicateResolved) {
}

TEST_F(HeadersTest, PreferInserted) {
auto Edit = insert("<y>");
EXPECT_TRUE(Edit);
EXPECT_TRUE(StringRef(Edit->newText).contains("<y>"));
auto Edit = insert("<y>", tooling::IncludeDirective::Include);
ASSERT_TRUE(Edit);
EXPECT_EQ(Edit->newText, "#include <y>\n");

Edit = insert("\"header.h\"", tooling::IncludeDirective::Import);
ASSERT_TRUE(Edit);
EXPECT_EQ(Edit->newText, "#import \"header.h\"\n");
}

TEST(Headers, NoHeaderSearchInfo) {
Expand Down
8 changes: 4 additions & 4 deletions clang-tools-extra/clangd/unittests/IndexTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,9 +567,9 @@ TEST(MergeTest, MergeIncludesOnDifferentDefinitions) {
L.Name = "left";
R.Name = "right";
L.ID = R.ID = SymbolID("hello");
L.IncludeHeaders.emplace_back("common", 1);
R.IncludeHeaders.emplace_back("common", 1);
R.IncludeHeaders.emplace_back("new", 1);
L.IncludeHeaders.emplace_back("common", 1, Symbol::Include);
R.IncludeHeaders.emplace_back("common", 1, Symbol::Include);
R.IncludeHeaders.emplace_back("new", 1, Symbol::Include);

// Both have no definition.
Symbol M = mergeSymbol(L, R);
Expand Down Expand Up @@ -615,7 +615,7 @@ TEST(MergeIndexTest, IncludeHeadersMerged) {
std::move(DynData), DynSize);

SymbolSlab::Builder StaticB;
S.IncludeHeaders.push_back({"<header>", 0});
S.IncludeHeaders.push_back({"<header>", 0, Symbol::Include});
StaticB.insert(S);
auto StaticIndex =
MemIndex::build(std::move(StaticB).build(), RefSlab(), RelationSlab());
Expand Down
26 changes: 21 additions & 5 deletions clang-tools-extra/clangd/unittests/SerializationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,16 @@ ReturnType: 'int'
IncludeHeaders:
- Header: 'include1'
References: 7
Directives: [ Include ]
- Header: 'include2'
References: 3
Directives: [ Import ]
- Header: 'include3'
References: 2
Directives: [ Include, Import ]
- Header: 'include4'
References: 1
Directives: [ ]
...
---
!Symbol
Expand Down Expand Up @@ -114,8 +122,11 @@ Digest: EED8F5EAF25C453C

MATCHER_P(id, I, "") { return arg.ID == cantFail(SymbolID::fromStr(I)); }
MATCHER_P(qName, Name, "") { return (arg.Scope + arg.Name).str() == Name; }
MATCHER_P2(IncludeHeaderWithRef, IncludeHeader, References, "") {
return (arg.IncludeHeader == IncludeHeader) && (arg.References == References);
MATCHER_P3(IncludeHeaderWithRefAndDirectives, IncludeHeader, References,
SupportedDirectives, "") {
return (arg.IncludeHeader == IncludeHeader) &&
(arg.References == References) &&
(arg.SupportedDirectives == SupportedDirectives);
}

auto readIndexFile(llvm::StringRef Text) {
Expand Down Expand Up @@ -148,9 +159,14 @@ TEST(SerializationTest, YAMLConversions) {
EXPECT_EQ(static_cast<uint8_t>(Sym1.Flags), 129);
EXPECT_TRUE(Sym1.Flags & Symbol::IndexedForCodeCompletion);
EXPECT_FALSE(Sym1.Flags & Symbol::Deprecated);
EXPECT_THAT(Sym1.IncludeHeaders,
UnorderedElementsAre(IncludeHeaderWithRef("include1", 7u),
IncludeHeaderWithRef("include2", 3u)));
EXPECT_THAT(
Sym1.IncludeHeaders,
UnorderedElementsAre(
IncludeHeaderWithRefAndDirectives("include1", 7u, Symbol::Include),
IncludeHeaderWithRefAndDirectives("include2", 3u, Symbol::Import),
IncludeHeaderWithRefAndDirectives("include3", 2u,
Symbol::Include | Symbol::Import),
IncludeHeaderWithRefAndDirectives("include4", 1u, Symbol::Invalid)));

EXPECT_THAT(Sym2, qName("clang::Foo2"));
EXPECT_EQ(Sym2.Signature, "-sig");
Expand Down
5 changes: 4 additions & 1 deletion clang/include/clang/Tooling/Inclusions/HeaderAnalysis.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ namespace tooling {
/// Returns true if the given physical file is a self-contained header.
///
/// A header is considered self-contained if
// - it has a proper header guard or has been #imported
// - it has a proper header guard or has been #imported or contains #import(s)
// - *and* it doesn't have a dont-include-me pattern.
///
/// This function can be expensive as it may scan the source code to find out
/// dont-include-me pattern heuristically.
bool isSelfContainedHeader(const FileEntry *FE, const SourceManager &SM,
HeaderSearch &HeaderInfo);

/// This scans the given source code to see if it contains #import(s).
bool codeContainsImports(llvm::StringRef Code);

/// If Text begins an Include-What-You-Use directive, returns it.
/// Given "// IWYU pragma: keep", returns "keep".
/// Input is a null-terminated char* as provided by SM.getCharacterData().
Expand Down
24 changes: 15 additions & 9 deletions clang/include/clang/Tooling/Inclusions/HeaderIncludes.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ class IncludeCategoryManager {
SmallVector<llvm::Regex, 4> CategoryRegexs;
};

enum class IncludeDirective { Include, Import };

/// Generates replacements for inserting or deleting #include directives in a
/// file.
class HeaderIncludes {
public:
HeaderIncludes(llvm::StringRef FileName, llvm::StringRef Code,
const IncludeStyle &Style);

/// Inserts an #include directive of \p Header into the code. If \p IsAngled
/// is true, \p Header will be quoted with <> in the directive; otherwise, it
/// will be quoted with "".
/// Inserts an #include or #import directive of \p Header into the code.
/// If \p IsAngled is true, \p Header will be quoted with <> in the directive;
/// otherwise, it will be quoted with "".
///
/// When searching for points to insert new header, this ignores #include's
/// after the #include block(s) in the beginning of a file to avoid inserting
Expand All @@ -71,26 +73,30 @@ class HeaderIncludes {
/// \p IncludeName already exists (with exactly the same spelling), this
/// returns std::nullopt.
llvm::Optional<tooling::Replacement> insert(llvm::StringRef Header,
bool IsAngled) const;
bool IsAngled,
IncludeDirective Directive) const;

/// Removes all existing #includes of \p Header quoted with <> if \p IsAngled
/// is true or "" if \p IsAngled is false.
/// This doesn't resolve the header file path; it only deletes #includes with
/// exactly the same spelling.
/// Removes all existing #includes and #imports of \p Header quoted with <> if
/// \p IsAngled is true or "" if \p IsAngled is false.
/// This doesn't resolve the header file path; it only deletes #includes and
/// #imports with exactly the same spelling.
tooling::Replacements remove(llvm::StringRef Header, bool IsAngled) const;

// Matches a whole #include directive.
static const llvm::Regex IncludeRegex;

private:
struct Include {
Include(StringRef Name, tooling::Range R) : Name(Name), R(R) {}
Include(StringRef Name, tooling::Range R, IncludeDirective D)
: Name(Name), R(R), Directive(D) {}

// An include header quoted with either <> or "".
std::string Name;
// The range of the whole line of include directive including any leading
// whitespaces and trailing comment.
tooling::Range R;
// Either #include or #import.
IncludeDirective Directive;
};

void addExistingInclude(Include IncludeToAdd, unsigned NextLineOffset);
Expand Down
3 changes: 2 additions & 1 deletion clang/lib/Format/Format.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3302,7 +3302,8 @@ fixCppIncludeInsertions(StringRef Code, const tooling::Replacements &Replaces,
(void)Matched;
auto IncludeName = Matches[2];
auto Replace =
Includes.insert(IncludeName.trim("\"<>"), IncludeName.startswith("<"));
Includes.insert(IncludeName.trim("\"<>"), IncludeName.startswith("<"),
tooling::IncludeDirective::Include);
if (Replace) {
auto Err = Result.add(*Replace);
if (Err) {
Expand Down
40 changes: 34 additions & 6 deletions clang/lib/Tooling/Inclusions/HeaderAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ bool isErrorAboutInclude(llvm::StringRef Line) {
}

// Heuristically headers that only want to be included via an umbrella.
bool isDontIncludeMeHeader(llvm::MemoryBufferRef Buffer) {
StringRef Content = Buffer.getBuffer();
bool isDontIncludeMeHeader(StringRef Content) {
llvm::StringRef Line;
// Only sniff up to 100 lines or 10KB.
Content = Content.take_front(100 * 100);
Expand All @@ -50,19 +49,48 @@ bool isDontIncludeMeHeader(llvm::MemoryBufferRef Buffer) {
return false;
}

bool isImportLine(llvm::StringRef Line) {
Line = Line.ltrim();
if (!Line.consume_front("#"))
return false;
Line = Line.ltrim();
return Line.startswith("import");
}

llvm::StringRef getFileContents(const FileEntry *FE, const SourceManager &SM) {
return const_cast<SourceManager &>(SM)
.getMemoryBufferForFileOrNone(FE)
.value_or(llvm::MemoryBufferRef())
.getBuffer();
}

} // namespace

bool isSelfContainedHeader(const FileEntry *FE, const SourceManager &SM,
HeaderSearch &HeaderInfo) {
assert(FE);
if (!HeaderInfo.isFileMultipleIncludeGuarded(FE) &&
!HeaderInfo.hasFileBeenImported(FE))
!HeaderInfo.hasFileBeenImported(FE) &&
// Any header that contains #imports is supposed to be #import'd so no
// need to check for anything but the main-file.
(SM.getFileEntryForID(SM.getMainFileID()) != FE ||
!codeContainsImports(getFileContents(FE, SM))))
return false;
// This pattern indicates that a header can't be used without
// particular preprocessor state, usually set up by another header.
return !isDontIncludeMeHeader(
const_cast<SourceManager &>(SM).getMemoryBufferForFileOrNone(FE).value_or(
llvm::MemoryBufferRef()));
return !isDontIncludeMeHeader(getFileContents(FE, SM));
}

bool codeContainsImports(llvm::StringRef Code) {
// Only sniff up to 100 lines or 10KB.
Code = Code.take_front(100 * 100);
llvm::StringRef Line;
for (unsigned I = 0; I < 100 && !Code.empty(); ++I) {
std::tie(Line, Code) = Code.split('\n');
if (isImportLine(Line))
return true;
}
return false;
}

llvm::Optional<StringRef> parseIWYUPragma(const char *Text) {
Expand Down
21 changes: 14 additions & 7 deletions clang/lib/Tooling/Inclusions/HeaderIncludes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ HeaderIncludes::HeaderIncludes(StringRef FileName, StringRef Code,
addExistingInclude(
Include(Matches[2],
tooling::Range(
Offset, std::min(Line.size() + 1, Code.size() - Offset))),
Offset, std::min(Line.size() + 1, Code.size() - Offset)),
Matches[1] == "import" ? tooling::IncludeDirective::Import
: tooling::IncludeDirective::Include),
NextLineOffset);
}
Offset = NextLineOffset;
Expand Down Expand Up @@ -342,17 +344,20 @@ void HeaderIncludes::addExistingInclude(Include IncludeToAdd,
}

llvm::Optional<tooling::Replacement>
HeaderIncludes::insert(llvm::StringRef IncludeName, bool IsAngled) const {
HeaderIncludes::insert(llvm::StringRef IncludeName, bool IsAngled,
IncludeDirective Directive) const {
assert(IncludeName == trimInclude(IncludeName));
// If a <header> ("header") already exists in code, "header" (<header>) with
// different quotation will still be inserted.
// different quotation and/or directive will still be inserted.
// FIXME: figure out if this is the best behavior.
auto It = ExistingIncludes.find(IncludeName);
if (It != ExistingIncludes.end())
if (It != ExistingIncludes.end()) {
for (const auto &Inc : It->second)
if ((IsAngled && StringRef(Inc.Name).startswith("<")) ||
(!IsAngled && StringRef(Inc.Name).startswith("\"")))
if (Inc.Directive == Directive &&
((IsAngled && StringRef(Inc.Name).startswith("<")) ||
(!IsAngled && StringRef(Inc.Name).startswith("\""))))
return std::nullopt;
}
std::string Quoted =
std::string(llvm::formatv(IsAngled ? "<{0}>" : "\"{0}\"", IncludeName));
StringRef QuotedName = Quoted;
Expand All @@ -371,8 +376,10 @@ HeaderIncludes::insert(llvm::StringRef IncludeName, bool IsAngled) const {
}
}
assert(InsertOffset <= Code.size());
llvm::StringRef DirectiveSpelling =
Directive == IncludeDirective::Include ? "include" : "import";
std::string NewInclude =
std::string(llvm::formatv("#include {0}\n", QuotedName));
llvm::formatv("#{0} {1}\n", DirectiveSpelling, QuotedName);
// When inserting headers at end of the code, also append '\n' to the code
// if it does not end with '\n'.
// FIXME: when inserting multiple #includes at the end of code, only one
Expand Down
31 changes: 31 additions & 0 deletions clang/unittests/Tooling/HeaderAnalysisTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,42 @@ TEST(HeaderAnalysisTest, IsSelfContained) {
EXPECT_TRUE(isSelfContainedHeader(FM.getFile("headerguard.h").get(), SM, HI));
EXPECT_TRUE(isSelfContainedHeader(FM.getFile("pragmaonce.h").get(), SM, HI));
EXPECT_TRUE(isSelfContainedHeader(FM.getFile("imported.h").get(), SM, HI));
EXPECT_TRUE(
isSelfContainedHeader(SM.getFileEntryForID(SM.getMainFileID()), SM, HI));

EXPECT_FALSE(isSelfContainedHeader(FM.getFile("unguarded.h").get(), SM, HI));
EXPECT_FALSE(isSelfContainedHeader(FM.getFile("bad.h").get(), SM, HI));
}

TEST(HeaderAnalysisTest, CodeContainsImports) {
EXPECT_TRUE(codeContainsImports(R"cpp(
#include "foo.h"
#import "NSFoo.h"
int main() {
foo();
}
)cpp"));

EXPECT_TRUE(codeContainsImports(R"cpp(
#include "foo.h"
int main() {
foo();
}
#import "NSFoo.h"
)cpp"));

EXPECT_FALSE(codeContainsImports(R"cpp(
#include "foo.h"
int main() {
foo();
}
)cpp"));
}

TEST(HeaderAnalysisTest, ParseIWYUPragma) {
EXPECT_THAT(parseIWYUPragma("// IWYU pragma: keep"), ValueIs(Eq("keep")));
EXPECT_THAT(parseIWYUPragma("// IWYU pragma: keep me\netc"),
Expand Down
25 changes: 23 additions & 2 deletions clang/unittests/Tooling/HeaderIncludesTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ namespace {

class HeaderIncludesTest : public ::testing::Test {
protected:
std::string insert(llvm::StringRef Code, llvm::StringRef Header) {
std::string insert(llvm::StringRef Code, llvm::StringRef Header,
IncludeDirective Directive = IncludeDirective::Include) {
HeaderIncludes Includes(FileName, Code, Style);
assert(Header.startswith("\"") || Header.startswith("<"));
auto R = Includes.insert(Header.trim("\"<>"), Header.startswith("<"));
auto R =
Includes.insert(Header.trim("\"<>"), Header.startswith("<"), Directive);
if (!R)
return std::string(Code);
auto Result = applyAllReplacements(Code, Replacements(*R));
Expand Down Expand Up @@ -60,6 +62,25 @@ TEST_F(HeaderIncludesTest, RepeatedIncludes) {
EXPECT_EQ(Expected, insert(Code, "\"a2.h\""));
}

TEST_F(HeaderIncludesTest, InsertImportWithSameInclude) {
std::string Code = "#include \"a.h\"\n";
std::string Expected = Code + "#import \"a.h\"\n";
EXPECT_EQ(Expected, insert(Code, "\"a.h\"", IncludeDirective::Import));
}

TEST_F(HeaderIncludesTest, DontInsertAlreadyImported) {
std::string Code = "#import \"a.h\"\n";
EXPECT_EQ(Code, insert(Code, "\"a.h\"", IncludeDirective::Import));
}

TEST_F(HeaderIncludesTest, DeleteImportAndSameInclude) {
std::string Code = R"cpp(
#include <abc.h>
#import <abc.h>
int x;)cpp";
EXPECT_EQ("\nint x;", remove(Code, "<abc.h>"));
}

TEST_F(HeaderIncludesTest, NoExistingIncludeWithDefine) {
std::string Code = "#ifndef A_H\n"
"#define A_H\n"
Expand Down