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