clang-tools 19.0.0git
TimeSubtractionCheck.cpp
Go to the documentation of this file.
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
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
17using namespace clang::ast_matchers;
18
19namespace clang::tidy::abseil {
20
21// Returns `true` if `Range` is inside a macro definition.
22static 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
30static 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
47static 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
66static 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
80static 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
88void 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
94void 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
129void 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(
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
CharSourceRange Range
SourceRange for the file name.
::clang::DynTypedNode Node
DiagnosticBuilder diag(SourceLocation Loc, StringRef Description, DiagnosticIDs::Level Level=DiagnosticIDs::Warning)
Add a diagnostic with the check's name.
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
ClangTidyChecks that register ASTMatchers should do the actual work in here.
void registerMatchers(ast_matchers::MatchFinder *Finder) override
Override this to register AST matchers with Finder.
const std::pair< llvm::StringRef, llvm::StringRef > & getDurationInverseForScale(DurationScale Scale)
Given a Scale return the fully qualified inverse functions for it.
std::optional< DurationScale > getScaleForTimeInverse(llvm::StringRef Name)
Given the name of an inverse Time function (e.g., ToUnixSeconds), return its DurationScale,...
static bool isConstructorAssignment(const MatchFinder::MatchResult &Result, const Expr *Node)
static bool parensRequired(const MatchFinder::MatchResult &Result, const Expr *Node)
static bool isArgument(const MatchFinder::MatchResult &Result, const Expr *Node)
llvm::StringRef getDurationFactoryForScale(DurationScale Scale)
Returns the factory function name for a given Scale.
static bool isReturn(const MatchFinder::MatchResult &Result, const Expr *Node)
static bool insideMacroDefinition(const MatchFinder::MatchResult &Result, SourceRange Range)
std::string rewriteExprFromNumberToTime(const ast_matchers::MatchFinder::MatchResult &Result, DurationScale Scale, const Expr *Node)
Assuming Node has a type int representing a time instant of Scale since The Epoch,...