From 40912a8a912b91cb228d8a78fd64a825a44a9350 Mon Sep 17 00:00:00 2001 From: Shiyu Date: Tue, 18 Feb 2025 21:40:43 +0800 Subject: [PATCH] Implement `Hover` on `Expression` and `Identifier` and some fix. (#83) --- CMakeLists.txt | 1 + include/AST/Selection.h | 157 ++++++- include/Basic/SourceConverter.h | 4 +- include/Compiler/AST.h | 10 +- include/Compiler/Directive.h | 15 + include/Feature/Hover.h | 13 +- src/AST/Selection.cpp | 266 ++++++----- src/Basic/SourceCode.cpp | 6 +- src/Basic/SourceConverter.cpp | 4 +- src/Compiler/Directive.cpp | 13 +- src/Driver/unit_tests.cc | 3 +- src/Feature/Hover.cpp | 781 ++++++++++++++++++++++++++++---- src/Feature/InlayHint.cpp | 5 +- unittests/AST/Selection.cpp | 414 +++++++++++++++++ unittests/Feature/Hover.cpp | 210 ++++++++- xmake.lua | 1 + 16 files changed, 1672 insertions(+), 231 deletions(-) create mode 100644 unittests/AST/Selection.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f2a1103..9f4c0264 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ target_link_libraries(integration_tests PRIVATE clice-core) if(CLICE_ENABLE_TEST) file(GLOB_RECURSE CLICE_TEST_SOURCES "${CMAKE_SOURCE_DIR}/unittests/*/*.cpp") add_executable(unit_tests "${CLICE_TEST_SOURCES}" "${CMAKE_SOURCE_DIR}/src/Driver/unit_tests.cc") + target_include_directories(unit_tests PUBLIC "${CMAKE_SOURCE_DIR}") FetchContent_Declare( googletest diff --git a/include/AST/Selection.h b/include/AST/Selection.h index d278afcb..8357dcb2 100644 --- a/include/AST/Selection.h +++ b/include/AST/Selection.h @@ -1,8 +1,9 @@ #pragma once +#include +#include + #include -#include "clang/AST/ASTTypeTraits.h" -#include "clang/Tooling/Syntax/Tokens.h" namespace clice { @@ -14,22 +15,166 @@ namespace clice { // expand macro(one step by step). // invert if. +class ASTInfo; + +namespace { +class SelectionBuilder; +} + class SelectionTree { + friend class SelectionBuilder; + public: - struct Node { - Node* parent; - clang::DynTypedNode node; - llvm::SmallVector children; + /// The extent to which an selection is covered by the AST node. + enum class CoverageKind : unsigned { + /// For example, if the selection is + /// + /// void f() { + /// int x = 1; + /// ^^^ + /// } + /// + /// The FunctionDecl `f()` and VarDecl `x` would fully cover the selection. + Full, + + /// For example, if the selection is + /// + /// if (x == 1) { + /// ^^^^^^^^^^^^^ + /// int y = 2; + /// } + /// + /// The IfStmt would fully cover the selection while the Expr `x == 1` would partially + /// cover the selection. + Partial, }; + /// An AST node is involved in the selection, either selected directly or some descendant node + /// is selected. + struct Node { + /// The AST node that is selected. + clang::DynTypedNode dynNode; + + /// The extent to which the selection is covered by the AST node. + CoverageKind kind; + + /// In most cases, there is only 1 child in a selected node. Use SmallVector with stack + /// capability 1 to reduce the size of Node. + llvm::SmallVector children; + + /// The parent node in the selection tree. nullptr for root node. + Node* parent; + + template + bool isOneOf() const { + return dynNode.get() || (dynNode.get() || ...); + } + }; + + /// Construct an empty selection tree. SelectionTree() = default; + SelectionTree(const SelectionTree&) = delete; + SelectionTree& operator= (const SelectionTree&) = delete; + + SelectionTree(SelectionTree&&) = default; + SelectionTree& operator= (SelectionTree&&) = default; + + /// Check if there is any selection. + bool hasValue() const { + return root != nullptr; + } + + // Return nullptr if there is no selection. + const Node* getRoot() const { + return root; + } + + std::deque& children() { + return storage; + } + + const std::deque& children() const { + return storage; + } + + /// Return true to continue the walk, false to stop. + using Walker = llvm::function_ref; + + /// Return true if the walk is completed, false if the walk is interrupted. + bool walkDfs(Walker ops) const { + if(!root) + return true; + + llvm::SmallVector stack; + stack.push_back(root); + while(!stack.empty()) { + auto node = stack.pop_back_val(); + + if(!ops(node)) + return false; + + for(auto child: node->children) { + stack.push_back(child); + } + } + + return true; + } + + /// Return true if the walk is completed, false if the walk is interrupted. + bool walkBfs(Walker ops) const { + if(!root) + return true; + + std::deque queue; + queue.push_back(root); + + while(!queue.empty()) { + auto node = queue.front(); + queue.pop_front(); + + if(!ops(node)) + return false; + + for(auto child: node->children) { + queue.push_front(child); + } + } + + return true; + } + + explicit operator bool () const { + return hasValue(); + } + + void dump(llvm::raw_ostream& os, clang::ASTContext& context) const; + + static SelectionTree selectOffsetRange(std::uint32_t begin, + std::uint32_t end, + clang::ASTContext& context, + clang::syntax::TokenBuffer& tokens) { + return SelectionTree(begin, end, context, tokens); + } + + static SelectionTree selectToken(const clang::syntax::Token& token, + clang::ASTContext& context, + clang::syntax::TokenBuffer& tokens); + +private: + /// Construct a selection tree from the given source range. `start` and `end` means offset from + /// file start location, these arguments should come from function `SourceConverter::toOffset`. SelectionTree(std::uint32_t begin, std::uint32_t end, clang::ASTContext& context, clang::syntax::TokenBuffer& tokens); + // The root node of selection tree. Node* root; + + // The AST nodes was stored in the order from root to leaf. + // Use deque as the stable pointer storage. std::deque storage; }; diff --git a/include/Basic/SourceConverter.h b/include/Basic/SourceConverter.h index 3536082a..43561b0d 100644 --- a/include/Basic/SourceConverter.h +++ b/include/Basic/SourceConverter.h @@ -40,14 +40,14 @@ public: proto::Range toRange(clang::SourceRange range, const clang::SourceManager& SM) const; /// Same as the above, but input is a `LocalSourceRange` and the content is provided. - proto::Range toRange(LocalSourceRange range, llvm::StringRef conent) const; + proto::Range toRange(LocalSourceRange range, llvm::StringRef content) const; /// Convert a clang::SourceRange to LocalSourceRange. LocalSourceRange toLocalRange(clang::SourceRange range, const clang::SourceManager& SM) const; /// Convert a proto::Position to a file offset in the content with the specified /// encoding kind. - std::size_t toOffset(llvm::StringRef content, proto::Position position) const; + std::uint32_t toOffset(llvm::StringRef content, proto::Position position) const; /// Get the encoding kind of the content in LSP protocol. proto::PositionEncodingKind encodingKind() const { diff --git a/include/Compiler/AST.h b/include/Compiler/AST.h index c506a6e1..2f57d078 100644 --- a/include/Compiler/AST.h +++ b/include/Compiler/AST.h @@ -1,6 +1,7 @@ #pragma once #include "Directive.h" +#include "Basic/SourceCode.h" #include "AST/Resolver.h" #include "Basic/SourceCode.h" #include "clang/Frontend/CompilerInstance.h" @@ -67,13 +68,20 @@ public: return instance->getASTContext().getTranslationUnitDecl(); } + clang::FileID mainFileID() { + return srcMgr().getMainFileID(); + } + /// The interested file ID. For file without header context, it is the main file ID. /// For file with header context, it is the file ID of header file. clang::FileID getInterestedFile() { return interested; } -public: + llvm::StringRef getMainFileContent() { + return getFileContent(srcMgr(), mainFileID()); + } + /// All files involved in building the AST. const llvm::DenseSet& files(); diff --git a/include/Compiler/Directive.h b/include/Compiler/Directive.h index 68a72443..340994e5 100644 --- a/include/Compiler/Directive.h +++ b/include/Compiler/Directive.h @@ -14,6 +14,21 @@ struct Include { /// Location of the `include`. clang::SourceLocation location; + + /// The name of the included file. + std::string fileName; + + /// If the file was found via an absolute include path, `searchPath` will be empty. + /// For framework includes, the `searchPath` and `relativePath` will be split up. + /// + /// For example, if an include of "Some/Some.h" is found via the framework path + /// "path/to/Frameworks/Some.framework/Headers/Some.h" + /// `searchPath` will be "path/to/Frameworks/Some.framework/Headers" + /// `relativePath` will be "Some.h". + std::string searchPath; + + /// See `searchPath`. + std::string relativePath; }; /// Information about `__has_include` directive. diff --git a/include/Feature/Hover.h b/include/Feature/Hover.h index 68982633..911f62d9 100644 --- a/include/Feature/Hover.h +++ b/include/Feature/Hover.h @@ -6,16 +6,16 @@ namespace clice { class ASTInfo; -class SourceConverter; namespace proto { +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#hoverParams struct HoverParams { // The text document. URI textDocument; // The position inside the text document. - Position postion; + Position position; }; } // namespace proto @@ -40,7 +40,7 @@ enum class MemoryLayoutRenderKind : uint8_t { struct HoverOption { /// The maximum number of fields to show in the hover of a class/struct/enum. 0 means show all. - uint16_t maxFieldsCount; + uint16_t maxFieldsCount = 0; /// TODO: /// The maximum number of derived classes to show in the hover of a pure virtual class. @@ -84,10 +84,9 @@ struct Result { Result hover(const clang::Decl* decl, const config::HoverOption& option); /// Compute inlay hints for MainfileID in given param and config. -Result hover(proto::HoverParams param, - ASTInfo& info, - const SourceConverter& converter, - const config::HoverOption& option); +std::optional hover(const proto::HoverParams& param, + ASTInfo& AST, + const config::HoverOption& option); proto::MarkupContent toLspType(Result hover); diff --git a/src/AST/Selection.cpp b/src/AST/Selection.cpp index dee8cc2f..88ef480c 100644 --- a/src/AST/Selection.cpp +++ b/src/AST/Selection.cpp @@ -1,110 +1,137 @@ -#include +#include +#include -#include "AST/Selection.h" -#include "clang/AST/RecursiveASTVisitor.h" +#include + +#include namespace clice { namespace { -class SelectionBuilder { -public: +struct SelectionBuilder { + using Token = clang::syntax::Token; + using OffsetPair = std::pair; + SelectionBuilder(std::uint32_t begin, std::uint32_t end, clang::ASTContext& context, clang::syntax::TokenBuffer& buffer) : context(context), buffer(buffer) { + assert(end >= begin && "End offset should be greater than or equal to begin offset."); + // The location in clang AST is token-based, of course. Because the parser // processes tokens from the lexer. So we need to find boundary tokens at first. - auto& sm = context.getSourceManager(); // FIXME: support other file. - auto tokens = buffer.spelledTokens(sm.getMainFileID()); + // FIXME: support other file. + auto& src = context.getSourceManager(); + auto tokens = buffer.spelledTokens(src.getMainFileID()); + auto bound = selectionBound(tokens, {begin, end}, src); - left = std::to_address( - std::partition_point(tokens.begin(), tokens.end(), [&](const auto& token) { - // int xxxx = 3; - // ^^^^^^ - // expect to find the first token whose end location is greater than or equal to - // `begin`. - return sm.getFileOffset(token.endLocation()) < begin; - })); + left = bound.first, right = bound.second; + } - rigth = std::to_address( - std::partition_point(tokens.rbegin(), tokens.rend(), [&](const auto& token) { - // int xxxx = 3; - // ^^^^^^ - // expect to find the first token whose start location is less than or equal to - // `end`. - return sm.getFileOffset(token.location()) > end; - })); + /// Construct a selection builder from two boundary tokens. the `left` and `right` should come + /// from `fixSelectionBound`. + /// The constructor is used for unittest. + SelectionBuilder(const Token* left, + const Token* right, + clang::ASTContext& context, + clang::syntax::TokenBuffer& buffer) : + left(left), right(right), context(context), buffer(buffer) {} - if(left == tokens.end() || rigth == tokens.end()) { - std::terminate(); - return; - } + /// Compute 2 boundary tokens by given pair of offset as the selection range, the `end` of + /// pair should be greater than `begin`. + static auto selectionBound(llvm::ArrayRef tokens, + OffsetPair offsets, + const clang::SourceManager& src) + -> std::pair { + auto [begin, end] = offsets; + assert(end >= begin && "Can not build a selection range for a invalid OffsetPair"); + + // int xxxx = 3; + // ^^^^^^ + // expect to find the first token whose end location is greater than `begin`. + auto left = std::partition_point(tokens.begin(), tokens.end(), [&](const auto& token) { + return src.getFileOffset(token.endLocation()) <= begin; + }); + + // int xxxx = 3; + // ^^^^^^ + // expect to find the last token whose start location is less than to `end`. + auto right = std::partition_point(left, tokens.end(), [&](const auto& token) { + return src.getFileOffset(token.location()) < end; + }); + + // right - 1: the right is the first token whose start location is greater than `end`. + return {left, right - 1}; + } + + bool isValidOffsetRange() const { + const auto tokens = buffer.spelledTokens(context.getSourceManager().getMainFileID()); + return left != tokens.end() && right != tokens.end(); } template - auto getSourceRange(const Node* node) -> clang::SourceRange { - if constexpr(std::is_base_of_v) { + clang::SourceRange getSourceRange(const Node* node) { + if constexpr(std::is_base_of_v) return node->getRange(); - } else { + else return node->getSourceRange(); - } - } - - template - bool isSkippable(const Node* node) { - if constexpr(requires { node->isImplicit(); }) { - if(node->isImplicit()) { - return true; - } - } - - auto range = getSourceRange(node); - if(range.isInvalid()) { - return true; - } - - // range.dump(context.getSourceManager()); - // dump(node); - - return false; } template bool hook(const Node* node, const Callback& callback) { - if(isSkippable(node)) { + if constexpr(requires { node->isImplicit(); }) + if(node->isImplicit()) + return true; + + clang::SourceRange range = getSourceRange(node); + if(range.isInvalid()) return true; - } - storage.emplace_back(SelectionTree::Node{nullptr, clang::DynTypedNode::create(*node)}); - auto range = getSourceRange(node); - - llvm::outs() << "-----------------------------------------\n"; - range.dump(context.getSourceManager()); - clang::SourceRange(left->location(), rigth->location()).dump(context.getSourceManager()); - - // FIXME: currently we only consider fully nested case. - // consider supporting partially nested case. - - // if the boundary tokens contain the source range of node, it means - // the node is selected. store the father node and skip its children. - if(left->location() <= range.getBegin() && rigth->location() >= range.getEnd()) { - if(!stack.empty()) { - llvm::outs() << "selected\n"; - stack.top()->children.push_back(&storage.back()); - } + // No overlap, the node is not selected. + if(range.getEnd() < left->location() || range.getBegin() > right->endLocation()) return true; - } - if(range.getBegin() <= left->location() && range.getEnd() >= rigth->location()) { - // if the source range of node contains the boundary tokens, its - // children may be selected. so traverse them recursively. - llvm::outs() << "select\n"; + // There is overlap between source range of node and selection, by default it is partial. + auto coverage = SelectionTree::CoverageKind::Partial; + + // The source range of current node contains the boundary tokens. it' a full coverage. + if(range.getBegin() <= left->location() && range.getEnd() >= right->location()) + coverage = SelectionTree::CoverageKind::Full; + + SelectionTree::Node selected{ + .dynNode = clang::DynTypedNode::create(*node), + .kind = coverage, + .parent = stack.empty() ? nullptr : stack.top(), + }; + + // Store the selected node and link it to its father node. + storage.push_back(std::move(selected)); + if(!stack.empty()) + stack.top()->children.push_back(&storage.back()); + + SelectionTree::Node& current = storage.back(); + + // For a full coverage case, node's children may also full coverage the selection range. so + // traverse them recursively until the node cover the selection range partially. + if(coverage == SelectionTree::CoverageKind::Full) { stack.emplace(&storage.back()); bool ret = callback(); + stack.pop(); return ret; } + /// For the given selection of a clang::TagDecl: + /// class X {/* something */}; + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// we correct the selection to full source range of class X without semi: + /// class X {/* something */}; + /// ^^^^^^^^^^^^^^^^^^^^^^^^^ + if constexpr(std::derived_from) { + if(right->kind() == clang::tok::semi) + current.kind = SelectionTree::CoverageKind::Full; + } + return true; } @@ -132,30 +159,32 @@ public: SelectionTree build(); -private: /// the two boundary tokens. const clang::syntax::Token* left; - const clang::syntax::Token* rigth; + const clang::syntax::Token* right; + clang::ASTContext& context; clang::syntax::TokenBuffer& buffer; + /// father nodes stack. std::stack stack; std::deque storage; }; -/*/ - -/*/ -class SelectionCollector : public clang::RecursiveASTVisitor { -public: - SelectionCollector(SelectionBuilder& builder) : builder(builder) {} - +struct SelectionCollector : public clang::RecursiveASTVisitor { using Base = clang::RecursiveASTVisitor; + SelectionBuilder& builder; + + SelectionCollector(SelectionBuilder& builder) : builder(builder) {} + bool TraverseDecl(clang::Decl* decl) { + if(!decl) + return true; + /// `TranslationUnitDecl` has invalid location information. /// So we process it separately. - if(llvm::isa_and_nonnull(decl)) { + if(llvm::isa(decl)) { return Base::TraverseDecl(decl); } @@ -171,9 +200,9 @@ public: } /// we don't care about the node without location information, so skip them. - bool shouldWalkTypesOfTypeLocs() { - return false; - } + // bool shouldWalkTypesOfTypeLocs() { + // return false; + // } bool TraverseType(clang::QualType) { return true; @@ -193,53 +222,48 @@ public: return builder.hook(&loc, [&] { return Base::TraverseTypeLoc(loc); }); } - bool TraverseNestedNameSpecifierLoc(clang::NestedNameSpecifierLoc NNS) { + bool TraverseNestedNameSpecifierLoc(const clang::NestedNameSpecifierLoc& NNS) { return builder.hook(&NNS, [&] { return Base::TraverseNestedNameSpecifierLoc(NNS); }); } - bool TraverseTemplateArgumentLoc(const clang::TemplateArgumentLoc& argument) { - return builder.hook(&argument, [&] { return Base::TraverseTemplateArgumentLoc(argument); }); + bool TraverseTemplateArgumentLoc(const clang::TemplateArgumentLoc& A) { + return builder.hook(&A, [&] { return Base::TraverseTemplateArgumentLoc(A); }); } - bool TraverseCXXBaseSpecifier(const clang::CXXBaseSpecifier& base) { - return builder.hook(&base, [&] { return Base::TraverseCXXBaseSpecifier(base); }); + bool TraverseCXXBaseSpecifier(const clang::CXXBaseSpecifier& BS) { + return builder.hook(&BS, [&] { return Base::TraverseCXXBaseSpecifier(BS); }); } - bool TraverseConstructorInitializer(clang::CXXCtorInitializer* init) { - return builder.hook(init, [&] { return Base::TraverseConstructorInitializer(init); }); + bool TraverseConstructorInitializer(clang::CXXCtorInitializer* I) { + return builder.hook(I, [&] { return Base::TraverseConstructorInitializer(I); }); } - // bool TraverseDeclarationNameInfo(clang::DeclarationNameInfo info) { - // return builder.hook(&info, [&] { - // return Base::TraverseDeclarationNameInfo(info); - // }); - // } - - // FIXME: figure out concept in clang AST. + /// FIXME: figure out concept in clang AST. bool TraverseConceptReference(clang::ConceptReference* concept_) { return true; } - -private: - SelectionBuilder& builder; }; SelectionTree SelectionBuilder::build() { SelectionCollector collector(*this); - collector.TraverseAST(context); + + if(isValidOffsetRange()) + collector.TraverseAST(context); SelectionTree tree; - tree.root = stack.empty() ? nullptr : stack.top(); - tree.storage = std::move(storage); + if(!storage.empty()) { + storage.shrink_to_fit(); + tree.storage = std::move(storage); + tree.root = &tree.storage.front(); + } return tree; } -void dump(const SelectionTree::Node* node, clang::ASTContext& context) { +void dumpImpl(llvm::raw_ostream& os, const SelectionTree::Node* node, clang::ASTContext& context) { if(node) { - node->node.dump(llvm::outs(), context); - for(auto child: node->children) { - dump(child, context); - } + node->dynNode.dump(os, context); + for(auto child: node->children) + dumpImpl(os, child, context); } } @@ -249,12 +273,20 @@ SelectionTree::SelectionTree(std::uint32_t begin, std::uint32_t end, clang::ASTContext& context, clang::syntax::TokenBuffer& tokens) { - SelectionBuilder builder(begin, end, context, tokens); - auto tree = builder.build(); - root = tree.root; - llvm::outs() << "----------------------------------------\n"; - dump(root, context); + *this = builder.build(); +} + +void SelectionTree::dump(llvm::raw_ostream& os, clang::ASTContext& context) const { + if(hasValue()) + dumpImpl(os, root, context); +} + +SelectionTree SelectionTree::selectToken(const clang::syntax::Token& token, + clang::ASTContext& context, + clang::syntax::TokenBuffer& tokens) { + auto range = token.range(context.getSourceManager()); + return SelectionTree(range.beginOffset(), range.endOffset(), context, tokens); } } // namespace clice diff --git a/src/Basic/SourceCode.cpp b/src/Basic/SourceCode.cpp index 77f9a657..3d521172 100644 --- a/src/Basic/SourceCode.cpp +++ b/src/Basic/SourceCode.cpp @@ -1,7 +1,7 @@ #include "Basic/SourceCode.h" -#include "llvm/ADT/FunctionExtras.h" -#include "clang/Basic/SourceManager.h" -#include "clang/Lex/Lexer.h" + +#include +#include namespace clice { diff --git a/src/Basic/SourceConverter.cpp b/src/Basic/SourceConverter.cpp index eddfd79c..1eed4eec 100644 --- a/src/Basic/SourceConverter.cpp +++ b/src/Basic/SourceConverter.cpp @@ -157,8 +157,8 @@ LocalSourceRange SourceConverter::toLocalRange(clang::SourceRange range, }; } -std::size_t SourceConverter::toOffset(llvm::StringRef content, proto::Position position) const { - std::size_t offset = 0; +std::uint32_t SourceConverter::toOffset(llvm::StringRef content, proto::Position position) const { + std::uint32_t offset = 0; for(auto i = 0; i < position.line; i++) { auto pos = content.find('\n'); assert(pos != llvm::StringRef::npos && "Line value is out of range"); diff --git a/src/Compiler/Directive.cpp b/src/Compiler/Directive.cpp index 7dd9d89a..a563dcb0 100644 --- a/src/Compiler/Directive.cpp +++ b/src/Compiler/Directive.cpp @@ -91,19 +91,22 @@ struct PPCallback : public clang::PPCallbacks { void InclusionDirective(clang::SourceLocation hashLoc, const clang::Token& includeTok, - llvm::StringRef, + llvm::StringRef fileName, bool, clang::CharSourceRange, clang::OptionalFileEntryRef, - llvm::StringRef, - llvm::StringRef, + llvm::StringRef searchPath, + llvm::StringRef relativePath, const clang::Module*, bool, clang::SrcMgr::CharacteristicKind) override { prevFID = SM.getFileID(hashLoc); directives[prevFID].includes.emplace_back(Include{ - {}, - includeTok.getLocation(), + .fid = {}, + .location = includeTok.getLocation(), + .fileName = fileName.str(), + .searchPath = searchPath.str(), + .relativePath = relativePath.str(), }); } diff --git a/src/Driver/unit_tests.cc b/src/Driver/unit_tests.cc index d3551872..4a08ffb0 100644 --- a/src/Driver/unit_tests.cc +++ b/src/Driver/unit_tests.cc @@ -40,6 +40,7 @@ int main(int argc, char** argv) { } } - return RUN_ALL_TESTS(); + bool res = RUN_ALL_TESTS(); + return res; } diff --git a/src/Feature/Hover.cpp b/src/Feature/Hover.cpp index 61924217..0d76b4fc 100644 --- a/src/Feature/Hover.cpp +++ b/src/Feature/Hover.cpp @@ -1,18 +1,22 @@ - #include "Feature/Hover.h" +#include "AST/Selection.h" +#include "Compiler/AST.h" +#include "Support/FileSystem.h" + +#include #include +#ifdef _WIN32 #include #include +#endif template struct Match : Ts... { using Ts::operator()...; }; -using LocalStr = std::pmr::string; - namespace clice { namespace { @@ -88,19 +92,17 @@ struct PrettyType { } }; -struct Field : WithDocument, MemoryLayout { +struct Field : WithDocument, WithScope, MemoryLayout { PrettyType type; std::string name; - std::string scope; - clang::AccessSpecifier access; size_t estimated_size() const { - return WithDocument::estimated_size() + sizeof(MemoryLayout) + type.estimated_size() + - name.size() + scope.size(); + return WithDocument::estimated_size() + WithScope::estimated_size() + sizeof(MemoryLayout) + + type.estimated_size() + name.size(); } }; @@ -142,6 +144,9 @@ struct Enum : HoverBase, WithDocument, WithScope { /// - #define FOO(Name) /// - template class Foo {}; struct Param { + /// In case the template parameter, kind is `Type` parameter. + SymbolKind kind; + /// The printable parameter type, e.g. "int", or "typename" (in /// TemplateParameters) PrettyType type; @@ -208,6 +213,9 @@ struct Var : HoverBase, WithDocument, WithScope { bool isExtern = false; bool isDeprecated = false; + /// Non empty if the variable has a constexpr evaluated value.; + // std::string value; + // Non empty if isDeprecated is true. std::string deprecateReason; @@ -217,6 +225,93 @@ struct Var : HoverBase, WithDocument, WithScope { } }; +/// Include directive. +struct Header : HoverBase { + + std::string absPath; + + /// TODO: symbols provided by the header + /// std::vector provides; + + size_t estimated_size() const { + return HoverBase::estimated_size() + absPath.size(); + } +}; + +struct Numeric { + SymbolKind symbol = SymbolKind::Number; + + llvm::StringRef rawText; + + std::variant value; + + size_t estimated_size() const { + return 16; + } +}; + +struct Keyword { + SymbolKind symbol = SymbolKind::Keyword; + + clang::tok::TokenKind tkkind; + + llvm::StringRef spelling; + + constexpr static std::string_view CppRefKeywordBaseUrl = + "https://en.cppreference.com/w/cpp/keyword/"; + + static std::string cpprefLink(clang::tok::TokenKind keyword) { + std::string url{CppRefKeywordBaseUrl}; + url += clang::tok::getTokenName(keyword); + return url; + } + + size_t estimated_size() const { + return 64; + } +}; + +struct Literal { + SymbolKind symbol = SymbolKind::String; + + clang::tok::TokenKind kind; + + std::string content; + + /// User-defined suffix of the literal, e.g. "_s" in "123"_s, maybe empty. + std::string udSuffix; + + size_t estimated_size() const { + return content.size() + 32; + } + + static std::string_view stringLiteralKindName(clang::tok::TokenKind kind) { + switch(kind) { + case clang::tok::char_constant: return "char_constant"; + case clang::tok::string_literal: return "string_literal"; + case clang::tok::wide_string_literal: return "wide_string_literal"; + case clang::tok::utf8_string_literal: return "utf8_string_literal"; + case clang::tok::utf16_string_literal: return "utf16_string_literal"; + case clang::tok::utf32_string_literal: return "utf32_string_literal"; + // case clang::tok::binary_data: return "binary_data"; + default: return "unknown"; + } + } +}; + +struct Expression : HoverBase { + + PrettyType type; + + /// TODO: + std::string evaluated; + + size_t estimated_size() const { + return HoverBase::estimated_size() + evaluated.size() + type.estimated_size(); + } +}; + +/// Make HoverInfo default constructible. struct Empty : std::monostate { size_t estimated_size() const { return 0; @@ -224,7 +319,19 @@ struct Empty : std::monostate { }; /// Use variant to store different types of hover information to reduce the memory usage. -struct HoverInfo : public std::variant { +struct HoverInfo : + public std::variant { /// Return the estimated size of the hover information in bytes. size_t estimated_size() const { auto size_counter = Match{ @@ -244,7 +351,7 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor HoverInfo hover; - void recFillScope(const clang::DeclContext* DC, WithScope& scope) { + static void recFillScope(const clang::DeclContext* DC, WithScope& scope) { if(DC->isTranslationUnit()) return; @@ -269,11 +376,11 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor scope.local += RD->getName(), scope.local += "::"; } else if(auto FD = llvm::dyn_cast(DC)) { recFillScope(FD->getDeclContext(), scope); - scope.local += FD->getName(), scope.local += "::"; + scope.local += FD->getNameAsString(), scope.local += "::"; } } - void fillScope(const clang::DeclContext* DC, WithScope& scope) { + static void fillScope(const clang::DeclContext* DC, WithScope& scope) { recFillScope(DC, scope); for(auto& lens: {std::ref(scope.local), std::ref(scope.namespac)}) { @@ -286,9 +393,7 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor void VisitNamespaceDecl(const clang::NamespaceDecl* ND) { Namespace ns; ns.symbol = SymbolKind::Namespace; - ns.name = ND->getName(); - - recFillScope(ND->getDeclContext(), ns); + fillScope(ND, ns); ns.source = ""; hover.emplace(std::move(ns)); @@ -302,16 +407,7 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor lay.size = ctx.getTypeSizeInChars(QT).getQuantity(); } - void VisitRecordDecl(const clang::RecordDecl* RD) { - Record rc; - - // For class (partial) template specialization, use existing one to reuse its template - // params. - if(std::holds_alternative(hover)) { - rc = std::get(std::move(hover)); - hover.emplace(); - } - + void fillRecord(const clang::RecordDecl* RD, Record& rc) { rc.symbol = RD->isStruct() ? SymbolKind::Struct : SymbolKind::Class; rc.name = RD->getNameAsString(); @@ -320,23 +416,31 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor rc.source = ""; rc.document = ""; - hover.emplace(std::move(rc)); for(auto FD: RD->fields()) { - VisitFieldDecl(FD); + Field fd; + fillField(FD, fd); + rc.fields.push_back(std::move(fd)); } } + void VisitRecordDecl(const clang::RecordDecl* RD) { + Record rc; + fillRecord(RD, rc); + + hover.emplace(std::move(rc)); + } + void VisitClassTemplateDecl(const clang::ClassTemplateDecl* TD) { Record rc; - for(const auto& tpara: *TD->getTemplateParameters()) { Param p; p.name = tpara->getName(); rc.templateParams.push_back(std::move(p)); } - hover.emplace(std::move(rc)); + fillRecord(TD->getTemplatedDecl(), rc); + hover.emplace(std::move(rc)); VisitRecordDecl(llvm::dyn_cast(TD->getTemplatedDecl())); } @@ -369,15 +473,12 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor } } - void VisitFieldDecl(const clang::FieldDecl* FD) { - assert(std::holds_alternative(hover) && "Must be a Record hover"); - - Field field; + void fillField(const clang::FieldDecl* FD, Field& field) { field.access = FD->getAccess(); field.name = FD->getNameAsString(); auto RD = llvm::dyn_cast(FD->getDeclContext()); - field.scope = RD->isAnonymousStructOrUnion() ? "(anonymous)" : RD->getName(); + fillScope(RD, field); fillMemoryLayout(FD, RD, field); auto ty = FD->getType(); @@ -389,9 +490,12 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor field.type.type = ty.getAsString(); field.type.akaType = canonicalTy.getAsString(); } + } - auto& record = std::get(hover); - record.fields.push_back(std::move(field)); + void VisitFieldDecl(const clang::FieldDecl* FD) { + Field field; + fillField(FD, field); + hover.emplace(std::move(field)); } void VisitEnumDecl(const clang::EnumDecl* ED) { @@ -435,20 +539,12 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor hover.emplace(std::move(enm)); } - void VisitFunctionDecl(const clang::FunctionDecl* FD) { - Fn fn; - - /// For function template, use existing one to reuse its template parameters. - if(std::holds_alternative(hover)) { - fn = std::get(std::move(hover)); - hover.emplace(); - } - + static void fillFunction(const clang::FunctionDecl* FD, Fn& fn) { if(fn.symbol != SymbolKind::Method && fn.symbol != SymbolKind::Operator) { fn.symbol = SymbolKind::Function; } - fn.name = FD->getName(); + fn.name = FD->getNameAsString(); fn.isConstexpr = FD->isConstexpr(); fn.isConsteval = FD->isConsteval(); fn.isInlined = FD->isInlined(); @@ -484,7 +580,11 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor fn.source = ""; fn.document = ""; + } + void VisitFunctionDecl(const clang::FunctionDecl* FD) { + Fn fn; + fillFunction(FD, fn); hover.emplace(std::move(fn)); } @@ -493,32 +593,87 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor for(auto tpara: *TD->getTemplateParameters()) { Param p; + p.kind = SymbolKind::Type; p.name = tpara->getName(); fn.templateParams.push_back(std::move(p)); } + fillFunction(TD->getAsFunction(), fn); hover.emplace(std::move(fn)); - VisitFunctionDecl(TD->getAsFunction()); + } + + void VisitTemplateTypeParmDecl(const clang::TemplateTypeParmDecl* TTPD) { + Var var; + + var.symbol = SymbolKind::Type; + var.name = TTPD->getName(); + if(var.name.empty()) + var.name = "(unnamed)"; + + fillScope(TTPD->getDeclContext(), var); + var.type.type = TTPD->getNameAsString(); + + hover.emplace(std::move(var)); + } + + void VisitNonTypeTemplateParmDecl(const clang::NonTypeTemplateParmDecl* NTPD) { + Var var; + + var.symbol = SymbolKind::Parameter; + var.name = NTPD->getName(); + if(var.name.empty()) + var.name = "(unnamed)"; + + fillScope(NTPD->getDeclContext(), var); + + auto qty = NTPD->getType(); + var.type.type = qty.getAsString(); + if(auto cqty = qty.getCanonicalType().getAsString(); cqty != var.type.type) { + var.type.akaType = std::move(cqty); + } + + var.document = ""; + var.source = ""; + + hover.emplace(std::move(var)); + } + + void VisitTemplateTemplateParmDecl(const clang::TemplateTemplateParmDecl* TTPD) { + Var var; + + var.symbol = SymbolKind::Type; + var.name = TTPD->getName(); + if(var.name.empty()) + var.name = "(unnamed)"; + + fillScope(TTPD->getDeclContext(), var); + var.type.type = TTPD->getNameAsString(); + + hover.emplace(std::move(var)); } void VisitCXXMethodDecl(const clang::CXXMethodDecl* MD) { Fn fn; fn.symbol = SymbolKind::Method; + fillFunction(MD, fn); hover.emplace(std::move(fn)); - - VisitFunctionDecl(MD); } - void VisitVarDecl(const clang::VarDecl* VD) { - Var var; - var.symbol = SymbolKind::Variable; + static void fillVarInfo(const clang::VarDecl* VD, Var& var) { var.name = VD->getName(); + if(var.name.empty()) + var.name = "(unnamed)"; + var.isConstexpr = VD->isConstexpr(); var.isFileScope = VD->isFileVarDecl(); var.isLocal = VD->isLocalVarDecl(); var.isStaticLocal = VD->isStaticLocal(); var.isExtern = VD->isLocalExternDecl(); var.isDeprecated = VD->isDeprecated(&var.deprecateReason); + if(!var.isDeprecated && var.name.starts_with("_")) { + var.isDeprecated = true; + var.deprecateReason = "Manually marked as throwaway variable"; + } fillScope(VD->getDeclContext(), var); @@ -528,16 +683,339 @@ struct DeclHoverBuilder : public clang::ConstDeclVisitor var.type.akaType = std::move(cqty); } - auto& ctx = VD->getASTContext(); - var.size = ctx.getTypeSizeInChars(qty).getQuantity(); - var.align = ctx.getTypeAlignInChars(qty).getQuantity(); + if(!qty->isDependentType()) { + auto& ctx = VD->getASTContext(); + var.size = ctx.getTypeSizeInChars(qty).getQuantity(); + var.align = ctx.getTypeAlignInChars(qty).getQuantity(); + } var.document = ""; var.source = ""; + } + + void VisitVarDecl(const clang::VarDecl* VD) { + Var var; + var.symbol = SymbolKind::Variable; + fillVarInfo(VD, var); hover.emplace(std::move(var)); } + + void VisitParmVarDecl(const clang::ParmVarDecl* PVD) { + Var var; + var.symbol = SymbolKind::Parameter; + fillVarInfo(PVD, var); + hover.emplace(std::move(var)); + } + + static HoverInfo build(const clang::Decl* decl, const config::HoverOption& option) { + assert(decl && "Must be non-null pointer"); + DeclHoverBuilder builder{.option = option}; + builder.Visit(decl); + return std::move(builder.hover); + } }; +using Token = clang::syntax::Token; +using namespace clang::tok; + +llvm::SmallVector pickBestToken(llvm::ArrayRef& touching) { + constexpr auto ranker = [](const Token& tk) -> uint32_t { + auto kind = tk.kind(); + if(isAnyIdentifier(kind)) + return 10; + + if(llvm::is_contained({kw_auto, kw_decltype}, kind)) + return 9; + + if(isStringLiteral(kind)) + return 7; + + auto prefix_ops = { + l_square, + r_square, + star, + minus, + exclaim, + numeric_constant, + clang::tok::pipe, + }; + if(llvm::is_contained(prefix_ops, kind)) + return 6; + + // keyword or function call. + if(getKeywordSpelling(kind) || llvm::is_contained({l_paren, r_paren}, kind)) + return 5; + + if(getPunctuatorSpelling(kind)) + return 0; + + return 1; + }; + + llvm::SmallVector ranked{touching}; + std::ranges::sort(ranked, [ranker](const Token& lhs, const Token& rhs) { + return ranker(lhs) > ranker(rhs); + }); + return ranked; +} + +struct ExprHoverBuilder { + + using Node = SelectionTree::Node; + + template + struct Accept : + public std::variant>...> { + + using Cases = std::variant>...>; + + const Node* deepest = nullptr; + + template + void accept(const Node* node) { + if(auto ptr = node->dynNode.get

()) { + this->template emplace>>(ptr); + deepest = node; + } + } + + bool accept(const Node* node) { + // Return true to get the deepest node. + return (accept(node), ...), true; + } + }; + + using PreciseExprMatcher = Accept; + + const SelectionTree& tree; + ASTInfo& AST; + const config::HoverOption& option; + + /// The final matched expression node by `PreciseExprMatcher`. + const Node* target = nullptr; + + using Res = std::optional; + + Res operator() (const clang::DeclRefExpr* DR) const { + return DeclHoverBuilder::build(DR->getDecl(), option); + } + + Res operator() (const clang::CXXMemberCallExpr* MC) const { + return DeclHoverBuilder::build(MC->getCalleeDecl(), option); + } + + Res operator() (const clang::CallExpr* C) const { + return DeclHoverBuilder::build(C->getCalleeDecl(), option); + } + + Res operator() (const clang::CXXNamedCastExpr* NC) const { + auto dest = NC->getTypeAsWritten(); + if(dest->isFundamentalType()) { + return std::nullopt; + } + + Expression expr; + expr.symbol = SymbolKind::Variable; + expr.name = NC->getStmtClassName(); + + expr.type.type = dest.getAsString(); + if(auto qty = dest.getCanonicalType(); qty != dest) { + expr.type.akaType = qty.getAsString(); + } + + expr.source = ""; + return HoverInfo{std::move(expr)}; + } + + // By default, return empty. + template + Res operator() (O) const { + return std::nullopt; + } + + static Res build(ASTInfo& AST, const SelectionTree& tree, const config::HoverOption& option) { + PreciseExprMatcher expr; + if(!tree.walkDfs([&expr](const Node* node) { return expr.accept(node); })) { + return std::nullopt; + } + + ExprHoverBuilder builder{tree, AST, option, expr.deepest}; + return std::visit(builder, expr); + } +}; + +namespace hit { + +std::optional header(llvm::ArrayRef includes, ASTInfo& AST, uint32_t line) { + auto lineof = [&SM = AST.srcMgr()](const Include& inc) { + return SM.getPresumedLineNumber(inc.location); + }; + + if(includes.empty() || lineof(includes.back()) < line) { + return std::nullopt; + } + + for(auto& inc: includes) { + if(lineof(inc) != line) + continue; + + Header ic; + ic.symbol = SymbolKind::Header; + ic.name = inc.fileName; + ic.absPath = path::join(inc.searchPath, inc.relativePath); + return HoverInfo{std::move(ic)}; + } + + return std::nullopt; +} + +std::optional numeric(const Token& token, ASTInfo& AST) { + if(auto kind = token.kind(); kind == numeric_constant) { + llvm::StringRef text = token.text(AST.srcMgr()); + auto& Ctx = AST.context(); + clang::NumericLiteralParser parser(text, + token.location(), + AST.srcMgr(), + Ctx.getLangOpts(), + Ctx.getTargetInfo(), + Ctx.getDiagnostics()); + llvm::APInt apint; + if(!parser.GetIntegerValue(apint)) { + return HoverInfo{ + Numeric{.rawText = text, .value = apint} + }; + } + + llvm::APFloat apfloat{0.0}; + if(parser.GetFloatValue(apfloat, llvm::RoundingMode::NearestTiesToEven)) { + return HoverInfo{ + Numeric{.rawText = text, .value = apfloat} + }; + } + + std::string reason = std::format("Parse numeric literal failed, text: {}", text); + llvm_unreachable(reason.c_str()); + } + return std::nullopt; +} + +std::optional keyword(const Token& token) { + if(auto spelling = getKeywordSpelling(token.kind())) { + return HoverInfo{ + Keyword{.tkkind = token.kind(), .spelling = spelling} + }; + } + return std::nullopt; +} + +std::optional literal(const Token& token, ASTInfo& AST) { + if(isStringLiteral(token.kind())) { + auto& Ctx = AST.context(); + clang::Token raw; + bool isFail = clang::Lexer::getRawToken(token.location(), + raw, + AST.srcMgr(), + Ctx.getLangOpts(), + /*IgnoreWhiteSpace=*/true); + if(!isFail) { + clang::StringLiteralParser parser(raw, + AST.srcMgr(), + Ctx.getLangOpts(), + Ctx.getTargetInfo()); + auto text = parser.GetString(); + auto udsuffix = parser.getUDSuffix(); + return HoverInfo{ + Literal{.kind = token.kind(), .content = text.str(), .udSuffix = udsuffix.str()} + }; + } + } + + return std::nullopt; +} + +std::optional deduced(const Token& token, + ASTInfo& AST, + const SelectionTree& tree, + const config::HoverOption& option) { + if(token.kind() != kw_auto && token.kind() != kw_decltype) { + return std::nullopt; + } + + const SelectionTree::Node* ctx = nullptr; + auto findDeclContext = [&AST, &ctx](const SelectionTree::Node* node) -> bool { + // `decltype(auto)` is `AutoTypeLoc`. + if(auto AT = node->dynNode.get()) { + ctx = node->parent; + return false; + } + if(auto DT = node->dynNode.get()) { + ctx = node->parent; + return false; + } + return true; + }; + + if(tree.walkDfs(findDeclContext)) + return std::nullopt; + + if(auto dynKind = ctx->dynNode.getNodeKind(); + dynKind.isSame(clang::ASTNodeKind::getFromNodeKind())) { + ctx = ctx->parent; + } + + const clang::Decl* decl = ctx->dynNode.get(); + assert(decl && "Selected Node must be a valid pointer"); + + return DeclHoverBuilder::build(decl, option); +} + +std::optional declaration(const SelectionTree& tree, const config::HoverOption& option) { + const clang::Decl* decl = nullptr; + + // Find the most inner declaration node. + tree.walkDfs([&decl](const SelectionTree::Node* node) { + if(auto D = node->dynNode.get()) + decl = D; + return true; + }); + + if(!decl) { + return std::nullopt; + } + + return DeclHoverBuilder::build(decl, option); +} + +std::optional expression(ASTInfo& AST, + const SelectionTree& tree, + const config::HoverOption& option) { + return ExprHoverBuilder::build(AST, tree, option); +} + +std::optional detect(const Token& token, + ASTInfo& AST, + const config::HoverOption& option) { + auto cheap = hit::numeric(token, AST).or_else([&]() { return hit::literal(token, AST); }); + auto expensive = [&]() -> std::optional { + const auto tree = SelectionTree::selectToken(token, AST.context(), AST.tokBuf()); + if(!tree.hasValue()) + return std::nullopt; + + return deduced(token, AST, tree, option) + .or_else([&]() { return hit::expression(AST, tree, option); }) + .or_else([&]() { return hit::declaration(tree, option); }) + .or_else([&]() { return hit::keyword(token); }); + }; + + // Try some cheap cases first to avoid construct a selection tree. + return cheap.or_else(expensive); +} + +} // namespace hit + struct MarkdownPrinter { using Self = MarkdownPrinter; @@ -631,11 +1109,11 @@ struct MarkdownPrinter { } template - Self& doc(const HasDocument& bs) { + Self& doc(const HasDocument& doc) { if(!option.documentation) return *this; - return oln("{}", bs.document); + return oln("{}", doc.document); } Self& mem(const MemoryLayout& lay, bool isField) { @@ -727,27 +1205,48 @@ struct MarkdownPrinter { // clang-format on } - void operator() (const Enum& enm) { + void operator() (const Field& fd) { + // clang-format off + + SymbolKind kind = SymbolKind::Field; + // Block 1: title and namespace + v("{} {} `{}`", H3, kind.name(), fd.name).ln() + .scope(fd).ln() + .hln() + + // Block 2: type + .v("Type: `{}`", fd.type.type).o(" (aka `{}`)", fd.type.akaType).ln() + .mem(fd, /*isField=*/true) + .hln() + + // Block 3: optional document + .doc(fd) + .hln(); + + // clang-format on + } + + void operator() (const Enum& em) { // clang-format off // Block 1: title and namespace - title(enm).v(" `({})`", enm.implType).ln() - .scope(enm).vif(!enm.isScoped, ", (unscoped)").ln() + title(em).v(" `({})`", em.implType).ln() + .scope(em).vif(!em.isScoped, ", (unscoped)").ln() .hln() // Block 2: optional document - .doc(enm) + .doc(em) .hln() // Block 3: items - .vif(!enm.items.empty(), "{} items:", enm.items.size()).ln() - .iter(enm.items, [this](const Enum::EnumItem& it) { + .vif(!em.items.empty(), "{} items:", em.items.size()).ln() + .iter(em.items, [this](const Enum::EnumItem& it) { v("+ {} = `{}`", it.name, it.value).ln(); }) .hln() // Block 4: source code - .vln("{}", enm.source); + .vln("{}", em.source); // clang-format on } @@ -814,7 +1313,7 @@ struct MarkdownPrinter { // Block 2: type .tags(var) .v("Type: `{}`", var.type.type).o(" (aka `{}`)", var.type.akaType).ln() - .vln("size = {} bytes, align = {} bytes", var.size, var.align) + .vif(var.size, "size = {} bytes, align = {} bytes", var.size, var.align).ln() .hln() // Block 3: optional document @@ -827,8 +1326,102 @@ struct MarkdownPrinter { // clang-format on } + void operator() (const Header& ic) { + // clang-format off + title(ic) + .hln() + + .vln("`{}`", ic.absPath); + // clang-format on + } + + void operator() (const Keyword& kw) { + // clang-format off + v("{} {} `{}`", H3, kw.symbol.name(), clang::tok::getTokenName(kw.tkkind)) + .hln() + + .vln("See: [{0}]({0})", kw.cpprefLink(kw.tkkind)); + // clang-format on + } + + void operator() (const Numeric& nm) { + bool isInteger = true; + llvm::SmallString<64> bin; + llvm::SmallString<32> dec; + llvm::SmallString<32> hex; + + auto fmtter = Match{ + [&](const llvm::APInt& apint) { + apint.toString(bin, 2, /*Signed=*/true); + apint.toString(dec, 10, /*Signed=*/true); + apint.toString(hex, 16, /*Signed=*/true); + }, + [&](const llvm::APFloat& apfloat) { + isInteger = false; + apfloat.toString(dec, 10); + hex.resize_for_overwrite( + apfloat.convertToHexString(hex.begin(), + 16, + /*UpperCase=*/true, + llvm::RoundingMode::NearestTiesToEven)); + }, + }; + std::visit(fmtter, nm.value); + + constexpr auto sv = [](llvm::StringRef str) -> std::string_view { + return std::string_view{str.data(), str.size()}; + }; + + if(isInteger) { + // clang-format off + v("{} {} `{}`", H3, nm.symbol.name(), sv(nm.rawText)) + .hln() + .vln("Binary: `{}`", sv(bin)) + .vln("Decimal: `{}`", sv(dec)) + .vln("Hexadecimal: `{}`", sv(hex)); + // clang-format on + } else { + // clang-format off + v("{} {} `{}`", H3, nm.symbol.name(), sv(nm.rawText)) + .hln() + .vln("Decimal: `{}`", sv(dec)) + .vln("Hexadecimal: `{}`", sv(hex)); + // clang-format on + } + } + + void operator() (const Literal& lit) { + // clang-format off + v("{} {} `{}`", H3, lit.symbol.name(), lit.stringLiteralKindName(lit.kind)) + .hln() + + .o("User Defined Suffix: `{}`", lit.udSuffix) + .hln() + + .vln("size: {} bytes", lit.content.size() + 1) // null-terminated + .hln() + + .vln("{}", lit.content); + // clang-format on + } + + void operator() (const Expression& expr) { + // clang-format off + v("{} Expression `{}`", H3, expr.name) + .hln() + + .v("type: `{}`", expr.type.type).o(" (aka `{}`)", expr.type.akaType).ln() + .hln() + + .vln("{}", expr.source); + // clang-format on + } + void operator() (const Empty&) { - buffer = ""; + // Show nothing in release mode to avoid crash. +#ifndef NDEBUG + llvm_unreachable("Empty just used to defualt-construct a HoverInfo"); +#endif } /// Render the hover information to markdown text. @@ -859,27 +1452,53 @@ struct MarkdownPrinter { namespace clice::feature::hover { -Result hover(const clang::Decl* decl, const config::HoverOption& option) { - assert(decl && "Must be non-null pointer"); +namespace { - DeclHoverBuilder builder{.option = option}; - builder.Visit(decl); - return {.markdown = MarkdownPrinter::print(builder.hover, option)}; +Result toMarkdown(const HoverInfo& hover, const config::HoverOption& option) { + return {.markdown = MarkdownPrinter::print(hover, option)}; } -/// Compute inlay hints for MainfileID in given param and config. -Result hover(proto::HoverParams param, - ASTInfo& info, - const SourceConverter& converter, - const config::HoverOption& option) { - assert(false && "Not implemented yet"); - return {}; +std::optional + hover(uint32_t line, uint32_t col, ASTInfo& AST, const config::HoverOption& option) { + + auto srcLoc = AST.srcMgr().translateLineCol(AST.mainFileID(), line, col); + if(srcLoc.isInvalid()) + return std::nullopt; + + auto tokens = clang::syntax::spelledTokensTouching(srcLoc, AST.tokBuf()); + if(tokens.empty()) + return std::nullopt; + + // Check if the position is in a include directive. + llvm::ArrayRef includes = AST.directives()[AST.mainFileID()].includes; + if(auto hit = hit::header(includes, AST, line)) + return hit; + + auto candidates = pickBestToken(tokens); + for(const auto& token: candidates) { + if(auto hit = hit::detect(token, AST, option)) + return hit; + } + + return std::nullopt; +} + +} // namespace + +Result hover(const clang::Decl* decl, const config::HoverOption& option) { + return toMarkdown(DeclHoverBuilder::build(decl, option), option); +} + +std::optional hover(const proto::HoverParams& param, + ASTInfo& AST, + const config::HoverOption& option) { + // Convert 0-0 based lsp position to clang 1-1 based loction. + return hover(param.position.line + 1, param.position.character + 1, AST, option) + .transform([&option](HoverInfo&& hv) { return toMarkdown(hv, option); }); } proto::MarkupContent toLspType(Result hover) { - proto::MarkupContent markup; - markup.value = std::move(hover.markdown); - return markup; + return {.value = std::move(hover.markdown)}; } } // namespace clice::feature::hover diff --git a/src/Feature/InlayHint.cpp b/src/Feature/InlayHint.cpp index 99f0ed1a..4c69f866 100644 --- a/src/Feature/InlayHint.cpp +++ b/src/Feature/InlayHint.cpp @@ -998,9 +998,7 @@ Result inlayHints(proto::InlayHintParams param, ASTInfo& info, const SourceConverter& converter, const config::InlayHintOption& option) { - const clang::SourceManager& src = info.srcMgr(); - - llvm::StringRef codeText = src.getBufferData(src.getMainFileID()); + llvm::StringRef codeText = info.getMainFileContent(); // Take 0-0 based Lsp Location from `param.range` and convert it to offset pair. LocalSourceRange requestRange{ @@ -1008,6 +1006,7 @@ Result inlayHints(proto::InlayHintParams param, .end = static_cast(converter.toOffset(codeText, param.range.end)), }; + const clang::SourceManager& src = info.srcMgr(); // If request range is invalid, use the whole main file as the restrict range. if(requestRange.begin >= requestRange.end) { clang::FileID main = src.getMainFileID(); diff --git a/unittests/AST/Selection.cpp b/unittests/AST/Selection.cpp new file mode 100644 index 00000000..7ab2aae0 --- /dev/null +++ b/unittests/AST/Selection.cpp @@ -0,0 +1,414 @@ +#include "src/AST/Selection.cpp" +#include "Basic/SourceConverter.h" + +#include "Test/CTest.h" + +namespace clice { + +namespace testing { +namespace { + +using OffsetRange = std::pair; + +OffsetRange takeWholeFile(ASTInfo& info) { + auto& src = info.srcMgr(); + auto fileID = src.getMainFileID(); + auto begin = src.getFileOffset(src.getLocForStartOfFile(fileID)); + auto end = src.getFileOffset(src.getLocForEndOfFile(fileID)); + return {begin, end}; +} + +void debug(llvm::raw_ostream& os, + const SelectionTree::Node* node, + bool showCoverage = true, + size_t depth = 0) { + for(auto i = 0; i < depth; i++) + os << " "; + + if(auto typeLoc = node->dynNode.get()) { + if(typeLoc->getTypeLocClass() == clang::TypeLoc::TypeLocClass::Qualified) + os << "QualifiedTypeLoc"; + else + os << typeLoc->getType()->getTypeClassName() << "TypeLoc"; + } else + os << node->dynNode.getNodeKind().asStringRef(); + + if(showCoverage) + os << '(' << refl::enum_name(node->kind) << ')'; + + os << '\n'; + + for(auto& child: node->children) + debug(os, child, showCoverage, depth + 1); +} + +void debug(const SelectionTree& tree) { + if(tree) { + llvm::outs() << "----------------------------------------\n"; + debug(llvm::outs(), tree.getRoot()); + } +} + +struct SelectionTester : public Tester { + + const SourceConverter cvtr = SourceConverter(proto::PositionEncodingKind::UTF8); + + SelectionTester(llvm::StringRef file, llvm::StringRef content) : Tester(file, content) {} + + std::uint32_t getOffsetAt(llvm::StringRef id) { + return cvtr.toOffset(params.content, locations.at(id)); + } + + void expectPreorderSequence(const SelectionTree& tree, + llvm::ArrayRef kinds) { + std::string buffer; + buffer.reserve(256); + llvm::raw_string_ostream os(buffer); + debug(os, tree.getRoot(), /*showCoverage=*/false); + + llvm::StringRef view = os.str(); + for(auto kind: kinds) { + auto strRepr = kind.asStringRef(); + auto pos = view.find(strRepr); + EXPECT_NE(pos, llvm::StringRef::npos); + view = view.ltrim().drop_front(strRepr.size()); + } + } +}; + +using namespace clang; + +template +std::array makeNodeSequence() { + return {ASTNodeKind::getFromNodeKind()...}; +} + +TEST(Selection, VarDeclSelectionBoundary) { + const char* code = R"cpp( +$(b1)int xxx$(b2)yyy$(e1) = 1$(e2);$(e3) +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + std::vector selects; + for(int begin = 1; begin <= 2; begin++) { + for(int end = 1; end <= 3; end++) { + uint32_t bp = tx.getOffsetAt(std::format("b{}", begin)); + uint32_t ep = tx.getOffsetAt(std::format("e{}", end)); + selects.push_back({bp, ep}); + } + } + + auto& info = tx.info; + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + for(auto& [begin, end]: selects) { + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); + } +} + +TEST(Selection, ParmVarDeclBoundary) { + const char* code = R"cpp( +void f($(b1)int xxx$(b2)yyy$(e1) = 1$(e2)) {} +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + std::vector selects; + for(int begin = 1; begin <= 2; begin++) { + for(int end = 1; end <= 2; end++) { + uint32_t bp = tx.getOffsetAt(std::format("b{}", begin)); + uint32_t ep = tx.getOffsetAt(std::format("e{}", end)); + selects.push_back({bp, ep}); + } + } + + auto& info = tx.info; + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + for(auto& [begin, end]: selects) { + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); + } +} + +TEST(Selection, SingleStmt) { + const char* code = R"cpp( +namespace test { + int f() { + $(stmt_begin)int x = 1;$(stmt_end) + return 0; + } +} +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + auto& info = tx.info; + + uint32_t begin = tx.getOffsetAt("stmt_begin"); + uint32_t end = tx.getOffsetAt("stmt_end"); + + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + EXPECT_EQ(left->kind(), clang::tok::kw_int); + EXPECT_EQ(right->kind(), clang::tok::semi); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); +} + +TEST(Selection, MultiStmt) { + const char* code = R"cpp( +namespace test { + int f() { + $(multi_begin)int x = 1; + int y = x + 1; + if (y) { x -= 1; } + $(multi_end) + return 0; + } +} +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + auto& info = tx.info; + + uint32_t begin = tx.getOffsetAt("multi_begin"); + uint32_t end = tx.getOffsetAt("multi_end"); + + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + EXPECT_EQ(left->kind(), clang::tok::kw_int); + EXPECT_EQ(right->kind(), clang::tok::r_brace); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = + makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); +} + +TEST(Selection, EntireClass) { + const char* code = R"cpp( +namespace test{ +$(class_begin)class Test { + int x; + int y; + + void f(); +};$(class_end) +} +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + auto& info = tx.info; + + uint32_t begin = tx.getOffsetAt("class_begin"); + uint32_t end = tx.getOffsetAt("class_end"); + + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + EXPECT_EQ(left->kind(), clang::tok::kw_class); + EXPECT_EQ(right->kind(), clang::tok::semi); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); +} + +TEST(Selection, ClassField) { + const char* code = R"cpp( +class Test { + int $(begin)x$(end); + int y; +}; +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + auto& info = tx.info; + + uint32_t begin = tx.getOffsetAt("begin"); + uint32_t end = tx.getOffsetAt("end"); + + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + EXPECT_EQ(left->kind(), clang::tok::identifier); + EXPECT_EQ(right->kind(), clang::tok::identifier); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); +} + +TEST(Selection, IfCondExpr) { + const char* code = R"cpp( +void f(int& x){ + if ($(begin1)x $(begin2)==$(end2) 1$(end1)) {} +} +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + auto& info = tx.info; + + { + uint32_t begin = tx.getOffsetAt("begin1"); + uint32_t end = tx.getOffsetAt("end1"); + + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + EXPECT_EQ(left->kind(), clang::tok::identifier); + EXPECT_EQ(right->kind(), clang::tok::numeric_constant); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); + } + + { + uint32_t begin = tx.getOffsetAt("begin2"); + uint32_t end = tx.getOffsetAt("end2"); + + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + auto [left, right] = SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + auto lk = left->kind(); + auto rk = right->kind(); + + EXPECT_EQ(left->kind(), clang::tok::equalequal); + EXPECT_EQ(right->kind(), clang::tok::equalequal); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); + } +} + +TEST(Selection, ClassMethod) { + const char* code = R"cpp( +class Test { + $(b1)void $(b2)f(int x, int y) $(b3){$(e1) + int z = x + y;$(e2) + }$(e3) +}; +)cpp"; + + SelectionTester tx("main.cpp", code); + tx.run(); + + { // {b1, b2} X {e1, e2, e3} + std::vector b12_e123; + for(int begin = 1; begin <= 2; begin++) { + for(int end = 1; end <= 3; end++) { + uint32_t bp = tx.getOffsetAt(std::format("b{}", begin)); + uint32_t ep = tx.getOffsetAt(std::format("e{}", end)); + b12_e123.push_back({bp, ep}); + } + } + + auto& info = tx.info; + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + for(auto& [begin, end]: b12_e123) { + auto [left, right] = + SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + auto kinds = makeNodeSequence(); + tx.expectPreorderSequence(tree, kinds); + } + } + + { + // {b3} X {e1, e2, e3} + std::vector b3_e123; + for(int begin = 3; begin <= 3; begin++) { + for(int end = 1; end <= 3; end++) { + uint32_t bp = tx.getOffsetAt(std::format("b{}", begin)); + uint32_t ep = tx.getOffsetAt(std::format("e{}", end)); + b3_e123.push_back({bp, ep}); + } + } + + auto& info = tx.info; + auto tokens = info->tokBuf().spelledTokens(info->srcMgr().getMainFileID()); + for(auto& [begin, end]: b3_e123) { + auto [left, right] = + SelectionBuilder::selectionBound(tokens, {begin, end}, info->srcMgr()); + + SelectionBuilder builder(left, right, info->context(), info->tokBuf()); + auto tree = builder.build(); + // debug(tree); + + // for b3 X e1, only care about the method body. + auto kinds = makeNodeSequence(); + + // for b3 X e23, ther is also a partial coverage of DeclStmt, but don't check it here. + // auto kinds = makeNodeSequence(); + + tx.expectPreorderSequence(tree, kinds); + } + } +} + +} // namespace + +} // namespace testing + +} // namespace clice diff --git a/unittests/Feature/Hover.cpp b/unittests/Feature/Hover.cpp index d314b8e1..8301160a 100644 --- a/unittests/Feature/Hover.cpp +++ b/unittests/Feature/Hover.cpp @@ -1,6 +1,7 @@ #include "Test/CTest.h" #include "Feature/Hover.h" -#include "Basic/SourceConverter.h" + +#include "src/Feature/Hover.cpp" #include @@ -17,13 +18,15 @@ struct DeclCollector : public clang::RecursiveASTVisitor { llvm::StringMap decls; bool VisitNamedDecl(const clang::NamedDecl* decl) { - assert(decl && "???"); - decls[decl->getName()] = decl; + assert(decl && "Decl must be a valid pointer"); + decls[decl->getNameAsString()] = decl; return true; } }; struct Hover : public ::testing::Test { + using HoverChecker = llvm::function_ref&)>; + protected: std::optional tester; @@ -42,6 +45,19 @@ protected: decls = std::move(collector.decls); } + void runWithHeader(llvm::StringRef source, + llvm::StringRef header, + const config::HoverOption& option = DefaultOption) { + tester.emplace("main.cpp", source); + tester->addFile(path::join(".", "header.h"), header); + tester->run(); + + auto& info = tester->info; + DeclCollector collector; + collector.TraverseTranslationUnitDecl(info->tu()); + decls = std::move(collector.decls); + } + const clang::Decl* getValidDeclPtr(llvm::StringRef name) { auto ptr = decls.lookup(name); EXPECT_TRUE(bool(ptr)); @@ -65,6 +81,44 @@ protected: // llvm::outs() << result.markdown << '\n'; EXPECT_EQ(mdText, result.markdown); } + + template + static bool is(std::optional& hover) { + EXPECT_TRUE(hover.has_value()); + if(hover.has_value()) { + return std::holds_alternative(*hover); + } + return false; + } + + static bool isNone(std::optional& hover) { + bool no = !hover.has_value(); + EXPECT_TRUE(no); + return no; + } + + void EXPECT_HOVER_TYPE(llvm::StringRef key, + HoverChecker checker, + config::HoverOption option = DefaultOption) { + auto pos = tester->locations.at(key); + auto hoverInfo = hover(pos.line + 1, pos.character + 1, *tester->info, option); + + bool checkResult = checker(hoverInfo); + // if(hoverInfo.has_value()) { + // llvm::outs() << "======[" << key << "]======\n"; + // llvm::outs() << toMarkdown(*hoverInfo, option).markdown << '\n'; + // } + EXPECT_TRUE(checkResult); + } + + using Fmtter = std::string(int index); + + void EXPECT_TYPES_N(Fmtter fmt, int n, HoverChecker checker) { + for(int i = 1; i <= n; i++) { + auto key = fmt(i); + EXPECT_HOVER_TYPE(key, checker); + } + }; }; TEST_F(Hover, Namespace) { @@ -325,6 +379,156 @@ ___ EXPECT_HOVER("x1", FREE_STYLE); } +TEST_F(Hover, HeaderAndNamespace) { + auto header = R"cpp()cpp"; + + auto code = R"cpp( +#in$(h1)clude "head$(h3)er.h"$(h4) +#in$(h2)clude + +$(n1)names$(n2)pace$(n3) outt$(n4)er { + + namespac$(n5)e $(n6){ + + nam$(n7)espace inne$(n8)r { + + }$(n9) + + } + +}$(n10) + +)cpp"; + + runWithHeader(code, header); + + EXPECT_TYPES_N([](int i) { return std::format("h{}", i); }, 5, is

); + EXPECT_TYPES_N([](int i) { return std::format("n{}", i); }, 8, is); +} + +TEST_F(Hover, VariableAndLiteral) { + auto code = R"cpp( + // introduce size_t + #include + + long operator ""_w(const char*, size_t) { + return 1; + }; + + aut$(v1)o$(v2) i$(v3)1$(v4) = $(n1)1$(n2); + auto$(v5) i2 = $(n3)-$(n4)1$(n5); + + auto l1$(v6) = $(l1)"test$(l2)_string_li$(l3)t"; + auto l2 = R$(l4)"tes$(l5)t(raw_string_lit)test"$(l6); + auto l3 = u8$(l7)"test$(l8)_string_li$(l9)t"; + auto l$(v7)4 = $(l10)"$(l11)udf_string$(l12)"_w; +)cpp"; + run(code); + + EXPECT_TYPES_N([](int i) { return std::format("v{}", i); }, 7, is); + EXPECT_TYPES_N([](int i) { return std::format("l{}", i); }, 12, is); +} + +TEST_F(Hover, FunctionDeclAndParameter) { + auto code = R"cpp( + // introduce size_t + #include + + i$(f1)nt$(f2) f() { + return 0; + } + + lo$(f3)ng oper$(f4)ator ""_w(const char* str, std::si$(p1)ze_t$(p2) leng$(p3)th) {$(f5) + return 1; + }; + + + struct A { + int f$(f6)n(i$(p4)nt par$(p5)am) { + return param; + } + + voi$(f7)d ope$(f8)rator()$(f9)(int par$(p6)am) {} + }; + + + templ$(f10)ate + void templ$(f11)ate_func1(T1 le$(p10)ft, T2 righ$(p11)t)$(f12) {} + + template + void templ$(f13)ate_func2() {} + + template typenam$(p17)e Outt$(p18)er> + void templ$(f14)ate_func3() {} + +)cpp"; + run(code); + + EXPECT_TYPES_N([](int i) { return std::format("f{}", i); }, 12, is); + EXPECT_TYPES_N([](int i) { return std::format("p{}", i); }, 18, is); +} + +TEST_F(Hover, AutoAndDecltype) { + auto code = R"cpp( + +$(a1)aut$(a2)o$(a3) i = -1; + +$(d1)dec$(d2)ltype$(d3)(i) j = 2; + +struct A { int x; }; + +aut$(a4)o va$(a5)r = A{}; + +a$(fa)uto f1() { return 1; } + +de$(fn_decltype)cltype(au$(fn_decltype_auto)to) f2() {} + +int f3(au$(fn_para_auto)to x) {} + +)cpp"; + + run(code); + + EXPECT_TYPES_N([](int i) { return std::format("a{}", i); }, 5, is); + EXPECT_TYPES_N([](int i) { return std::format("d{}", i); }, 3, is); + + EXPECT_HOVER_TYPE("fa", is); + EXPECT_HOVER_TYPE("fn_decltype", is); + EXPECT_HOVER_TYPE("fn_decltype_auto", is); + + /// FIXME: It seems a bug of SelectionTree, which cannot select any node of `f3`; + /// EXPECT_HOVER_TYPE("fn_para_auto", is); +} + +TEST_F(Hover, Expr) { + auto code = R"cpp( +int xxxx = 1; +int yyyy = xx$(e1)xx; + +struct A { + int function(int param) { + return thi$(e2)s$(e3)->$(e4)funct$(e5)ion(para$(e6)m); + } + + int fn(int param) { + return static$(e7)_cast(nul$(e8)lptr)->function(par$(e9)am); + } +}; +)cpp"; + + run(code); + + EXPECT_HOVER_TYPE("e1", is); + EXPECT_HOVER_TYPE("e2", is); + EXPECT_HOVER_TYPE("e3", is); + EXPECT_HOVER_TYPE("e4", is); + EXPECT_HOVER_TYPE("e5", is); + EXPECT_HOVER_TYPE("e6", is); + EXPECT_HOVER_TYPE("e7", is); + EXPECT_HOVER_TYPE("e8", is); + EXPECT_HOVER_TYPE("e9", is); +} + } // namespace } // namespace clice::testing diff --git a/xmake.lua b/xmake.lua index f82f3065..4a18d600 100644 --- a/xmake.lua +++ b/xmake.lua @@ -89,6 +89,7 @@ target("unit_tests") set_default(false) set_kind("binary") add_files("src/Driver/unit_tests.cc", "unittests/**.cpp") + add_includedirs(".", {public = true}) add_deps("clice-core") add_packages("gtest")