| 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 | |