1 //===--- TimeSubtractionCheck.cpp - clang-tidy ----------------------------===//
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
7 //===----------------------------------------------------------------------===//
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"
17 using namespace clang::ast_matchers
;
19 namespace clang::tidy::abseil
{
21 // Returns `true` if `Range` is inside a macro definition.
22 static bool insideMacroDefinition(const MatchFinder::MatchResult
&Result
,
24 return !clang::Lexer::makeFileCharRange(
25 clang::CharSourceRange::getCharRange(Range
),
26 *Result
.SourceManager
, Result
.Context
->getLangOpts())
30 static bool isConstructorAssignment(const MatchFinder::MatchResult
&Result
,
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
>(
39 callExpr(hasParent(materializeTemporaryExpr(hasParent(
40 cxxConstructExpr(hasParent(exprWithCleanups(
41 hasParent(varDecl()))))))))
43 callExpr(hasParent(varDecl())).bind("e"))),
44 *Node
, *Result
.Context
)) != nullptr;
47 static bool isArgument(const MatchFinder::MatchResult
&Result
,
49 // For the same reason as in isConstructorAssignment two AST shapes need to be
51 return selectFirst
<const Expr
>(
55 expr(hasParent(materializeTemporaryExpr(
56 hasParent(cxxConstructExpr(
57 hasParent(callExpr()),
58 unless(hasParent(cxxOperatorCallExpr())))))))
60 expr(hasParent(callExpr()),
61 unless(hasParent(cxxOperatorCallExpr())))
63 *Node
, *Result
.Context
)) != nullptr;
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
69 return selectFirst
<const Expr
>(
72 expr(hasParent(materializeTemporaryExpr(hasParent(
73 cxxConstructExpr(hasParent(exprWithCleanups(
74 hasParent(returnStmt()))))))))
76 expr(hasParent(returnStmt())).bind("e"))),
77 *Node
, *Result
.Context
)) != nullptr;
80 static bool parensRequired(const MatchFinder::MatchResult
&Result
,
82 // TODO: Figure out any more contexts in which we can omit the surrounding
84 return !(isConstructorAssignment(Result
, Node
) || isArgument(Result
, Node
) ||
85 isReturn(Result
, Node
));
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
);
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");
101 auto TimeInverseMatcher
= callExpr(callee(
102 functionDecl(hasName((llvm::Twine("::absl::") + TimeInverse
).str()))
103 .bind("func_decl")));
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'.
112 callee(functionDecl(hasName(getDurationFactoryForScale(*Scale
)))),
113 hasArgument(0, binaryOperator(hasOperatorName("-"),
114 hasLHS(TimeInverseMatcher
))
117 Finder
->addMatcher(CallMatcher
, this);
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
))
125 Finder
->addMatcher(OperandMatcher
, this);
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()))
136 std::optional
<DurationScale
> Scale
= getScaleForTimeInverse(InverseName
);
140 const auto *OuterCall
= Result
.Nodes
.getNodeAs
<CallExpr
>("outer_call");
142 if (insideMacroDefinition(Result
, OuterCall
->getSourceRange()))
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
);
152 (llvm::Twine(NeedParens
? "(" : "") +
153 rewriteExprFromNumberToTime(Result
, *Scale
, BinOp
->getLHS()) + " - " +
154 rewriteExprFromNumberToTime(Result
, *Scale
, BinOp
->getRHS()) +
155 (NeedParens
? ")" : ""))
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
)))))
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
);
177 (llvm::Twine(NeedParens
? "(" : "") +
178 rewriteExprFromNumberToTime(Result
, *Scale
, BinOp
->getLHS()) +
180 rewriteExprFromNumberToTime(Result
, *Scale
, BinOp
->getRHS()) +
181 (NeedParens
? ")" : ""))
184 // In the last case, just convert the arguments and wrap the result in
185 // the correct inverse function.
189 getDurationInverseForScale(*Scale
).second
.str().substr(2)) +
190 "(" + rewriteExprFromNumberToTime(Result
, *Scale
, BinOp
->getLHS()) +
192 rewriteExprFromNumberToTime(Result
, *Scale
, BinOp
->getRHS()) + ")")
198 } // namespace clang::tidy::abseil