xref: /llvm-project/clang-tools-extra/clang-tidy/abseil/TimeSubtractionCheck.cpp (revision 11a411a49b62c129bba551df4587dd446fcdc660)
1 //===--- TimeSubtractionCheck.cpp - clang-tidy ----------------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include "TimeSubtractionCheck.h"
10 #include "DurationRewriter.h"
11 #include "clang/AST/ASTContext.h"
12 #include "clang/ASTMatchers/ASTMatchFinder.h"
13 #include "clang/Lex/Lexer.h"
14 #include "clang/Tooling/FixIt.h"
15 #include <optional>
16 
17 using namespace clang::ast_matchers;
18 
19 namespace clang::tidy::abseil {
20 
21 // Returns `true` if `Range` is inside a macro definition.
insideMacroDefinition(const MatchFinder::MatchResult & Result,SourceRange Range)22 static bool insideMacroDefinition(const MatchFinder::MatchResult &Result,
23                                   SourceRange Range) {
24   return !clang::Lexer::makeFileCharRange(
25               clang::CharSourceRange::getCharRange(Range),
26               *Result.SourceManager, Result.Context->getLangOpts())
27               .isValid();
28 }
29 
isConstructorAssignment(const MatchFinder::MatchResult & Result,const Expr * Node)30 static bool isConstructorAssignment(const MatchFinder::MatchResult &Result,
31                                     const Expr *Node) {
32   // For C++14 and earlier there are elidable constructors that must be matched
33   // in hasParent. The elidable constructors do not exist in C++17 and later and
34   // therefore an additional check that does not match against the elidable
35   // constructors are needed for this case.
36   return selectFirst<const Expr>(
37              "e",
38              match(expr(anyOf(
39                        callExpr(hasParent(materializeTemporaryExpr(hasParent(
40                                     cxxConstructExpr(hasParent(exprWithCleanups(
41                                         hasParent(varDecl()))))))))
42                            .bind("e"),
43                        callExpr(hasParent(varDecl())).bind("e"))),
44                    *Node, *Result.Context)) != nullptr;
45 }
46 
isArgument(const MatchFinder::MatchResult & Result,const Expr * Node)47 static bool isArgument(const MatchFinder::MatchResult &Result,
48                        const Expr *Node) {
49   // For the same reason as in isConstructorAssignment two AST shapes need to be
50   // matched here.
51   return selectFirst<const Expr>(
52              "e",
53              match(
54                  expr(anyOf(
55                      expr(hasParent(materializeTemporaryExpr(
56                               hasParent(cxxConstructExpr(
57                                   hasParent(callExpr()),
58                                   unless(hasParent(cxxOperatorCallExpr())))))))
59                          .bind("e"),
60                      expr(hasParent(callExpr()),
61                           unless(hasParent(cxxOperatorCallExpr())))
62                          .bind("e"))),
63                  *Node, *Result.Context)) != nullptr;
64 }
65 
isReturn(const MatchFinder::MatchResult & Result,const Expr * Node)66 static bool isReturn(const MatchFinder::MatchResult &Result, const Expr *Node) {
67   // For the same reason as in isConstructorAssignment two AST shapes need to be
68   // matched here.
69   return selectFirst<const Expr>(
70              "e",
71              match(expr(anyOf(
72                        expr(hasParent(materializeTemporaryExpr(hasParent(
73                                 cxxConstructExpr(hasParent(exprWithCleanups(
74                                     hasParent(returnStmt()))))))))
75                            .bind("e"),
76                        expr(hasParent(returnStmt())).bind("e"))),
77                    *Node, *Result.Context)) != nullptr;
78 }
79 
parensRequired(const MatchFinder::MatchResult & Result,const Expr * Node)80 static bool parensRequired(const MatchFinder::MatchResult &Result,
81                            const Expr *Node) {
82   // TODO: Figure out any more contexts in which we can omit the surrounding
83   // parentheses.
84   return !(isConstructorAssignment(Result, Node) || isArgument(Result, Node) ||
85            isReturn(Result, Node));
86 }
87 
emitDiagnostic(const Expr * Node,llvm::StringRef Replacement)88 void TimeSubtractionCheck::emitDiagnostic(const Expr *Node,
89                                           llvm::StringRef Replacement) {
90   diag(Node->getBeginLoc(), "perform subtraction in the time domain")
91       << FixItHint::CreateReplacement(Node->getSourceRange(), Replacement);
92 }
93 
registerMatchers(MatchFinder * Finder)94 void TimeSubtractionCheck::registerMatchers(MatchFinder *Finder) {
95   for (const char *ScaleName :
96        {"Hours", "Minutes", "Seconds", "Millis", "Micros", "Nanos"}) {
97     std::string TimeInverse = (llvm::Twine("ToUnix") + ScaleName).str();
98     std::optional<DurationScale> Scale = getScaleForTimeInverse(TimeInverse);
99     assert(Scale && "Unknown scale encountered");
100 
101     auto TimeInverseMatcher = callExpr(callee(
102         functionDecl(hasName((llvm::Twine("::absl::") + TimeInverse).str()))
103             .bind("func_decl")));
104 
105     // Match the cases where we know that the result is a 'Duration' and the
106     // first argument is a 'Time'. Just knowing the type of the first operand
107     // is not sufficient, since the second operand could be either a 'Time' or
108     // a 'Duration'. If we know the result is a 'Duration', we can then infer
109     // that the second operand must be a 'Time'.
110     auto CallMatcher =
111         callExpr(
112             callee(functionDecl(hasName(getDurationFactoryForScale(*Scale)))),
113             hasArgument(0, binaryOperator(hasOperatorName("-"),
114                                           hasLHS(TimeInverseMatcher))
115                                .bind("binop")))
116             .bind("outer_call");
117     Finder->addMatcher(CallMatcher, this);
118 
119     // Match cases where we know the second operand is a 'Time'. Since
120     // subtracting a 'Time' from a 'Duration' is not defined, in these cases,
121     // we always know the first operand is a 'Time' if the second is a 'Time'.
122     auto OperandMatcher =
123         binaryOperator(hasOperatorName("-"), hasRHS(TimeInverseMatcher))
124             .bind("binop");
125     Finder->addMatcher(OperandMatcher, this);
126   }
127 }
128 
check(const MatchFinder::MatchResult & Result)129 void TimeSubtractionCheck::check(const MatchFinder::MatchResult &Result) {
130   const auto *BinOp = Result.Nodes.getNodeAs<BinaryOperator>("binop");
131   std::string InverseName =
132       Result.Nodes.getNodeAs<FunctionDecl>("func_decl")->getNameAsString();
133   if (insideMacroDefinition(Result, BinOp->getSourceRange()))
134     return;
135 
136   std::optional<DurationScale> Scale = getScaleForTimeInverse(InverseName);
137   if (!Scale)
138     return;
139 
140   const auto *OuterCall = Result.Nodes.getNodeAs<CallExpr>("outer_call");
141   if (OuterCall) {
142     if (insideMacroDefinition(Result, OuterCall->getSourceRange()))
143       return;
144 
145     // We're working with the first case of matcher, and need to replace the
146     // entire 'Duration' factory call. (Which also means being careful about
147     // our order-of-operations and optionally putting in some parenthesis.
148     bool NeedParens = parensRequired(Result, OuterCall);
149 
150     emitDiagnostic(
151         OuterCall,
152         (llvm::Twine(NeedParens ? "(" : "") +
153          rewriteExprFromNumberToTime(Result, *Scale, BinOp->getLHS()) + " - " +
154          rewriteExprFromNumberToTime(Result, *Scale, BinOp->getRHS()) +
155          (NeedParens ? ")" : ""))
156             .str());
157   } else {
158     // We're working with the second case of matcher, and either just need to
159     // change the arguments, or perhaps remove an outer function call. In the
160     // latter case (addressed first), we also need to worry about parenthesis.
161     const auto *MaybeCallArg = selectFirst<const CallExpr>(
162         "arg", match(expr(hasAncestor(
163                          callExpr(callee(functionDecl(hasName(
164                                       getDurationFactoryForScale(*Scale)))))
165                              .bind("arg"))),
166                      *BinOp, *Result.Context));
167     if (MaybeCallArg && MaybeCallArg->getArg(0)->IgnoreImpCasts() == BinOp &&
168         !insideMacroDefinition(Result, MaybeCallArg->getSourceRange())) {
169       // Handle the case where the matched expression is inside a call which
170       // converts it from the inverse to a Duration.  In this case, we replace
171       // the outer with just the subtraction expression, which gives the right
172       // type and scale, taking care again about parenthesis.
173       bool NeedParens = parensRequired(Result, MaybeCallArg);
174 
175       emitDiagnostic(
176           MaybeCallArg,
177           (llvm::Twine(NeedParens ? "(" : "") +
178            rewriteExprFromNumberToTime(Result, *Scale, BinOp->getLHS()) +
179            " - " +
180            rewriteExprFromNumberToTime(Result, *Scale, BinOp->getRHS()) +
181            (NeedParens ? ")" : ""))
182               .str());
183     } else {
184       // In the last case, just convert the arguments and wrap the result in
185       // the correct inverse function.
186       emitDiagnostic(
187           BinOp,
188           (llvm::Twine(
189                getDurationInverseForScale(*Scale).second.str().substr(2)) +
190            "(" + rewriteExprFromNumberToTime(Result, *Scale, BinOp->getLHS()) +
191            " - " +
192            rewriteExprFromNumberToTime(Result, *Scale, BinOp->getRHS()) + ")")
193               .str());
194     }
195   }
196 }
197 
198 } // namespace clang::tidy::abseil
199