1 | //===--- UseConstraintsCheck.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 "UseConstraintsCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
12 | #include "clang/Lex/Lexer.h" |
13 | |
14 | #include "../utils/LexerUtils.h" |
15 | |
16 | #include <optional> |
17 | #include <utility> |
18 | |
19 | using namespace clang::ast_matchers; |
20 | |
21 | namespace clang::tidy::modernize { |
22 | |
23 | struct EnableIfData { |
24 | TemplateSpecializationTypeLoc Loc; |
25 | TypeLoc Outer; |
26 | }; |
27 | |
28 | namespace { |
29 | AST_MATCHER(FunctionDecl, hasOtherDeclarations) { |
30 | auto It = Node.redecls_begin(); |
31 | auto EndIt = Node.redecls_end(); |
32 | |
33 | if (It == EndIt) |
34 | return false; |
35 | |
36 | ++It; |
37 | return It != EndIt; |
38 | } |
39 | } // namespace |
40 | |
41 | void UseConstraintsCheck::registerMatchers(MatchFinder *Finder) { |
42 | Finder->addMatcher( |
43 | NodeMatch: functionTemplateDecl( |
44 | // Skip external libraries included as system headers |
45 | unless(isExpansionInSystemHeader()), |
46 | has(functionDecl(unless(hasOtherDeclarations()), isDefinition(), |
47 | hasReturnTypeLoc(ReturnMatcher: typeLoc().bind(ID: "return"))) |
48 | .bind(ID: "function"))) |
49 | .bind(ID: "functionTemplate"), |
50 | Action: this); |
51 | } |
52 | |
53 | static std::optional<TemplateSpecializationTypeLoc> |
54 | matchEnableIfSpecializationImplTypename(TypeLoc TheType) { |
55 | if (const auto Dep = TheType.getAs<DependentNameTypeLoc>()) { |
56 | const IdentifierInfo *Identifier = Dep.getTypePtr()->getIdentifier(); |
57 | ElaboratedTypeKeyword Keyword = Dep.getTypePtr()->getKeyword(); |
58 | if (!Identifier || Identifier->getName() != "type"|| |
59 | (Keyword != ElaboratedTypeKeyword::Typename && |
60 | Keyword != ElaboratedTypeKeyword::None)) { |
61 | return std::nullopt; |
62 | } |
63 | TheType = Dep.getQualifierLoc().getTypeLoc(); |
64 | if (TheType.isNull()) |
65 | return std::nullopt; |
66 | } |
67 | |
68 | if (const auto SpecializationLoc = |
69 | TheType.getAs<TemplateSpecializationTypeLoc>()) { |
70 | |
71 | const auto *Specialization = |
72 | dyn_cast<TemplateSpecializationType>(SpecializationLoc.getTypePtr()); |
73 | if (!Specialization) |
74 | return std::nullopt; |
75 | |
76 | const TemplateDecl *TD = |
77 | Specialization->getTemplateName().getAsTemplateDecl(); |
78 | if (!TD || TD->getName() != "enable_if") |
79 | return std::nullopt; |
80 | |
81 | int NumArgs = SpecializationLoc.getNumArgs(); |
82 | if (NumArgs != 1 && NumArgs != 2) |
83 | return std::nullopt; |
84 | |
85 | return SpecializationLoc; |
86 | } |
87 | return std::nullopt; |
88 | } |
89 | |
90 | static std::optional<TemplateSpecializationTypeLoc> |
91 | matchEnableIfSpecializationImplTrait(TypeLoc TheType) { |
92 | if (const auto Elaborated = TheType.getAs<ElaboratedTypeLoc>()) |
93 | TheType = Elaborated.getNamedTypeLoc(); |
94 | |
95 | if (const auto SpecializationLoc = |
96 | TheType.getAs<TemplateSpecializationTypeLoc>()) { |
97 | |
98 | const auto *Specialization = |
99 | dyn_cast<TemplateSpecializationType>(SpecializationLoc.getTypePtr()); |
100 | if (!Specialization) |
101 | return std::nullopt; |
102 | |
103 | const TemplateDecl *TD = |
104 | Specialization->getTemplateName().getAsTemplateDecl(); |
105 | if (!TD || TD->getName() != "enable_if_t") |
106 | return std::nullopt; |
107 | |
108 | if (!Specialization->isTypeAlias()) |
109 | return std::nullopt; |
110 | |
111 | if (const auto *AliasedType = |
112 | dyn_cast<DependentNameType>(Specialization->getAliasedType())) { |
113 | ElaboratedTypeKeyword Keyword = AliasedType->getKeyword(); |
114 | if (AliasedType->getIdentifier()->getName() != "type"|| |
115 | (Keyword != ElaboratedTypeKeyword::Typename && |
116 | Keyword != ElaboratedTypeKeyword::None)) { |
117 | return std::nullopt; |
118 | } |
119 | } else { |
120 | return std::nullopt; |
121 | } |
122 | int NumArgs = SpecializationLoc.getNumArgs(); |
123 | if (NumArgs != 1 && NumArgs != 2) |
124 | return std::nullopt; |
125 | |
126 | return SpecializationLoc; |
127 | } |
128 | return std::nullopt; |
129 | } |
130 | |
131 | static std::optional<TemplateSpecializationTypeLoc> |
132 | matchEnableIfSpecializationImpl(TypeLoc TheType) { |
133 | if (auto EnableIf = matchEnableIfSpecializationImplTypename(TheType)) |
134 | return EnableIf; |
135 | return matchEnableIfSpecializationImplTrait(TheType); |
136 | } |
137 | |
138 | static std::optional<EnableIfData> |
139 | matchEnableIfSpecialization(TypeLoc TheType) { |
140 | if (const auto Pointer = TheType.getAs<PointerTypeLoc>()) |
141 | TheType = Pointer.getPointeeLoc(); |
142 | else if (const auto Reference = TheType.getAs<ReferenceTypeLoc>()) |
143 | TheType = Reference.getPointeeLoc(); |
144 | if (const auto Qualified = TheType.getAs<QualifiedTypeLoc>()) |
145 | TheType = Qualified.getUnqualifiedLoc(); |
146 | |
147 | if (auto EnableIf = matchEnableIfSpecializationImpl(TheType)) |
148 | return EnableIfData{std::move(*EnableIf), TheType}; |
149 | return std::nullopt; |
150 | } |
151 | |
152 | static std::pair<std::optional<EnableIfData>, const Decl *> |
153 | matchTrailingTemplateParam(const FunctionTemplateDecl *FunctionTemplate) { |
154 | // For non-type trailing param, match very specifically |
155 | // 'template <..., enable_if_type<Condition, Type> = Default>' where |
156 | // enable_if_type is 'enable_if' or 'enable_if_t'. E.g., 'template <typename |
157 | // T, enable_if_t<is_same_v<T, bool>, int*> = nullptr> |
158 | // |
159 | // Otherwise, match a trailing default type arg. |
160 | // E.g., 'template <typename T, typename = enable_if_t<is_same_v<T, bool>>>' |
161 | |
162 | const TemplateParameterList *TemplateParams = |
163 | FunctionTemplate->getTemplateParameters(); |
164 | if (TemplateParams->size() == 0) |
165 | return {}; |
166 | |
167 | const NamedDecl *LastParam = |
168 | TemplateParams->getParam(Idx: TemplateParams->size() - 1); |
169 | if (const auto *LastTemplateParam = |
170 | dyn_cast<NonTypeTemplateParmDecl>(LastParam)) { |
171 | |
172 | if (!LastTemplateParam->hasDefaultArgument() || |
173 | !LastTemplateParam->getName().empty()) |
174 | return {}; |
175 | |
176 | return {matchEnableIfSpecialization( |
177 | LastTemplateParam->getTypeSourceInfo()->getTypeLoc()), |
178 | LastTemplateParam}; |
179 | } |
180 | if (const auto *LastTemplateParam = |
181 | dyn_cast<TemplateTypeParmDecl>(LastParam)) { |
182 | if (LastTemplateParam->hasDefaultArgument() && |
183 | LastTemplateParam->getIdentifier() == nullptr) { |
184 | return { |
185 | matchEnableIfSpecialization(LastTemplateParam->getDefaultArgument() |
186 | .getTypeSourceInfo() |
187 | ->getTypeLoc()), |
188 | LastTemplateParam}; |
189 | } |
190 | } |
191 | return {}; |
192 | } |
193 | |
194 | template <typename T> |
195 | static SourceLocation getRAngleFileLoc(const SourceManager &SM, |
196 | const T &Element) { |
197 | // getFileLoc handles the case where the RAngle loc is part of a synthesized |
198 | // '>>', which ends up allocating a 'scratch space' buffer in the source |
199 | // manager. |
200 | return SM.getFileLoc(Loc: Element.getRAngleLoc()); |
201 | } |
202 | |
203 | static SourceRange |
204 | getConditionRange(ASTContext &Context, |
205 | const TemplateSpecializationTypeLoc &EnableIf) { |
206 | // TemplateArgumentLoc's SourceRange End is the location of the last token |
207 | // (per UnqualifiedId docs). E.g., in `enable_if<AAA && BBB>`, the End |
208 | // location will be the first 'B' in 'BBB'. |
209 | const LangOptions &LangOpts = Context.getLangOpts(); |
210 | const SourceManager &SM = Context.getSourceManager(); |
211 | if (EnableIf.getNumArgs() > 1) { |
212 | TemplateArgumentLoc NextArg = EnableIf.getArgLoc(i: 1); |
213 | return {EnableIf.getLAngleLoc().getLocWithOffset(Offset: 1), |
214 | utils::lexer::findPreviousTokenKind( |
215 | Start: NextArg.getSourceRange().getBegin(), SM, LangOpts, TK: tok::comma)}; |
216 | } |
217 | |
218 | return {EnableIf.getLAngleLoc().getLocWithOffset(Offset: 1), |
219 | getRAngleFileLoc(SM, Element: EnableIf)}; |
220 | } |
221 | |
222 | static SourceRange getTypeRange(ASTContext &Context, |
223 | const TemplateSpecializationTypeLoc &EnableIf) { |
224 | TemplateArgumentLoc Arg = EnableIf.getArgLoc(i: 1); |
225 | const LangOptions &LangOpts = Context.getLangOpts(); |
226 | const SourceManager &SM = Context.getSourceManager(); |
227 | return {utils::lexer::findPreviousTokenKind(Start: Arg.getSourceRange().getBegin(), |
228 | SM, LangOpts, TK: tok::comma) |
229 | .getLocWithOffset(Offset: 1), |
230 | getRAngleFileLoc(SM, Element: EnableIf)}; |
231 | } |
232 | |
233 | // Returns the original source text of the second argument of a call to |
234 | // enable_if_t. E.g., in enable_if_t<Condition, TheType>, this function |
235 | // returns 'TheType'. |
236 | static std::optional<StringRef> |
237 | getTypeText(ASTContext &Context, |
238 | const TemplateSpecializationTypeLoc &EnableIf) { |
239 | if (EnableIf.getNumArgs() > 1) { |
240 | const LangOptions &LangOpts = Context.getLangOpts(); |
241 | const SourceManager &SM = Context.getSourceManager(); |
242 | bool Invalid = false; |
243 | StringRef Text = Lexer::getSourceText(Range: CharSourceRange::getCharRange( |
244 | R: getTypeRange(Context, EnableIf)), |
245 | SM, LangOpts, Invalid: &Invalid) |
246 | .trim(); |
247 | if (Invalid) |
248 | return std::nullopt; |
249 | |
250 | return Text; |
251 | } |
252 | |
253 | return "void"; |
254 | } |
255 | |
256 | static std::optional<SourceLocation> |
257 | findInsertionForConstraint(const FunctionDecl *Function, ASTContext &Context) { |
258 | SourceManager &SM = Context.getSourceManager(); |
259 | const LangOptions &LangOpts = Context.getLangOpts(); |
260 | |
261 | if (const auto *Constructor = dyn_cast<CXXConstructorDecl>(Val: Function)) { |
262 | for (const CXXCtorInitializer *Init : Constructor->inits()) { |
263 | if (Init->getSourceOrder() == 0) |
264 | return utils::lexer::findPreviousTokenKind(Start: Init->getSourceLocation(), |
265 | SM, LangOpts, TK: tok::colon); |
266 | } |
267 | if (!Constructor->inits().empty()) |
268 | return std::nullopt; |
269 | } |
270 | if (Function->isDeleted()) { |
271 | SourceLocation FunctionEnd = Function->getSourceRange().getEnd(); |
272 | return utils::lexer::findNextAnyTokenKind(Start: FunctionEnd, SM, LangOpts, |
273 | TK: tok::equal, TKs: tok::equal); |
274 | } |
275 | const Stmt *Body = Function->getBody(); |
276 | if (!Body) |
277 | return std::nullopt; |
278 | |
279 | return Body->getBeginLoc(); |
280 | } |
281 | |
282 | static bool isPrimaryExpression(const Expr *Expression) { |
283 | // This function is an incomplete approximation of checking whether |
284 | // an Expr is a primary expression. In particular, if this function |
285 | // returns true, the expression is a primary expression. The converse |
286 | // is not necessarily true. |
287 | |
288 | if (const auto *Cast = dyn_cast<ImplicitCastExpr>(Val: Expression)) |
289 | Expression = Cast->getSubExprAsWritten(); |
290 | if (isa<ParenExpr, DependentScopeDeclRefExpr>(Val: Expression)) |
291 | return true; |
292 | |
293 | return false; |
294 | } |
295 | |
296 | // Return the original source text of an enable_if_t condition, i.e., the |
297 | // first template argument). For example, in |
298 | // 'enable_if_t<FirstCondition || SecondCondition, AType>', the text |
299 | // the text 'FirstCondition || SecondCondition' is returned. |
300 | static std::optional<std::string> getConditionText(const Expr *ConditionExpr, |
301 | SourceRange ConditionRange, |
302 | ASTContext &Context) { |
303 | SourceManager &SM = Context.getSourceManager(); |
304 | const LangOptions &LangOpts = Context.getLangOpts(); |
305 | |
306 | SourceLocation PrevTokenLoc = ConditionRange.getEnd(); |
307 | if (PrevTokenLoc.isInvalid()) |
308 | return std::nullopt; |
309 | |
310 | const bool SkipComments = false; |
311 | Token PrevToken; |
312 | std::tie(args&: PrevToken, args&: PrevTokenLoc) = utils::lexer::getPreviousTokenAndStart( |
313 | Location: PrevTokenLoc, SM, LangOpts, SkipComments); |
314 | bool EndsWithDoubleSlash = |
315 | PrevToken.is(K: tok::comment) && |
316 | Lexer::getSourceText(Range: CharSourceRange::getCharRange( |
317 | B: PrevTokenLoc, E: PrevTokenLoc.getLocWithOffset(Offset: 2)), |
318 | SM, LangOpts) == "//"; |
319 | |
320 | bool Invalid = false; |
321 | llvm::StringRef ConditionText = Lexer::getSourceText( |
322 | Range: CharSourceRange::getCharRange(R: ConditionRange), SM, LangOpts, Invalid: &Invalid); |
323 | if (Invalid) |
324 | return std::nullopt; |
325 | |
326 | auto AddParens = [&](llvm::StringRef Text) -> std::string { |
327 | if (isPrimaryExpression(Expression: ConditionExpr)) |
328 | return Text.str(); |
329 | return "("+ Text.str() + ")"; |
330 | }; |
331 | |
332 | if (EndsWithDoubleSlash) |
333 | return AddParens(ConditionText); |
334 | return AddParens(ConditionText.trim()); |
335 | } |
336 | |
337 | // Handle functions that return enable_if_t, e.g., |
338 | // template <...> |
339 | // enable_if_t<Condition, ReturnType> function(); |
340 | // |
341 | // Return a vector of FixItHints if the code can be replaced with |
342 | // a C++20 requires clause. In the example above, returns FixItHints |
343 | // to result in |
344 | // template <...> |
345 | // ReturnType function() requires Condition {} |
346 | static std::vector<FixItHint> handleReturnType(const FunctionDecl *Function, |
347 | const TypeLoc &ReturnType, |
348 | const EnableIfData &EnableIf, |
349 | ASTContext &Context) { |
350 | TemplateArgumentLoc EnableCondition = EnableIf.Loc.getArgLoc(0); |
351 | |
352 | SourceRange ConditionRange = getConditionRange(Context, EnableIf.Loc); |
353 | |
354 | std::optional<std::string> ConditionText = getConditionText( |
355 | ConditionExpr: EnableCondition.getSourceExpression(), ConditionRange, Context); |
356 | if (!ConditionText) |
357 | return {}; |
358 | |
359 | std::optional<StringRef> TypeText = getTypeText(Context, EnableIf.Loc); |
360 | if (!TypeText) |
361 | return {}; |
362 | |
363 | SmallVector<AssociatedConstraint, 3> ExistingConstraints; |
364 | Function->getAssociatedConstraints(ACs&: ExistingConstraints); |
365 | if (!ExistingConstraints.empty()) { |
366 | // FIXME - Support adding new constraints to existing ones. Do we need to |
367 | // consider subsumption? |
368 | return {}; |
369 | } |
370 | |
371 | std::optional<SourceLocation> ConstraintInsertionLoc = |
372 | findInsertionForConstraint(Function, Context); |
373 | if (!ConstraintInsertionLoc) |
374 | return {}; |
375 | |
376 | std::vector<FixItHint> FixIts; |
377 | FixIts.push_back(x: FixItHint::CreateReplacement( |
378 | RemoveRange: CharSourceRange::getTokenRange(R: EnableIf.Outer.getSourceRange()), |
379 | Code: *TypeText)); |
380 | FixIts.push_back(x: FixItHint::CreateInsertion( |
381 | InsertionLoc: *ConstraintInsertionLoc, Code: "requires "+ *ConditionText + " ")); |
382 | return FixIts; |
383 | } |
384 | |
385 | // Handle enable_if_t in a trailing template parameter, e.g., |
386 | // template <..., enable_if_t<Condition, Type> = Type{}> |
387 | // ReturnType function(); |
388 | // |
389 | // Return a vector of FixItHints if the code can be replaced with |
390 | // a C++20 requires clause. In the example above, returns FixItHints |
391 | // to result in |
392 | // template <...> |
393 | // ReturnType function() requires Condition {} |
394 | static std::vector<FixItHint> |
395 | handleTrailingTemplateType(const FunctionTemplateDecl *FunctionTemplate, |
396 | const FunctionDecl *Function, |
397 | const Decl *LastTemplateParam, |
398 | const EnableIfData &EnableIf, ASTContext &Context) { |
399 | SourceManager &SM = Context.getSourceManager(); |
400 | const LangOptions &LangOpts = Context.getLangOpts(); |
401 | |
402 | TemplateArgumentLoc EnableCondition = EnableIf.Loc.getArgLoc(0); |
403 | |
404 | SourceRange ConditionRange = getConditionRange(Context, EnableIf.Loc); |
405 | |
406 | std::optional<std::string> ConditionText = getConditionText( |
407 | ConditionExpr: EnableCondition.getSourceExpression(), ConditionRange, Context); |
408 | if (!ConditionText) |
409 | return {}; |
410 | |
411 | SmallVector<AssociatedConstraint, 3> ExistingConstraints; |
412 | Function->getAssociatedConstraints(ACs&: ExistingConstraints); |
413 | if (!ExistingConstraints.empty()) { |
414 | // FIXME - Support adding new constraints to existing ones. Do we need to |
415 | // consider subsumption? |
416 | return {}; |
417 | } |
418 | |
419 | SourceRange RemovalRange; |
420 | const TemplateParameterList *TemplateParams = |
421 | FunctionTemplate->getTemplateParameters(); |
422 | if (!TemplateParams || TemplateParams->size() == 0) |
423 | return {}; |
424 | |
425 | if (TemplateParams->size() == 1) { |
426 | RemovalRange = |
427 | SourceRange(TemplateParams->getTemplateLoc(), |
428 | getRAngleFileLoc(SM, Element: *TemplateParams).getLocWithOffset(Offset: 1)); |
429 | } else { |
430 | RemovalRange = |
431 | SourceRange(utils::lexer::findPreviousTokenKind( |
432 | Start: LastTemplateParam->getSourceRange().getBegin(), SM, |
433 | LangOpts, TK: tok::comma), |
434 | getRAngleFileLoc(SM, Element: *TemplateParams)); |
435 | } |
436 | |
437 | std::optional<SourceLocation> ConstraintInsertionLoc = |
438 | findInsertionForConstraint(Function, Context); |
439 | if (!ConstraintInsertionLoc) |
440 | return {}; |
441 | |
442 | std::vector<FixItHint> FixIts; |
443 | FixIts.push_back( |
444 | x: FixItHint::CreateRemoval(RemoveRange: CharSourceRange::getCharRange(R: RemovalRange))); |
445 | FixIts.push_back(x: FixItHint::CreateInsertion( |
446 | InsertionLoc: *ConstraintInsertionLoc, Code: "requires "+ *ConditionText + " ")); |
447 | return FixIts; |
448 | } |
449 | |
450 | void UseConstraintsCheck::check(const MatchFinder::MatchResult &Result) { |
451 | const auto *FunctionTemplate = |
452 | Result.Nodes.getNodeAs<FunctionTemplateDecl>(ID: "functionTemplate"); |
453 | const auto *Function = Result.Nodes.getNodeAs<FunctionDecl>(ID: "function"); |
454 | const auto *ReturnType = Result.Nodes.getNodeAs<TypeLoc>(ID: "return"); |
455 | if (!FunctionTemplate || !Function || !ReturnType) |
456 | return; |
457 | |
458 | // Check for |
459 | // |
460 | // Case 1. Return type of function |
461 | // |
462 | // template <...> |
463 | // enable_if_t<Condition, ReturnType>::type function() {} |
464 | // |
465 | // Case 2. Trailing template parameter |
466 | // |
467 | // template <..., enable_if_t<Condition, Type> = Type{}> |
468 | // ReturnType function() {} |
469 | // |
470 | // or |
471 | // |
472 | // template <..., typename = enable_if_t<Condition, void>> |
473 | // ReturnType function() {} |
474 | // |
475 | |
476 | // Case 1. Return type of function |
477 | if (auto EnableIf = matchEnableIfSpecialization(*ReturnType)) { |
478 | diag(Loc: ReturnType->getBeginLoc(), |
479 | Description: "use C++20 requires constraints instead of enable_if") |
480 | << handleReturnType(Function, *ReturnType, *EnableIf, *Result.Context); |
481 | return; |
482 | } |
483 | |
484 | // Case 2. Trailing template parameter |
485 | if (auto [EnableIf, LastTemplateParam] = |
486 | matchTrailingTemplateParam(FunctionTemplate); |
487 | EnableIf && LastTemplateParam) { |
488 | diag(LastTemplateParam->getSourceRange().getBegin(), |
489 | "use C++20 requires constraints instead of enable_if") |
490 | << handleTrailingTemplateType(FunctionTemplate, Function, |
491 | LastTemplateParam, *EnableIf, |
492 | *Result.Context); |
493 | return; |
494 | } |
495 | } |
496 | |
497 | } // namespace clang::tidy::modernize |
498 |
Definitions
- EnableIfData
- hasOtherDeclarations
- registerMatchers
- matchEnableIfSpecializationImplTypename
- matchEnableIfSpecializationImplTrait
- matchEnableIfSpecializationImpl
- matchEnableIfSpecialization
- matchTrailingTemplateParam
- getRAngleFileLoc
- getConditionRange
- getTypeRange
- getTypeText
- findInsertionForConstraint
- isPrimaryExpression
- getConditionText
- handleReturnType
- handleTrailingTemplateType
Update your C++ knowledge – Modern C++11/14/17 Training
Find out more