1 | //===--- UseTrailingReturnTypeCheck.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 "UseTrailingReturnTypeCheck.h" |
10 | #include "clang/AST/ASTContext.h" |
11 | #include "clang/AST/RecursiveASTVisitor.h" |
12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
13 | #include "clang/Lex/Preprocessor.h" |
14 | #include "clang/Tooling/FixIt.h" |
15 | #include "llvm/ADT/StringExtras.h" |
16 | |
17 | #include <cctype> |
18 | #include <optional> |
19 | |
20 | using namespace clang::ast_matchers; |
21 | |
22 | namespace clang::tidy::modernize { |
23 | namespace { |
24 | struct UnqualNameVisitor : public RecursiveASTVisitor<UnqualNameVisitor> { |
25 | public: |
26 | UnqualNameVisitor(const FunctionDecl &F) : F(F) {} |
27 | |
28 | bool Collision = false; |
29 | |
30 | bool shouldWalkTypesOfTypeLocs() const { return false; } |
31 | |
32 | bool visitUnqualName(StringRef UnqualName) { |
33 | // Check for collisions with function arguments. |
34 | for (ParmVarDecl *Param : F.parameters()) |
35 | if (const IdentifierInfo *Ident = Param->getIdentifier()) |
36 | if (Ident->getName() == UnqualName) { |
37 | Collision = true; |
38 | return true; |
39 | } |
40 | return false; |
41 | } |
42 | |
43 | bool TraverseTypeLoc(TypeLoc TL, bool Elaborated = false) { |
44 | if (TL.isNull()) |
45 | return true; |
46 | |
47 | if (!Elaborated) { |
48 | switch (TL.getTypeLocClass()) { |
49 | case TypeLoc::Record: |
50 | if (visitUnqualName( |
51 | UnqualName: TL.getAs<RecordTypeLoc>().getTypePtr()->getDecl()->getName())) |
52 | return false; |
53 | break; |
54 | case TypeLoc::Enum: |
55 | if (visitUnqualName( |
56 | UnqualName: TL.getAs<EnumTypeLoc>().getTypePtr()->getDecl()->getName())) |
57 | return false; |
58 | break; |
59 | case TypeLoc::TemplateSpecialization: |
60 | if (visitUnqualName(UnqualName: TL.getAs<TemplateSpecializationTypeLoc>() |
61 | .getTypePtr() |
62 | ->getTemplateName() |
63 | .getAsTemplateDecl() |
64 | ->getName())) |
65 | return false; |
66 | break; |
67 | case TypeLoc::Typedef: |
68 | if (visitUnqualName( |
69 | UnqualName: TL.getAs<TypedefTypeLoc>().getTypePtr()->getDecl()->getName())) |
70 | return false; |
71 | break; |
72 | case TypeLoc::Using: |
73 | if (visitUnqualName(UnqualName: TL.getAs<UsingTypeLoc>() |
74 | .getTypePtr() |
75 | ->getFoundDecl() |
76 | ->getName())) |
77 | return false; |
78 | break; |
79 | default: |
80 | break; |
81 | } |
82 | } |
83 | |
84 | return RecursiveASTVisitor<UnqualNameVisitor>::TraverseTypeLoc(TL); |
85 | } |
86 | |
87 | // Replace the base method in order to call our own |
88 | // TraverseTypeLoc(). |
89 | bool TraverseQualifiedTypeLoc(QualifiedTypeLoc TL) { |
90 | return TraverseTypeLoc(TL: TL.getUnqualifiedLoc()); |
91 | } |
92 | |
93 | // Replace the base version to inform TraverseTypeLoc that the type is |
94 | // elaborated. |
95 | bool TraverseElaboratedTypeLoc(ElaboratedTypeLoc TL) { |
96 | if (TL.getQualifierLoc() && |
97 | !TraverseNestedNameSpecifierLoc(NNS: TL.getQualifierLoc())) |
98 | return false; |
99 | const auto *T = TL.getTypePtr(); |
100 | return TraverseTypeLoc(TL: TL.getNamedTypeLoc(), |
101 | Elaborated: T->getKeyword() != ElaboratedTypeKeyword::None || |
102 | T->getQualifier()); |
103 | } |
104 | |
105 | bool VisitDeclRefExpr(DeclRefExpr *S) { |
106 | DeclarationName Name = S->getNameInfo().getName(); |
107 | return S->getQualifierLoc() || Name.isEmpty() || !Name.isIdentifier() || |
108 | !visitUnqualName(UnqualName: Name.getAsIdentifierInfo()->getName()); |
109 | } |
110 | |
111 | private: |
112 | const FunctionDecl &F; |
113 | }; |
114 | } // namespace |
115 | |
116 | constexpr llvm::StringLiteral Message = |
117 | "use a trailing return type for this function" ; |
118 | |
119 | static SourceLocation expandIfMacroId(SourceLocation Loc, |
120 | const SourceManager &SM) { |
121 | if (Loc.isMacroID()) |
122 | Loc = expandIfMacroId(Loc: SM.getImmediateExpansionRange(Loc).getBegin(), SM); |
123 | assert(!Loc.isMacroID() && |
124 | "SourceLocation must not be a macro ID after recursive expansion" ); |
125 | return Loc; |
126 | } |
127 | |
128 | SourceLocation UseTrailingReturnTypeCheck::findTrailingReturnTypeSourceLocation( |
129 | const FunctionDecl &F, const FunctionTypeLoc &FTL, const ASTContext &Ctx, |
130 | const SourceManager &SM, const LangOptions &LangOpts) { |
131 | // We start with the location of the closing parenthesis. |
132 | SourceRange ExceptionSpecRange = F.getExceptionSpecSourceRange(); |
133 | if (ExceptionSpecRange.isValid()) |
134 | return Lexer::getLocForEndOfToken(Loc: ExceptionSpecRange.getEnd(), Offset: 0, SM, |
135 | LangOpts); |
136 | |
137 | // If the function argument list ends inside of a macro, it is dangerous to |
138 | // start lexing from here - bail out. |
139 | SourceLocation ClosingParen = FTL.getRParenLoc(); |
140 | if (ClosingParen.isMacroID()) |
141 | return {}; |
142 | |
143 | SourceLocation Result = |
144 | Lexer::getLocForEndOfToken(Loc: ClosingParen, Offset: 0, SM, LangOpts); |
145 | |
146 | // Skip subsequent CV and ref qualifiers. |
147 | std::pair<FileID, unsigned> Loc = SM.getDecomposedLoc(Loc: Result); |
148 | StringRef File = SM.getBufferData(FID: Loc.first); |
149 | const char *TokenBegin = File.data() + Loc.second; |
150 | Lexer Lexer(SM.getLocForStartOfFile(FID: Loc.first), LangOpts, File.begin(), |
151 | TokenBegin, File.end()); |
152 | Token T; |
153 | while (!Lexer.LexFromRawLexer(Result&: T)) { |
154 | if (T.is(K: tok::raw_identifier)) { |
155 | IdentifierInfo &Info = Ctx.Idents.get( |
156 | Name: StringRef(SM.getCharacterData(SL: T.getLocation()), T.getLength())); |
157 | T.setIdentifierInfo(&Info); |
158 | T.setKind(Info.getTokenID()); |
159 | } |
160 | |
161 | if (T.isOneOf(K1: tok::amp, Ks: tok::ampamp, Ks: tok::kw_const, Ks: tok::kw_volatile, |
162 | Ks: tok::kw_restrict)) { |
163 | Result = T.getEndLoc(); |
164 | continue; |
165 | } |
166 | break; |
167 | } |
168 | return Result; |
169 | } |
170 | |
171 | static bool isCvr(Token T) { |
172 | return T.isOneOf(K1: tok::kw_const, Ks: tok::kw_volatile, Ks: tok::kw_restrict); |
173 | } |
174 | |
175 | static bool isSpecifier(Token T) { |
176 | return T.isOneOf(K1: tok::kw_constexpr, Ks: tok::kw_inline, Ks: tok::kw_extern, |
177 | Ks: tok::kw_static, Ks: tok::kw_friend, Ks: tok::kw_virtual); |
178 | } |
179 | |
180 | static std::optional<ClassifiedToken> |
181 | classifyToken(const FunctionDecl &F, Preprocessor &PP, Token Tok) { |
182 | ClassifiedToken CT; |
183 | CT.T = Tok; |
184 | CT.IsQualifier = true; |
185 | CT.IsSpecifier = true; |
186 | bool ContainsQualifiers = false; |
187 | bool ContainsSpecifiers = false; |
188 | bool ContainsSomethingElse = false; |
189 | |
190 | Token End; |
191 | End.startToken(); |
192 | End.setKind(tok::eof); |
193 | SmallVector<Token, 2> Stream{Tok, End}; |
194 | |
195 | // FIXME: do not report these token to Preprocessor.TokenWatcher. |
196 | PP.EnterTokenStream(Toks: Stream, DisableMacroExpansion: false, /*IsReinject=*/false); |
197 | while (true) { |
198 | Token T; |
199 | PP.Lex(Result&: T); |
200 | if (T.is(K: tok::eof)) |
201 | break; |
202 | |
203 | bool Qual = isCvr(T); |
204 | bool Spec = isSpecifier(T); |
205 | CT.IsQualifier &= Qual; |
206 | CT.IsSpecifier &= Spec; |
207 | ContainsQualifiers |= Qual; |
208 | ContainsSpecifiers |= Spec; |
209 | ContainsSomethingElse |= !Qual && !Spec; |
210 | } |
211 | |
212 | // If the Token/Macro contains more than one type of tokens, we would need |
213 | // to split the macro in order to move parts to the trailing return type. |
214 | if (ContainsQualifiers + ContainsSpecifiers + ContainsSomethingElse > 1) |
215 | return std::nullopt; |
216 | |
217 | return CT; |
218 | } |
219 | |
220 | std::optional<SmallVector<ClassifiedToken, 8>> |
221 | UseTrailingReturnTypeCheck::classifyTokensBeforeFunctionName( |
222 | const FunctionDecl &F, const ASTContext &Ctx, const SourceManager &SM, |
223 | const LangOptions &LangOpts) { |
224 | SourceLocation BeginF = expandIfMacroId(F.getBeginLoc(), SM); |
225 | SourceLocation BeginNameF = expandIfMacroId(F.getLocation(), SM); |
226 | |
227 | // Create tokens for everything before the name of the function. |
228 | std::pair<FileID, unsigned> Loc = SM.getDecomposedLoc(Loc: BeginF); |
229 | StringRef File = SM.getBufferData(FID: Loc.first); |
230 | const char *TokenBegin = File.data() + Loc.second; |
231 | Lexer Lexer(SM.getLocForStartOfFile(FID: Loc.first), LangOpts, File.begin(), |
232 | TokenBegin, File.end()); |
233 | Token T; |
234 | SmallVector<ClassifiedToken, 8> ClassifiedTokens; |
235 | while (!Lexer.LexFromRawLexer(Result&: T) && |
236 | SM.isBeforeInTranslationUnit(LHS: T.getLocation(), RHS: BeginNameF)) { |
237 | if (T.is(K: tok::raw_identifier)) { |
238 | IdentifierInfo &Info = Ctx.Idents.get( |
239 | Name: StringRef(SM.getCharacterData(SL: T.getLocation()), T.getLength())); |
240 | |
241 | if (Info.hasMacroDefinition()) { |
242 | const MacroInfo *MI = PP->getMacroInfo(II: &Info); |
243 | if (!MI || MI->isFunctionLike()) { |
244 | // Cannot handle function style macros. |
245 | diag(F.getLocation(), Message); |
246 | return std::nullopt; |
247 | } |
248 | } |
249 | |
250 | T.setIdentifierInfo(&Info); |
251 | T.setKind(Info.getTokenID()); |
252 | } |
253 | |
254 | if (std::optional<ClassifiedToken> CT = classifyToken(F, PP&: *PP, Tok: T)) |
255 | ClassifiedTokens.push_back(Elt: *CT); |
256 | else { |
257 | diag(F.getLocation(), Message); |
258 | return std::nullopt; |
259 | } |
260 | } |
261 | |
262 | return ClassifiedTokens; |
263 | } |
264 | |
265 | static bool hasAnyNestedLocalQualifiers(QualType Type) { |
266 | bool Result = Type.hasLocalQualifiers(); |
267 | if (Type->isPointerType()) |
268 | Result = Result || hasAnyNestedLocalQualifiers( |
269 | Type: Type->castAs<PointerType>()->getPointeeType()); |
270 | if (Type->isReferenceType()) |
271 | Result = Result || hasAnyNestedLocalQualifiers( |
272 | Type: Type->castAs<ReferenceType>()->getPointeeType()); |
273 | return Result; |
274 | } |
275 | |
276 | SourceRange UseTrailingReturnTypeCheck::findReturnTypeAndCVSourceRange( |
277 | const FunctionDecl &F, const TypeLoc &ReturnLoc, const ASTContext &Ctx, |
278 | const SourceManager &SM, const LangOptions &LangOpts) { |
279 | |
280 | // We start with the range of the return type and expand to neighboring |
281 | // qualifiers (const, volatile and restrict). |
282 | SourceRange ReturnTypeRange = F.getReturnTypeSourceRange(); |
283 | if (ReturnTypeRange.isInvalid()) { |
284 | // Happens if e.g. clang cannot resolve all includes and the return type is |
285 | // unknown. |
286 | diag(F.getLocation(), Message); |
287 | return {}; |
288 | } |
289 | |
290 | |
291 | // If the return type has no local qualifiers, it's source range is accurate. |
292 | if (!hasAnyNestedLocalQualifiers(Type: F.getReturnType())) |
293 | return ReturnTypeRange; |
294 | |
295 | // Include qualifiers to the left and right of the return type. |
296 | std::optional<SmallVector<ClassifiedToken, 8>> MaybeTokens = |
297 | classifyTokensBeforeFunctionName(F, Ctx, SM, LangOpts); |
298 | if (!MaybeTokens) |
299 | return {}; |
300 | const SmallVector<ClassifiedToken, 8> &Tokens = *MaybeTokens; |
301 | |
302 | ReturnTypeRange.setBegin(expandIfMacroId(Loc: ReturnTypeRange.getBegin(), SM)); |
303 | ReturnTypeRange.setEnd(expandIfMacroId(Loc: ReturnTypeRange.getEnd(), SM)); |
304 | |
305 | bool ExtendedLeft = false; |
306 | for (size_t I = 0; I < Tokens.size(); I++) { |
307 | // If we found the beginning of the return type, include left qualifiers. |
308 | if (!SM.isBeforeInTranslationUnit(LHS: Tokens[I].T.getLocation(), |
309 | RHS: ReturnTypeRange.getBegin()) && |
310 | !ExtendedLeft) { |
311 | assert(I <= size_t(std::numeric_limits<int>::max()) && |
312 | "Integer overflow detected" ); |
313 | for (int J = static_cast<int>(I) - 1; J >= 0 && Tokens[J].IsQualifier; |
314 | J--) |
315 | ReturnTypeRange.setBegin(Tokens[J].T.getLocation()); |
316 | ExtendedLeft = true; |
317 | } |
318 | // If we found the end of the return type, include right qualifiers. |
319 | if (SM.isBeforeInTranslationUnit(LHS: ReturnTypeRange.getEnd(), |
320 | RHS: Tokens[I].T.getLocation())) { |
321 | for (size_t J = I; J < Tokens.size() && Tokens[J].IsQualifier; J++) |
322 | ReturnTypeRange.setEnd(Tokens[J].T.getLocation()); |
323 | break; |
324 | } |
325 | } |
326 | |
327 | assert(!ReturnTypeRange.getBegin().isMacroID() && |
328 | "Return type source range begin must not be a macro" ); |
329 | assert(!ReturnTypeRange.getEnd().isMacroID() && |
330 | "Return type source range end must not be a macro" ); |
331 | return ReturnTypeRange; |
332 | } |
333 | |
334 | void UseTrailingReturnTypeCheck::keepSpecifiers( |
335 | std::string &ReturnType, std::string &Auto, SourceRange ReturnTypeCVRange, |
336 | const FunctionDecl &F, const FriendDecl *Fr, const ASTContext &Ctx, |
337 | const SourceManager &SM, const LangOptions &LangOpts) { |
338 | // Check if there are specifiers inside the return type. E.g. unsigned |
339 | // inline int. |
340 | const auto *M = dyn_cast<CXXMethodDecl>(Val: &F); |
341 | if (!F.isConstexpr() && !F.isInlineSpecified() && |
342 | F.getStorageClass() != SC_Extern && F.getStorageClass() != SC_Static && |
343 | !Fr && !(M && M->isVirtualAsWritten())) |
344 | return; |
345 | |
346 | // Tokenize return type. If it contains macros which contain a mix of |
347 | // qualifiers, specifiers and types, give up. |
348 | std::optional<SmallVector<ClassifiedToken, 8>> MaybeTokens = |
349 | classifyTokensBeforeFunctionName(F, Ctx, SM, LangOpts); |
350 | if (!MaybeTokens) |
351 | return; |
352 | |
353 | // Find specifiers, remove them from the return type, add them to 'auto'. |
354 | unsigned int ReturnTypeBeginOffset = |
355 | SM.getDecomposedLoc(Loc: ReturnTypeCVRange.getBegin()).second; |
356 | size_t InitialAutoLength = Auto.size(); |
357 | unsigned int DeletedChars = 0; |
358 | for (ClassifiedToken CT : *MaybeTokens) { |
359 | if (SM.isBeforeInTranslationUnit(LHS: CT.T.getLocation(), |
360 | RHS: ReturnTypeCVRange.getBegin()) || |
361 | SM.isBeforeInTranslationUnit(LHS: ReturnTypeCVRange.getEnd(), |
362 | RHS: CT.T.getLocation())) |
363 | continue; |
364 | if (!CT.IsSpecifier) |
365 | continue; |
366 | |
367 | // Add the token to 'auto' and remove it from the return type, including |
368 | // any whitespace following the token. |
369 | unsigned int TOffset = SM.getDecomposedLoc(Loc: CT.T.getLocation()).second; |
370 | assert(TOffset >= ReturnTypeBeginOffset && |
371 | "Token location must be after the beginning of the return type" ); |
372 | unsigned int TOffsetInRT = TOffset - ReturnTypeBeginOffset - DeletedChars; |
373 | unsigned int TLengthWithWS = CT.T.getLength(); |
374 | while (TOffsetInRT + TLengthWithWS < ReturnType.size() && |
375 | llvm::isSpace(C: ReturnType[TOffsetInRT + TLengthWithWS])) |
376 | TLengthWithWS++; |
377 | std::string Specifier = ReturnType.substr(pos: TOffsetInRT, n: TLengthWithWS); |
378 | if (!llvm::isSpace(C: Specifier.back())) |
379 | Specifier.push_back(c: ' '); |
380 | Auto.insert(pos1: Auto.size() - InitialAutoLength, str: Specifier); |
381 | ReturnType.erase(pos: TOffsetInRT, n: TLengthWithWS); |
382 | DeletedChars += TLengthWithWS; |
383 | } |
384 | } |
385 | |
386 | void UseTrailingReturnTypeCheck::registerMatchers(MatchFinder *Finder) { |
387 | auto F = functionDecl( |
388 | unless(anyOf(hasTrailingReturn(), returns(InnerMatcher: voidType()), |
389 | cxxConversionDecl(), cxxMethodDecl(isImplicit())))) |
390 | .bind(ID: "Func" ); |
391 | |
392 | Finder->addMatcher(NodeMatch: F, Action: this); |
393 | Finder->addMatcher(NodeMatch: friendDecl(hasDescendant(F)).bind(ID: "Friend" ), Action: this); |
394 | } |
395 | |
396 | void UseTrailingReturnTypeCheck::registerPPCallbacks( |
397 | const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) { |
398 | this->PP = PP; |
399 | } |
400 | |
401 | void UseTrailingReturnTypeCheck::check(const MatchFinder::MatchResult &Result) { |
402 | assert(PP && "Expected registerPPCallbacks() to have been called before so " |
403 | "preprocessor is available" ); |
404 | |
405 | const auto *F = Result.Nodes.getNodeAs<FunctionDecl>(ID: "Func" ); |
406 | const auto *Fr = Result.Nodes.getNodeAs<FriendDecl>(ID: "Friend" ); |
407 | assert(F && "Matcher is expected to find only FunctionDecls" ); |
408 | |
409 | // Three-way comparison operator<=> is syntactic sugar and generates implicit |
410 | // nodes for all other operators. |
411 | if (F->getLocation().isInvalid() || F->isImplicit()) |
412 | return; |
413 | |
414 | // Skip functions which return 'auto' and defaulted operators. |
415 | const auto *AT = F->getDeclaredReturnType()->getAs<AutoType>(); |
416 | if (AT != nullptr && |
417 | ((!AT->isConstrained() && AT->getKeyword() == AutoTypeKeyword::Auto && |
418 | !hasAnyNestedLocalQualifiers(Type: F->getDeclaredReturnType())) || |
419 | F->isDefaulted())) |
420 | return; |
421 | |
422 | // TODO: implement those |
423 | if (F->getDeclaredReturnType()->isFunctionPointerType() || |
424 | F->getDeclaredReturnType()->isMemberFunctionPointerType() || |
425 | F->getDeclaredReturnType()->isMemberPointerType()) { |
426 | diag(F->getLocation(), Message); |
427 | return; |
428 | } |
429 | |
430 | const ASTContext &Ctx = *Result.Context; |
431 | const SourceManager &SM = *Result.SourceManager; |
432 | const LangOptions &LangOpts = getLangOpts(); |
433 | |
434 | const TypeSourceInfo *TSI = F->getTypeSourceInfo(); |
435 | if (!TSI) |
436 | return; |
437 | |
438 | auto FTL = TSI->getTypeLoc().IgnoreParens().getAs<FunctionTypeLoc>(); |
439 | if (!FTL) { |
440 | // FIXME: This may happen if we have __attribute__((...)) on the function. |
441 | // We abort for now. Remove this when the function type location gets |
442 | // available in clang. |
443 | diag(F->getLocation(), Message); |
444 | return; |
445 | } |
446 | |
447 | SourceLocation InsertionLoc = |
448 | findTrailingReturnTypeSourceLocation(F: *F, FTL: FTL, Ctx, SM, LangOpts); |
449 | if (InsertionLoc.isInvalid()) { |
450 | diag(F->getLocation(), Message); |
451 | return; |
452 | } |
453 | |
454 | // Using the declared return type via F->getDeclaredReturnType().getAsString() |
455 | // discards user formatting and order of const, volatile, type, whitespace, |
456 | // space before & ... . |
457 | SourceRange ReturnTypeCVRange = |
458 | findReturnTypeAndCVSourceRange(F: *F, ReturnLoc: FTL.getReturnLoc(), Ctx, SM, LangOpts); |
459 | if (ReturnTypeCVRange.isInvalid()) |
460 | return; |
461 | |
462 | // Check if unqualified names in the return type conflict with other entities |
463 | // after the rewrite. |
464 | // FIXME: this could be done better, by performing a lookup of all |
465 | // unqualified names in the return type in the scope of the function. If the |
466 | // lookup finds a different entity than the original entity identified by the |
467 | // name, then we can either not perform a rewrite or explicitly qualify the |
468 | // entity. Such entities could be function parameter names, (inherited) class |
469 | // members, template parameters, etc. |
470 | UnqualNameVisitor UNV{*F}; |
471 | UNV.TraverseTypeLoc(TL: FTL.getReturnLoc()); |
472 | if (UNV.Collision) { |
473 | diag(F->getLocation(), Message); |
474 | return; |
475 | } |
476 | |
477 | SourceLocation ReturnTypeEnd = |
478 | Lexer::getLocForEndOfToken(Loc: ReturnTypeCVRange.getEnd(), Offset: 0, SM, LangOpts); |
479 | StringRef CharAfterReturnType = Lexer::getSourceText( |
480 | Range: CharSourceRange::getCharRange(B: ReturnTypeEnd, |
481 | E: ReturnTypeEnd.getLocWithOffset(Offset: 1)), |
482 | SM, LangOpts); |
483 | bool NeedSpaceAfterAuto = |
484 | CharAfterReturnType.empty() || !llvm::isSpace(C: CharAfterReturnType[0]); |
485 | |
486 | std::string Auto = NeedSpaceAfterAuto ? "auto " : "auto" ; |
487 | std::string ReturnType = |
488 | std::string(tooling::fixit::getText(Node: ReturnTypeCVRange, Context: Ctx)); |
489 | keepSpecifiers(ReturnType, Auto, ReturnTypeCVRange, F: *F, Fr, Ctx, SM, |
490 | LangOpts); |
491 | |
492 | diag(F->getLocation(), Message) |
493 | << FixItHint::CreateReplacement(RemoveRange: ReturnTypeCVRange, Code: Auto) |
494 | << FixItHint::CreateInsertion(InsertionLoc, Code: " -> " + ReturnType); |
495 | } |
496 | |
497 | } // namespace clang::tidy::modernize |
498 | |