clang-tools 24.0.0git
FunctionCognitiveComplexityCheck.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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
11#include "clang/AST/Decl.h"
12#include "clang/AST/DeclBase.h"
13#include "clang/AST/Expr.h"
14#include "clang/AST/RecursiveASTVisitor.h"
15#include "clang/AST/Stmt.h"
16#include "clang/ASTMatchers/ASTMatchFinder.h"
17#include "clang/ASTMatchers/ASTMatchers.h"
18#include "clang/ASTMatchers/ASTMatchersInternal.h"
19#include "clang/Basic/Diagnostic.h"
20#include "clang/Basic/DiagnosticIDs.h"
21#include "clang/Basic/LLVM.h"
22#include "clang/Basic/SourceLocation.h"
23#include "llvm/ADT/BitmaskEnum.h"
24#include "llvm/Support/ErrorHandling.h"
25#include <array>
26#include <cassert>
27#include <optional>
28#include <stack>
29#include <utility>
30
31using namespace clang::ast_matchers;
32
34namespace {
35
36struct CognitiveComplexity final {
37 // Any increment is based on some combination of reasons.
38 // For details you can look at the Specification at
39 // https://www.sonarsource.com/docs/CognitiveComplexity.pdf
40 // or user-facing docs at
41 // https://clang.llvm.org/extra/clang-tidy/checks/readability/function-cognitive-complexity.html
42 // Here are all the possible reasons:
43 enum Criteria : uint8_t {
44 None = 0U,
45
46 // B1, increases cognitive complexity (by 1)
47 // What causes it:
48 // * if, else if, else, ConditionalOperator (not BinaryConditionalOperator)
49 // * SwitchStmt
50 // * ForStmt, CXXForRangeStmt
51 // * WhileStmt, DoStmt
52 // * CXXCatchStmt
53 // * GotoStmt, IndirectGotoStmt (but not BreakStmt, ContinueStmt)
54 // * sequences of binary logical operators (BinOpLAnd, BinOpLOr)
55 // * each method in a recursion cycle (not implemented)
56 Increment = 1U << 0,
57
58 // B2, increases current nesting level (by 1)
59 // What causes it:
60 // * if, else if, else, ConditionalOperator (not BinaryConditionalOperator)
61 // * SwitchStmt
62 // * ForStmt, CXXForRangeStmt
63 // * WhileStmt, DoStmt
64 // * CXXCatchStmt
65 // * nested CXXConstructor, CXXDestructor, CXXMethod (incl. C++11 Lambda)
66 // * GNU Statement Expression
67 // * Apple Block declaration
68 IncrementNesting = 1U << 1,
69
70 // B3, increases cognitive complexity by the current nesting level
71 // Applied before IncrementNesting
72 // What causes it:
73 // * IfStmt, ConditionalOperator (not BinaryConditionalOperator)
74 // * SwitchStmt
75 // * ForStmt, CXXForRangeStmt
76 // * WhileStmt, DoStmt
77 // * CXXCatchStmt
78 PenalizeNesting = 1U << 2,
79
80 All = Increment | PenalizeNesting | IncrementNesting,
81
82 LLVM_MARK_AS_BITMASK_ENUM(PenalizeNesting),
83 };
84
85 // The helper struct used to record one increment occurrence, with all the
86 // details necessary.
87 struct Detail {
88 const SourceLocation Loc; // What caused the increment?
89 const unsigned short Nesting; // How deeply nested is Loc located?
90 const Criteria C; // The criteria of the increment
91
92 Detail(SourceLocation SLoc, unsigned short CurrentNesting, Criteria Crit)
93 : Loc(SLoc), Nesting(CurrentNesting), C(Crit) {}
94
95 // To minimize the sizeof(Detail), we only store the minimal info there.
96 // This function is used to convert from the stored info into the usable
97 // information - what message to output, how much of an increment did this
98 // occurrence actually result in.
99 std::pair<unsigned, unsigned short> process() const {
100 assert(C != Criteria::None && "invalid criteria");
101
102 unsigned MsgId = 0; // The id of the message to output.
103 unsigned short Increment = 0; // How much of an increment?
104
105 if (C == Criteria::All) {
106 Increment = 1 + Nesting;
107 MsgId = 0;
108 } else if (C == (Criteria::Increment | Criteria::IncrementNesting)) {
109 Increment = 1;
110 MsgId = 1;
111 } else if (C == Criteria::Increment) {
112 Increment = 1;
113 MsgId = 2;
114 } else if (C == Criteria::IncrementNesting) {
115 Increment = 0; // Unused in this message.
116 MsgId = 3;
117 } else {
118 llvm_unreachable("should not get to here.");
119 }
120
121 return {MsgId, Increment};
122 }
123 };
124
125 // Limit of 25 is the "upstream"'s default.
126 static constexpr unsigned DefaultLimit = 25U;
127
128 // Based on the publicly-available numbers for some big open-source projects
129 // https://sonarcloud.io/projects?languages=c%2Ccpp&size=5 we can estimate:
130 // value ~20 would result in no allocs for 98% of functions, ~12 for 96%, ~10
131 // for 91%, ~8 for 88%, ~6 for 84%, ~4 for 77%, ~2 for 64%, and ~1 for 37%.
132 static_assert(sizeof(Detail) <= 8,
133 "Since we use SmallVector to minimize the amount of "
134 "allocations, we also need to consider the price we pay for "
135 "that in terms of stack usage. "
136 "Thus, it is good to minimize the size of the Detail struct.");
137 SmallVector<Detail, DefaultLimit> Details; // 25 elements is 200 bytes.
138 // Yes, 25 is a magic number. This is the seemingly-sane default for the
139 // upper limit for function cognitive complexity. Thus it would make sense
140 // to avoid allocations for any function that does not violate the limit.
141
142 // The grand total Cognitive Complexity of the function.
143 unsigned Total = 0;
144
145 // The function used to store new increment, calculate the total complexity.
146 void account(SourceLocation Loc, unsigned short Nesting, Criteria C);
147};
148
149} // namespace
150
151// All the possible messages that can be output. The choice of the message
152// to use is based of the combination of the CognitiveComplexity::Criteria.
153// It would be nice to have it in CognitiveComplexity struct, but then it is
154// not static.
155static constexpr std::array<StringRef, 4> Msgs = {{
156 // B1 + B2 + B3
157 "+%0, including nesting penalty of %1, nesting level increased to %2",
158
159 // B1 + B2
160 "+%0, nesting level increased to %2",
161
162 // B1
163 "+%0",
164
165 // B2
166 "nesting level increased to %2",
167}};
168
169void CognitiveComplexity::account(SourceLocation Loc, unsigned short Nesting,
170 Criteria C) {
171 C &= Criteria::All;
172 assert(C != Criteria::None && "invalid criteria");
173
174 Details.emplace_back(Loc, Nesting, C);
175 const Detail &D = Details.back();
176
177 const auto [MsgId, Increase] = D.process();
178
179 Total += Increase;
180}
181
182namespace {
183
184class FunctionASTVisitor final
185 : public RecursiveASTVisitor<FunctionASTVisitor> {
186 using Base = RecursiveASTVisitor<FunctionASTVisitor>;
187
188 // If set to true, macros are ignored during analysis.
189 const bool IgnoreMacros;
190
191 // The current nesting level (increased by Criteria::IncrementNesting).
192 unsigned short CurrentNestingLevel = 0;
193
194 // Used to efficiently know the last type of the binary sequence operator
195 // that was encountered. It would make sense for the function call to start
196 // the new sequence, thus it is a stack.
197 using OBO = std::optional<BinaryOperator::Opcode>;
198 std::stack<OBO, SmallVector<OBO, 4>> BinaryOperatorsStack;
199
200public:
201 explicit FunctionASTVisitor(const bool IgnoreMacros)
202 : IgnoreMacros(IgnoreMacros) {}
203
204 bool traverseStmtWithIncreasedNestingLevel(Stmt *Node) {
205 ++CurrentNestingLevel;
206 const bool ShouldContinue = Base::TraverseStmt(Node);
207 --CurrentNestingLevel;
208 return ShouldContinue;
209 }
210
211 bool traverseDeclWithIncreasedNestingLevel(Decl *Node) {
212 ++CurrentNestingLevel;
213 const bool ShouldContinue = Base::TraverseDecl(Node);
214 --CurrentNestingLevel;
215 return ShouldContinue;
216 }
217
218 bool TraverseIfStmt(IfStmt *Node, bool InElseIf = false) {
219 if (!Node)
220 return Base::TraverseIfStmt(Node);
221
222 {
223 CognitiveComplexity::Criteria Reasons =
224 CognitiveComplexity::Criteria::None;
225
226 // "If" increases cognitive complexity.
227 Reasons |= CognitiveComplexity::Criteria::Increment;
228 // "If" increases nesting level.
229 Reasons |= CognitiveComplexity::Criteria::IncrementNesting;
230
231 if (!InElseIf) {
232 // "If" receives a nesting increment commensurate with it's nested
233 // depth, if it is not part of "else if".
234 Reasons |= CognitiveComplexity::Criteria::PenalizeNesting;
235 }
236
237 CC.account(Node->getIfLoc(), CurrentNestingLevel, Reasons);
238 }
239
240 // If this IfStmt is *NOT* "else if", then only the body (i.e. "Then" and
241 // "Else") is traversed with increased Nesting level.
242 // However if this IfStmt *IS* "else if", then Nesting level is increased
243 // for the whole IfStmt (i.e. for "Init", "Cond", "Then" and "Else").
244
245 if (!InElseIf) {
246 if (!TraverseStmt(Node->getInit()))
247 return false;
248
249 if (!TraverseStmt(Node->getCond()))
250 return false;
251 } else {
252 if (!traverseStmtWithIncreasedNestingLevel(Node->getInit()))
253 return false;
254
255 if (!traverseStmtWithIncreasedNestingLevel(Node->getCond()))
256 return false;
257 }
258
259 // "Then" always increases nesting level.
260 if (!traverseStmtWithIncreasedNestingLevel(Node->getThen()))
261 return false;
262
263 if (!Node->getElse())
264 return true;
265
266 if (auto *E = dyn_cast<IfStmt>(Node->getElse()))
267 return TraverseIfStmt(E, true);
268
269 {
270 CognitiveComplexity::Criteria Reasons =
271 CognitiveComplexity::Criteria::None;
272
273 // "Else" increases cognitive complexity.
274 Reasons |= CognitiveComplexity::Criteria::Increment;
275 // "Else" increases nesting level.
276 Reasons |= CognitiveComplexity::Criteria::IncrementNesting;
277 // "Else" DOES NOT receive a nesting increment commensurate with it's
278 // nested depth.
279
280 CC.account(Node->getElseLoc(), CurrentNestingLevel, Reasons);
281 }
282
283 // "Else" always increases nesting level.
284 return traverseStmtWithIncreasedNestingLevel(Node->getElse());
285 }
286
287// The currently-being-processed stack entry, which is always the top.
288#define CurrentBinaryOperator BinaryOperatorsStack.top()
289
290 // In a sequence of binary logical operators, if the new operator is different
291 // from the previous one, then the cognitive complexity is increased.
292 bool TraverseBinaryOperator(BinaryOperator *Op) {
293 if (!Op || !Op->isLogicalOp())
294 return Base::TraverseBinaryOperator(Op);
295
296 // Make sure that there is always at least one frame in the stack.
297 if (BinaryOperatorsStack.empty())
298 BinaryOperatorsStack.emplace();
299
300 // If this is the first binary operator that we are processing, or the
301 // previous binary operator was different, there is an increment.
302 if (!CurrentBinaryOperator || Op->getOpcode() != CurrentBinaryOperator)
303 CC.account(Op->getOperatorLoc(), CurrentNestingLevel,
304 CognitiveComplexity::Criteria::Increment);
305
306 // We might encounter a function call, which starts a new sequence, thus
307 // we need to save the current previous binary operator.
308 const std::optional<BinaryOperator::Opcode> BinOpCopy(
310
311 // Record the operator that we are currently processing and traverse it.
312 CurrentBinaryOperator = Op->getOpcode();
313 const bool ShouldContinue = Base::TraverseBinaryOperator(Op);
314
315 // And restore the previous binary operator, which might be nonexistent.
316 CurrentBinaryOperator = BinOpCopy;
317
318 return ShouldContinue;
319 }
320
321 // It would make sense for the function call to start the new binary
322 // operator sequence, thus let's make sure that it creates a new stack frame.
323 bool TraverseCallExpr(CallExpr *Node) {
324 // If we are not currently processing any binary operator sequence, then
325 // no Node-handling is needed.
326 if (!Node || BinaryOperatorsStack.empty() || !CurrentBinaryOperator)
327 return Base::TraverseCallExpr(Node);
328
329 // Else, do add [uninitialized] frame to the stack, and traverse call.
330 BinaryOperatorsStack.emplace();
331 const bool ShouldContinue = Base::TraverseCallExpr(Node);
332 // And remove the top frame.
333 BinaryOperatorsStack.pop();
334
335 return ShouldContinue;
336 }
337
338#undef CurrentBinaryOperator
339
340 bool TraverseStmt(Stmt *Node) {
341 if (!Node)
342 return Base::TraverseStmt(Node);
343
344 if (IgnoreMacros && Node->getBeginLoc().isMacroID())
345 return true;
346
347 // Three following switch()'es have huge duplication, but it is better to
348 // keep them separate, to simplify comparing them with the Specification.
349
350 CognitiveComplexity::Criteria Reasons = CognitiveComplexity::Criteria::None;
351 SourceLocation Location = Node->getBeginLoc();
352
353 // B1. Increments
354 // There is an increment for each of the following:
355 switch (Node->getStmtClass()) {
356 // if, else if, else are handled in TraverseIfStmt(),
357 // FIXME: "each method in a recursion cycle" Increment is not implemented.
358 case Stmt::ConditionalOperatorClass:
359 case Stmt::SwitchStmtClass:
360 case Stmt::ForStmtClass:
361 case Stmt::CXXForRangeStmtClass:
362 case Stmt::WhileStmtClass:
363 case Stmt::DoStmtClass:
364 case Stmt::CXXCatchStmtClass:
365 case Stmt::GotoStmtClass:
366 case Stmt::IndirectGotoStmtClass:
367 Reasons |= CognitiveComplexity::Criteria::Increment;
368 break;
369 default:
370 // break LABEL, continue LABEL increase cognitive complexity,
371 // but they are not supported in C++ or C.
372 // Regular break/continue do not increase cognitive complexity.
373 break;
374 }
375
376 // B2. Nesting level
377 // The following structures increment the nesting level:
378 switch (Node->getStmtClass()) {
379 // if, else if, else are handled in TraverseIfStmt(),
380 // Nested methods and such are handled in TraverseDecl.
381 case Stmt::ConditionalOperatorClass:
382 case Stmt::SwitchStmtClass:
383 case Stmt::ForStmtClass:
384 case Stmt::CXXForRangeStmtClass:
385 case Stmt::WhileStmtClass:
386 case Stmt::DoStmtClass:
387 case Stmt::CXXCatchStmtClass:
388 case Stmt::LambdaExprClass:
389 case Stmt::StmtExprClass:
390 Reasons |= CognitiveComplexity::Criteria::IncrementNesting;
391 break;
392 default:
393 break;
394 }
395
396 // B3. Nesting increments
397 // The following structures receive a nesting increment
398 // commensurate with their nested depth inside B2 structures:
399 switch (Node->getStmtClass()) {
400 // if, else if, else are handled in TraverseIfStmt().
401 case Stmt::ConditionalOperatorClass:
402 case Stmt::SwitchStmtClass:
403 case Stmt::ForStmtClass:
404 case Stmt::CXXForRangeStmtClass:
405 case Stmt::WhileStmtClass:
406 case Stmt::DoStmtClass:
407 case Stmt::CXXCatchStmtClass:
408 Reasons |= CognitiveComplexity::Criteria::PenalizeNesting;
409 break;
410 default:
411 break;
412 }
413
414 if (Node->getStmtClass() == Stmt::ConditionalOperatorClass) {
415 // A little beautification.
416 // For conditional operator "cond ? true : false" point at the "?"
417 // symbol.
418 Location = cast<ConditionalOperator>(Node)->getQuestionLoc();
419 }
420
421 // If we have found any reasons, let's account it.
422 if (Reasons & CognitiveComplexity::Criteria::All)
423 CC.account(Location, CurrentNestingLevel, Reasons);
424
425 // Did we decide that the nesting level should be increased?
426 if (!(Reasons & CognitiveComplexity::Criteria::IncrementNesting))
427 return Base::TraverseStmt(Node);
428
429 return traverseStmtWithIncreasedNestingLevel(Node);
430 }
431
432 // The parameter MainAnalyzedFunction is needed to differentiate between the
433 // cases where TraverseDecl() is the entry point from
434 // FunctionCognitiveComplexityCheck::check() and the cases where it was called
435 // from the FunctionASTVisitor itself. Explanation: if we get a function
436 // definition (e.g. constructor, destructor, method), the Cognitive Complexity
437 // specification states that the Nesting level shall be increased. But if this
438 // function is the entry point, then the Nesting level should not be
439 // increased. Thus that parameter is there and is used to fall-through
440 // directly to traversing if this is the main function that is being analyzed.
441 bool TraverseDecl(Decl *Node, bool MainAnalyzedFunction = false) {
442 if (!Node || MainAnalyzedFunction)
443 return Base::TraverseDecl(Node);
444
445 // B2. Nesting level
446 // The following structures increment the nesting level:
447 switch (Node->getKind()) {
448 case Decl::Function:
449 case Decl::CXXMethod:
450 case Decl::CXXConstructor:
451 case Decl::CXXDestructor:
452 case Decl::Block:
453 break;
454 default:
455 // If this is something else, we use early return!
456 return Base::TraverseDecl(Node);
457 break;
458 }
459
460 CC.account(Node->getBeginLoc(), CurrentNestingLevel,
461 CognitiveComplexity::Criteria::IncrementNesting);
462
463 return traverseDeclWithIncreasedNestingLevel(Node);
464 }
465
466 CognitiveComplexity CC;
467};
468
469} // namespace
470
472 StringRef Name, ClangTidyContext *Context)
473 : ClangTidyCheck(Name, Context),
474 Threshold(Options.get("Threshold", CognitiveComplexity::DefaultLimit)),
475 DescribeBasicIncrements(Options.get("DescribeBasicIncrements", true)),
476 IgnoreMacros(Options.get("IgnoreMacros", false)) {}
477
480 Options.store(Opts, "Threshold", Threshold);
481 Options.store(Opts, "DescribeBasicIncrements", DescribeBasicIncrements);
482 Options.store(Opts, "IgnoreMacros", IgnoreMacros);
483}
484
486 Finder->addMatcher(
487 functionDecl(isDefinition(),
488 unless(anyOf(isDefaulted(), isDeleted(), isWeak())))
489 .bind("func"),
490 this);
491 Finder->addMatcher(lambdaExpr().bind("lambda"), this);
492}
493
495 const MatchFinder::MatchResult &Result) {
496 FunctionASTVisitor Visitor(IgnoreMacros);
497 SourceLocation Loc;
498
499 const auto *TheDecl = Result.Nodes.getNodeAs<FunctionDecl>("func");
500 const auto *TheLambdaExpr = Result.Nodes.getNodeAs<LambdaExpr>("lambda");
501 if (TheDecl) {
502 assert(TheDecl->hasBody() &&
503 "The matchers should only match the functions that "
504 "have user-provided body.");
505 Loc = TheDecl->getLocation();
506 Visitor.TraverseDecl(const_cast<FunctionDecl *>(TheDecl), true);
507 } else {
508 Loc = TheLambdaExpr->getBeginLoc();
509 Visitor.TraverseLambdaExpr(const_cast<LambdaExpr *>(TheLambdaExpr));
510 }
511
512 if (Visitor.CC.Total <= Threshold)
513 return;
514
515 if (TheDecl)
516 diag(Loc, "function %0 has cognitive complexity of %1 (threshold %2)")
517 << TheDecl << Visitor.CC.Total << Threshold;
518 else
519 diag(Loc, "lambda has cognitive complexity of %0 (threshold %1)")
520 << Visitor.CC.Total << Threshold;
521
522 if (!DescribeBasicIncrements)
523 return;
524
525 // Output all the basic increments of complexity.
526 for (const auto &Detail : Visitor.CC.Details) {
527 auto [MsgId, Increase] = Detail.process();
528 assert(MsgId < Msgs.size() && "MsgId should always be valid");
529 // Increase, on the other hand, can be 0.
530
531 diag(Detail.Loc, Msgs[MsgId], DiagnosticIDs::Note)
532 << Increase << Detail.Nesting << 1 + Detail.Nesting;
533 }
534}
535
536} // namespace clang::tidy::readability
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
static constexpr std::array< StringRef, 4 > Msgs
llvm::StringMap< ClangTidyValue > OptionMap