1 | //===--- DurationFactoryScaleCheck.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 "DurationFactoryScaleCheck.h" |
10 | #include "DurationRewriter.h" |
11 | #include "clang/AST/ASTContext.h" |
12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
13 | #include "clang/Tooling/FixIt.h" |
14 | #include <optional> |
15 | |
16 | using namespace clang::ast_matchers; |
17 | |
18 | namespace clang::tidy::abseil { |
19 | |
20 | // Given the name of a duration factory function, return the appropriate |
21 | // `DurationScale` for that factory. If no factory can be found for |
22 | // `FactoryName`, return `std::nullopt`. |
23 | static std::optional<DurationScale> |
24 | getScaleForFactory(llvm::StringRef FactoryName) { |
25 | return llvm::StringSwitch<std::optional<DurationScale>>(FactoryName) |
26 | .Case(S: "Nanoseconds" , Value: DurationScale::Nanoseconds) |
27 | .Case(S: "Microseconds" , Value: DurationScale::Microseconds) |
28 | .Case(S: "Milliseconds" , Value: DurationScale::Milliseconds) |
29 | .Case(S: "Seconds" , Value: DurationScale::Seconds) |
30 | .Case(S: "Minutes" , Value: DurationScale::Minutes) |
31 | .Case(S: "Hours" , Value: DurationScale::Hours) |
32 | .Default(Value: std::nullopt); |
33 | } |
34 | |
35 | // Given either an integer or float literal, return its value. |
36 | // One and only one of `IntLit` and `FloatLit` should be provided. |
37 | static double getValue(const IntegerLiteral *IntLit, |
38 | const FloatingLiteral *FloatLit) { |
39 | if (IntLit) |
40 | return IntLit->getValue().getLimitedValue(); |
41 | |
42 | assert(FloatLit != nullptr && "Neither IntLit nor FloatLit set" ); |
43 | return FloatLit->getValueAsApproximateDouble(); |
44 | } |
45 | |
46 | // Given the scale of a duration and a `Multiplier`, determine if `Multiplier` |
47 | // would produce a new scale. If so, return a tuple containing the new scale |
48 | // and a suitable Multiplier for that scale, otherwise `std::nullopt`. |
49 | static std::optional<std::tuple<DurationScale, double>> |
50 | getNewScaleSingleStep(DurationScale OldScale, double Multiplier) { |
51 | switch (OldScale) { |
52 | case DurationScale::Hours: |
53 | if (Multiplier <= 1.0 / 60.0) |
54 | return std::make_tuple(args: DurationScale::Minutes, args: Multiplier * 60.0); |
55 | break; |
56 | |
57 | case DurationScale::Minutes: |
58 | if (Multiplier >= 60.0) |
59 | return std::make_tuple(args: DurationScale::Hours, args: Multiplier / 60.0); |
60 | if (Multiplier <= 1.0 / 60.0) |
61 | return std::make_tuple(args: DurationScale::Seconds, args: Multiplier * 60.0); |
62 | break; |
63 | |
64 | case DurationScale::Seconds: |
65 | if (Multiplier >= 60.0) |
66 | return std::make_tuple(args: DurationScale::Minutes, args: Multiplier / 60.0); |
67 | if (Multiplier <= 1e-3) |
68 | return std::make_tuple(args: DurationScale::Milliseconds, args: Multiplier * 1e3); |
69 | break; |
70 | |
71 | case DurationScale::Milliseconds: |
72 | if (Multiplier >= 1e3) |
73 | return std::make_tuple(args: DurationScale::Seconds, args: Multiplier / 1e3); |
74 | if (Multiplier <= 1e-3) |
75 | return std::make_tuple(args: DurationScale::Microseconds, args: Multiplier * 1e3); |
76 | break; |
77 | |
78 | case DurationScale::Microseconds: |
79 | if (Multiplier >= 1e3) |
80 | return std::make_tuple(args: DurationScale::Milliseconds, args: Multiplier / 1e3); |
81 | if (Multiplier <= 1e-3) |
82 | return std::make_tuple(args: DurationScale::Nanoseconds, args: Multiplier * 1e-3); |
83 | break; |
84 | |
85 | case DurationScale::Nanoseconds: |
86 | if (Multiplier >= 1e3) |
87 | return std::make_tuple(args: DurationScale::Microseconds, args: Multiplier / 1e3); |
88 | break; |
89 | } |
90 | |
91 | return std::nullopt; |
92 | } |
93 | |
94 | // Given the scale of a duration and a `Multiplier`, determine if `Multiplier` |
95 | // would produce a new scale. If so, return it, otherwise `std::nullopt`. |
96 | static std::optional<DurationScale> getNewScale(DurationScale OldScale, |
97 | double Multiplier) { |
98 | while (Multiplier != 1.0) { |
99 | std::optional<std::tuple<DurationScale, double>> Result = |
100 | getNewScaleSingleStep(OldScale, Multiplier); |
101 | if (!Result) |
102 | break; |
103 | if (std::get<1>(t&: *Result) == 1.0) |
104 | return std::get<0>(t&: *Result); |
105 | Multiplier = std::get<1>(t&: *Result); |
106 | OldScale = std::get<0>(t&: *Result); |
107 | } |
108 | |
109 | return std::nullopt; |
110 | } |
111 | |
112 | void DurationFactoryScaleCheck::registerMatchers(MatchFinder *Finder) { |
113 | Finder->addMatcher( |
114 | NodeMatch: callExpr( |
115 | callee(InnerMatcher: functionDecl(DurationFactoryFunction()).bind(ID: "call_decl" )), |
116 | hasArgument( |
117 | N: 0, |
118 | InnerMatcher: ignoringImpCasts(InnerMatcher: anyOf( |
119 | cxxFunctionalCastExpr( |
120 | hasDestinationType( |
121 | InnerMatcher: anyOf(isInteger(), realFloatingPointType())), |
122 | hasSourceExpression(InnerMatcher: initListExpr())), |
123 | integerLiteral(equals(Value: 0)), floatLiteral(equals(Value: 0.0)), |
124 | binaryOperator(hasOperatorName(Name: "*" ), |
125 | hasEitherOperand(InnerMatcher: ignoringImpCasts( |
126 | InnerMatcher: anyOf(integerLiteral(), floatLiteral())))) |
127 | .bind(ID: "mult_binop" ), |
128 | binaryOperator(hasOperatorName(Name: "/" ), hasRHS(InnerMatcher: floatLiteral())) |
129 | .bind(ID: "div_binop" ))))) |
130 | .bind(ID: "call" ), |
131 | Action: this); |
132 | } |
133 | |
134 | void DurationFactoryScaleCheck::check(const MatchFinder::MatchResult &Result) { |
135 | const auto *Call = Result.Nodes.getNodeAs<CallExpr>(ID: "call" ); |
136 | |
137 | // Don't try to replace things inside of macro definitions. |
138 | if (Call->getExprLoc().isMacroID()) |
139 | return; |
140 | |
141 | const Expr *Arg = Call->getArg(Arg: 0)->IgnoreParenImpCasts(); |
142 | // Arguments which are macros are ignored. |
143 | if (Arg->getBeginLoc().isMacroID()) |
144 | return; |
145 | |
146 | // We first handle the cases of literal zero (both float and integer). |
147 | if (isLiteralZero(Result, Node: *Arg)) { |
148 | diag(Loc: Call->getBeginLoc(), |
149 | Description: "use ZeroDuration() for zero-length time intervals" ) |
150 | << FixItHint::CreateReplacement(Call->getSourceRange(), |
151 | "absl::ZeroDuration()" ); |
152 | return; |
153 | } |
154 | |
155 | const auto *CallDecl = Result.Nodes.getNodeAs<FunctionDecl>(ID: "call_decl" ); |
156 | std::optional<DurationScale> MaybeScale = |
157 | getScaleForFactory(CallDecl->getName()); |
158 | if (!MaybeScale) |
159 | return; |
160 | |
161 | DurationScale Scale = *MaybeScale; |
162 | const Expr *Remainder = nullptr; |
163 | std::optional<DurationScale> NewScale; |
164 | |
165 | // We next handle the cases of multiplication and division. |
166 | if (const auto *MultBinOp = |
167 | Result.Nodes.getNodeAs<BinaryOperator>(ID: "mult_binop" )) { |
168 | // For multiplication, we need to look at both operands, and consider the |
169 | // cases where a user is multiplying by something such as 1e-3. |
170 | |
171 | // First check the LHS |
172 | const auto *IntLit = llvm::dyn_cast<IntegerLiteral>(Val: MultBinOp->getLHS()); |
173 | const auto *FloatLit = llvm::dyn_cast<FloatingLiteral>(Val: MultBinOp->getLHS()); |
174 | if (IntLit || FloatLit) { |
175 | NewScale = getNewScale(OldScale: Scale, Multiplier: getValue(IntLit, FloatLit)); |
176 | if (NewScale) |
177 | Remainder = MultBinOp->getRHS(); |
178 | } |
179 | |
180 | // If we weren't able to scale based on the LHS, check the RHS |
181 | if (!NewScale) { |
182 | IntLit = llvm::dyn_cast<IntegerLiteral>(Val: MultBinOp->getRHS()); |
183 | FloatLit = llvm::dyn_cast<FloatingLiteral>(Val: MultBinOp->getRHS()); |
184 | if (IntLit || FloatLit) { |
185 | NewScale = getNewScale(OldScale: Scale, Multiplier: getValue(IntLit, FloatLit)); |
186 | if (NewScale) |
187 | Remainder = MultBinOp->getLHS(); |
188 | } |
189 | } |
190 | } else if (const auto *DivBinOp = |
191 | Result.Nodes.getNodeAs<BinaryOperator>(ID: "div_binop" )) { |
192 | // We next handle division. |
193 | // For division, we only check the RHS. |
194 | const auto *FloatLit = llvm::cast<FloatingLiteral>(Val: DivBinOp->getRHS()); |
195 | |
196 | std::optional<DurationScale> NewScale = |
197 | getNewScale(OldScale: Scale, Multiplier: 1.0 / FloatLit->getValueAsApproximateDouble()); |
198 | if (NewScale) { |
199 | const Expr *Remainder = DivBinOp->getLHS(); |
200 | |
201 | // We've found an appropriate scaling factor and the new scale, so output |
202 | // the relevant fix. |
203 | diag(Loc: Call->getBeginLoc(), Description: "internal duration scaling can be removed" ) |
204 | << FixItHint::CreateReplacement( |
205 | Call->getSourceRange(), |
206 | (llvm::Twine(getDurationFactoryForScale(Scale: *NewScale)) + "(" + |
207 | tooling::fixit::getText(Node: *Remainder, Context: *Result.Context) + ")" ) |
208 | .str()); |
209 | } |
210 | } |
211 | |
212 | if (NewScale) { |
213 | assert(Remainder && "No remainder found" ); |
214 | // We've found an appropriate scaling factor and the new scale, so output |
215 | // the relevant fix. |
216 | diag(Loc: Call->getBeginLoc(), Description: "internal duration scaling can be removed" ) |
217 | << FixItHint::CreateReplacement( |
218 | Call->getSourceRange(), |
219 | (llvm::Twine(getDurationFactoryForScale(Scale: *NewScale)) + "(" + |
220 | tooling::fixit::getText(Node: *Remainder, Context: *Result.Context) + ")" ) |
221 | .str()); |
222 | } |
223 | } |
224 | |
225 | } // namespace clang::tidy::abseil |
226 | |