xref: /llvm-project/clang-tools-extra/clang-tidy/readability/FunctionSizeCheck.cpp (revision 7a73da4c85a12341752a4573c55ebff46ba20db0)
1 //===-- FunctionSizeCheck.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 "FunctionSizeCheck.h"
10 #include "clang/AST/RecursiveASTVisitor.h"
11 #include "clang/ASTMatchers/ASTMatchFinder.h"
12 #include "llvm/ADT/BitVector.h"
13 
14 using namespace clang::ast_matchers;
15 
16 namespace clang::tidy::readability {
17 namespace {
18 
19 class FunctionASTVisitor : public RecursiveASTVisitor<FunctionASTVisitor> {
20   using Base = RecursiveASTVisitor<FunctionASTVisitor>;
21 
22 public:
VisitVarDecl(VarDecl * VD)23   bool VisitVarDecl(VarDecl *VD) {
24     // Do not count function params.
25     // Do not count decomposition declarations (C++17's structured bindings).
26     if (StructNesting == 0 &&
27         !(isa<ParmVarDecl>(VD) || isa<DecompositionDecl>(VD)))
28       ++Info.Variables;
29     return true;
30   }
VisitBindingDecl(BindingDecl * BD)31   bool VisitBindingDecl(BindingDecl *BD) {
32     // Do count each of the bindings (in the decomposition declaration).
33     if (StructNesting == 0)
34       ++Info.Variables;
35     return true;
36   }
37 
TraverseStmt(Stmt * Node)38   bool TraverseStmt(Stmt *Node) {
39     if (!Node)
40       return Base::TraverseStmt(Node);
41 
42     if (TrackedParent.back() && !isa<CompoundStmt>(Node))
43       ++Info.Statements;
44 
45     switch (Node->getStmtClass()) {
46     case Stmt::IfStmtClass:
47     case Stmt::WhileStmtClass:
48     case Stmt::DoStmtClass:
49     case Stmt::CXXForRangeStmtClass:
50     case Stmt::ForStmtClass:
51     case Stmt::SwitchStmtClass:
52       ++Info.Branches;
53       [[fallthrough]];
54     case Stmt::CompoundStmtClass:
55       TrackedParent.push_back(true);
56       break;
57     default:
58       TrackedParent.push_back(false);
59       break;
60     }
61 
62     Base::TraverseStmt(Node);
63 
64     TrackedParent.pop_back();
65 
66     return true;
67   }
68 
TraverseCompoundStmt(CompoundStmt * Node)69   bool TraverseCompoundStmt(CompoundStmt *Node) {
70     // If this new compound statement is located in a compound statement, which
71     // is already nested NestingThreshold levels deep, record the start location
72     // of this new compound statement.
73     if (CurrentNestingLevel == Info.NestingThreshold)
74       Info.NestingThresholders.push_back(Node->getBeginLoc());
75 
76     ++CurrentNestingLevel;
77     Base::TraverseCompoundStmt(Node);
78     --CurrentNestingLevel;
79 
80     return true;
81   }
82 
TraverseDecl(Decl * Node)83   bool TraverseDecl(Decl *Node) {
84     TrackedParent.push_back(false);
85     Base::TraverseDecl(Node);
86     TrackedParent.pop_back();
87     return true;
88   }
89 
TraverseLambdaExpr(LambdaExpr * Node)90   bool TraverseLambdaExpr(LambdaExpr *Node) {
91     ++StructNesting;
92     Base::TraverseLambdaExpr(Node);
93     --StructNesting;
94     return true;
95   }
96 
TraverseCXXRecordDecl(CXXRecordDecl * Node)97   bool TraverseCXXRecordDecl(CXXRecordDecl *Node) {
98     ++StructNesting;
99     Base::TraverseCXXRecordDecl(Node);
100     --StructNesting;
101     return true;
102   }
103 
TraverseStmtExpr(StmtExpr * SE)104   bool TraverseStmtExpr(StmtExpr *SE) {
105     ++StructNesting;
106     Base::TraverseStmtExpr(SE);
107     --StructNesting;
108     return true;
109   }
110 
111   struct FunctionInfo {
112     unsigned Lines = 0;
113     unsigned Statements = 0;
114     unsigned Branches = 0;
115     unsigned NestingThreshold = 0;
116     unsigned Variables = 0;
117     std::vector<SourceLocation> NestingThresholders;
118   };
119   FunctionInfo Info;
120   llvm::BitVector TrackedParent;
121   unsigned StructNesting = 0;
122   unsigned CurrentNestingLevel = 0;
123 };
124 
125 } // namespace
126 
FunctionSizeCheck(StringRef Name,ClangTidyContext * Context)127 FunctionSizeCheck::FunctionSizeCheck(StringRef Name, ClangTidyContext *Context)
128     : ClangTidyCheck(Name, Context),
129       LineThreshold(Options.get("LineThreshold", DefaultLineThreshold)),
130       StatementThreshold(
131           Options.get("StatementThreshold", DefaultStatementThreshold)),
132       BranchThreshold(Options.get("BranchThreshold", DefaultBranchThreshold)),
133       ParameterThreshold(
134           Options.get("ParameterThreshold", DefaultParameterThreshold)),
135       NestingThreshold(
136           Options.get("NestingThreshold", DefaultNestingThreshold)),
137       VariableThreshold(
138           Options.get("VariableThreshold", DefaultVariableThreshold)) {}
139 
storeOptions(ClangTidyOptions::OptionMap & Opts)140 void FunctionSizeCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
141   Options.store(Opts, "LineThreshold", LineThreshold);
142   Options.store(Opts, "StatementThreshold", StatementThreshold);
143   Options.store(Opts, "BranchThreshold", BranchThreshold);
144   Options.store(Opts, "ParameterThreshold", ParameterThreshold);
145   Options.store(Opts, "NestingThreshold", NestingThreshold);
146   Options.store(Opts, "VariableThreshold", VariableThreshold);
147 }
148 
registerMatchers(MatchFinder * Finder)149 void FunctionSizeCheck::registerMatchers(MatchFinder *Finder) {
150   // Lambdas ignored - historically considered part of enclosing function.
151   // FIXME: include them instead? Top-level lambdas are currently never counted.
152   Finder->addMatcher(functionDecl(unless(isInstantiated()),
153                                   unless(cxxMethodDecl(ofClass(isLambda()))))
154                          .bind("func"),
155                      this);
156 }
157 
check(const MatchFinder::MatchResult & Result)158 void FunctionSizeCheck::check(const MatchFinder::MatchResult &Result) {
159   const auto *Func = Result.Nodes.getNodeAs<FunctionDecl>("func");
160 
161   FunctionASTVisitor Visitor;
162   Visitor.Info.NestingThreshold = NestingThreshold.value_or(-1);
163   Visitor.TraverseDecl(const_cast<FunctionDecl *>(Func));
164   auto &FI = Visitor.Info;
165 
166   if (FI.Statements == 0)
167     return;
168 
169   // Count the lines including whitespace and comments. Really simple.
170   if (const Stmt *Body = Func->getBody()) {
171     SourceManager *SM = Result.SourceManager;
172     if (SM->isWrittenInSameFile(Body->getBeginLoc(), Body->getEndLoc())) {
173       FI.Lines = SM->getSpellingLineNumber(Body->getEndLoc()) -
174                  SM->getSpellingLineNumber(Body->getBeginLoc());
175     }
176   }
177 
178   unsigned ActualNumberParameters = Func->getNumParams();
179 
180   if ((LineThreshold && FI.Lines > LineThreshold) ||
181       (StatementThreshold && FI.Statements > StatementThreshold) ||
182       (BranchThreshold && FI.Branches > BranchThreshold) ||
183       (ParameterThreshold && ActualNumberParameters > ParameterThreshold) ||
184       !FI.NestingThresholders.empty() ||
185       (VariableThreshold && FI.Variables > VariableThreshold)) {
186     diag(Func->getLocation(),
187          "function %0 exceeds recommended size/complexity thresholds")
188         << Func;
189   }
190 
191   if (LineThreshold && FI.Lines > LineThreshold) {
192     diag(Func->getLocation(),
193          "%0 lines including whitespace and comments (threshold %1)",
194          DiagnosticIDs::Note)
195         << FI.Lines << LineThreshold.value();
196   }
197 
198   if (StatementThreshold && FI.Statements > StatementThreshold) {
199     diag(Func->getLocation(), "%0 statements (threshold %1)",
200          DiagnosticIDs::Note)
201         << FI.Statements << StatementThreshold.value();
202   }
203 
204   if (BranchThreshold && FI.Branches > BranchThreshold) {
205     diag(Func->getLocation(), "%0 branches (threshold %1)", DiagnosticIDs::Note)
206         << FI.Branches << BranchThreshold.value();
207   }
208 
209   if (ParameterThreshold && ActualNumberParameters > ParameterThreshold) {
210     diag(Func->getLocation(), "%0 parameters (threshold %1)",
211          DiagnosticIDs::Note)
212         << ActualNumberParameters << ParameterThreshold.value();
213   }
214 
215   for (const auto &CSPos : FI.NestingThresholders) {
216     diag(CSPos, "nesting level %0 starts here (threshold %1)",
217          DiagnosticIDs::Note)
218         << NestingThreshold.value() + 1 << NestingThreshold.value();
219   }
220 
221   if (VariableThreshold && FI.Variables > VariableThreshold) {
222     diag(Func->getLocation(), "%0 variables (threshold %1)",
223          DiagnosticIDs::Note)
224         << FI.Variables << VariableThreshold.value();
225   }
226 }
227 
228 } // namespace clang::tidy::readability
229