Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Metrics/CyclomaticComplexityRule.swift
2023-10-29 11:17:45 -07:00

179 lines
5.3 KiB
Swift

import Foundation
import SwiftSyntax
@SwiftSyntaxRule
struct CyclomaticComplexityRule: Rule {
var configuration = CyclomaticComplexityConfiguration()
static let description = RuleDescription(
identifier: "cyclomatic_complexity",
name: "Cyclomatic Complexity",
description: "Complexity of function bodies should be limited.",
kind: .metrics,
nonTriggeringExamples: [
Example("""
func f1() {
if true {
for _ in 1..5 { }
}
if false { }
}
"""),
Example("""
func f(code: Int) -> Int {
switch code {
case 0: fallthrough
case 1: return 1
case 2: return 1
case 3: return 1
case 4: return 1
case 5: return 1
case 6: return 1
case 7: return 1
case 8: return 1
default: return 1
}
}
"""),
Example("""
func f1() {
if true {}; if true {}; if true {}; if true {}; if true {}; if true {}
func f2() {
if true {}; if true {}; if true {}; if true {}; if true {}
}
}
""")
],
triggeringExamples: [
Example("""
↓func f1() {
if true {
if true {
if false {}
}
}
if false {}
let i = 0
switch i {
case 1: break
case 2: break
case 3: break
case 4: break
default: break
}
for _ in 1...5 {
guard true else {
return
}
}
}
""")
]
)
}
private extension CyclomaticComplexityRule {
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(
ignoresCaseStatements: configuration.ignoresCaseStatements
).walk(tree: body, handler: \.complexity)
for parameter in configuration.params where complexity > parameter.value {
let reason = "Function should have 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(set) var complexity = 0
let ignoresCaseStatements: Bool
init(ignoresCaseStatements: Bool) {
self.ignoresCaseStatements = ignoresCaseStatements
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: ForStmtSyntax) {
complexity += 1
}
override func visitPost(_ node: IfExprSyntax) {
complexity += 1
}
override func visitPost(_ node: GuardStmtSyntax) {
complexity += 1
}
override func visitPost(_ node: RepeatStmtSyntax) {
complexity += 1
}
override func visitPost(_ node: WhileStmtSyntax) {
complexity += 1
}
override func visitPost(_ node: CatchClauseSyntax) {
complexity += 1
}
override func visitPost(_ node: SwitchCaseSyntax) {
if !ignoresCaseStatements {
complexity += 1
}
}
override func visitPost(_ node: FallThroughStmtSyntax) {
// Switch complexity is reduced by `fallthrough` cases
if !ignoresCaseStatements {
complexity -= 1
}
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
}
}
private extension DeclModifierListSyntax {
var staticOrClassModifier: TokenSyntax? {
first { element in
let kind = element.name.tokenKind
return kind == .keyword(.static) || kind == .keyword(.class)
}?.name
}
}