Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cognitive Complexity Rule #5838

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
[Jordan Rose](https://github.com/jrose-signal)
[SimplyDanny](https://github.com/SimplyDanny)

* Add `Cognitive Complexity Rule`
[lorwe](https://github.com/lorwe)
[#3335](https://github.com/realm/SwiftLint/issues/3335)

#### Bug Fixes

* Run command plugin in whole package if no targets are defined in the
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public let builtInRules: [any Rule.Type] = [
ClosureEndIndentationRule.self,
ClosureParameterPositionRule.self,
ClosureSpacingRule.self,
CognitiveComplexityRule.self,
CollectionAlignmentRule.self,
ColonRule.self,
CommaInheritanceRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import Foundation
import SwiftSyntax

@SwiftSyntaxRule
struct CognitiveComplexityRule: Rule {
var configuration = CognitiveComplexityConfiguration()

static let description = RuleDescription(
identifier: "cognitive_complexity",
name: "Cognitive Complexity",
description: "Cognitive complexity of function bodies should be limited.",
kind: .metrics,
nonTriggeringExamples: [
Example("""
func f1(count: Int, buffer: [Int]) -> Int {
if count == 0
|| buffer.count = 0 {
return 0
}
var sum = 0
for index in 0..<buffer.count {
if buffer[index] > 0
&& buffer[index] <= 10 {
if buffer.count > 10 {
if buffer[index] % 2 == 0 {
sum += buffer[index]
} else if sum > 0 {
sum -= buffer[index]
}
}
}
}
if sum < 0 {
return -sum
}
return sum
}
"""),
Example("""
func f2(count: Int, buffer: [Int]) -> Int {
var sum = 0
for index in 0..<buffer.count {
if buffer[index] > 0 && buffer[index] <= 10 {
if buffer.count > 10 {
switch buffer[index] % 2 {
case 0:
if sum > 0 {
sum += buffer[index]
}
default:
if sum > 0 {
sum -= buffer[index]
}
}
}
}
}
return sum
}
"""),
],
triggeringExamples: [
Example("""
func f3(count: Int, buffer: [Int]) -> Int {
guard count > 0,
buffer.count > 0 {
return 0
}
var sum = 0
for index in 0..<buffer.count {
if buffer[index] > 0
&& buffer[index] <= 10 {
if buffer.count > 10 {
if buffer[index] % 2 == 0 {
sum += buffer[index]
} else if sum > 0 {
sum -= buffer[index]
} else if sum < 0 {
sum += buffer[index]
}
}
}
}
if sum < 0 {
return -sum
}
return sum
}
"""),
]
)
}

private extension CognitiveComplexityRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: FunctionDeclSyntax) {
guard let body = node.body else {
return
}

// for legacy reasons, we try to put the violation in the static or class keyword
let violationToken = node.modifiers.staticOrClassModifier ?? node.funcKeyword
validate(body: body, violationToken: violationToken)
}

override func visitPost(_ node: InitializerDeclSyntax) {
guard let body = node.body else {
return
}

validate(body: body, violationToken: node.initKeyword)
}

private func validate(body: CodeBlockSyntax, violationToken: TokenSyntax) {
let complexity = ComplexityVisitor(
ignoresLogicalOperatorSequences: configuration.ignoresLogicalOperatorSequences
).walk(tree: body, handler: \.complexity)

for parameter in configuration.params where complexity > parameter.value {
let reason = "Function should have cognitive complexity \(configuration.length.warning) or less; " +
"currently complexity is \(complexity)"

let violation = ReasonedRuleViolation(
position: violationToken.positionAfterSkippingLeadingTrivia,
reason: reason,
severity: parameter.severity
)
violations.append(violation)
return
}
}
}

private class ComplexityVisitor: SyntaxVisitor {
private let ignoresLogicalOperatorSequences: Bool
private(set) var complexity = 0
private var nesting = 0

init(ignoresLogicalOperatorSequences: Bool) {
self.ignoresLogicalOperatorSequences = ignoresLogicalOperatorSequences
super.init(viewMode: .sourceAccurate)
}

override func visit(_: ForStmtSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_: ForStmtSyntax) {
nesting -= 1
complexity += nesting + 1
}

override func visit(_: IfExprSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_ node: IfExprSyntax) {
nesting -= 1
let nesting = node.parent?.as(IfExprSyntax.self)?.elseBody?.is(IfExprSyntax.self) == true ? 0 : nesting
if ignoresLogicalOperatorSequences {
complexity += nesting + 1
} else {
complexity += nesting + node.conditions.sequenceCount
}
}

override func visit(_: GuardStmtSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_ node: GuardStmtSyntax) {
nesting -= 1
if ignoresLogicalOperatorSequences {
complexity += nesting + 1
} else {
complexity += nesting + node.conditions.sequenceCount
}
}

override func visit(_: RepeatStmtSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_: RepeatStmtSyntax) {
nesting -= 1
complexity += nesting + 1
}

override func visit(_: WhileStmtSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_: WhileStmtSyntax) {
nesting -= 1
complexity += nesting + 1
}

override func visit(_: CatchClauseSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_: CatchClauseSyntax) {
nesting -= 1
complexity += nesting + 1
}

override func visitPost(_: SwitchExprSyntax) {
complexity += 1
}

override func visitPost(_: TernaryExprSyntax) {
complexity += 1
}

override func visitPost(_ node: BreakStmtSyntax) {
if node.label != nil {
complexity += 1
}
}

override func visitPost(_ node: ContinueStmtSyntax) {
if node.label != nil {
complexity += 1
}
}

override func visit(_: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_: FunctionDeclSyntax) {
nesting -= 1
}

override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
nesting += 1
return .visitChildren
}

override func visitPost(_: ClosureExprSyntax) {
nesting -= 1
}
}
}

private extension DeclModifierListSyntax {
var staticOrClassModifier: TokenSyntax? {
first { element in
let kind = element.name.tokenKind
return kind == .keyword(.static) || kind == .keyword(.class)
}?.name
}
}

private extension ConditionElementListSyntax {
var sequenceCount: Int {
description.components(separatedBy: .newlines).count
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import SourceKittenFramework
import SwiftLintCore

@AutoConfigParser
struct CognitiveComplexityConfiguration: RuleConfiguration {
typealias Parent = CognitiveComplexityRule

@ConfigurationElement(inline: true)
private(set) var length = SeverityLevelsConfiguration<Parent>(warning: 15, error: 20)
@ConfigurationElement(key: "ignores_logical_operator_sequences")
private(set) var ignoresLogicalOperatorSequences = false

var params: [RuleParameter<Int>] {
length.params
}
}
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ final class ClosureSpacingRuleGeneratedTests: SwiftLintTestCase {
}
}

final class CognitiveComplexityRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(CognitiveComplexityRule.description)
}
}

final class CollectionAlignmentRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(CollectionAlignmentRule.description)
Expand Down