SwiftLint/Source/SwiftLintBuiltInRules/Rules/Metrics/CyclomaticComplexityRule.swift

139 lines
4.7 KiB
Swift

import Foundation
import SourceKittenFramework
struct CyclomaticComplexityRule: ASTRule, ConfigurationProviderRule {
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 0: return 1
case 0: return 1
case 0: return 1
case 0: return 1
case 0: return 1
case 0: return 1
case 0: return 1
case 0: 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
}
}
}
""")
]
)
func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard SwiftDeclarationKind.functionKinds.contains(kind) else {
return []
}
let complexity = measureComplexity(in: file, dictionary: dictionary)
for parameter in configuration.params where complexity > parameter.value {
let offset = dictionary.offset ?? 0
let reason = "Function should have complexity \(configuration.length.warning) or less; " +
"currently complexity is \(complexity)"
return [StyleViolation(ruleDescription: Self.description,
severity: parameter.severity,
location: Location(file: file, byteOffset: offset),
reason: reason)]
}
return []
}
private func measureComplexity(in file: SwiftLintFile, dictionary: SourceKittenDictionary) -> Int {
var hasSwitchStatements = false
let complexity = dictionary.substructure.reduce(0) { complexity, subDict in
guard subDict.kind != nil else {
return complexity
}
if let declarationKind = subDict.declarationKind,
SwiftDeclarationKind.functionKinds.contains(declarationKind) {
return complexity
}
guard let statementKind = subDict.statementKind else {
return complexity + measureComplexity(in: file, dictionary: subDict)
}
if statementKind == .switch {
hasSwitchStatements = true
}
let score = configuration.complexityStatements.contains(statementKind) ? 1 : 0
return complexity +
score +
measureComplexity(in: file, dictionary: subDict)
}
if hasSwitchStatements && !configuration.ignoresCaseStatements {
return reduceSwitchComplexity(initialComplexity: complexity, file: file, dictionary: dictionary)
}
return complexity
}
// Switch complexity is reduced by `fallthrough` cases
private func reduceSwitchComplexity(initialComplexity complexity: Int, file: SwiftLintFile,
dictionary: SourceKittenDictionary) -> Int {
let bodyRange = dictionary.bodyByteRange ?? ByteRange(location: 0, length: 0)
let contents = file.stringView.substringWithByteRange(bodyRange) ?? ""
let fallthroughCount = contents.components(separatedBy: "fallthrough").count - 1
return complexity - fallthroughCount
}
}