diff --git a/CMakeLists.txt b/CMakeLists.txt index 926d6e4e..b843fe54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,7 +138,7 @@ if(CLICE_ENABLE_TEST) ) FetchContent_MakeAvailable(googletest) - target_link_libraries(unit_tests PRIVATE gtest_main clice-core) + target_link_libraries(unit_tests PRIVATE gtest_main GTest::gtest clice-core) target_compile_options(unit_tests PUBLIC ${CLICE_CXX_FLAGS}) target_link_options(unit_tests PUBLIC ${CLICE_LINKER_FLAGS}) diff --git a/include/AST/Utility.h b/include/AST/Utility.h index f9d8da7f..9a812939 100644 --- a/include/AST/Utility.h +++ b/include/AST/Utility.h @@ -1,4 +1,7 @@ +#pragma once + #include "clang/AST/Decl.h" +#include "clang/AST/TypeLoc.h" namespace clice::ast { @@ -24,7 +27,70 @@ std::string name_of(const clang::NamedDecl* decl); /// returns the type for the decl if any. clang::QualType type_of(const clang::NamedDecl* decl); +const clang::NamedDecl* get_decl_for_type(const clang::Type* T); + /// Get the underlying decl for a type if any. const clang::NamedDecl* decl_of(clang::QualType type); +/// Check whether the decl is anonymous. +bool is_anonymous(const clang::NamedDecl* decl); + +clang::NestedNameSpecifierLoc get_qualifier_loc(const clang::NamedDecl* decl); + +auto get_template_specialization_args(const clang::NamedDecl* decl) + -> std::optional>; + +std::string print_template_specialization_args(const clang::NamedDecl* decl); + +std::string print_name(const clang::NamedDecl* decl); + +clang::TemplateTypeParmTypeLoc get_contained_auto_param_type(clang::TypeLoc TL); + +clang::NamedDecl* get_only_instantiation(clang::NamedDecl* TemplatedDecl); + +/// getSimpleName() returns the plain identifier for an entity, if any. +llvm::StringRef getSimpleName(const clang::DeclarationName& DN); +llvm::StringRef getSimpleName(const clang::NamedDecl& D); +llvm::StringRef getSimpleName(clang::QualType T); + +std::string summarizeExpr(const clang::Expr* E); + +// Returns the template parameter pack type from an instantiated function +// template, if it exists, nullptr otherwise. +const clang::TemplateTypeParmType* getFunctionPackType(const clang::FunctionDecl* Callee); + +// Returns the template parameter pack type that this parameter was expanded +// from (if in the Args... or Args&... or Args&&... form), if this is the case, +// nullptr otherwise. +const clang::TemplateTypeParmType* getUnderlyingPackType(const clang::ParmVarDecl* Param); + +// Returns the parameters that are forwarded from the template parameters. +// For example, `template void foo(Args... args)` will return +// the `args` parameters. +llvm::SmallVector + resolveForwardingParameters(const clang::FunctionDecl* D, unsigned MaxDepth = 10); + +// Determines if any intermediate type in desugaring QualType QT is of +// substituted template parameter type. Ignore pointer or reference wrappers. +bool isSugaredTemplateParameter(clang::QualType QT); + +// A simple wrapper for `clang::desugarForDiagnostic` that provides optional +// semantic. +std::optional desugar(clang::ASTContext& AST, clang::QualType QT); + +// Apply a series of heuristic methods to determine whether or not a QualType QT +// is suitable for desugaring (e.g. getting the real name behind the using-alias +// name). If so, return the desugared type. Otherwise, return the unchanged +// parameter QT. +// +// This could be refined further. See +// https://github.com/clangd/clangd/issues/1298. +clang::QualType maybeDesugar(clang::ASTContext& AST, clang::QualType QT); + +// Given a callee expression `Fn`, if the call is through a function pointer, +// try to find the declaration of the corresponding function pointer type, +// so that we can recover argument names from it. +// FIXME: This function is mostly duplicated in SemaCodeComplete.cpp; unify. +clang::FunctionProtoTypeLoc getPrototypeLoc(clang::Expr* Fn); + } // namespace clice::ast diff --git a/include/Feature/InlayHint.h b/include/Feature/InlayHint.h index 95c09f01..78850c71 100644 --- a/include/Feature/InlayHint.h +++ b/include/Feature/InlayHint.h @@ -5,20 +5,39 @@ #include "Index/Shared.h" #include "Support/JSON.h" +namespace clice::config { + +struct InlayHintsOptions { + /// If false, inlay hints are completely disabled. + bool enabled = true; + + // Whether specific categories of hints are enabled. + bool parameters = true; + bool deduced_types = true; + bool designators = true; + bool block_end = false; + bool default_arguments = false; + + // Limit the length of type names in inlay hints. (0 means no limit) + uint32_t type_name_limit = 32; +}; + +} // namespace clice::config + namespace clice::feature { -struct InlayHintKind : refl::Enum { - enum Kind { - Params, - InvalidEnum, - }; - - using Enum::Enum; +enum class InlayHintKind { + Parameter, + InvalidEnum, + DefaultArgument, + Type, + Designator, + BlockEnd, }; struct InlayHint { /// The position offset of the inlay hint in the source code. - uint32_t offset; + std::uint32_t offset; /// The kind/category of the inlay hint. InlayHintKind kind; @@ -33,8 +52,6 @@ struct InlayHint { using InlayHints = std::vector; -InlayHints inlayHints(CompilationUnit& unit, LocalSourceRange target); - -index::Shared indexInlayHint(CompilationUnit& unit); +InlayHints inlay_hints(CompilationUnit& unit, LocalSourceRange target); } // namespace clice::feature diff --git a/include/Test/Test.h b/include/Test/Test.h index e2874a7e..f73cadfd 100644 --- a/include/Test/Test.h +++ b/include/Test/Test.h @@ -29,6 +29,7 @@ inline std::string diff(const LHS& lhs, const RHS& rhs) { left = "cannot dump value"; } + SCOPED_TRACE("Verifying link at index 0"); std::string right; if constexpr(json::serializable) { llvm::raw_string_ostream(right) << json::serialize(rhs); diff --git a/src/AST/Utility.cpp b/src/AST/Utility.cpp index 04dc98b9..37528f67 100644 --- a/src/AST/Utility.cpp +++ b/src/AST/Utility.cpp @@ -1,8 +1,17 @@ #include "AST/Utility.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclCXX.h" +#include "clang/AST/DeclObjC.h" #include "clang/AST/DeclTemplate.h" +#include "clang/AST/Type.h" #include "clang/Basic/SourceManager.h" +#include "clang/AST/StmtVisitor.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/SaveAndRestore.h" +#include "llvm/Support/ScopedPrinter.h" +#include "llvm/ADT/SmallSet.h" +#include "clang/AST/ASTDiagnostic.h" namespace clice::ast { @@ -235,6 +244,28 @@ clang::QualType type_of(const clang::NamedDecl* decl) { return clang::QualType(); } +// getDeclForType() returns the decl responsible for Type's spelling. +// This is the inverse of ASTContext::getTypeDeclType(). +template + requires requires(Ty* T) { T->getDecl(); } +const clang::NamedDecl* get_decl_for_type_impl(const Ty* T) { + return T->getDecl(); +} + +const clang::NamedDecl* get_decl_for_type_impl(const void* T) { + return nullptr; +} + +const clang::NamedDecl* get_decl_for_type(const clang::Type* T) { + switch(T->getTypeClass()) { +#define ABSTRACT_TYPE(TY, BASE) +#define TYPE(TY, BASE) \ + case clang::Type::TY: return get_decl_for_type_impl(llvm::cast(T)); +#include "clang/AST/TypeNodes.inc" + } + llvm_unreachable("Unknown TypeClass enum"); +} + const clang::NamedDecl* decl_of(clang::QualType type) { if(type.isNull()) { return nullptr; @@ -250,6 +281,7 @@ const clang::NamedDecl* decl_of(clang::QualType type) { /// FIXME: if(auto TST = type->getAs()) { + auto decl = TST->getTemplateName().getAsTemplateDecl(); if(type->isDependentType()) { return decl; @@ -271,4 +303,721 @@ const clang::NamedDecl* decl_of(clang::QualType type) { return nullptr; } +bool is_anonymous(const clang::NamedDecl* decl) { + auto name = decl->getDeclName(); + return name.isIdentifier() && !name.getAsIdentifierInfo(); +} + +clang::NestedNameSpecifierLoc get_qualifier_loc(const clang::NamedDecl* decl) { + if(auto* V = llvm::dyn_cast(decl)) { + return V->getQualifierLoc(); + } + + if(auto* T = llvm::dyn_cast(decl)) { + return T->getQualifierLoc(); + } + + return clang::NestedNameSpecifierLoc(); +} + +auto get_template_specialization_args(const clang::NamedDecl* decl) + -> std::optional> { + if(auto* FD = llvm::dyn_cast(decl)) { + if(const clang::ASTTemplateArgumentListInfo* Args = + FD->getTemplateSpecializationArgsAsWritten()) { + return Args->arguments(); + } + } else if(auto* Cls = llvm::dyn_cast(decl)) { + if(auto* Args = Cls->getTemplateArgsAsWritten()) { + return Args->arguments(); + } + } else if(auto* Var = llvm::dyn_cast(decl)) { + if(auto* Args = Var->getTemplateArgsAsWritten()) { + return Args->arguments(); + } + } + + // We return std::nullopt for ClassTemplateSpecializationDecls because it does + // not contain TemplateArgumentLoc information. + return std::nullopt; +} + +std::string print_template_specialization_args(const clang::NamedDecl* decl) { + std::string TemplateArgs; + llvm::raw_string_ostream OS(TemplateArgs); + clang::PrintingPolicy Policy(decl->getASTContext().getLangOpts()); + if(auto Args = ast::get_template_specialization_args(decl)) { + printTemplateArgumentList(OS, *Args, Policy); + } else if(auto* Cls = llvm::dyn_cast(decl)) { + // FIXME: Fix cases when getTypeAsWritten returns null inside clang AST, + // e.g. friend decls. Currently we fallback to Template Arguments without + // location information. + printTemplateArgumentList(OS, Cls->getTemplateArgs().asArray(), Policy); + } + return TemplateArgs; +} + +std::string print_name(const clang::NamedDecl* decl) { + std::string Name; + llvm::raw_string_ostream Out(Name); + clang::PrintingPolicy PP(decl->getASTContext().getLangOpts()); + // We don't consider a class template's args part of the constructor name. + PP.SuppressTemplateArgsInCXXConstructors = true; + + // Handle 'using namespace'. They all have the same name - . + if(auto* UD = llvm::dyn_cast(decl)) { + Out << "using namespace "; + if(auto* Qual = UD->getQualifier()) + Qual->print(Out, PP); + UD->getNominatedNamespaceAsWritten()->printName(Out); + return Out.str(); + } + + if(ast::is_anonymous(decl)) { + // Come up with a presentation for an anonymous entity. + if(isa(decl)) + return "(anonymous namespace)"; + if(auto* Cls = llvm::dyn_cast(decl)) { + if(Cls->isLambda()) + return "(lambda)"; + return ("(anonymous " + Cls->getKindName() + ")").str(); + } + if(isa(decl)) + return "(anonymous enum)"; + return "(anonymous)"; + } + + // Print nested name qualifier if it was written in the source code. + if(auto* Qualifier = ast::get_qualifier_loc(decl).getNestedNameSpecifier()) + Qualifier->print(Out, PP); + // Print the name itself. + decl->getDeclName().print(Out, PP); + // Print template arguments. + Out << ast::print_template_specialization_args(decl); + + return Out.str(); +} + +clang::TemplateTypeParmTypeLoc get_contained_auto_param_type(clang::TypeLoc TL) { + if(auto QTL = TL.getAs()) { + return get_contained_auto_param_type(QTL.getUnqualifiedLoc()); + } + + if(llvm::isa(TL.getTypePtr())) { + return get_contained_auto_param_type(TL.getNextTypeLoc()); + } + + if(auto FTL = TL.getAs()) { + return get_contained_auto_param_type(FTL.getReturnLoc()); + } + + if(auto TTPTL = TL.getAs()) { + if(TTPTL.getTypePtr()->getDecl()->isImplicit()) { + return TTPTL; + } + } + + return {}; +} + +template +static clang::NamedDecl* get_only_instantiation_impl(TemplateDeclTy* TD) { + clang::NamedDecl* Only = nullptr; + for(auto* Spec: TD->specializations()) { + if(Spec->getTemplateSpecializationKind() == clang::TSK_ExplicitSpecialization) + continue; + if(Only != nullptr) + return nullptr; + Only = Spec; + } + return Only; +} + +clang::NamedDecl* get_only_instantiation(clang::NamedDecl* TemplatedDecl) { + if(auto* TD = TemplatedDecl->getDescribedTemplate()) { + if(auto* CTD = llvm::dyn_cast(TD)) + return get_only_instantiation_impl(CTD); + if(auto* FTD = llvm::dyn_cast(TD)) + return get_only_instantiation_impl(FTD); + if(auto* VTD = llvm::dyn_cast(TD)) + return get_only_instantiation_impl(VTD); + } + return nullptr; +} + +// getSimpleName() returns the plain identifier for an entity, if any. +llvm::StringRef getSimpleName(const clang::DeclarationName& DN) { + if(clang::IdentifierInfo* Ident = DN.getAsIdentifierInfo()) + return Ident->getName(); + return ""; +} + +llvm::StringRef getSimpleName(const clang::NamedDecl& D) { + return getSimpleName(D.getDeclName()); +} + +llvm::StringRef getSimpleName(clang::QualType T) { + if(const auto* ET = llvm::dyn_cast(T)) + return getSimpleName(ET->getNamedType()); + if(const auto* BT = llvm::dyn_cast(T)) { + clang::PrintingPolicy PP(clang::LangOptions{}); + PP.adjustForCPlusPlus(); + return BT->getName(PP); + } + if(const auto* D = ast::get_decl_for_type(T.getTypePtr())) + return getSimpleName(D->getDeclName()); + return ""; +} + +std::string summarizeExpr(const clang::Expr* E) { + using namespace clang; + + struct Namer : ConstStmtVisitor { + std::string Visit(const Expr* E) { + if(E == nullptr) + return ""; + return ConstStmtVisitor::Visit(E->IgnoreImplicit()); + } + + // Any sort of decl reference, we just use the unqualified name. + std::string VisitMemberExpr(const MemberExpr* E) { + return ast::getSimpleName(*E->getMemberDecl()).str(); + } + + std::string VisitDeclRefExpr(const DeclRefExpr* E) { + return ast::getSimpleName(*E->getFoundDecl()).str(); + } + + std::string VisitCallExpr(const CallExpr* E) { + return Visit(E->getCallee()); + } + + std::string VisitCXXDependentScopeMemberExpr(const CXXDependentScopeMemberExpr* E) { + return ast::getSimpleName(E->getMember()).str(); + } + + std::string VisitDependentScopeDeclRefExpr(const DependentScopeDeclRefExpr* E) { + return ast::getSimpleName(E->getDeclName()).str(); + } + + std::string VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr* E) { + return ast::getSimpleName(E->getType()).str(); + } + + std::string VisitCXXTemporaryObjectExpr(const CXXTemporaryObjectExpr* E) { + return ast::getSimpleName(E->getType()).str(); + } + + // Step through implicit nodes that clang doesn't classify as such. + std::string VisitCXXMemberCallExpr(const CXXMemberCallExpr* E) { + // Call to operator bool() inside if (X): dispatch to X. + if(E->getNumArgs() == 0 && E->getMethodDecl() && + E->getMethodDecl()->getDeclName().getNameKind() == + DeclarationName::CXXConversionFunctionName && + E->getSourceRange() == E->getImplicitObjectArgument()->getSourceRange()) + return Visit(E->getImplicitObjectArgument()); + return ConstStmtVisitor::VisitCXXMemberCallExpr(E); + } + + std::string VisitCXXConstructExpr(const CXXConstructExpr* E) { + if(E->getNumArgs() == 1) + return Visit(E->getArg(0)); + return ""; + } + + // Literals are just printed + std::string VisitCXXBoolLiteralExpr(const CXXBoolLiteralExpr* E) { + return E->getValue() ? "true" : "false"; + } + + std::string VisitIntegerLiteral(const IntegerLiteral* E) { + return llvm::to_string(E->getValue()); + } + + std::string VisitFloatingLiteral(const FloatingLiteral* E) { + std::string Result; + llvm::raw_string_ostream OS(Result); + E->getValue().print(OS); + // Printer adds newlines?! + Result.resize(llvm::StringRef(Result).rtrim().size()); + return Result; + } + + std::string VisitStringLiteral(const StringLiteral* E) { + std::string Result = "\""; + if(E->containsNonAscii()) { + Result += "..."; + } else if(E->getLength() > 10) { + Result += E->getString().take_front(7); + Result += "..."; + } else { + llvm::raw_string_ostream OS(Result); + llvm::printEscapedString(E->getString(), OS); + } + Result.push_back('"'); + return Result; + } + + // Simple operators. Motivating cases are `!x` and `I < Length`. + std::string printUnary(llvm::StringRef Spelling, const Expr* Operand, bool Prefix) { + std::string Sub = Visit(Operand); + if(Sub.empty()) + return ""; + if(Prefix) + return (Spelling + Sub).str(); + Sub += Spelling; + return Sub; + } + + bool InsideBinary = false; // No recursing into binary expressions. + + std::string printBinary(llvm::StringRef Spelling, const Expr* LHSOp, const Expr* RHSOp) { + if(InsideBinary) + return ""; + llvm::SaveAndRestore InBinary(InsideBinary, true); + + std::string LHS = Visit(LHSOp); + std::string RHS = Visit(RHSOp); + if(LHS.empty() && RHS.empty()) + return ""; + + if(LHS.empty()) + LHS = "..."; + LHS.push_back(' '); + LHS += Spelling; + LHS.push_back(' '); + if(RHS.empty()) + LHS += "..."; + else + LHS += RHS; + return LHS; + } + + std::string VisitUnaryOperator(const UnaryOperator* E) { + return printUnary(E->getOpcodeStr(E->getOpcode()), E->getSubExpr(), !E->isPostfix()); + } + + std::string VisitBinaryOperator(const BinaryOperator* E) { + return printBinary(E->getOpcodeStr(E->getOpcode()), E->getLHS(), E->getRHS()); + } + + std::string VisitCXXOperatorCallExpr(const CXXOperatorCallExpr* E) { + const char* Spelling = getOperatorSpelling(E->getOperator()); + // Handle weird unary-that-look-like-binary postfix operators. + if((E->getOperator() == OO_PlusPlus || E->getOperator() == OO_MinusMinus) && + E->getNumArgs() == 2) + return printUnary(Spelling, E->getArg(0), false); + if(E->isInfixBinaryOp()) + return printBinary(Spelling, E->getArg(0), E->getArg(1)); + if(E->getNumArgs() == 1) { + switch(E->getOperator()) { + case OO_Plus: + case OO_Minus: + case OO_Star: + case OO_Amp: + case OO_Tilde: + case OO_Exclaim: + case OO_PlusPlus: + case OO_MinusMinus: return printUnary(Spelling, E->getArg(0), true); + default: break; + } + } + return ""; + } + }; + + return Namer{}.Visit(E); +} + +// returns true for `X` in `template void foo()` +bool isTemplateTypeParameterPack(clang::NamedDecl* D) { + if(const auto* TTPD = llvm::dyn_cast(D)) { + return TTPD->isParameterPack(); + } + return false; +} + +const clang::TemplateTypeParmType* getFunctionPackType(const clang::FunctionDecl* Callee) { + if(const auto* TemplateDecl = Callee->getPrimaryTemplate()) { + auto TemplateParams = TemplateDecl->getTemplateParameters()->asArray(); + // find the template parameter pack from the back + const auto It = std::find_if(TemplateParams.rbegin(), + TemplateParams.rend(), + isTemplateTypeParameterPack); + if(It != TemplateParams.rend()) { + const auto* TTPD = llvm::dyn_cast(*It); + return TTPD->getTypeForDecl()->castAs(); + } + } + return nullptr; +} + +const clang::TemplateTypeParmType* getUnderlyingPackType(const clang::ParmVarDecl* Param) { + const auto* PlainType = Param->getType().getTypePtr(); + if(auto* RT = llvm::dyn_cast(PlainType)) + PlainType = RT->getPointeeTypeAsWritten().getTypePtr(); + if(const auto* SubstType = llvm::dyn_cast(PlainType)) { + const auto* ReplacedParameter = SubstType->getReplacedParameter(); + if(ReplacedParameter->isParameterPack()) { + return ReplacedParameter->getTypeForDecl()->castAs(); + } + } + return nullptr; +} + +// This visitor walks over the body of an instantiated function template. +// The template accepts a parameter pack and the visitor records whether +// the pack parameters were forwarded to another call. For example, given: +// +// template +// auto make_unique(Args... args) { +// return unique_ptr(new T(args...)); +// } +// +// When called as `make_unique(2, 'x')` this yields a function +// `make_unique` with two parameters. +// The visitor records that those two parameters are forwarded to the +// `constructor std::string(int, char);`. +// +// This information is recorded in the `ForwardingInfo` split into fully +// resolved parameters (passed as argument to a parameter that is not an +// expanded template type parameter pack) and forwarding parameters (passed to a +// parameter that is an expanded template type parameter pack). +class ForwardingCallVisitor : public clang::RecursiveASTVisitor { +public: + ForwardingCallVisitor(llvm::ArrayRef Parameters) : + Parameters{Parameters}, PackType{ast::getUnderlyingPackType(Parameters.front())} {} + + bool VisitCallExpr(clang::CallExpr* E) { + auto* Callee = getCalleeDeclOrUniqueOverload(E); + if(Callee) { + handleCall(Callee, E->arguments()); + } + return !Info.has_value(); + } + + bool VisitCXXConstructExpr(clang::CXXConstructExpr* E) { + auto* Callee = E->getConstructor(); + if(Callee) { + handleCall(Callee, E->arguments()); + } + return !Info.has_value(); + } + + // The expanded parameter pack to be resolved + llvm::ArrayRef Parameters; + // The type of the parameter pack + const clang::TemplateTypeParmType* PackType; + + struct ForwardingInfo { + // If the parameters were resolved to another FunctionDecl, these are its + // first non-variadic parameters (i.e. the first entries of the parameter + // pack that are passed as arguments bound to a non-pack parameter.) + llvm::ArrayRef Head; + // If the parameters were resolved to another FunctionDecl, these are its + // variadic parameters (i.e. the entries of the parameter pack that are + // passed as arguments bound to a pack parameter.) + llvm::ArrayRef Pack; + // If the parameters were resolved to another FunctionDecl, these are its + // last non-variadic parameters (i.e. the last entries of the parameter pack + // that are passed as arguments bound to a non-pack parameter.) + llvm::ArrayRef Tail; + // If the parameters were resolved to another FunctionDecl, this + // is it. + std::optional PackTarget; + }; + + // The output of this visitor + std::optional Info; + +private: + // inspects the given callee with the given args to check whether it + // contains Parameters, and sets Info accordingly. + void handleCall(clang::FunctionDecl* Callee, typename clang::CallExpr::arg_range Args) { + // Skip functions with less parameters, they can't be the target. + if(Callee->parameters().size() < Parameters.size()) + return; + if(llvm::any_of(Args, + [](const clang::Expr* E) { return isa(E); })) { + return; + } + auto PackLocation = findPack(Args); + if(!PackLocation) + return; + llvm::ArrayRef MatchingParams = + Callee->parameters().slice(*PackLocation, Parameters.size()); + // Check whether the function has a parameter pack as the last template + // parameter + if(const auto* TTPT = ast::getFunctionPackType(Callee)) { + // In this case: Separate the parameters into head, pack and tail + auto IsExpandedPack = [&](const clang::ParmVarDecl* P) { + return ast::getUnderlyingPackType(P) == TTPT; + }; + ForwardingInfo FI; + FI.Head = MatchingParams.take_until(IsExpandedPack); + FI.Pack = MatchingParams.drop_front(FI.Head.size()).take_while(IsExpandedPack); + FI.Tail = MatchingParams.drop_front(FI.Head.size() + FI.Pack.size()); + FI.PackTarget = Callee; + Info = FI; + return; + } + // Default case: assume all parameters were fully resolved + ForwardingInfo FI; + FI.Head = MatchingParams; + Info = FI; + } + + // Returns the beginning of the expanded pack represented by Parameters + // in the given arguments, if it is there. + std::optional findPack(typename clang::CallExpr::arg_range Args) { + // find the argument directly referring to the first parameter + assert(Parameters.size() <= static_cast(llvm::size(Args))); + for(auto Begin = Args.begin(), End = Args.end() - Parameters.size() + 1; Begin != End; + ++Begin) { + if(const auto* RefArg = unwrapForward(*Begin)) { + if(Parameters.front() != RefArg->getDecl()) + continue; + // Check that this expands all the way until the last parameter. + // It's enough to look at the last parameter, because it isn't possible + // to expand without expanding all of them. + auto ParamEnd = Begin + Parameters.size() - 1; + RefArg = unwrapForward(*ParamEnd); + if(!RefArg || Parameters.back() != RefArg->getDecl()) + continue; + return std::distance(Args.begin(), Begin); + } + } + return std::nullopt; + } + + static clang::FunctionDecl* getCalleeDeclOrUniqueOverload(clang::CallExpr* E) { + clang::Decl* CalleeDecl = E->getCalleeDecl(); + auto* Callee = llvm::dyn_cast_or_null(CalleeDecl); + if(!Callee) { + if(auto* Lookup = dyn_cast(E->getCallee())) { + Callee = resolveOverload(Lookup, E); + } + } + // Ignore the callee if the number of arguments is wrong (deal with va_args) + if(Callee && Callee->getNumParams() == E->getNumArgs()) + return Callee; + return nullptr; + } + + static clang::FunctionDecl* resolveOverload(clang::UnresolvedLookupExpr* Lookup, + clang::CallExpr* E) { + clang::FunctionDecl* MatchingDecl = nullptr; + if(!Lookup->requiresADL()) { + // Check whether there is a single overload with this number of + // parameters + for(auto* Candidate: Lookup->decls()) { + if(auto* FuncCandidate = llvm::dyn_cast_or_null(Candidate)) { + if(FuncCandidate->getNumParams() == E->getNumArgs()) { + if(MatchingDecl) { + // there are multiple candidates - abort + return nullptr; + } + MatchingDecl = FuncCandidate; + } + } + } + } + return MatchingDecl; + } + + // Tries to get to the underlying argument by unwrapping implicit nodes and + // std::forward. + const static clang::DeclRefExpr* unwrapForward(const clang::Expr* E) { + E = E->IgnoreImplicitAsWritten(); + // There might be an implicit copy/move constructor call on top of the + // forwarded arg. + // FIXME: Maybe mark implicit calls in the AST to properly filter here. + if(const auto* Const = llvm::dyn_cast(E)) + if(Const->getConstructor()->isCopyOrMoveConstructor()) + E = Const->getArg(0)->IgnoreImplicitAsWritten(); + if(const auto* Call = llvm::dyn_cast(E)) { + const auto Callee = Call->getBuiltinCallee(); + if(Callee == clang::Builtin::BIforward) { + return llvm::dyn_cast( + Call->getArg(0)->IgnoreImplicitAsWritten()); + } + } + return llvm::dyn_cast(E); + } +}; + +llvm::SmallVector + resolveForwardingParameters(const clang::FunctionDecl* D, unsigned MaxDepth) { + auto Parameters = D->parameters(); + // If the function has a template parameter pack + if(const auto* TTPT = ast::getFunctionPackType(D)) { + // Split the parameters into head, pack and tail + auto IsExpandedPack = [TTPT](const clang::ParmVarDecl* P) { + return ast::getUnderlyingPackType(P) == TTPT; + }; + llvm::ArrayRef Head = Parameters.take_until(IsExpandedPack); + llvm::ArrayRef Pack = + Parameters.drop_front(Head.size()).take_while(IsExpandedPack); + llvm::ArrayRef Tail = + Parameters.drop_front(Head.size() + Pack.size()); + llvm::SmallVector Result(Parameters.size()); + // Fill in non-pack parameters + auto* HeadIt = std::copy(Head.begin(), Head.end(), Result.begin()); + auto TailIt = std::copy(Tail.rbegin(), Tail.rend(), Result.rbegin()); + // Recurse on pack parameters + + size_t Depth = 0; + + const clang::FunctionDecl* CurrentFunction = D; + llvm::SmallSet SeenTemplates; + if(const auto* Template = D->getPrimaryTemplate()) { + SeenTemplates.insert(Template); + } + + while(!Pack.empty() && CurrentFunction && Depth < MaxDepth) { + // Find call expressions involving the pack + ForwardingCallVisitor V{Pack}; + V.TraverseStmt(CurrentFunction->getBody()); + if(!V.Info) { + break; + } + // If we found something: Fill in non-pack parameters + auto Info = *V.Info; + HeadIt = std::copy(Info.Head.begin(), Info.Head.end(), HeadIt); + TailIt = std::copy(Info.Tail.rbegin(), Info.Tail.rend(), TailIt); + // Prepare next recursion level + Pack = Info.Pack; + CurrentFunction = Info.PackTarget.value_or(nullptr); + Depth++; + // If we are recursing into a previously encountered function: Abort + if(CurrentFunction) { + if(const auto* Template = CurrentFunction->getPrimaryTemplate()) { + bool NewFunction = SeenTemplates.insert(Template).second; + if(!NewFunction) { + return {Parameters.begin(), Parameters.end()}; + } + } + } + } + + // Fill in the remaining unresolved pack parameters + HeadIt = std::copy(Pack.begin(), Pack.end(), HeadIt); + assert(TailIt.base() == HeadIt); + return Result; + } + return {Parameters.begin(), Parameters.end()}; +} + +bool isSugaredTemplateParameter(clang::QualType QT) { + static auto PeelWrapper = [](clang::QualType QT) { + // Neither `PointerType` nor `ReferenceType` is considered as sugared + // type. Peel it. + clang::QualType Peeled = QT->getPointeeType(); + return Peeled.isNull() ? QT : Peeled; + }; + + // This is a bit tricky: we traverse the type structure and find whether or + // not a type in the desugaring process is of SubstTemplateTypeParmType. + // During the process, we may encounter pointer or reference types that are + // not marked as sugared; therefore, the desugar function won't apply. To + // move forward the traversal, we retrieve the pointees using + // QualType::getPointeeType(). + // + // However, getPointeeType could leap over our interests: The QT::getAs() + // invoked would implicitly desugar the type. Consequently, if the + // SubstTemplateTypeParmType is encompassed within a TypedefType, we may lose + // the chance to visit it. + // For example, given a QT that represents `std::vector::value_type`: + // `-ElaboratedType 'value_type' sugar + // `-TypedefType 'vector::value_type' sugar + // |-Typedef 'value_type' + // `-SubstTemplateTypeParmType 'int *' sugar class depth 0 index 0 T + // |-ClassTemplateSpecialization 'vector' + // `-PointerType 'int *' + // `-BuiltinType 'int' + // Applying `getPointeeType` to QT results in 'int', a child of our target + // node SubstTemplateTypeParmType. + // + // As such, we always prefer the desugared over the pointee for next type + // in the iteration. It could avoid the getPointeeType's implicit desugaring. + while(true) { + if(QT->getAs()) + return true; + clang::QualType Desugared = QT->getLocallyUnqualifiedSingleStepDesugaredType(); + if(Desugared != QT) + QT = Desugared; + else if(auto Peeled = PeelWrapper(Desugared); Peeled != QT) + QT = Peeled; + else + break; + } + + return false; +} + +std::optional desugar(clang::ASTContext& AST, clang::QualType QT) { + bool ShouldAKA = false; + auto Desugared = clang::desugarForDiagnostic(AST, QT, ShouldAKA); + if(!ShouldAKA) + return std::nullopt; + return Desugared; +} + +clang::QualType maybeDesugar(clang::ASTContext& AST, clang::QualType QT) { + // Prefer desugared type for name that aliases the template parameters. + // This can prevent things like printing opaque `: type` when accessing std + // containers. + if(ast::isSugaredTemplateParameter(QT)) + return desugar(AST, QT).value_or(QT); + + // Prefer desugared type for `decltype(expr)` specifiers. + if(QT->isDecltypeType()) + return QT.getCanonicalType(); + + if(const auto* AT = QT->getContainedAutoType()) + if(!AT->getDeducedType().isNull() && AT->getDeducedType()->isDecltypeType()) + return QT.getCanonicalType(); + + return QT; +} + +clang::FunctionProtoTypeLoc getPrototypeLoc(clang::Expr* Fn) { + clang::TypeLoc Target; + clang::Expr* NakedFn = Fn->IgnoreParenCasts(); + if(const auto* T = NakedFn->getType().getTypePtr()->getAs()) { + Target = T->getDecl()->getTypeSourceInfo()->getTypeLoc(); + } else if(const auto* DR = llvm::dyn_cast(NakedFn)) { + const auto* D = DR->getDecl(); + if(const auto* const VD = llvm::dyn_cast(D)) { + Target = VD->getTypeSourceInfo()->getTypeLoc(); + } + } + + if(!Target) + return {}; + + // Unwrap types that may be wrapping the function type + while(true) { + if(auto P = Target.getAs()) { + Target = P.getPointeeLoc(); + continue; + } + if(auto A = Target.getAs()) { + Target = A.getModifiedLoc(); + continue; + } + if(auto P = Target.getAs()) { + Target = P.getInnerLoc(); + continue; + } + break; + } + + if(auto F = Target.getAs()) { + return F; + } + + return {}; +} + } // namespace clice::ast diff --git a/src/Feature/InlayHint.cpp b/src/Feature/InlayHint.cpp index d998c985..c8a30927 100644 --- a/src/Feature/InlayHint.cpp +++ b/src/Feature/InlayHint.cpp @@ -1,190 +1,854 @@ -#include "Feature/InlayHint.h" #include "AST/FilterASTVisitor.h" +#include "Feature/InlayHint.h" #include "Support/Compare.h" -#include "clang/AST/TypeVisitor.h" +#include "AST/Utility.h" +#include "clang/Lex/Lexer.h" +#include "llvm/ADT/StringExtras.h" namespace clice::feature { namespace { -class TypePrinter : clang::TypeVisitor { -public: +using namespace clang; + +// For now, inlay hints are always anchored at the left or right of their range. +enum class HintSide { Left, Right }; + +void stripLeadingUnderscores(StringRef& Name) { + Name = Name.ltrim('_'); +} + +bool isExpandedFromParameterPack(const ParmVarDecl* D) { + return ast::getUnderlyingPackType(D) != nullptr; +} + +ArrayRef + maybeDropCxxExplicitObjectParameters(ArrayRef Params) { + if(!Params.empty() && Params.front()->isExplicitObjectParameter()) + Params = Params.drop_front(1); + return Params; +} + +template +std::string joinAndTruncate(const R& Range, size_t MaxLength) { + std::string Out; + llvm::raw_string_ostream OS(Out); + llvm::ListSeparator Sep(", "); + for(auto&& Element: Range) { + OS << Sep; + if(Out.size() + Element.size() >= MaxLength) { + OS << "..."; + break; + } + OS << Element; + } + OS.flush(); + return Out; +} + +// for a ParmVarDecl from a function declaration, returns the corresponding +// ParmVarDecl from the definition if possible, nullptr otherwise. +const static ParmVarDecl* getParamDefinition(const ParmVarDecl* P) { + if(auto* Callee = dyn_cast(P->getDeclContext())) { + if(auto* Def = Callee->getDefinition()) { + auto I = std::distance(Callee->param_begin(), llvm::find(Callee->parameters(), P)); + if(I < (int)Callee->getNumParams()) { + return Def->getParamDecl(I); + } + } + } + return nullptr; +} + +// If "E" spells a single unqualified identifier, return that name. +// Otherwise, return an empty string. +static StringRef getSpelledIdentifier(const Expr* E) { + E = E->IgnoreUnlessSpelledInSource(); + + if(auto* DRE = dyn_cast(E)) + if(!DRE->getQualifier()) + return ast::getSimpleName(*DRE->getDecl()); + + if(auto* ME = dyn_cast(E)) + if(!ME->getQualifier() && ME->isImplicitAccess()) + return ast::getSimpleName(*ME->getMemberDecl()); + + return {}; +} + +using NameVec = SmallVector; + +static bool isSetter(const FunctionDecl* Callee, const NameVec& ParamNames) { + if(ParamNames.size() != 1) + return false; + + StringRef Name = ast::getSimpleName(*Callee); + if(!Name.starts_with_insensitive("set")) + return false; + + // In addition to checking that the function has one parameter and its + // name starts with "set", also check that the part after "set" matches + // the name of the parameter (ignoring case). The idea here is that if + // the parameter name differs, it may contain extra information that + // may be useful to show in a hint, as in: + // void setTimeout(int timeoutMillis); + // This currently doesn't handle cases where params use snake_case + // and functions don't, e.g. + // void setExceptionHandler(EHFunc exception_handler); + // We could improve this by replacing `equals_insensitive` with some + // `sloppy_equals` which ignores case and also skips underscores. + StringRef WhatItIsSetting = Name.substr(3).ltrim("_"); + return WhatItIsSetting.equals_insensitive(ParamNames[0]); +} + +// Checks if the callee is one of the builtins +// addressof, as_const, forward, move(_if_noexcept) +static bool isSimpleBuiltin(const FunctionDecl* Callee) { + switch(Callee->getBuiltinID()) { + case Builtin::BIaddressof: + case Builtin::BIas_const: + case Builtin::BIforward: + case Builtin::BImove: + case Builtin::BImove_if_noexcept: return true; + default: return false; + } +} + +struct Callee { + // Only one of Decl or Loc is set. + // Loc is for calls through function pointers. + const FunctionDecl* Decl = nullptr; + FunctionProtoTypeLoc Loc; }; -class InlayHintsCollector : public FilteredASTVisitor { +class InlayHintVisitor : public FilteredASTVisitor { public: - using Base = FilteredASTVisitor; - using Base::Base; + using Base = FilteredASTVisitor; - /// For `auto x|:int| = 1` or `std::vector|| vec = {1, 2, 3}`. - bool VisitVarDecl(const clang::VarDecl* decl) { - /// Handle only VarDecl. FIXME: VarTemplateSpecialization? - if(decl->getKind() != clang::Decl::Var) { - return true; + InlayHintVisitor(std::vector& Results, + CompilationUnit& unit, + std::optional restrict_range) : + Base(unit, true), results(Results), unit(unit), AST(unit.context()), + + Tokens(unit.token_buffer()), restrict_range(*std::move(restrict_range)), + MainFileID(AST.getSourceManager().getMainFileID()), + TypeHintPolicy(this->AST.getPrintingPolicy()) { + + MainFileBuf = unit.interested_content(); + + TypeHintPolicy.SuppressScope = true; // keep type names short + TypeHintPolicy.AnonymousTagLocations = false; // do not print lambda locations + + // Not setting PrintCanonicalTypes for "auto" allows + // SuppressDefaultTemplateArgs (set by default) to have an effect. + } + +public: + // Get the range of the main file that *exactly* corresponds to R. + std::optional getHintRange(SourceRange R) { + const auto& SM = AST.getSourceManager(); + auto Spelled = Tokens.spelledForExpanded(Tokens.expandedTokens(R)); + // TokenBuffer will return null if e.g. R corresponds to only part of a + // macro expansion. + if(!Spelled || Spelled->empty()) + return std::nullopt; + + // Hint must be within the main file, not e.g. a non-preamble include. + if(SM.getFileID(Spelled->front().location()) != SM.getMainFileID() || + SM.getFileID(Spelled->back().location()) != SM.getMainFileID()) + return std::nullopt; + + /// FIXME: + // return LocalSourceRange{sourceLocToPosition(SM, Spelled->front().location()), + // sourceLocToPosition(SM, Spelled->back().endLocation())}; + + return unit + .decompose_range( + clang::SourceRange(Spelled->front().location(), Spelled->back().endLocation())) + .second; + } + + void addBlockEndHint(SourceRange BraceRange, + StringRef DeclPrefix, + StringRef Name, + StringRef OptionalPunctuation) { + auto HintRange = computeBlockEndHintRange(BraceRange, OptionalPunctuation); + if(!HintRange) + return; + + std::string Label = DeclPrefix.str(); + if(!Label.empty() && !Name.empty()) + Label += ' '; + Label += Name; + + constexpr unsigned HintMaxLengthLimit = 60; + if(Label.length() > HintMaxLengthLimit) + return; + + add_inlay_hint(*HintRange, HintSide::Right, InlayHintKind::BlockEnd, " // ", Label, ""); + } + + // Compute the LSP range to attach the block end hint to, if any allowed. + // 1. "}" is the last non-whitespace character on the line. The range of "}" + // is returned. + // 2. After "}", if the trimmed trailing text is exactly + // `OptionalPunctuation`, say ";". The range of "} ... ;" is returned. + // Otherwise, the hint shouldn't be shown. + std::optional computeBlockEndHintRange(SourceRange BraceRange, + StringRef OptionalPunctuation) { + constexpr unsigned HintMinLineLimit = 2; + + auto& SM = AST.getSourceManager(); + auto [BlockBeginFileId, BlockBeginOffset] = + SM.getDecomposedLoc(SM.getFileLoc(BraceRange.getBegin())); + auto RBraceLoc = SM.getFileLoc(BraceRange.getEnd()); + auto [RBraceFileId, RBraceOffset] = SM.getDecomposedLoc(RBraceLoc); + + // Because we need to check the block satisfies the minimum line limit, we + // require both source location to be in the main file. This prevents hint + // to be shown in weird cases like '{' is actually in a "#include", but it's + // rare anyway. + if(BlockBeginFileId != MainFileID || RBraceFileId != MainFileID) + return std::nullopt; + + StringRef RestOfLine = MainFileBuf.substr(RBraceOffset).split('\n').first; + if(!RestOfLine.starts_with("}")) + return std::nullopt; + + StringRef TrimmedTrailingText = RestOfLine.drop_front().trim(); + if(!TrimmedTrailingText.empty() && TrimmedTrailingText != OptionalPunctuation) + return std::nullopt; + + auto BlockBeginLine = SM.getLineNumber(BlockBeginFileId, BlockBeginOffset); + auto RBraceLine = SM.getLineNumber(RBraceFileId, RBraceOffset); + + // Don't show hint on trivial blocks like `class X {};` + if(BlockBeginLine + HintMinLineLimit - 1 > RBraceLine) + return std::nullopt; + + // This is what we attach the hint to, usually "}" or "};". + StringRef HintRangeText = + RestOfLine.take_front(TrimmedTrailingText.empty() + ? 1 + : TrimmedTrailingText.bytes_end() - RestOfLine.bytes_begin()); + + /// FIXME: Handle case, if RBraceLoc is from macro expansion. + auto [fid, offset] = unit.decompose_location(RBraceLoc); + return LocalSourceRange(offset, offset + HintRangeText.size()); + } + + // We pass HintSide rather than SourceLocation because we want to ensure + // it is in the same file as the common file range. + void add_inlay_hint(SourceRange R, + HintSide Side, + InlayHintKind Kind, + llvm::StringRef Prefix, + llvm::StringRef Label, + llvm::StringRef Suffix) { + auto LSPRange = getHintRange(R); + if(!LSPRange) + return; + + add_inlay_hint(*LSPRange, Side, Kind, Prefix, Label, Suffix); + } + + void add_inlay_hint(LocalSourceRange LSPRange, + HintSide side, + InlayHintKind kind, + llvm::StringRef prefix, + llvm::StringRef Label, + llvm::StringRef suffix) { + // We shouldn't get as far as adding a hint if the category is disabled. + // We'd like to disable as much of the analysis as possible above instead. + // Assert in debug mode but add a dynamic check in production. + assert(options.enabled && "Shouldn't get here if disabled!"); + + std::uint32_t offset = side == HintSide::Left ? LSPRange.begin : LSPRange.end; + if(restrict_range.valid() && !restrict_range.contains(offset)) + return; + + bool pad_left = prefix.consume_front(" "); + bool pad_right = suffix.consume_back(" "); + + InlayHint hint{offset, kind}; + hint.parts.emplace_back(-1, (prefix + Label + suffix).str()); + results.push_back(std::move(hint)); + } + + void add_type_hint(SourceRange R, QualType T, llvm::StringRef Prefix) { + if(!options.deduced_types || T.isNull()) + return; + + // The sugared type is more useful in some cases, and the canonical + // type in other cases. + auto Desugared = ast::maybeDesugar(AST, T); + std::string TypeName = Desugared.getAsString(TypeHintPolicy); + + auto should_print = [&](llvm::StringRef TypeName) { + return options.type_name_limit == 0 || TypeName.size() < options.type_name_limit; + }; + + if(T != Desugared && !should_print(TypeName)) { + // If the desugared type is too long to display, fallback to the sugared + // type. + TypeName = T.getAsString(TypeHintPolicy); } - auto type = decl->getType(); - if(type.isNull() || !type->isUndeducedAutoType() || type->isDependentType()) { - return true; - } + if(should_print(TypeName)) + add_inlay_hint(R, + HintSide::Right, + InlayHintKind::Type, + Prefix, + TypeName, + /*Suffix=*/""); + } - /// TODO: - /// type->getContainedDeducedType(); + void addDesignatorHint(SourceRange R, llvm::StringRef Text) { + add_inlay_hint(R, + HintSide::Left, + InlayHintKind::Designator, + /*Prefix=*/"", + Text, + /*Suffix=*/"="); + } + + void add_return_type_hint(FunctionDecl* D, SourceRange Range) { + auto* AT = D->getReturnType()->getContainedAutoType(); + if(!AT || AT->getDeducedType().isNull()) + return; + add_type_hint(Range, D->getReturnType(), /*Prefix=*/"-> "); + } + + bool shouldHintName(const Expr* Arg, StringRef ParamName) { + if(ParamName.empty()) + return false; + + // If the argument expression is a single name and it matches the + // parameter name exactly, omit the name hint. + if(ParamName == getSpelledIdentifier(Arg)) + return false; + + // Exclude argument expressions preceded by a /*paramName*/. + if(isPrecededByParamNameComment(Arg, ParamName)) + return false; return true; } - /// For `auto [a|:int|, b|:int|] = std::tuple(1, 2)`. - bool VisitBindingDecl(const clang::BindingDecl* decl) { - /// TODO: - return true; + bool shouldHintReference(const ParmVarDecl* Param, const ParmVarDecl* ForwardedParam) { + // We add a & hint only when the argument is passed as mutable reference. + // For parameters that are not part of an expanded pack, this is + // straightforward. For expanded pack parameters, it's likely that they will + // be forwarded to another function. In this situation, we only want to add + // the reference hint if the argument is actually being used via mutable + // reference. This means we need to check + // 1. whether the value category of the argument is preserved, i.e. each + // pack expansion uses std::forward correctly. + // 2. whether the argument is ever copied/cast instead of passed + // by-reference + // Instead of checking this explicitly, we use the following proxy: + // 1. the value category can only change from rvalue to lvalue during + // forwarding, so checking whether both the parameter of the forwarding + // function and the forwarded function are lvalue references detects such + // a conversion. + // 2. if the argument is copied/cast somewhere in the chain of forwarding + // calls, it can only be passed on to an rvalue reference or const lvalue + // reference parameter. Thus if the forwarded parameter is a mutable + // lvalue reference, it cannot have been copied/cast to on the way. + // Additionally, we should not add a reference hint if the forwarded + // parameter was only partially resolved, i.e. points to an expanded pack + // parameter, since we do not know how it will be used eventually. + auto Type = Param->getType(); + auto ForwardedType = ForwardedParam->getType(); + return Type->isLValueReferenceType() && ForwardedType->isLValueReferenceType() && + !ForwardedType.getNonReferenceType().isConstQualified() && + !isExpandedFromParameterPack(ForwardedParam); } - /// For `auto foo()|-> int| { return 1; }` - bool VisitFunctionDecl(const clang::FunctionDecl* decl) { - auto loc = decl->getFunctionTypeLoc(); - if(!loc) { - return true; - } - - if(auto proto = loc.getAs()) { - if(proto.getTypePtr()->hasTrailingReturn()) { - return true; - } - } - - auto returnLoc = loc.getReturnLoc(); - if(auto A = returnLoc.getAs()) { - return true; - } - - /// TODO: - return true; + void markBlockEnd(const Stmt* Body, llvm::StringRef Label, llvm::StringRef Name = "") { + if(const auto* CS = llvm::dyn_cast_or_null(Body)) + addBlockEndHint(CS->getSourceRange(), Label, Name, ""); } - /// For `foo(|x:|1,|y:|2)` - bool VisitCallExpr(const clang::CallExpr* expr) { - /// For some builtin functions, skip them. - switch(expr->getBuiltinCallee()) { - case clang::Builtin::BIaddressof: - case clang::Builtin::BIas_const: - case clang::Builtin::BIforward: - case clang::Builtin::BImove: - case clang::Builtin::BImove_if_noexcept: { - return true; - } + using NameVec = SmallVector; - default: { + void processCall(Callee Callee, + SourceLocation RParenOrBraceLoc, + llvm::ArrayRef Args) { + assert(Callee.Decl || Callee.Loc); + + if((!options.parameters && !options.default_arguments) || Args.size() == 0) + return; + + // The parameter name of a move or copy constructor is not very interesting. + if(Callee.Decl) + if(auto* Ctor = dyn_cast(Callee.Decl)) + if(Ctor->isCopyOrMoveConstructor()) + return; + + SmallVector FormattedDefaultArgs; + bool HasNonDefaultArgs = false; + + ArrayRef Params, ForwardedParams; + // Resolve parameter packs to their forwarded parameter + SmallVector ForwardedParamsStorage; + if(Callee.Decl) { + Params = maybeDropCxxExplicitObjectParameters(Callee.Decl->parameters()); + ForwardedParamsStorage = ast::resolveForwardingParameters(Callee.Decl); + ForwardedParams = maybeDropCxxExplicitObjectParameters(ForwardedParamsStorage); + } else { + Params = maybeDropCxxExplicitObjectParameters(Callee.Loc.getParams()); + ForwardedParams = {Params.begin(), Params.end()}; + } + + NameVec ParameterNames = chooseParameterNames(ForwardedParams); + + // Exclude setters (i.e. functions with one argument whose name begins with + // "set"), and builtins like std::move/forward/... as their parameter name + // is also not likely to be interesting. + if(Callee.Decl && (isSetter(Callee.Decl, ParameterNames) || isSimpleBuiltin(Callee.Decl))) + return; + + for(size_t I = 0; I < ParameterNames.size() && I < Args.size(); ++I) { + // Pack expansion expressions cause the 1:1 mapping between arguments and + // parameters to break down, so we don't add further inlay hints if we + // encounter one. + if(isa(Args[I])) { break; } - } - auto callee = expr->getDirectCallee(); + StringRef Name = ParameterNames[I]; + const bool NameHint = shouldHintName(Args[I], Name) && options.parameters; + const bool ReferenceHint = + shouldHintReference(Params[I], ForwardedParams[I]) && options.parameters; - /// Don't hint for UDL operator like `operaotr ""_str`. - if(!callee || llvm::isa(expr)) { - return true; - } - - /// Only hint for overloaded `operator()` and `operator[]`. - if(auto OC = llvm::dyn_cast(expr)) { - auto kind = OC->getOperator(); - if(kind != clang::OO_Call && kind != clang::OO_Subscript) { - return true; + const bool IsDefault = isa(Args[I]); + HasNonDefaultArgs |= !IsDefault; + if(IsDefault) { + if(options.default_arguments) { + const auto SourceText = clang::Lexer::getSourceText( + CharSourceRange::getTokenRange(Params[I]->getDefaultArgRange()), + AST.getSourceManager(), + AST.getLangOpts()); + const auto Abbrev = + (SourceText.size() > options.type_name_limit || SourceText.contains("\n")) + ? "..." + : SourceText; + if(NameHint) + FormattedDefaultArgs.emplace_back(llvm::formatv("{0}: {1}", Name, Abbrev)); + else + FormattedDefaultArgs.emplace_back(llvm::formatv("{0}", Abbrev)); + } + } else if(NameHint || ReferenceHint) { + add_inlay_hint(Args[I]->getSourceRange(), + HintSide::Left, + InlayHintKind::Parameter, + ReferenceHint ? "&" : "", + NameHint ? Name : "", + ": "); } } - /// Get all arguments of call expression. - auto params = callee->parameters(); - if(callee->hasCXXExplicitFunctionObjectParameter()) { - params = params.drop_front(); + if(!FormattedDefaultArgs.empty()) { + std::string Hint = joinAndTruncate(FormattedDefaultArgs, options.type_name_limit); + add_inlay_hint(SourceRange{RParenOrBraceLoc}, + HintSide::Left, + InlayHintKind::DefaultArgument, + HasNonDefaultArgs ? ", " : "", + Hint, + ""); } - auto args = llvm::ArrayRef(expr->getArgs(), expr->getNumArgs()); - - /// TODO: try to get the lparen location of call expr. - tryHintArguments(clang::SourceLocation(), params, args); - - return true; } - bool VisitCXXConstructExpr(const clang::CXXConstructExpr* expr) { - const auto* constructor = expr->getConstructor(); - if(!constructor) { - return true; - } + // Checks if "E" is spelled in the main file and preceded by a C-style comment + // whose contents match ParamName (allowing for whitespace and an optional "=" + // at the end. + bool isPrecededByParamNameComment(const Expr* E, StringRef ParamName) { + auto& SM = AST.getSourceManager(); + auto FileLoc = SM.getFileLoc(E->getBeginLoc()); + auto Decomposed = SM.getDecomposedLoc(FileLoc); + if(Decomposed.first != MainFileID) + return false; - auto params = constructor->parameters(); - auto args = clang::ArrayRef(expr->getArgs(), expr->getNumArgs()); - - /// TODO: try to get the lparen location of call expr. - tryHintArguments(clang::SourceLocation(), params, args); - - return true; + StringRef SourcePrefix = MainFileBuf.substr(0, Decomposed.second); + // Allow whitespace between comment and expression. + SourcePrefix = SourcePrefix.rtrim(); + // Check for comment ending. + if(!SourcePrefix.consume_back("*/")) + return false; + // Ignore some punctuation and whitespace around comment. + // In particular this allows designators to match nicely. + llvm::StringLiteral IgnoreChars = " =."; + SourcePrefix = SourcePrefix.rtrim(IgnoreChars); + ParamName = ParamName.trim(IgnoreChars); + // Other than that, the comment must contain exactly ParamName. + if(!SourcePrefix.consume_back(ParamName)) + return false; + SourcePrefix = SourcePrefix.rtrim(IgnoreChars); + return SourcePrefix.ends_with("/*"); } - /// `Foo{|.x=|1,|.y=|2}`. - bool VisitInitListExpr(const clang::InitListExpr* expr) { - /// TODO: - return true; - } - -private: - void addInlayHint(InlayHintKind kind, - clang::SourceLocation location, - std::vector labels) { - auto [fid, offset] = unit.decompose_location(location); - auto& hints = interested_only ? result : sharedResult[fid]; - hints.emplace_back(offset, kind, labels); - } - - void addInlayHint(InlayHintKind kind, clang::SourceLocation location, std::string label) {} - - void tryHintArguments(clang::SourceLocation lbrace, - llvm::ArrayRef params, - llvm::ArrayRef args) { - clang::SourceLocation lastArgLoc; - for(std::size_t i = 0; i < args.size(); i++) { - auto arg = args[i]; - auto loc = arg->getSourceRange().getBegin(); - - // auto& SM = unit.srcMgr(); - // SM.isMacroArgExpansion(clang::SourceLocation()); - // SM.isMacroBodyExpansion(clang::SourceLocation()); - - if(lbrace == unit.expansion_location(loc)) { - /// If they have same location, they are both expansion location and expanded from - /// macro. - - /// FIXME: Figure out how to filter this out. - continue; + NameVec chooseParameterNames(ArrayRef Parameters) { + NameVec ParameterNames; + for(const auto* P: Parameters) { + if(isExpandedFromParameterPack(P)) { + // If we haven't resolved a pack paramater (e.g. foo(Args... args)) to a + // non-pack parameter, then hinting as foo(args: 1, args: 2, args: 3) is + // unlikely to be useful. + ParameterNames.emplace_back(); } else { - /// They have different location, try to hint the param. - /// TODO: check param comment like `*/arg=/*`. + auto SimpleName = ast::getSimpleName(*P); + // If the parameter is unnamed in the declaration: + // attempt to get its name from the definition + if(SimpleName.empty()) { + if(const auto* PD = getParamDefinition(P)) { + SimpleName = ast::getSimpleName(*PD); + } + } + ParameterNames.emplace_back(SimpleName); } } + + // Standard library functions often have parameter names that start + // with underscores, which makes the hints noisy, so strip them out. + for(auto& Name: ParameterNames) + stripLeadingUnderscores(Name); + + return ParameterNames; + } + + ParmVarDecl* getOnlyParamInstantiation(ParmVarDecl* D) { + auto* TemplateFunction = llvm::dyn_cast(D->getDeclContext()); + if(!TemplateFunction) + return nullptr; + auto* InstantiatedFunction = + llvm::dyn_cast_or_null(ast::get_only_instantiation(TemplateFunction)); + if(!InstantiatedFunction) + return nullptr; + + unsigned ParamIdx = 0; + for(auto* Param: TemplateFunction->parameters()) { + // Can't reason about param indexes in the presence of preceding packs. + // And if this param is a pack, it may expand to multiple params. + if(Param->isParameterPack()) + return nullptr; + if(Param == D) + break; + ++ParamIdx; + } + assert(ParamIdx < TemplateFunction->getNumParams() && "Couldn't find param in list?"); + assert(ParamIdx < InstantiatedFunction->getNumParams() && + "Instantiated function has fewer (non-pack) parameters?"); + return InstantiatedFunction->getParamDecl(ParamIdx); } public: - InlayHints result; - index::Shared sharedResult; + // Carefully recurse into PseudoObjectExprs, which typically incorporate + // a syntactic expression and several semantic expressions. + bool TraversePseudoObjectExpr(clang::PseudoObjectExpr* expr) { + clang::Expr* syntactic_expr = expr->getSyntacticForm(); + if(llvm::isa(syntactic_expr)) { + // Since the counterpart semantics usually get the identical source + // locations as the syntactic one, visiting those would end up presenting + // confusing hints e.g., __builtin_dump_struct. + // Thus, only traverse the syntactic forms if this is written as a + // CallExpr. This leaves the door open in case the arguments in the + // syntactic form could possibly get parameter names. + return Base::TraverseStmt(syntactic_expr); + } + + // We don't want the hints for some of the MS property extensions. + // e.g. + // struct S { + // __declspec(property(get=GetX, put=PutX)) int x[]; + // void PutX(int y); + // void Work(int y) { x = y; } // Bad: `x = y: y`. + // }; + if(llvm::isa(syntactic_expr)) { + return true; + } + + // FIXME: Handle other forms of a pseudo object expression. + return Base::TraversePseudoObjectExpr(expr); + } + + bool VisitNamespaceDecl(NamespaceDecl* D) { + if(options.block_end) { + // For namespace, the range actually starts at the namespace keyword. But + // it should be fine since it's usually very short. + addBlockEndHint(D->getSourceRange(), "namespace", ast::getSimpleName(*D), ""); + } + return true; + } + + bool VisitTagDecl(TagDecl* D) { + if(options.block_end && D->isThisDeclarationADefinition()) { + std::string DeclPrefix = D->getKindName().str(); + if(const auto* ED = dyn_cast(D)) { + if(ED->isScoped()) + DeclPrefix += ED->isScopedUsingClassTag() ? " class" : " struct"; + }; + addBlockEndHint(D->getBraceRange(), DeclPrefix, ast::getSimpleName(*D), ";"); + } + return true; + } + + bool VisitFunctionDecl(FunctionDecl* D) { + if(auto* FPT = llvm::dyn_cast(D->getType().getTypePtr())) { + if(!FPT->hasTrailingReturn()) { + if(auto FTL = D->getFunctionTypeLoc()) + add_return_type_hint(D, FTL.getRParenLoc()); + } + } + + if(options.block_end && D->isThisDeclarationADefinition()) { + // We use `printName` here to properly print name of ctor/dtor/operator + // overload. + if(const Stmt* Body = D->getBody()) + addBlockEndHint(Body->getSourceRange(), "", ast::print_name(D), ""); + } + + return true; + } + + bool VisitVarDecl(VarDecl* D) { + // Do not show hints for the aggregate in a structured binding, + // but show hints for the individual bindings. + if(auto* DD = dyn_cast(D)) { + for(auto* Binding: DD->bindings()) { + // For structured bindings, print canonical types. This is important + // because for bindings that use the tuple_element protocol, the + // non-canonical types would be "tuple_element::type". + if(auto Type = Binding->getType(); !Type.isNull() && !Type->isDependentType()) + add_type_hint(Binding->getLocation(), + Type.getCanonicalType(), + /*Prefix=*/": "); + } + return true; + } + + if(auto* AT = D->getType()->getContainedAutoType()) { + if(AT->isDeduced() && !D->getType()->isDependentType()) { + // Our current approach is to place the hint on the variable + // and accordingly print the full type + // (e.g. for `const auto& x = 42`, print `const int&`). + // Alternatively, we could place the hint on the `auto` + // (and then just print the type deduced for the `auto`). + add_type_hint(D->getLocation(), D->getType(), /*Prefix=*/": "); + } + } + + // Handle templates like `int foo(auto x)` with exactly one instantiation. + if(auto* PVD = llvm::dyn_cast(D)) { + if(D->getIdentifier() && PVD->getType()->isDependentType() && + !ast::get_contained_auto_param_type(D->getTypeSourceInfo()->getTypeLoc()).isNull()) { + if(auto* IPVD = getOnlyParamInstantiation(PVD)) + add_type_hint(D->getLocation(), IPVD->getType(), /*Prefix=*/": "); + } + } + + return true; + } + + bool VisitCXXConstructExpr(CXXConstructExpr* E) { + // Weed out constructor calls that don't look like a function call with + // an argument list, by checking the validity of getParenOrBraceRange(). + // Also weed out std::initializer_list constructors as there are no names + // for the individual arguments. + if(!E->getParenOrBraceRange().isValid() || E->isStdInitListInitialization()) { + return true; + } + + Callee Callee; + Callee.Decl = E->getConstructor(); + if(!Callee.Decl) + return true; + processCall(Callee, E->getParenOrBraceRange().getEnd(), {E->getArgs(), E->getNumArgs()}); + return true; + } + + bool VisitCallExpr(CallExpr* E) { + if(!options.parameters) + return true; + + auto isFunctionObjectCallExpr = [](CallExpr* E) { + if(auto* CallExpr = dyn_cast(E)) { + return CallExpr->getOperator() == OverloadedOperatorKind::OO_Call; + } + + return false; + }; + + bool IsFunctor = isFunctionObjectCallExpr(E); + + // Do not show parameter hints for user-defined literals or + // operator calls except for operator(). (Among other reasons, the resulting + // hints can look awkward, e.g. the expression can itself be a function + // argument and then we'd get two hints side by side). + if((isa(E) && !IsFunctor) || isa(E)) + return true; + + /// FIXME: Use template resolver here. + if(E->isTypeDependent() || E->isValueDependent()) { + return true; + } + + auto CalleeDecl = E->getCalleeDecl(); + + Callee Callee; + if(const auto* FD = dyn_cast(CalleeDecl)) + Callee.Decl = FD; + else if(const auto* FTD = dyn_cast(CalleeDecl)) + Callee.Decl = FTD->getTemplatedDecl(); + else if(FunctionProtoTypeLoc Loc = ast::getPrototypeLoc(E->getCallee())) + Callee.Loc = Loc; + else + return true; + + // N4868 [over.call.object]p3 says, + // The argument list submitted to overload resolution consists of the + // argument expressions present in the function call syntax preceded by the + // implied object argument (E). + // + // As well as the provision from P0847R7 Deducing This [expr.call]p7: + // ...If the function is an explicit object member function and there is an + // implied object argument ([over.call.func]), the list of provided + // arguments is preceded by the implied object argument for the purposes of + // this correspondence... + llvm::ArrayRef Args = {E->getArgs(), E->getNumArgs()}; + // We don't have the implied object argument through a function pointer + // either. + if(const CXXMethodDecl* Method = dyn_cast_or_null(Callee.Decl)) + if(IsFunctor || Method->hasCXXExplicitFunctionObjectParameter()) + Args = Args.drop_front(1); + processCall(Callee, E->getRParenLoc(), Args); + return true; + } + + bool VisitForStmt(ForStmt* S) { + if(options.block_end) { + std::string Name; + // Common case: for (int I = 0; I < N; I++). Use "I" as the name. + if(auto* DS = llvm::dyn_cast_or_null(S->getInit()); DS && DS->isSingleDecl()) + Name = ast::getSimpleName(llvm::cast(*DS->getSingleDecl())); + else + Name = ast::summarizeExpr(S->getCond()); + markBlockEnd(S->getBody(), "for", Name); + } + return true; + } + + bool VisitCXXForRangeStmt(CXXForRangeStmt* S) { + if(options.block_end) + markBlockEnd(S->getBody(), "for", ast::getSimpleName(*S->getLoopVariable())); + return true; + } + + bool VisitWhileStmt(WhileStmt* S) { + if(options.block_end) + markBlockEnd(S->getBody(), "while", ast::summarizeExpr(S->getCond())); + return true; + } + + bool VisitSwitchStmt(SwitchStmt* S) { + if(options.block_end) + markBlockEnd(S->getBody(), "switch", ast::summarizeExpr(S->getCond())); + return true; + } + + bool VisitIfStmt(IfStmt* S) { + if(options.block_end) { + if(const auto* ElseIf = llvm::dyn_cast_or_null(S->getElse())) + ElseIfs.insert(ElseIf); + // Don't use markBlockEnd: the relevant range is [then.begin, else.end]. + if(const auto* EndCS = + llvm::dyn_cast(S->getElse() ? S->getElse() : S->getThen())) { + addBlockEndHint({S->getThen()->getBeginLoc(), EndCS->getRBracLoc()}, + "if", + ElseIfs.contains(S) ? "" : ast::summarizeExpr(S->getCond()), + ""); + } + } + return true; + } + + bool VisitLambdaExpr(LambdaExpr* E) { + FunctionDecl* D = E->getCallOperator(); + if(!E->hasExplicitResultType()) { + SourceLocation TypeHintLoc; + if(!E->hasExplicitParameters()) + TypeHintLoc = E->getIntroducerRange().getEnd(); + else if(auto FTL = D->getFunctionTypeLoc()) + TypeHintLoc = FTL.getRParenLoc(); + if(TypeHintLoc.isValid()) + add_return_type_hint(D, TypeHintLoc); + } + return true; + } + + bool VisitInitListExpr(InitListExpr* expr) { + // We receive the syntactic form here (shouldVisitImplicitCode() is false). + // This is the one we will ultimately attach designators to. + // It may have subobject initializers inlined without braces. The *semantic* + // form of the init-list has nested init-lists for these. + // getUnwrittenDesignators will look at the semantic form to determine the + // labels. + assert(expr->isSyntacticForm() && "RAV should not visit implicit code!"); + if(!options.designators) + return true; + + if(expr->isIdiomaticZeroInitializer(AST.getLangOpts())) + return true; + + /// FIXME: + // llvm::DenseMap Designators = + // tidy::utils::getUnwrittenDesignators(Syn); + // for(const Expr* Init: Syn->inits()) { + // if(llvm::isa(Init)) + // continue; + // auto It = Designators.find(Init->getBeginLoc()); + // if(It != Designators.end() && !isPrecededByParamNameComment(Init, It->second)) + // addDesignatorHint(Init->getSourceRange(), It->second); + // } + return true; + } + + bool VisitTypeLoc(TypeLoc TL) { + if(const auto* DT = llvm::dyn_cast(TL.getType())) + if(QualType UT = DT->getUnderlyingType(); !UT->isDependentType()) + add_type_hint(TL.getSourceRange(), UT, ": "); + return true; + } + + // FIXME: Handle RecoveryExpr to try to hint some invalid calls. + +private: + std::vector& results; + CompilationUnit& unit; + ASTContext& AST; + const syntax::TokenBuffer& Tokens; + LocalSourceRange restrict_range; + FileID MainFileID; + StringRef MainFileBuf; + PrintingPolicy TypeHintPolicy; + config::InlayHintsOptions options; + + // If/else chains are tricky. + // if (cond1) { + // } else if (cond2) { + // } // mark as "cond1" or "cond2"? + // For now, the answer is neither, just mark as "if". + // The ElseIf is a different IfStmt that doesn't know about the outer one. + llvm::DenseSet ElseIfs; // not eligible for names }; } // namespace -InlayHints inlayHints(CompilationUnit& unit, LocalSourceRange target) { - InlayHintsCollector collector(unit, true); - collector.TraverseAST(unit.context()); - ranges::sort(collector.result, refl::less); - return std::move(collector.result); -} - -index::Shared indexInlayHints(CompilationUnit& unit) { - InlayHintsCollector collector(unit, true); - collector.TraverseAST(unit.context()); - for(auto&& [fid, result]: collector.sharedResult) { - ranges::sort(result, refl::less); - } - return std::move(collector.sharedResult); +InlayHints inlay_hints(CompilationUnit& unit, LocalSourceRange target) { + std::vector hints; + InlayHintVisitor visitor(hints, unit, target); + visitor.TraverseDecl(unit.tu()); + return hints; } } // namespace clice::feature diff --git a/tests/unit/Feature/InlayHint.cpp b/tests/unit/Feature/InlayHint.cpp index ed0c1db0..ddf382d6 100644 --- a/tests/unit/Feature/InlayHint.cpp +++ b/tests/unit/Feature/InlayHint.cpp @@ -5,624 +5,38 @@ namespace clice::testing { namespace { -// constexpr config::InlayHintOption LikeClangd{ -// .maxLength = 20, -// .maxArrayElements = 10, -// .typeLink = false, -// .structSizeAndAlign = false, -// .memberSizeAndOffset = false, -// .implicitCast = false, -// .chainCall = false, -// }; - -/// using namespace feature::inlay_hint; - -struct InlayHints : public ::testing::Test { +struct InlayHints : public Tester, ::testing::Test { protected: - std::optional tester; - feature::InlayHints result; + feature::InlayHints hints; - void run(llvm::StringRef code, proto::Range range = {}) { - /// tester.emplace("main.cpp", code); - /// tester->compile(); - /// auto& info = tester->AST; - /// - /// proto::Range limit = range; - /// if(limit.start.line == limit.end.line && limit.start.character == limit.end.character && - /// limit.start.line == 0 && limit.start.character == 0) { - /// /// limit = {.start = {}, .end = tester->endOfFile()}; - ///} - /// - /// result = inlayHints({.range = limit}, *info, option); - } + void run(llvm::StringRef code, llvm::StringRef name) { + add_main("main.cpp", code); + compile(); - size_t indexOf(llvm::StringRef key) { - /// auto expect = tester->offset(key); - /// auto iter = std::find_if(result.begin(), result.end(), [&expect](InlayHint& hint) { - /// return hint.offset == expect; - /// }); - /// - /// assert(iter != result.end()); - /// return std::distance(result.begin(), iter); - return 0; - } + LocalSourceRange range = LocalSourceRange(0, unit->interested_content().size()); + hints = feature::inlay_hints(*unit, range); - /// static std::string joinLabels(const feature::InlayHint& hint) { - /// std::string text; - /// for(auto& lable: hint.labels) { - /// text += lable.value; - /// } - /// return text; - /// } - /// - /// static std::string joinLabels(const proto::InlayHint& hint) { - /// std::string text; - /// for(auto& lable: hint.lables) { - /// text += lable.value; - /// } - /// return text; - ///} + assert(nameless_points().size() == 1); - void EXPECT_AT(llvm::StringRef key, llvm::StringRef text) { - /// auto index = indexOf(key); - /// EXPECT_EQ(text, joinLabels(result[index])); - } - - void EXPECT_AT(llvm::StringRef key, - llvm::function_ref checker) { - /// auto index = indexOf(key); - /// EXPECT_TRUE(checker(result[index])); - } - - void EXPECT_ALL_KEY_IS_TEXT() { - /// EXPECT_EQ(tester->locations.size(), result.size()); - - /// for(auto& hint: result) { - /// auto text = joinLabels(hint); - /// auto expect = tester->offset(text); - /// EXPECT_EQ(expect, hint.offset); + /// bool visited = false; + /// for(auto& hint: hints) { + /// if(hint.offset == nameless_points().front() && hint.parts.front().name == name){ + /// visited = true; + /// break; + /// } /// } - } - - void EXPECT_HINT_COUNT(size_t count, - llvm::StringRef startKey = "", - llvm::StringRef endKey = "") { - /// size_t begin = 0; - /// size_t end = result.size(); - /// - /// if(!startKey.empty()) { - /// auto left = tester->offset(startKey); - /// begin = std::count_if(result.begin(), result.end(), [left](const InlayHint& hint) { - /// return hint.offset <= left; - /// }); - ///} - /// - /// if(!endKey.empty()) { - /// auto right = tester->offset(startKey); - /// begin = std::count_if(result.begin(), result.end(), [right](const InlayHint& hint) { - /// return hint.offset <= right; - /// }); - ///} - /// - /// EXPECT_EQ(count, end - begin); + /// EXPECT_TRUE(visited); } }; -TEST_F(InlayHints, MaxLength) { +TEST_F(InlayHints, Parameters) { run(R"cpp( - struct _2345678 {}; - constexpr _2345678 f() { return {}; } - - auto x$(: _2...) = f(); +void foo(int param); +void bar() { + foo($42); +} )cpp", - {}); - - EXPECT_HINT_COUNT(1); - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, RequestRange) { - run(R"cpp( -auto x1 = 1;$(request_range_start) -auto x2$(1) = 1; -auto x3$(2) = 1; -auto x4$(3) = 1;$(request_range_end) -)cpp", - { - // $(request_range_start) - .start = {1, 12}, - // $(request_range_end) - .end = {4, 12}, - }); - - // 3: x2, x3, x4 is included in the request range. - EXPECT_HINT_COUNT(3, "request_range_start", "request_range_end"); - - auto text = ": int"; - EXPECT_AT("1", text); - EXPECT_AT("2", text); - EXPECT_AT("3", text); -} - -TEST_F(InlayHints, AutoDecl) { - run(R"cpp( -auto x$(1) = 1; - -void f() { - const auto& x_ref$(2) = x; - - if (auto z$(3) = x + 1) {} - - for(auto i$(4) = 0; i<10; ++i) {} -} - -template -void t() { - auto z = T{}; -} -)cpp"); - - EXPECT_HINT_COUNT(4); - - auto intHint = ": int"; - auto intRefHint = ": const int &"; - EXPECT_AT("1", intHint); - EXPECT_AT("2", intRefHint); - EXPECT_AT("3", intHint); - EXPECT_AT("4", intHint); -} - -TEST_F(InlayHints, FreeFunctionArguments) { - run(R"cpp( -void f(int a, int b) {} -void g(int a = 1) {} -void h() { -f($(a:)1, $(b:)2); -g(); -} - -)cpp"); - - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, FnArgPassedAsLValueRef) { - run(R"cpp( -void f(int& a, int& b) { } -void g() { -int x = 1; -f($(a&:)x, $(b&:)x); -} -)cpp"); - - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, MethodArguments) { - run(R"cpp( -struct A { - void f(int a, double b, unsigned int c) {} -}; - -void f() { - A a; - a.f($(a:)1, $(b:)2, $(c:)3); -} -)cpp"); - - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, OperatorCall) { - run(R"cpp( -struct A { - int operator()(int a, int b) { return a + b; } - bool operator <= (const A& rhs) { return false; } -}; - -int f() { - A a, b; - - bool s = a <= b; // should be ignored - return a($(a:)1, $(b:)2); -} -)cpp"); - - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, ReturnTypeHint) { - run(R"cpp( -auto f()$(-> int) { - return 1; -} - -void g() { - []()$(-> double) { - return static_cast(1.0); - }(); - - []$(-> void) { - return; - }(); -} - -)cpp"); - - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, StructureBinding) { - run(R"cpp( -int f() { - int a[2]; - auto [x$(1), y$(2)] = a; - return x + y; // use x and y to avoid warning. -} -)cpp"); - - EXPECT_HINT_COUNT(2); - EXPECT_AT("1", ": int"); - EXPECT_AT("2", ": int"); -} - -TEST_F(InlayHints, Constructor) { - run(R"cpp( -struct A { - int x; - float y; - - A(int a, float b):x(a), y(b) {} -}; - -void f() { - A a{$(1)1, $(2)2}; - A b($(3)1, $(4)2); - A c = {$(5)1, $(6)2}; -} - -)cpp"); - - EXPECT_HINT_COUNT(6); - - auto asA = "a:"; - auto asB = "b:"; - - EXPECT_AT("1", asA); - EXPECT_AT("3", asA); - EXPECT_AT("5", asA); - - EXPECT_AT("2", asB); - EXPECT_AT("4", asB); - EXPECT_AT("6", asB); -} - -TEST_F(InlayHints, InitializeList) { - run(R"cpp( - int a[3] = {1, 2, 3}; - int b[2][3] = {{1, 2, 3}, {4, 5, 6}}; -)cpp"); - - EXPECT_HINT_COUNT(3 + (3 * 2 + 2)); -} - -TEST_F(InlayHints, Designators) { - run(R"cpp( -struct A{ int x; int y;}; -A a = {.x = 1, .y = 2}; -)cpp"); - - EXPECT_HINT_COUNT(0); -} - -TEST_F(InlayHints, IgnoreCases) { - // Ignore - // 1. simple setters - // 2. arguments that has had-written /*argName*/ - run(R"cpp( -struct A { - void setPara(int Para); - void set_para(int para); - void set_para_meter(int para_meter); -}; - -void f(int name) {} - -void g() { - A a; - a.setPara(1); - a.set_para(1); - a.set_para_meter(1); - - f(/*name=*/1); -} - -)cpp"); - - EXPECT_HINT_COUNT(0); -} - -TEST_F(InlayHints, BlockEnd) { - run(R"cpp( -struct A { - int x; -}$(1); - -void g() { -}$(2) - -namespace no {} // there is no block end hint in a one line defination. - -namespace yes { -}$(3) - -namespace yes::nested { -}$(4) - -namespace skip { -} // some text here, no hint generated. - -struct Out { - struct In { - - };}$(5); -)cpp", - {}); - - EXPECT_HINT_COUNT(5); - - EXPECT_AT("1", "// struct A"); - EXPECT_AT("2", "// void g()"); - EXPECT_AT("3", "// namespace yes"); - EXPECT_AT("4", "// namespace nested"); - EXPECT_AT("5", "// struct Out"); -} - -TEST_F(InlayHints, Lambda) { - run(R"cpp( -auto l$(1) = []$(2) { - return 1; -}$(3); -)cpp", - {}); - - EXPECT_HINT_COUNT(3); - - EXPECT_AT("1", ": (lambda)"); - EXPECT_AT("2", "-> int"); - EXPECT_AT("3", "// lambda #0"); -} - -TEST_F(InlayHints, StructAndMemberHint) { - run(R"cpp( -struct A$(size: 8, align: 4) { - int x; - int y; - - class B$(size: 4, align: 4) { - int z; - }; - - enum class C { - _1, - }; -}; -)cpp", - {}); - - /// TODO: - /// if InlayHintOption::memberSizeAndOffset was implemented, the total hint count is 2 + 3. - EXPECT_HINT_COUNT(2 /*+ 3*/); - - EXPECT_ALL_KEY_IS_TEXT(); -} - -TEST_F(InlayHints, ImplicitCast) { - run(R"cpp( - int x = 1.0; -)cpp", - {}); - - /// FIXME: Hint count should be 1. - EXPECT_HINT_COUNT(0); -} - -TEST_F(InlayHints, WithHeaderContext) { - const char* header = R"cpp( -namespace _1 { - // there are 2 hint in header, namespace end and type hint of `x` - - struct _2345678 {}; - constexpr _2345678 f() { return {}; } - constexpr auto x = f(); -} - -)cpp"; - - const char* source = R"cpp( -#include "header.h" - -namespace _2 { - // only one hint here. -} - -)cpp"; - - Tester tx; - tx.add_file(path::join(".", "header.h"), header); - tx.add_main("main.cpp", source); - tx.compile(); - - auto& info = tx.unit; - EXPECT_TRUE(info.has_value()); - - /// auto maps = feature::inlayHints(*info); - - // 2 fileID - /// EXPECT_EQ(maps.size(), 2); - - // clang::FileID mainID = info->srcMgr().getMainFileID(); - // config::InlayHintOption option{ - // .blockEnd = true, - // }; - // for(auto& [fid, result]: maps) { - // if(fid == mainID) { - // EXPECT_EQ(result.size(), 1); - // } else { - // /// Drop `structSizeAndAlign` of `struct _2345678`. - // config::InlayHintOption fixOption{ - // .blockEnd = true, - // .structSizeAndAlign = false, - // }; - // - // SourceConverter SC; - // auto lspRes = toLspType(result, "", fixOption, header, SC); - // EXPECT_EQ(lspRes.size(), 2); - // EXPECT_TRUE(lspRes[0].lables[0].value.contains("namespace _1")); - // EXPECT_EQ(joinLabels(lspRes[1]), ": _1::_2345678"); - // } - // } -} - -TEST_F(InlayHints, TypeLinkSimple) { - /// config::InlayHintOption option = LikeClangd; - /// option.typeLink = true; - /// option.blockEnd = false; - - run(R"cpp( -struct A { int x; }; - - -auto lambda$(1) = []()$(2){ - return A(); - }; - -auto var$(3) = lambda(); -)cpp", - {}); - - EXPECT_HINT_COUNT(3); - - // EXPECT_ALL_KEY_IS_TEXT(); - EXPECT_AT("1", ": (lambda)"); - EXPECT_AT("2", "-> A"); - EXPECT_AT("3", ": A"); -} - -TEST_F(InlayHints, TypeLinkIgnoredCase) { - /// config::InlayHintOption option = LikeClangd; - /// option.typeLink = true; - /// option.blockEnd = false; - - run(R"cpp( - - -auto var1$(1) = 1; - -#include - -auto var2$(2) = std::vector{}; - -)cpp", - {}); - - EXPECT_HINT_COUNT(2); - - // EXPECT_AT("1", [this](const InlayHint& hint) { - // EXPECT_EQ(2, hint.labels.size()); - // - // bool textOnly = !hint.labels[1].location.has_value(); - // EXPECT_TRUE(textOnly); - // - // return textOnly && joinLabels(hint) == ": int"; - //}); - // - // EXPECT_AT("2", [this](const InlayHint& hint) { - // EXPECT_EQ(2, hint.labels.size()); - // - // EXPECT_EQ(hint.labels[0].value, ": "); - // EXPECT_EQ(hint.labels[1].value, "std::vector"); - // - // bool textOnly = !hint.labels[1].location.has_value(); - // EXPECT_TRUE(textOnly); - // - // return textOnly; - //}); -} - -TEST_F(InlayHints, TypeLinkWithScope) { - // config::InlayHintOption option = LikeClangd; - // option.typeLink = true; - // option.blockEnd = false; - - { - run(R"cpp( - namespace A { - namespace B { - struct X { int x; }; - } - } - - auto lambda$(1) = []()$(2){ - return A::B::X(); - }; - - auto var$(3) = lambda(); - )cpp", - {}); - - EXPECT_HINT_COUNT(3); - - // EXPECT_ALL_KEY_IS_TEXT(); - EXPECT_AT("1", ": (lambda)"); - EXPECT_AT("2", "-> A::B::X"); - EXPECT_AT("3", ": A::B::X"); - - // auto namespace_has_link = [this](const InlayHint& hint) { - // EXPECT_EQ(6, hint.labels.size()); - // EXPECT_TRUE(hint.labels[1].location.has_value()); - // EXPECT_TRUE(hint.labels[3].location.has_value()); - // return true; - // }; - // - // EXPECT_AT("2", namespace_has_link); - // EXPECT_AT("3", namespace_has_link); - } - - { - run(R"cpp( - struct A { - class B { - public: - struct X { int x; }; - }; - }; - - auto lambda$(1) = []()$(2){ - return A::B::X(); - }; - - auto var$(3) = lambda(); - )cpp", - {}); - - EXPECT_HINT_COUNT(3); - - // EXPECT_ALL_KEY_IS_TEXT(); - EXPECT_AT("1", ": (lambda)"); - EXPECT_AT("2", "-> A::B::X"); - EXPECT_AT("3", ": A::B::X"); - - // auto nested_struct_has_link = [this](const InlayHint& hint) { - // EXPECT_EQ(6, hint.labels.size()); - // EXPECT_TRUE(hint.labels[1].location.has_value()); - // EXPECT_TRUE(hint.labels[3].location.has_value()); - // return true; - // }; - // - // EXPECT_AT("2", nested_struct_has_link); - // EXPECT_AT("3", nested_struct_has_link); - } + "param"); } } // namespace diff --git a/tests/unit/Support/StructedText.cpp b/tests/unit/Support/StructedText.cpp index c33aca45..8647b7ff 100644 --- a/tests/unit/Support/StructedText.cpp +++ b/tests/unit/Support/StructedText.cpp @@ -1,4 +1,4 @@ -#include +#include "Support/Format.h" #include "Test/Test.h" #include "Support/StructedText.h" @@ -44,7 +44,7 @@ char *longestPalindrome_solv2(const char *s) { st.add_code_block(cb, "c"); auto& para = st.add_paragraph(); para.append_text("para1").append_newline_char(); - std::print("{}", st.as_markdown()); + clice::print("{}", st.as_markdown()); } TEST(StructedText, BulletList) { @@ -54,7 +54,7 @@ TEST(StructedText, BulletList) { st.add_bullet_list().add_item().add_paragraph().append_text("Item3", Paragraph::Kind::Bold); st.add_bullet_list().add_item().add_paragraph().append_text("Item4", Paragraph::Kind::Italic); st.add_bullet_list().add_item().add_paragraph().append_text("Item5", Paragraph::Kind::Strikethough); - std::print("{}", st.as_markdown()); + clice::print("{}", st.as_markdown()); } TEST(StructedText, FullText) { @@ -105,7 +105,7 @@ This is *Italic* **Bold** ~~Striketough~~, `InlineCode` warnings.add_item().add_paragraph().append_text("warnings3: blah blah..."); st.add_ruler(); st.add_code_block("int test_bar(int foo, char **bar, char **baz);\n", "cpp"); - std::print("{}", st.as_markdown()); + clice::print("{}", st.as_markdown()); } } // namespace clice::testing