| //===--- TimeSubtractionCheck.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 "TimeSubtractionCheck.h" |
| #include "DurationRewriter.h" |
| #include "clang/AST/ASTContext.h" |
| #include "clang/ASTMatchers/ASTMatchFinder.h" |
| #include "clang/Lex/Lexer.h" |
| #include "clang/Tooling/FixIt.h" |
| |
| using namespace clang::ast_matchers; |
| |
| namespace clang { |
| namespace tidy { |
| namespace abseil { |
| |
| // Returns `true` if `Range` is inside a macro definition. |
| static bool insideMacroDefinition(const MatchFinder::MatchResult &Result, |
| SourceRange Range) { |
| return !clang::Lexer::makeFileCharRange( |
| clang::CharSourceRange::getCharRange(Range), |
| *Result.SourceManager, Result.Context->getLangOpts()) |
| .isValid(); |
| } |
| |
| static bool isConstructorAssignment(const MatchFinder::MatchResult &Result, |
| const Expr *Node) { |
| // For C++14 and earlier there are elidable constructors that must be matched |
| // in hasParent. The elidable constructors do not exist in C++17 and later and |
| // therefore an additional check that does not match against the elidable |
| // constructors are needed for this case. |
| return selectFirst<const Expr>( |
| "e", |
| match(expr(anyOf( |
| callExpr(hasParent(materializeTemporaryExpr(hasParent( |
| cxxConstructExpr(hasParent(exprWithCleanups( |
| hasParent(varDecl())))))))) |
| .bind("e"), |
| callExpr(hasParent(varDecl())).bind("e"))), |
| *Node, *Result.Context)) != nullptr; |
| } |
| |
| static bool isArgument(const MatchFinder::MatchResult &Result, |
| const Expr *Node) { |
| // For the same reason as in isConstructorAssignment two AST shapes need to be |
| // matched here. |
| return selectFirst<const Expr>( |
| "e", |
| match( |
| expr(anyOf( |
| expr(hasParent(materializeTemporaryExpr( |
| hasParent(cxxConstructExpr( |
| hasParent(callExpr()), |
| unless(hasParent(cxxOperatorCallExpr()))))))) |
| .bind("e"), |
| expr(hasParent(callExpr()), |
| unless(hasParent(cxxOperatorCallExpr()))) |
| .bind("e"))), |
| *Node, *Result.Context)) != nullptr; |
| } |
| |
| static bool isReturn(const MatchFinder::MatchResult &Result, const Expr *Node) { |
| // For the same reason as in isConstructorAssignment two AST shapes need to be |
| // matched here. |
| return selectFirst<const Expr>( |
| "e", |
| match(expr(anyOf( |
| expr(hasParent(materializeTemporaryExpr(hasParent( |
| cxxConstructExpr(hasParent(exprWithCleanups( |
| hasParent(returnStmt())))))))) |
| .bind("e"), |
| expr(hasParent(returnStmt())).bind("e"))), |
| *Node, *Result.Context)) != nullptr; |
| } |
| |
| static bool parensRequired(const MatchFinder::MatchResult &Result, |
| const Expr *Node) { |
| // TODO: Figure out any more contexts in which we can omit the surrounding |
| // parentheses. |
| return !(isConstructorAssignment(Result, Node) || isArgument(Result, Node) || |
| isReturn(Result, Node)); |
| } |
| |
| void TimeSubtractionCheck::emitDiagnostic(const Expr *Node, |
| llvm::StringRef Replacement) { |
| diag(Node->getBeginLoc(), "perform subtraction in the time domain") |
| << FixItHint::CreateReplacement(Node->getSourceRange(), Replacement); |
| } |
| |
| void TimeSubtractionCheck::registerMatchers(MatchFinder *Finder) { |
| for (const char *ScaleName : |
| {"Hours", "Minutes", "Seconds", "Millis", "Micros", "Nanos"}) { |
| std::string TimeInverse = (llvm::Twine("ToUnix") + ScaleName).str(); |
| llvm::Optional<DurationScale> Scale = getScaleForTimeInverse(TimeInverse); |
| assert(Scale && "Unknown scale encountered"); |
| |
| auto TimeInverseMatcher = callExpr(callee( |
| functionDecl(hasName((llvm::Twine("::absl::") + TimeInverse).str())) |
| .bind("func_decl"))); |
| |
| // Match the cases where we know that the result is a 'Duration' and the |
| // first argument is a 'Time'. Just knowing the type of the first operand |
| // is not sufficient, since the second operand could be either a 'Time' or |
| // a 'Duration'. If we know the result is a 'Duration', we can then infer |
| // that the second operand must be a 'Time'. |
| auto CallMatcher = |
| callExpr( |
| callee(functionDecl(hasName(getDurationFactoryForScale(*Scale)))), |
| hasArgument(0, binaryOperator(hasOperatorName("-"), |
| hasLHS(TimeInverseMatcher)) |
| .bind("binop"))) |
| .bind("outer_call"); |
| Finder->addMatcher(CallMatcher, this); |
| |
| // Match cases where we know the second operand is a 'Time'. Since |
| // subtracting a 'Time' from a 'Duration' is not defined, in these cases, |
| // we always know the first operand is a 'Time' if the second is a 'Time'. |
| auto OperandMatcher = |
| binaryOperator(hasOperatorName("-"), hasRHS(TimeInverseMatcher)) |
| .bind("binop"); |
| Finder->addMatcher(OperandMatcher, this); |
| } |
| } |
| |
| void TimeSubtractionCheck::check(const MatchFinder::MatchResult &Result) { |
| const auto *BinOp = Result.Nodes.getNodeAs<BinaryOperator>("binop"); |
| std::string InverseName = |
| Result.Nodes.getNodeAs<FunctionDecl>("func_decl")->getNameAsString(); |
| if (insideMacroDefinition(Result, BinOp->getSourceRange())) |
| return; |
| |
| llvm::Optional<DurationScale> Scale = getScaleForTimeInverse(InverseName); |
| if (!Scale) |
| return; |
| |
| const auto *OuterCall = Result.Nodes.getNodeAs<CallExpr>("outer_call"); |
| if (OuterCall) { |
| if (insideMacroDefinition(Result, OuterCall->getSourceRange())) |
| return; |
| |
| // We're working with the first case of matcher, and need to replace the |
| // entire 'Duration' factory call. (Which also means being careful about |
| // our order-of-operations and optionally putting in some parenthesis. |
| bool NeedParens = parensRequired(Result, OuterCall); |
| |
| emitDiagnostic( |
| OuterCall, |
| (llvm::Twine(NeedParens ? "(" : "") + |
| rewriteExprFromNumberToTime(Result, *Scale, BinOp->getLHS()) + " - " + |
| rewriteExprFromNumberToTime(Result, *Scale, BinOp->getRHS()) + |
| (NeedParens ? ")" : "")) |
| .str()); |
| } else { |
| // We're working with the second case of matcher, and either just need to |
| // change the arguments, or perhaps remove an outer function call. In the |
| // latter case (addressed first), we also need to worry about parenthesis. |
| const auto *MaybeCallArg = selectFirst<const CallExpr>( |
| "arg", match(expr(hasAncestor( |
| callExpr(callee(functionDecl(hasName( |
| getDurationFactoryForScale(*Scale))))) |
| .bind("arg"))), |
| *BinOp, *Result.Context)); |
| if (MaybeCallArg && MaybeCallArg->getArg(0)->IgnoreImpCasts() == BinOp && |
| !insideMacroDefinition(Result, MaybeCallArg->getSourceRange())) { |
| // Handle the case where the matched expression is inside a call which |
| // converts it from the inverse to a Duration. In this case, we replace |
| // the outer with just the subtraction expression, which gives the right |
| // type and scale, taking care again about parenthesis. |
| bool NeedParens = parensRequired(Result, MaybeCallArg); |
| |
| emitDiagnostic( |
| MaybeCallArg, |
| (llvm::Twine(NeedParens ? "(" : "") + |
| rewriteExprFromNumberToTime(Result, *Scale, BinOp->getLHS()) + |
| " - " + |
| rewriteExprFromNumberToTime(Result, *Scale, BinOp->getRHS()) + |
| (NeedParens ? ")" : "")) |
| .str()); |
| } else { |
| // In the last case, just convert the arguments and wrap the result in |
| // the correct inverse function. |
| emitDiagnostic( |
| BinOp, |
| (llvm::Twine( |
| getDurationInverseForScale(*Scale).second.str().substr(2)) + |
| "(" + rewriteExprFromNumberToTime(Result, *Scale, BinOp->getLHS()) + |
| " - " + |
| rewriteExprFromNumberToTime(Result, *Scale, BinOp->getRHS()) + ")") |
| .str()); |
| } |
| } |
| } |
| |
| } // namespace abseil |
| } // namespace tidy |
| } // namespace clang |