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