Index: clang-tidy/bugprone/BugproneTidyModule.cpp =================================================================== --- clang-tidy/bugprone/BugproneTidyModule.cpp +++ clang-tidy/bugprone/BugproneTidyModule.cpp @@ -20,6 +20,7 @@ #include "ForwardDeclarationNamespaceCheck.h" #include "ForwardingReferenceOverloadCheck.h" #include "InaccurateEraseCheck.h" +#include "IncompleteComparisonOperatorCheck.h" #include "IncorrectRoundingsCheck.h" #include "IntegerDivisionCheck.h" #include "LambdaFunctionNameCheck.h" @@ -78,6 +79,8 @@ "bugprone-forwarding-reference-overload"); CheckFactories.registerCheck( "bugprone-inaccurate-erase"); + CheckFactories.registerCheck( + "bugprone-incomplete-comparison-operator"); CheckFactories.registerCheck( "bugprone-incorrect-roundings"); CheckFactories.registerCheck( Index: clang-tidy/bugprone/CMakeLists.txt =================================================================== --- clang-tidy/bugprone/CMakeLists.txt +++ clang-tidy/bugprone/CMakeLists.txt @@ -12,6 +12,7 @@ ForwardDeclarationNamespaceCheck.cpp ForwardingReferenceOverloadCheck.cpp InaccurateEraseCheck.cpp + IncompleteComparisonOperatorCheck.cpp IncorrectRoundingsCheck.cpp IntegerDivisionCheck.cpp LambdaFunctionNameCheck.cpp Index: clang-tidy/bugprone/IncompleteComparisonOperatorCheck.h =================================================================== --- /dev/null +++ clang-tidy/bugprone/IncompleteComparisonOperatorCheck.h @@ -0,0 +1,38 @@ +//===--- IncompleteComparisonOperatorCheck.h - clang-tidy -------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_INCOMPLETECOMPARISONOPERATORCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_INCOMPLETECOMPARISONOPERATORCHECK_H + +#include "../ClangTidy.h" +#include + +namespace clang { +namespace tidy { +namespace bugprone { + +/// Detects comparison operators that don't access all parameter type fields. +/// +/// For the user-facing documentation see: +/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone-incomplete-comparison-operator.html +class IncompleteComparisonOperatorCheck : public ClangTidyCheck { +public: + IncompleteComparisonOperatorCheck(StringRef Name, ClangTidyContext *Context); + void storeOptions(ClangTidyOptions::OptionMap &Opts) override; + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; + +private: + std::string CheckedFunctions; +}; + +} // namespace bugprone +} // namespace tidy +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_INCOMPLETECOMPARISONOPERATORCHECK_H Index: clang-tidy/bugprone/IncompleteComparisonOperatorCheck.cpp =================================================================== --- /dev/null +++ clang-tidy/bugprone/IncompleteComparisonOperatorCheck.cpp @@ -0,0 +1,249 @@ +//===--- IncompleteComparisonOperatorCheck.cpp - clang-tidy ---------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "IncompleteComparisonOperatorCheck.h" +#include "../utils/OptionsUtils.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/SmallSet.h" +#include + +using namespace clang::ast_matchers; + +namespace clang { +namespace tidy { +namespace bugprone { + +namespace { + +// Matches dependent types +AST_MATCHER(Type, isDependent) { return Node.isDependentType(); } + +// This class groups info about a comparison operator operand together +class OperandInfo { +public: + // Construct from function param declaration + OperandInfo(const ParmVarDecl *P) + : Param(P), + Record(Param->getType().getNonReferenceType()->getAsRecordDecl()) { + initFields(); + } + + // Construct from struct/class declaration. + // Can be used if there's no function param declaration for this operand (as + // is the case with LHS operand on member comparison operators). + // paramEquals() will always return false when this ctor is used. + OperandInfo(const RecordDecl *R) : Param(nullptr), Record(R) { initFields(); } + + bool paramEquals(const ParmVarDecl *P) const { return Param && P == Param; } + + bool recordEquals(const RecordDecl *R) const { return Record && R == Record; } + + const Type *getType() const { + return Record ? Record->getTypeForDecl() : nullptr; + } + + bool areAllFieldsAccessed() const { return Fields.empty(); } + + int fieldCount() const { return Fields.size(); } + + void fieldAccessed(const FieldDecl *F) { Fields.erase(F); } + + void setAllFieldsAccessed() { Fields.clear(); } + + void diagFields(ClangTidyCheck &Check, SourceLocation Loc) const { + for (auto It : Fields) + Check.diag(Loc, + (Param ? Param->getNameAsString() + "." : "") + + It->getNameAsString() + " not accessed", + DiagnosticIDs::Note); + } + +private: + void initFields() { + if (Record) + for (const FieldDecl *F : Record->fields()) + Fields.insert(F); + } + + const ParmVarDecl *Param; + const RecordDecl *Record; + llvm::SmallSet Fields; +}; + +// Remove pointers, refs, typedefs, qualifiers from T +const Type *getCanonicalNonPointerType(QualType T) { + QualType PointeeT = + T->isPointerType() ? T->getPointeeType() : T.getNonReferenceType(); + return PointeeT.getCanonicalType().getTypePtr(); +} + +// FieldAccessVisitor finds field accesses from the AST +class FieldAccessVisitor : public RecursiveASTVisitor { +public: + FieldAccessVisitor(OperandInfo &L, OperandInfo &R) : Lhs(L), Rhs(R) {} + + bool VisitMemberExpr(MemberExpr *M) { + auto Field = dyn_cast_or_null(M->getMemberDecl()); + + // Not a field access? -> move on + if (!Field) + return true; + + auto BaseDeclRef = dyn_cast_or_null(M->getBase()); + auto ParamDecl = BaseDeclRef + ? dyn_cast_or_null(BaseDeclRef->getDecl()) + : nullptr; + + if (Lhs.paramEquals(ParamDecl)) + Lhs.fieldAccessed(Field); + else if (Rhs.paramEquals(ParamDecl)) + Rhs.fieldAccessed(Field); + else { + // We don't know which object this field is accessed on -> consider both + // LHS and RHS covered + Lhs.fieldAccessed(Field); + Rhs.fieldAccessed(Field); + } + + // Continue traversal if not all fields have been accessed yet + return !Lhs.areAllFieldsAccessed() || !Rhs.areAllFieldsAccessed(); + } + + bool VisitCallExpr(CallExpr *C) { + auto Fun = dyn_cast_or_null(C->getCalleeDecl()); + + if (!Fun) { + // A function is called but we can't get a declaration for the function + // (this can happen inside templates) -> stop analysis and issue no + // warnings + Lhs.setAllFieldsAccessed(); + Rhs.setAllFieldsAccessed(); + return false; + } + + if (!isInteresting(Fun)) + return true; + + const FunctionDecl *FunDef = nullptr; + const bool HasBody = Fun->hasBody(FunDef); + + static constexpr int CALL_DEPTH_LIMIT = 10; + if (!HasBody || CallDepth >= CALL_DEPTH_LIMIT) { + // This is a function that we should analyze, but we can't traverse into + // it -> stop analysis and issue no warnings + Lhs.setAllFieldsAccessed(); + Rhs.setAllFieldsAccessed(); + return false; + } + + ++CallDepth; + const bool RetVal = TraverseDecl(const_cast(FunDef)); + --CallDepth; + return RetVal; + } + +private: + // A FunctionDecl is considered "interesting" for the purposes of this + // visitor, if + // - the function is a member of LHS or RHS type, OR + // - the function takes LHS or RHS type (or pointer to them) as parameter + bool isInteresting(const FunctionDecl *Fun) const { + auto Method = dyn_cast(Fun); + const bool OperandTypeMethod = + Method && (Lhs.recordEquals(Method->getParent()) || + Rhs.recordEquals(Method->getParent())); + + const bool HasOperandTypeAsParam = std::any_of( + Fun->param_begin(), Fun->param_end(), [this](const ParmVarDecl *Param) { + const Type *T = getCanonicalNonPointerType(Param->getType()); + return T == Lhs.getType() || T == Rhs.getType(); + }); + + return OperandTypeMethod || HasOperandTypeAsParam; + } + + OperandInfo &Lhs; + OperandInfo &Rhs; + int CallDepth = 0; +}; + +} // namespace + +IncompleteComparisonOperatorCheck::IncompleteComparisonOperatorCheck( + StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + CheckedFunctions(Options.get( + "CheckedFunctions", + "operator==;operator!=;operator<;operator>;operator<=;operator>=")) {} + +void IncompleteComparisonOperatorCheck::storeOptions( + ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "CheckedFunctions", CheckedFunctions); +} + +void IncompleteComparisonOperatorCheck::registerMatchers(MatchFinder *Finder) { + if (!getLangOpts().CPlusPlus) + return; + + const auto Ops = utils::options::parseStringList(CheckedFunctions); + Finder->addMatcher( + functionDecl( + hasAnyName(std::vector(Ops.begin(), Ops.end())), + isDefinition(), + unless(anyOf(isDeleted(), hasAnyParameter(hasType(isDependent()))))) + .bind("operator"), + this); +} + +void IncompleteComparisonOperatorCheck::check( + const MatchFinder::MatchResult &Result) { + const auto *Operator = Result.Nodes.getNodeAs("operator"); + + llvm::Optional Lhs; + llvm::Optional Rhs; + + switch (Operator->getNumParams()) { + case 1: { + // Member operator + const auto *Method = dyn_cast(Operator); + if (!Method) + return; + Lhs.emplace(Method->getParent()); + Rhs.emplace(Operator->getParamDecl(0)); + break; + } + case 2: { + // Non-member operator + Lhs.emplace(Operator->getParamDecl(0)); + Rhs.emplace(Operator->getParamDecl(1)); + break; + } + default: + return; + } + + if (Lhs->areAllFieldsAccessed() && Rhs->areAllFieldsAccessed()) + return; // Bail out early if no fields are found for the operand types + + FieldAccessVisitor Visitor{*Lhs, *Rhs}; + Visitor.TraverseDecl(const_cast(Operator)); + + if (!Lhs->areAllFieldsAccessed() || !Rhs->areAllFieldsAccessed()) { + diag(Operator->getBeginLoc(), "incomplete comparison operator"); + Lhs->diagFields(*this, Operator->getBeginLoc()); + Rhs->diagFields(*this, Operator->getBeginLoc()); + } +} + +} // namespace bugprone +} // namespace tidy +} // namespace clang Index: docs/ReleaseNotes.rst =================================================================== --- docs/ReleaseNotes.rst +++ docs/ReleaseNotes.rst @@ -91,6 +91,12 @@ Finds and fixes ``absl::Time`` subtraction expressions to do subtraction in the Time domain instead of the numeric domain. +- New :doc:`bugprone-incomplete-comparison-operator + ` check. + + Warns on comparison operator implementations that don't access all the + non-static data members of the compared types. + - New :doc:`google-readability-avoid-underscore-in-googletest-name ` check. Index: docs/clang-tidy/checks/bugprone-incomplete-comparison-operator.rst =================================================================== --- /dev/null +++ docs/clang-tidy/checks/bugprone-incomplete-comparison-operator.rst @@ -0,0 +1,38 @@ +.. title:: clang-tidy - bugprone-incomplete-comparison-operator + +bugprone-incomplete-comparison-operator +======================================= + +Warns on comparison operator implementations that don't access all the +non-static data members of the compared types. + +Examples: + +.. code-block:: c++ + + struct S { + int Member1; + int Member2; + }; + + // Warning - Lhs.Member2 and Rhs.Member2 not accessed + bool operator==(const S &Lhs, const S &Rhs) { + return Lhs.Member1 == Rhs.Member1; + } + + // No warning - suppressed with void cast + bool operator<(const S &Lhs, const S &Rhs) { + (void)Lhs.Member2; + (void)Rhs.Member2; + return Lhs.Member1 < Rhs.Member1; + } + +The checked operators can be configured. + +Options +------- + +.. option:: CheckedFunctions + + Semicolon-separated list of functions to check. Defaults to + ``operator==;operator!=;operator<;operator>;operator<=;operator>=``. Index: docs/clang-tidy/checks/list.rst =================================================================== --- docs/clang-tidy/checks/list.rst +++ docs/clang-tidy/checks/list.rst @@ -44,6 +44,7 @@ bugprone-forward-declaration-namespace bugprone-forwarding-reference-overload bugprone-inaccurate-erase + bugprone-incomplete-comparison-operator bugprone-incorrect-roundings bugprone-integer-division bugprone-lambda-function-name Index: test/clang-tidy/bugprone-incomplete-comparison-operator-custom.cpp =================================================================== --- /dev/null +++ test/clang-tidy/bugprone-incomplete-comparison-operator-custom.cpp @@ -0,0 +1,26 @@ +// RUN: %check_clang_tidy %s bugprone-incomplete-comparison-operator %t \ +// RUN: -config='{CheckOptions: \ +// RUN: [{key: bugprone-incomplete-comparison-operator.CheckedFunctions, \ +// RUN: value: "operator==;operator!="}]}' \ +// RUN: -- + +struct S { + int Member1; + int Member2; +}; + +bool operator==(const S &Lhs, const S &Rhs) { + return Lhs.Member1 == Rhs.Member1 && Lhs.Member2 == Rhs.Member2; +} + +bool operator!=(const S &Lhs, const S &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member2 not accessed + return Lhs.Member1 != Rhs.Member1; +} + +// operator not configured to be checked -> no warning +bool operator<(const S &Lhs, const S &Rhs) { + return Lhs.Member1 < Rhs.Member1; +} Index: test/clang-tidy/bugprone-incomplete-comparison-operator.cpp =================================================================== --- /dev/null +++ test/clang-tidy/bugprone-incomplete-comparison-operator.cpp @@ -0,0 +1,280 @@ +// RUN: %check_clang_tidy %s bugprone-incomplete-comparison-operator %t + +struct Good { + int Member1; + int Member2; +}; + +bool operator==(const Good &Lhs, const Good &Rhs) { + return Lhs.Member1 == Rhs.Member1 && Lhs.Member2 == Rhs.Member2; +} + +bool operator!=(const Good &Lhs, const Good &Rhs) { + return !(Lhs == Rhs); +} + +bool operator<(const Good &Lhs, const Good &Rhs) { + if (Lhs.Member1 != Rhs.Member1) + return Lhs.Member1 < Rhs.Member1; + return Lhs.Member2 < Rhs.Member2; +} + +bool operator>(const Good &Lhs, const Good &Rhs) { + return Rhs < Lhs; +} + +bool operator<=(const Good &Lhs, const Good &Rhs) { + return !(Rhs < Lhs); +} + +bool operator>=(const Good &Lhs, const Good &Rhs) { + return !(Lhs < Rhs); +} + +struct Bad { + int Member1; + int Member2; +}; + +bool operator==(const Bad &Lhs, const Bad &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member2 not accessed + return Lhs.Member1 == Rhs.Member1; +} + +bool operator!=(const Bad &Lhs, const Bad &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member2 not accessed + return !(Lhs == Rhs); +} + +bool operator<(const Bad &Lhs, const Bad &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member1 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member1 not accessed + return Lhs.Member2 < Rhs.Member2; +} + +bool operator>(const Bad &Lhs, const Bad &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member1 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member1 not accessed + return Rhs < Lhs; +} + +bool operator<=(const Bad &Lhs, const Bad &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member1 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member1 not accessed + return !(Rhs < Lhs); +} + +bool operator>=(const Bad &Lhs, const Bad &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member1 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member1 not accessed + return !(Lhs < Rhs); +} + +struct Ugly { + Good GoodParts; + Bad BadParts; +}; + +// Missing field access for only one of the params -> warning +bool operator==(const Ugly &Lhs, const Ugly &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Rhs.BadParts not accessed + return Lhs.GoodParts == Rhs.GoodParts && Lhs.BadParts == Lhs.BadParts; +} + +// Args are passed to a function in another TU -> no warning +bool testForInequality(const Ugly &, const Ugly &); +bool operator!=(const Ugly &Lhs, const Ugly &Rhs) { + return testForInequality(Lhs, Rhs); +} + +// Fields are passed to a function in another TU, but some fields are not accessed -> warning +bool compareGoodness(const Good &, const Good &); +bool operator<(const Ugly &Lhs, const Ugly &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.BadParts not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.BadParts not accessed + return compareGoodness(Lhs.GoodParts, Rhs.GoodParts); +} + +// Args are passed to recursive function -> no warning +bool strangeLoop(const Ugly &U) { return strangeLoop(U); } +bool operator>(const Ugly &Lhs, const Ugly &Rhs) { + return strangeLoop(Lhs); +} + +class Encapsulated { +public: + int get1() const { return Member1; } + int get2() const { return Member2; } + + // getters that are defined later in the TU + int query1() const; + int query2() const; + +private: + int Member1; + int Member2; +}; + +bool operator==(const Encapsulated &Lhs, const Encapsulated &Rhs) { + return Lhs.get1() == Rhs.get1() && Lhs.get2() == Rhs.get2(); +} + +bool operator!=(const Encapsulated &Lhs, const Encapsulated &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member2 not accessed + return Lhs.get1() != Rhs.get1(); +} + +bool operator<(const Encapsulated &Lhs, const Encapsulated &Rhs) { + if (Lhs.query1() != Rhs.query1()) + return Lhs.query1() < Rhs.query1(); + return Lhs.query2() < Rhs.query2(); +} + +bool operator>(const Encapsulated &Lhs, const Encapsulated &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member1 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member1 not accessed + return Lhs.query2() > Rhs.query2(); +} + +int Encapsulated::query1() const { + return Member1; +} + +int Encapsulated::query2() const { + return Member2; +} + +struct MemOps { + int Member1; + int Member2; + + bool operator==(const MemOps &Rhs) const { + return Member1 == Rhs.Member1 && Member2 == Rhs.Member2; + } + + bool operator!=(const MemOps &Rhs) const { + return !(*this == Rhs); + } + + bool operator<(const MemOps &Rhs) const { + // CHECK-NOTES: [[@LINE-1]]:3: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:3: note: Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:3: note: Rhs.Member2 not accessed + return Member1 < Rhs.Member1; + } + + bool operator>(const MemOps &Rhs) const { + // CHECK-NOTES: [[@LINE-1]]:3: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:3: note: Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:3: note: Rhs.Member2 not accessed + return Rhs < *this; + } +}; + +struct StaticMem { + int Member1; + static int Member2; +}; + +// static member not accessed -> no warning +bool operator==(const StaticMem &Lhs, const StaticMem &Rhs) { + return Lhs.Member1 == Rhs.Member1; +} + +// deleted operators -> no warning +struct DeletedOps { + int Member1; + bool operator==(const DeletedOps &) const = delete; +}; + +bool operator<(const DeletedOps &, const DeletedOps &) = delete; + +// comparisons between different types +struct TypeX { + int Member1; + int Member2; +}; + +struct TypeY { + int Member1; + int Member2; +}; + +bool operator==(const TypeX &X, const TypeY &Y) { + return X.Member1 == Y.Member1 && X.Member2 == Y.Member2; +} + +bool operator!=(const TypeX &X, const TypeY &Y) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: X.Member2 not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Y.Member2 not accessed + return X.Member1 != Y.Member1; +} + +template +struct TemplateType { + T Member; + + bool operator<(const TemplateType &Rhs) const { + return Member < Rhs.Member; + } + + bool operator>(const TemplateType &Rhs) const { + // CHECK-NOTES: [[@LINE-1]]:3: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:3: note: Member not accessed + // CHECK-NOTES: [[@LINE-3]]:3: note: Rhs.Member not accessed + return false; + } + + // template not instantiated -> no warning + bool operator<=(const TemplateType &Rhs) const { + return false; + } +}; + +template +bool operator==(const TemplateType &Lhs, const TemplateType &Rhs) { + return Lhs.Member == Rhs.Member; +} + +template +bool operator!=(const TemplateType &Lhs, const TemplateType &Rhs) { + // CHECK-NOTES: [[@LINE-1]]:1: warning: incomplete comparison operator + // CHECK-NOTES: [[@LINE-2]]:1: note: Lhs.Member not accessed + // CHECK-NOTES: [[@LINE-3]]:1: note: Rhs.Member not accessed + return false; +} + +// template not instantiated -> no warning +template +bool operator>=(const TemplateType &Lhs, const TemplateType &Rhs) { + return false; +} + +bool useTemplates(const TemplateType &Lhs, const TemplateType &Rhs) { + return Lhs == Rhs || Lhs != Rhs || Lhs < Rhs || Lhs > Rhs; +} + +struct Suppress { + int Member1; + int Member2; +}; + +bool operator==(const Suppress &Lhs, const Suppress &Rhs) { + (void)Lhs.Member2; + (void)Rhs.Member2; + return Lhs.Member1 == Rhs.Member1; +}