1 //===--- ConstReturnTypeCheck.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 "ConstReturnTypeCheck.h"
10 #include "../utils/LexerUtils.h"
11 #include "clang/AST/ASTContext.h"
12 #include "clang/ASTMatchers/ASTMatchFinder.h"
13 #include "clang/Basic/SourceLocation.h"
14 #include "clang/Lex/Lexer.h"
15 #include <optional>
16
17 using namespace clang::ast_matchers;
18
19 namespace clang::tidy::readability {
20
21 // Finds the location of the qualifying `const` token in the `FunctionDecl`'s
22 // return type. Returns `std::nullopt` when the return type is not
23 // `const`-qualified or `const` does not appear in `Def`'s source, like when the
24 // type is an alias or a macro.
25 static std::optional<Token>
findConstToRemove(const FunctionDecl * Def,const MatchFinder::MatchResult & Result)26 findConstToRemove(const FunctionDecl *Def,
27 const MatchFinder::MatchResult &Result) {
28 if (!Def->getReturnType().isLocalConstQualified())
29 return std::nullopt;
30
31 // Get the begin location for the function name, including any qualifiers
32 // written in the source (for out-of-line declarations). A FunctionDecl's
33 // "location" is the start of its name, so, when the name is unqualified, we
34 // use `getLocation()`.
35 SourceLocation NameBeginLoc = Def->getQualifier()
36 ? Def->getQualifierLoc().getBeginLoc()
37 : Def->getLocation();
38 // Since either of the locs can be in a macro, use `makeFileCharRange` to be
39 // sure that we have a consistent `CharSourceRange`, located entirely in the
40 // source file.
41 CharSourceRange FileRange = Lexer::makeFileCharRange(
42 CharSourceRange::getCharRange(Def->getBeginLoc(), NameBeginLoc),
43 *Result.SourceManager, Result.Context->getLangOpts());
44
45 if (FileRange.isInvalid())
46 return std::nullopt;
47
48 return utils::lexer::getQualifyingToken(
49 tok::kw_const, FileRange, *Result.Context, *Result.SourceManager);
50 }
51
52 namespace {
53
AST_MATCHER(QualType,isLocalConstQualified)54 AST_MATCHER(QualType, isLocalConstQualified) {
55 return Node.isLocalConstQualified();
56 }
57
58 struct CheckResult {
59 // Source range of the relevant `const` token in the definition being checked.
60 CharSourceRange ConstRange;
61
62 // FixItHints associated with the definition being checked.
63 llvm::SmallVector<clang::FixItHint, 4> Hints;
64
65 // Locations of any declarations that could not be fixed.
66 llvm::SmallVector<clang::SourceLocation, 4> DeclLocs;
67 };
68
69 } // namespace
70
71 // Does the actual work of the check.
checkDef(const clang::FunctionDecl * Def,const MatchFinder::MatchResult & MatchResult)72 static CheckResult checkDef(const clang::FunctionDecl *Def,
73 const MatchFinder::MatchResult &MatchResult) {
74 CheckResult Result;
75 std::optional<Token> Tok = findConstToRemove(Def, MatchResult);
76 if (!Tok)
77 return Result;
78
79 Result.ConstRange =
80 CharSourceRange::getCharRange(Tok->getLocation(), Tok->getEndLoc());
81 Result.Hints.push_back(FixItHint::CreateRemoval(Result.ConstRange));
82
83 // Fix the definition and any visible declarations, but don't warn
84 // separately for each declaration. Instead, associate all fixes with the
85 // single warning at the definition.
86 for (const FunctionDecl *Decl = Def->getPreviousDecl(); Decl != nullptr;
87 Decl = Decl->getPreviousDecl()) {
88 if (std::optional<Token> T = findConstToRemove(Decl, MatchResult))
89 Result.Hints.push_back(FixItHint::CreateRemoval(
90 CharSourceRange::getCharRange(T->getLocation(), T->getEndLoc())));
91 else
92 // `getInnerLocStart` gives the start of the return type.
93 Result.DeclLocs.push_back(Decl->getInnerLocStart());
94 }
95 return Result;
96 }
97
storeOptions(ClangTidyOptions::OptionMap & Opts)98 void ConstReturnTypeCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
99 Options.store(Opts, "IgnoreMacros", IgnoreMacros);
100 }
101
registerMatchers(MatchFinder * Finder)102 void ConstReturnTypeCheck::registerMatchers(MatchFinder *Finder) {
103 // Find all function definitions for which the return types are `const`
104 // qualified, ignoring decltype types.
105 Finder->addMatcher(
106 functionDecl(returns(isLocalConstQualified()),
107 anyOf(isDefinition(), cxxMethodDecl(isPure())),
108 // Overridden functions are not actionable.
109 unless(cxxMethodDecl(isOverride())))
110 .bind("func"),
111 this);
112 }
113
check(const MatchFinder::MatchResult & Result)114 void ConstReturnTypeCheck::check(const MatchFinder::MatchResult &Result) {
115 const auto *Def = Result.Nodes.getNodeAs<FunctionDecl>("func");
116 // Suppress the check if macros are involved.
117 if (IgnoreMacros &&
118 (Def->getBeginLoc().isMacroID() || Def->getEndLoc().isMacroID()))
119 return;
120
121 CheckResult CR = checkDef(Def, Result);
122 {
123 // Clang only supports one in-flight diagnostic at a time. So, delimit the
124 // scope of `Diagnostic` to allow further diagnostics after the scope. We
125 // use `getInnerLocStart` to get the start of the return type.
126 DiagnosticBuilder Diagnostic =
127 diag(Def->getInnerLocStart(),
128 "return type %0 is 'const'-qualified at the top level, which may "
129 "reduce code readability without improving const correctness")
130 << Def->getReturnType();
131 if (CR.ConstRange.isValid())
132 Diagnostic << CR.ConstRange;
133
134 // Do not propose fixes for virtual function.
135 const auto *Method = dyn_cast<CXXMethodDecl>(Def);
136 if (Method && Method->isVirtual())
137 return;
138
139 for (auto &Hint : CR.Hints)
140 Diagnostic << Hint;
141 }
142 for (auto Loc : CR.DeclLocs)
143 diag(Loc, "could not transform this declaration", DiagnosticIDs::Note);
144 }
145
146 } // namespace clang::tidy::readability
147