forked from OSchip/llvm-project
513 lines
18 KiB
C++
513 lines
18 KiB
C++
//== IdenticalExprChecker.cpp - Identical expression checker----------------==//
|
|
//
|
|
// The LLVM Compiler Infrastructure
|
|
//
|
|
// This file is distributed under the University of Illinois Open Source
|
|
// License. See LICENSE.TXT for details.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
///
|
|
/// \file
|
|
/// \brief This defines IdenticalExprChecker, a check that warns about
|
|
/// unintended use of identical expressions.
|
|
///
|
|
/// It checks for use of identical expressions with comparison operators and
|
|
/// inside conditional expressions.
|
|
///
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "ClangSACheckers.h"
|
|
#include "clang/AST/RecursiveASTVisitor.h"
|
|
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
|
|
#include "clang/StaticAnalyzer/Core/Checker.h"
|
|
#include "clang/StaticAnalyzer/Core/CheckerManager.h"
|
|
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
|
|
|
|
using namespace clang;
|
|
using namespace ento;
|
|
|
|
static bool isIdenticalStmt(const ASTContext &Ctx, const Stmt *Stmt1,
|
|
const Stmt *Stmt2, bool IgnoreSideEffects = false);
|
|
//===----------------------------------------------------------------------===//
|
|
// FindIdenticalExprVisitor - Identify nodes using identical expressions.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
namespace {
|
|
class FindIdenticalExprVisitor
|
|
: public RecursiveASTVisitor<FindIdenticalExprVisitor> {
|
|
BugReporter &BR;
|
|
const CheckerBase *Checker;
|
|
AnalysisDeclContext *AC;
|
|
public:
|
|
explicit FindIdenticalExprVisitor(BugReporter &B,
|
|
const CheckerBase *Checker,
|
|
AnalysisDeclContext *A)
|
|
: BR(B), Checker(Checker), AC(A) {}
|
|
// FindIdenticalExprVisitor only visits nodes
|
|
// that are binary operators, if statements or
|
|
// conditional operators.
|
|
bool VisitBinaryOperator(const BinaryOperator *B);
|
|
bool VisitIfStmt(const IfStmt *I);
|
|
bool VisitConditionalOperator(const ConditionalOperator *C);
|
|
|
|
private:
|
|
void reportIdenticalExpr(const BinaryOperator *B, bool CheckBitwise,
|
|
ArrayRef<SourceRange> Sr);
|
|
void checkBitwiseOrLogicalOp(const BinaryOperator *B, bool CheckBitwise);
|
|
void checkComparisonOp(const BinaryOperator *B);
|
|
};
|
|
} // end anonymous namespace
|
|
|
|
void FindIdenticalExprVisitor::reportIdenticalExpr(const BinaryOperator *B,
|
|
bool CheckBitwise,
|
|
ArrayRef<SourceRange> Sr) {
|
|
StringRef Message;
|
|
if (CheckBitwise)
|
|
Message = "identical expressions on both sides of bitwise operator";
|
|
else
|
|
Message = "identical expressions on both sides of logical operator";
|
|
|
|
PathDiagnosticLocation ELoc =
|
|
PathDiagnosticLocation::createOperatorLoc(B, BR.getSourceManager());
|
|
BR.EmitBasicReport(AC->getDecl(), Checker,
|
|
"Use of identical expressions",
|
|
categories::LogicError,
|
|
Message, ELoc, Sr);
|
|
}
|
|
|
|
void FindIdenticalExprVisitor::checkBitwiseOrLogicalOp(const BinaryOperator *B,
|
|
bool CheckBitwise) {
|
|
SourceRange Sr[2];
|
|
|
|
const Expr *LHS = B->getLHS();
|
|
const Expr *RHS = B->getRHS();
|
|
|
|
// Split operators as long as we still have operators to split on. We will
|
|
// get called for every binary operator in an expression so there is no need
|
|
// to check every one against each other here, just the right most one with
|
|
// the others.
|
|
while (const BinaryOperator *B2 = dyn_cast<BinaryOperator>(LHS)) {
|
|
if (B->getOpcode() != B2->getOpcode())
|
|
break;
|
|
if (isIdenticalStmt(AC->getASTContext(), RHS, B2->getRHS())) {
|
|
Sr[0] = RHS->getSourceRange();
|
|
Sr[1] = B2->getRHS()->getSourceRange();
|
|
reportIdenticalExpr(B, CheckBitwise, Sr);
|
|
}
|
|
LHS = B2->getLHS();
|
|
}
|
|
|
|
if (isIdenticalStmt(AC->getASTContext(), RHS, LHS)) {
|
|
Sr[0] = RHS->getSourceRange();
|
|
Sr[1] = LHS->getSourceRange();
|
|
reportIdenticalExpr(B, CheckBitwise, Sr);
|
|
}
|
|
}
|
|
|
|
bool FindIdenticalExprVisitor::VisitIfStmt(const IfStmt *I) {
|
|
const Stmt *Stmt1 = I->getThen();
|
|
const Stmt *Stmt2 = I->getElse();
|
|
|
|
// Check for identical inner condition:
|
|
//
|
|
// if (x<10) {
|
|
// if (x<10) {
|
|
// ..
|
|
if (const CompoundStmt *CS = dyn_cast<CompoundStmt>(Stmt1)) {
|
|
if (!CS->body_empty()) {
|
|
const IfStmt *InnerIf = dyn_cast<IfStmt>(*CS->body_begin());
|
|
if (InnerIf && isIdenticalStmt(AC->getASTContext(), I->getCond(), InnerIf->getCond(), /*ignoreSideEffects=*/ false)) {
|
|
PathDiagnosticLocation ELoc(InnerIf->getCond(), BR.getSourceManager(), AC);
|
|
BR.EmitBasicReport(AC->getDecl(), Checker, "Identical conditions",
|
|
categories::LogicError,
|
|
"conditions of the inner and outer statements are identical",
|
|
ELoc);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for identical conditions:
|
|
//
|
|
// if (b) {
|
|
// foo1();
|
|
// } else if (b) {
|
|
// foo2();
|
|
// }
|
|
if (Stmt1 && Stmt2) {
|
|
const Expr *Cond1 = I->getCond();
|
|
const Stmt *Else = Stmt2;
|
|
while (const IfStmt *I2 = dyn_cast_or_null<IfStmt>(Else)) {
|
|
const Expr *Cond2 = I2->getCond();
|
|
if (isIdenticalStmt(AC->getASTContext(), Cond1, Cond2, false)) {
|
|
SourceRange Sr = Cond1->getSourceRange();
|
|
PathDiagnosticLocation ELoc(Cond2, BR.getSourceManager(), AC);
|
|
BR.EmitBasicReport(AC->getDecl(), Checker, "Identical conditions",
|
|
categories::LogicError,
|
|
"expression is identical to previous condition",
|
|
ELoc, Sr);
|
|
}
|
|
Else = I2->getElse();
|
|
}
|
|
}
|
|
|
|
if (!Stmt1 || !Stmt2)
|
|
return true;
|
|
|
|
// Special handling for code like:
|
|
//
|
|
// if (b) {
|
|
// i = 1;
|
|
// } else
|
|
// i = 1;
|
|
if (const CompoundStmt *CompStmt = dyn_cast<CompoundStmt>(Stmt1)) {
|
|
if (CompStmt->size() == 1)
|
|
Stmt1 = CompStmt->body_back();
|
|
}
|
|
if (const CompoundStmt *CompStmt = dyn_cast<CompoundStmt>(Stmt2)) {
|
|
if (CompStmt->size() == 1)
|
|
Stmt2 = CompStmt->body_back();
|
|
}
|
|
|
|
if (isIdenticalStmt(AC->getASTContext(), Stmt1, Stmt2, true)) {
|
|
PathDiagnosticLocation ELoc =
|
|
PathDiagnosticLocation::createBegin(I, BR.getSourceManager(), AC);
|
|
BR.EmitBasicReport(AC->getDecl(), Checker,
|
|
"Identical branches",
|
|
categories::LogicError,
|
|
"true and false branches are identical", ELoc);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FindIdenticalExprVisitor::VisitBinaryOperator(const BinaryOperator *B) {
|
|
BinaryOperator::Opcode Op = B->getOpcode();
|
|
|
|
if (BinaryOperator::isBitwiseOp(Op))
|
|
checkBitwiseOrLogicalOp(B, true);
|
|
|
|
if (BinaryOperator::isLogicalOp(Op))
|
|
checkBitwiseOrLogicalOp(B, false);
|
|
|
|
if (BinaryOperator::isComparisonOp(Op))
|
|
checkComparisonOp(B);
|
|
|
|
// We want to visit ALL nodes (subexpressions of binary comparison
|
|
// expressions too) that contains comparison operators.
|
|
// True is always returned to traverse ALL nodes.
|
|
return true;
|
|
}
|
|
|
|
void FindIdenticalExprVisitor::checkComparisonOp(const BinaryOperator *B) {
|
|
BinaryOperator::Opcode Op = B->getOpcode();
|
|
|
|
//
|
|
// Special case for floating-point representation.
|
|
//
|
|
// If expressions on both sides of comparison operator are of type float,
|
|
// then for some comparison operators no warning shall be
|
|
// reported even if the expressions are identical from a symbolic point of
|
|
// view. Comparison between expressions, declared variables and literals
|
|
// are treated differently.
|
|
//
|
|
// != and == between float literals that have the same value should NOT warn.
|
|
// < > between float literals that have the same value SHOULD warn.
|
|
//
|
|
// != and == between the same float declaration should NOT warn.
|
|
// < > between the same float declaration SHOULD warn.
|
|
//
|
|
// != and == between eq. expressions that evaluates into float
|
|
// should NOT warn.
|
|
// < > between eq. expressions that evaluates into float
|
|
// should NOT warn.
|
|
//
|
|
const Expr *LHS = B->getLHS()->IgnoreParenImpCasts();
|
|
const Expr *RHS = B->getRHS()->IgnoreParenImpCasts();
|
|
|
|
const DeclRefExpr *DeclRef1 = dyn_cast<DeclRefExpr>(LHS);
|
|
const DeclRefExpr *DeclRef2 = dyn_cast<DeclRefExpr>(RHS);
|
|
const FloatingLiteral *FloatLit1 = dyn_cast<FloatingLiteral>(LHS);
|
|
const FloatingLiteral *FloatLit2 = dyn_cast<FloatingLiteral>(RHS);
|
|
if ((DeclRef1) && (DeclRef2)) {
|
|
if ((DeclRef1->getType()->hasFloatingRepresentation()) &&
|
|
(DeclRef2->getType()->hasFloatingRepresentation())) {
|
|
if (DeclRef1->getDecl() == DeclRef2->getDecl()) {
|
|
if ((Op == BO_EQ) || (Op == BO_NE)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} else if ((FloatLit1) && (FloatLit2)) {
|
|
if (FloatLit1->getValue().bitwiseIsEqual(FloatLit2->getValue())) {
|
|
if ((Op == BO_EQ) || (Op == BO_NE)) {
|
|
return;
|
|
}
|
|
}
|
|
} else if (LHS->getType()->hasFloatingRepresentation()) {
|
|
// If any side of comparison operator still has floating-point
|
|
// representation, then it's an expression. Don't warn.
|
|
// Here only LHS is checked since RHS will be implicit casted to float.
|
|
return;
|
|
} else {
|
|
// No special case with floating-point representation, report as usual.
|
|
}
|
|
|
|
if (isIdenticalStmt(AC->getASTContext(), B->getLHS(), B->getRHS())) {
|
|
PathDiagnosticLocation ELoc =
|
|
PathDiagnosticLocation::createOperatorLoc(B, BR.getSourceManager());
|
|
StringRef Message;
|
|
if (((Op == BO_EQ) || (Op == BO_LE) || (Op == BO_GE)))
|
|
Message = "comparison of identical expressions always evaluates to true";
|
|
else
|
|
Message = "comparison of identical expressions always evaluates to false";
|
|
BR.EmitBasicReport(AC->getDecl(), Checker,
|
|
"Compare of identical expressions",
|
|
categories::LogicError, Message, ELoc);
|
|
}
|
|
}
|
|
|
|
bool FindIdenticalExprVisitor::VisitConditionalOperator(
|
|
const ConditionalOperator *C) {
|
|
|
|
// Check if expressions in conditional expression are identical
|
|
// from a symbolic point of view.
|
|
|
|
if (isIdenticalStmt(AC->getASTContext(), C->getTrueExpr(),
|
|
C->getFalseExpr(), true)) {
|
|
PathDiagnosticLocation ELoc =
|
|
PathDiagnosticLocation::createConditionalColonLoc(
|
|
C, BR.getSourceManager());
|
|
|
|
SourceRange Sr[2];
|
|
Sr[0] = C->getTrueExpr()->getSourceRange();
|
|
Sr[1] = C->getFalseExpr()->getSourceRange();
|
|
BR.EmitBasicReport(
|
|
AC->getDecl(), Checker,
|
|
"Identical expressions in conditional expression",
|
|
categories::LogicError,
|
|
"identical expressions on both sides of ':' in conditional expression",
|
|
ELoc, Sr);
|
|
}
|
|
// We want to visit ALL nodes (expressions in conditional
|
|
// expressions too) that contains conditional operators,
|
|
// thus always return true to traverse ALL nodes.
|
|
return true;
|
|
}
|
|
|
|
/// \brief Determines whether two statement trees are identical regarding
|
|
/// operators and symbols.
|
|
///
|
|
/// Exceptions: expressions containing macros or functions with possible side
|
|
/// effects are never considered identical.
|
|
/// Limitations: (t + u) and (u + t) are not considered identical.
|
|
/// t*(u + t) and t*u + t*t are not considered identical.
|
|
///
|
|
static bool isIdenticalStmt(const ASTContext &Ctx, const Stmt *Stmt1,
|
|
const Stmt *Stmt2, bool IgnoreSideEffects) {
|
|
|
|
if (!Stmt1 || !Stmt2) {
|
|
return !Stmt1 && !Stmt2;
|
|
}
|
|
|
|
// If Stmt1 & Stmt2 are of different class then they are not
|
|
// identical statements.
|
|
if (Stmt1->getStmtClass() != Stmt2->getStmtClass())
|
|
return false;
|
|
|
|
const Expr *Expr1 = dyn_cast<Expr>(Stmt1);
|
|
const Expr *Expr2 = dyn_cast<Expr>(Stmt2);
|
|
|
|
if (Expr1 && Expr2) {
|
|
// If Stmt1 has side effects then don't warn even if expressions
|
|
// are identical.
|
|
if (!IgnoreSideEffects && Expr1->HasSideEffects(Ctx))
|
|
return false;
|
|
// If either expression comes from a macro then don't warn even if
|
|
// the expressions are identical.
|
|
if ((Expr1->getExprLoc().isMacroID()) || (Expr2->getExprLoc().isMacroID()))
|
|
return false;
|
|
|
|
// If all children of two expressions are identical, return true.
|
|
Expr::const_child_iterator I1 = Expr1->child_begin();
|
|
Expr::const_child_iterator I2 = Expr2->child_begin();
|
|
while (I1 != Expr1->child_end() && I2 != Expr2->child_end()) {
|
|
if (!*I1 || !*I2 || !isIdenticalStmt(Ctx, *I1, *I2, IgnoreSideEffects))
|
|
return false;
|
|
++I1;
|
|
++I2;
|
|
}
|
|
// If there are different number of children in the statements, return
|
|
// false.
|
|
if (I1 != Expr1->child_end())
|
|
return false;
|
|
if (I2 != Expr2->child_end())
|
|
return false;
|
|
}
|
|
|
|
switch (Stmt1->getStmtClass()) {
|
|
default:
|
|
return false;
|
|
case Stmt::CallExprClass:
|
|
case Stmt::ArraySubscriptExprClass:
|
|
case Stmt::OMPArraySectionExprClass:
|
|
case Stmt::ImplicitCastExprClass:
|
|
case Stmt::ParenExprClass:
|
|
case Stmt::BreakStmtClass:
|
|
case Stmt::ContinueStmtClass:
|
|
case Stmt::NullStmtClass:
|
|
return true;
|
|
case Stmt::CStyleCastExprClass: {
|
|
const CStyleCastExpr* CastExpr1 = cast<CStyleCastExpr>(Stmt1);
|
|
const CStyleCastExpr* CastExpr2 = cast<CStyleCastExpr>(Stmt2);
|
|
|
|
return CastExpr1->getTypeAsWritten() == CastExpr2->getTypeAsWritten();
|
|
}
|
|
case Stmt::ReturnStmtClass: {
|
|
const ReturnStmt *ReturnStmt1 = cast<ReturnStmt>(Stmt1);
|
|
const ReturnStmt *ReturnStmt2 = cast<ReturnStmt>(Stmt2);
|
|
|
|
return isIdenticalStmt(Ctx, ReturnStmt1->getRetValue(),
|
|
ReturnStmt2->getRetValue(), IgnoreSideEffects);
|
|
}
|
|
case Stmt::ForStmtClass: {
|
|
const ForStmt *ForStmt1 = cast<ForStmt>(Stmt1);
|
|
const ForStmt *ForStmt2 = cast<ForStmt>(Stmt2);
|
|
|
|
if (!isIdenticalStmt(Ctx, ForStmt1->getInit(), ForStmt2->getInit(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, ForStmt1->getCond(), ForStmt2->getCond(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, ForStmt1->getInc(), ForStmt2->getInc(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, ForStmt1->getBody(), ForStmt2->getBody(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
return true;
|
|
}
|
|
case Stmt::DoStmtClass: {
|
|
const DoStmt *DStmt1 = cast<DoStmt>(Stmt1);
|
|
const DoStmt *DStmt2 = cast<DoStmt>(Stmt2);
|
|
|
|
if (!isIdenticalStmt(Ctx, DStmt1->getCond(), DStmt2->getCond(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, DStmt1->getBody(), DStmt2->getBody(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
return true;
|
|
}
|
|
case Stmt::WhileStmtClass: {
|
|
const WhileStmt *WStmt1 = cast<WhileStmt>(Stmt1);
|
|
const WhileStmt *WStmt2 = cast<WhileStmt>(Stmt2);
|
|
|
|
if (!isIdenticalStmt(Ctx, WStmt1->getCond(), WStmt2->getCond(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, WStmt1->getBody(), WStmt2->getBody(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
return true;
|
|
}
|
|
case Stmt::IfStmtClass: {
|
|
const IfStmt *IStmt1 = cast<IfStmt>(Stmt1);
|
|
const IfStmt *IStmt2 = cast<IfStmt>(Stmt2);
|
|
|
|
if (!isIdenticalStmt(Ctx, IStmt1->getCond(), IStmt2->getCond(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, IStmt1->getThen(), IStmt2->getThen(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
if (!isIdenticalStmt(Ctx, IStmt1->getElse(), IStmt2->getElse(),
|
|
IgnoreSideEffects))
|
|
return false;
|
|
return true;
|
|
}
|
|
case Stmt::CompoundStmtClass: {
|
|
const CompoundStmt *CompStmt1 = cast<CompoundStmt>(Stmt1);
|
|
const CompoundStmt *CompStmt2 = cast<CompoundStmt>(Stmt2);
|
|
|
|
if (CompStmt1->size() != CompStmt2->size())
|
|
return false;
|
|
|
|
CompoundStmt::const_body_iterator I1 = CompStmt1->body_begin();
|
|
CompoundStmt::const_body_iterator I2 = CompStmt2->body_begin();
|
|
while (I1 != CompStmt1->body_end() && I2 != CompStmt2->body_end()) {
|
|
if (!isIdenticalStmt(Ctx, *I1, *I2, IgnoreSideEffects))
|
|
return false;
|
|
++I1;
|
|
++I2;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
case Stmt::CompoundAssignOperatorClass:
|
|
case Stmt::BinaryOperatorClass: {
|
|
const BinaryOperator *BinOp1 = cast<BinaryOperator>(Stmt1);
|
|
const BinaryOperator *BinOp2 = cast<BinaryOperator>(Stmt2);
|
|
return BinOp1->getOpcode() == BinOp2->getOpcode();
|
|
}
|
|
case Stmt::CharacterLiteralClass: {
|
|
const CharacterLiteral *CharLit1 = cast<CharacterLiteral>(Stmt1);
|
|
const CharacterLiteral *CharLit2 = cast<CharacterLiteral>(Stmt2);
|
|
return CharLit1->getValue() == CharLit2->getValue();
|
|
}
|
|
case Stmt::DeclRefExprClass: {
|
|
const DeclRefExpr *DeclRef1 = cast<DeclRefExpr>(Stmt1);
|
|
const DeclRefExpr *DeclRef2 = cast<DeclRefExpr>(Stmt2);
|
|
return DeclRef1->getDecl() == DeclRef2->getDecl();
|
|
}
|
|
case Stmt::IntegerLiteralClass: {
|
|
const IntegerLiteral *IntLit1 = cast<IntegerLiteral>(Stmt1);
|
|
const IntegerLiteral *IntLit2 = cast<IntegerLiteral>(Stmt2);
|
|
|
|
llvm::APInt I1 = IntLit1->getValue();
|
|
llvm::APInt I2 = IntLit2->getValue();
|
|
if (I1.getBitWidth() != I2.getBitWidth())
|
|
return false;
|
|
return I1 == I2;
|
|
}
|
|
case Stmt::FloatingLiteralClass: {
|
|
const FloatingLiteral *FloatLit1 = cast<FloatingLiteral>(Stmt1);
|
|
const FloatingLiteral *FloatLit2 = cast<FloatingLiteral>(Stmt2);
|
|
return FloatLit1->getValue().bitwiseIsEqual(FloatLit2->getValue());
|
|
}
|
|
case Stmt::StringLiteralClass: {
|
|
const StringLiteral *StringLit1 = cast<StringLiteral>(Stmt1);
|
|
const StringLiteral *StringLit2 = cast<StringLiteral>(Stmt2);
|
|
return StringLit1->getBytes() == StringLit2->getBytes();
|
|
}
|
|
case Stmt::MemberExprClass: {
|
|
const MemberExpr *MemberStmt1 = cast<MemberExpr>(Stmt1);
|
|
const MemberExpr *MemberStmt2 = cast<MemberExpr>(Stmt2);
|
|
return MemberStmt1->getMemberDecl() == MemberStmt2->getMemberDecl();
|
|
}
|
|
case Stmt::UnaryOperatorClass: {
|
|
const UnaryOperator *UnaryOp1 = cast<UnaryOperator>(Stmt1);
|
|
const UnaryOperator *UnaryOp2 = cast<UnaryOperator>(Stmt2);
|
|
return UnaryOp1->getOpcode() == UnaryOp2->getOpcode();
|
|
}
|
|
}
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// FindIdenticalExprChecker
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
namespace {
|
|
class FindIdenticalExprChecker : public Checker<check::ASTCodeBody> {
|
|
public:
|
|
void checkASTCodeBody(const Decl *D, AnalysisManager &Mgr,
|
|
BugReporter &BR) const {
|
|
FindIdenticalExprVisitor Visitor(BR, this, Mgr.getAnalysisDeclContext(D));
|
|
Visitor.TraverseDecl(const_cast<Decl *>(D));
|
|
}
|
|
};
|
|
} // end anonymous namespace
|
|
|
|
void ento::registerIdenticalExprChecker(CheckerManager &Mgr) {
|
|
Mgr.registerChecker<FindIdenticalExprChecker>();
|
|
}
|