From 169175c7d5c21955e8d080cc27e0c6c73213d301 Mon Sep 17 00:00:00 2001 From: Farid Khusainov Date: Thu, 24 Oct 2024 15:50:27 +0500 Subject: [PATCH 1/3] Added Cognitive Complexity Rule --- .bazelversion | 2 +- .../Models/BuiltInRules.swift | 1 + .../Metrics/CognitiveComplexityRule.swift | 266 ++++++++++++++++++ .../CognitiveComplexityConfiguration.swift | 16 ++ Tests/GeneratedTests/GeneratedTests.swift | 6 + 5 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Metrics/CognitiveComplexityRule.swift create mode 100644 Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CognitiveComplexityConfiguration.swift diff --git a/.bazelversion b/.bazelversion index 66ce77b7ea..ba7f754d0c 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.0.0 +7.4.0 diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index eeb3abce7c..7330ddd297 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -19,6 +19,7 @@ public let builtInRules: [any Rule.Type] = [ ClosureEndIndentationRule.self, ClosureParameterPositionRule.self, ClosureSpacingRule.self, + CognitiveComplexityRule.self, CollectionAlignmentRule.self, ColonRule.self, CommaInheritanceRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/CognitiveComplexityRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/CognitiveComplexityRule.swift new file mode 100644 index 0000000000..9356b99be5 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/CognitiveComplexityRule.swift @@ -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.. 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.. 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.. 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 { + 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 + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CognitiveComplexityConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CognitiveComplexityConfiguration.swift new file mode 100644 index 0000000000..13deadb187 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/CognitiveComplexityConfiguration.swift @@ -0,0 +1,16 @@ +import SourceKittenFramework +import SwiftLintCore + +@AutoConfigParser +struct CognitiveComplexityConfiguration: RuleConfiguration { + typealias Parent = CognitiveComplexityRule + + @ConfigurationElement(inline: true) + private(set) var length = SeverityLevelsConfiguration(warning: 15, error: 20) + @ConfigurationElement(key: "ignores_logical_operator_sequences") + private(set) var ignoresLogicalOperatorSequences = false + + var params: [RuleParameter] { + length.params + } +} diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index d00101a90b..8fb942f889 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -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) From 53ae94de86fa562313d783eadcd54b5bc469b0dc Mon Sep 17 00:00:00 2001 From: Farid Khusainov Date: Fri, 25 Oct 2024 10:49:39 +0500 Subject: [PATCH 2/3] Reverted .bazelversion --- .bazelversion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazelversion b/.bazelversion index ba7f754d0c..66ce77b7ea 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.4.0 +7.0.0 From 07e6252d21c7c96ebd886a2b70ed20f03427ad96 Mon Sep 17 00:00:00 2001 From: Farid Khusainov Date: Mon, 28 Oct 2024 11:30:34 +0500 Subject: [PATCH 3/3] Added CHANGELOG entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e32680d436..99df986a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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