Skip to content
Draft
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
10 changes: 6 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@
"args": [],
"cwd": "${workspaceFolder:swift-complexity}",
"name": "Debug SwiftComplexityCLI",
"program": "${workspaceFolder:swift-complexity}/.build/debug/SwiftComplexityCLI",
"preLaunchTask": "swift: Build Debug SwiftComplexityCLI"
"preLaunchTask": "swift: Build Debug SwiftComplexityCLI",
"target": "SwiftComplexityCLI",
"configuration": "debug"
},
{
"type": "swift",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:swift-complexity}",
"name": "Release SwiftComplexityCLI",
"program": "${workspaceFolder:swift-complexity}/.build/release/SwiftComplexityCLI",
"preLaunchTask": "swift: Build Release SwiftComplexityCLI"
"preLaunchTask": "swift: Build Release SwiftComplexityCLI",
"target": "SwiftComplexityCLI",
"configuration": "release"
}
]
}
22 changes: 20 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,19 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
// IndexStore-DB統合(LCOM4セマンティック解析用)
.package(url: "https://github.com/swiftlang/indexstore-db", branch: "main"),
],
targets: [
.target(
name: "SwiftComplexityCore",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
// IndexStore-DB統合(LCOM4セマンティック解析用)
.product(name: "IndexStoreDB", package: "indexstore-db"),
],
path: "Sources/SwiftComplexityCore",
),
Expand Down
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ A command-line tool to analyze Swift code complexity and quality metrics using s

## Features

- **Multiple Complexity Metrics**: Supports cyclomatic and cognitive complexity analysis
- **Multiple Complexity Metrics**: Supports cyclomatic complexity, cognitive complexity, and LCOM4 cohesion analysis
- **LCOM4 Class Cohesion**: High-precision (90-95%) class cohesion measurement using IndexStore-DB semantic analysis
- **Web-based Debug Interface**: Interactive browser-based complexity analyzer ([Try it online](https://swift-complexity.fummicc1.dev))
- **Xcode Integration**: Seamless integration with Xcode via Build Tool Plugin for complexity feedback during build phase
- **Xcode Diagnostics**: Display complexity warnings and errors directly in Xcode editor with accurate line numbers
Expand Down Expand Up @@ -46,6 +47,10 @@ swift run SwiftComplexityCLI Sources --format json --recursive

# Xcode diagnostics format (for IDE integration)
swift run SwiftComplexityCLI Sources --format xcode --threshold 15

# LCOM4 class cohesion analysis (requires swift build first)
swift build # Generate index
swift run SwiftComplexityCLI Sources --lcom4 --project-root .
```

## CLI Integration
Expand All @@ -65,10 +70,18 @@ swift run SwiftComplexityCLI Sources --threshold 15 --recursive

## Supported Complexity Metrics

### Function-level Metrics

- **Cyclomatic Complexity**: Measures the number of linearly independent paths through code
- **Cognitive Complexity**: Measures how difficult code is for humans to understand

*Future metrics planned: LCOM, cohesion/coupling indicators*
### Class-level Metrics

- **LCOM4 (Lack of Cohesion of Methods)**: Measures class cohesion by analyzing method-property relationships
- **Connected Components**: Counts independent groups of related methods
- **High Precision**: 90-95% accuracy using IndexStore-DB semantic analysis
- **Implicit self Detection**: Automatically detects both `self.property` and `property` accesses
- **Requirements**: Requires `swift build` to generate index data

## Documentation

Expand Down Expand Up @@ -100,11 +113,15 @@ Unified package with multiple components:
# Analyze with verbose output
swift run SwiftComplexityCLI Sources --verbose --recursive

# Exclude test files with pattern matching
# Exclude test files with pattern matching
swift run SwiftComplexityCLI Sources --recursive --exclude "*Test*.swift"

# Show only cognitive complexity above threshold
swift run SwiftComplexityCLI Sources --cognitive-only --threshold 5

# Analyze class cohesion with LCOM4
swift build # Generate index first
swift run SwiftComplexityCLI Sources --lcom4 --project-root . --format json
```

## Xcode Build Tool Plugin
Expand Down Expand Up @@ -173,6 +190,14 @@ File: Sources/ComplexityAnalyzer.swift
+------------------+----------+----------+

Total: 2 functions, Average Cyclomatic: 4.0, Average Cognitive: 4.5

Class Cohesion (LCOM4):
+------------------+----------+----------+----------+----------+
| Class/Struct | LCOM4 | Methods | Props | Level |
+------------------+----------+----------+----------+----------+
| ComplexityAnalyzer| 1 | 5 | 3 | High |
| FileProcessor | 2 | 8 | 4 | Moderate |
+------------------+----------+----------+----------+----------+
```

### Xcode Diagnostics Output
Expand All @@ -194,6 +219,12 @@ Total: 2 functions, Average Cyclomatic: 4.0, Average Cognitive: 4.5
- Swift 6.1+
- macOS 14+, iOS 13+, or Linux

### LCOM4 Feature (Optional)

- macOS 14+ (IndexStore-DB requirement)
- Project must be buildable with `swift build`
- Index data at `.build/index/store` (generated by build)

## License

MIT License
53 changes: 50 additions & 3 deletions Sources/SwiftComplexityCLI/ComplexityCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ public struct ComplexityCommand: AsyncParsableCommand {
)
public var cognitiveOnly: Bool = false

@Flag(
name: .long,
help: "Show LCOM4 class cohesion metrics"
)
public var lcom4: Bool = false

@Option(
name: .long,
help: "Project root directory for LCOM4 analysis (required for accurate semantic analysis)",
completion: .directory
)
public var projectRoot: String?

@Flag(
name: .shortAndLong,
help: "Recursively analyze directories"
Expand Down Expand Up @@ -102,11 +115,24 @@ public struct ComplexityCommand: AsyncParsableCommand {
throw ExitCode.failure
}

// Validate LCOM4 options
if lcom4 && projectRoot == nil {
print(
"Warning: --lcom4 requires --project-root for accurate semantic analysis. Using basic syntax analysis."
)
}

if verbose {
print("swift-complexity v\(Self.configuration.version)")
print("Analyzing paths: \(paths.joined(separator: ", "))")
print("Output format: \(format)")
print("Recursive: \(recursive)")
if lcom4 {
print("LCOM4 analysis: enabled")
if let projectRoot = projectRoot {
print("Project root: \(projectRoot)")
}
}
if !exclude.isEmpty {
print("Exclude patterns: \(exclude.joined(separator: ", "))")
}
Expand All @@ -117,7 +143,15 @@ public struct ComplexityCommand: AsyncParsableCommand {

// Execute analysis
do {
let analyzer = ComplexityAnalyzer()
// Create analyzer with optional project root for LCOM4
let analyzer: ComplexityAnalyzer
if lcom4, let projectRoot = projectRoot {
let projectURL = URL(fileURLWithPath: projectRoot)
analyzer = try ComplexityAnalyzer(projectRoot: projectURL)
} else {
analyzer = try ComplexityAnalyzer()
}

let fileProcessor = FileProcessor(analyzer: analyzer)

let processingOptions = ProcessingOptions(
Expand All @@ -136,6 +170,7 @@ public struct ComplexityCommand: AsyncParsableCommand {
let outputOptions = OutputOptions(
showCyclomaticOnly: cyclomaticOnly,
showCognitiveOnly: cognitiveOnly,
showLCOM4: lcom4,
threshold: threshold
)

Expand Down Expand Up @@ -175,9 +210,21 @@ public struct ComplexityCommand: AsyncParsableCommand {
|| function.cognitiveComplexity >= threshold
}

guard !filteredFunctions.isEmpty else { return nil }
// For LCOM4, filter classes with low cohesion (LCOM4 >= 3)
let filteredCohesions = result.classCohesions?.filter { cohesion in
cohesion.lcom4 >= 3
}

// Keep result if either functions or cohesions pass threshold
guard !filteredFunctions.isEmpty || filteredCohesions?.isEmpty == false else {
return nil
}

return ComplexityResult(filePath: result.filePath, functions: filteredFunctions)
return ComplexityResult(
filePath: result.filePath,
functions: filteredFunctions,
classCohesions: filteredCohesions
)
}
}

Expand Down
101 changes: 99 additions & 2 deletions Sources/SwiftComplexityCore/Analysis/ComplexityAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,33 @@ public actor ComplexityAnalyzer: ComplexityAnalyzing {
private let cyclomaticCalculator: CyclomaticComplexityCalculator
private let cognitiveCalculator: CognitiveComplexityCalculator
private let functionDetector: FunctionDetector
private let nominalTypeDetector: NominalTypeDetector
private let lcomCalculator: SemanticLCOMCalculator?
private let enableLCOM4: Bool

public init() {
/// Initialize the analyzer
/// - Parameter projectRoot: Optional project root URL for LCOM4 analysis. If nil, LCOM4 will be disabled.
public init(projectRoot: URL? = nil) throws {
self.cyclomaticCalculator = CyclomaticComplexityCalculator(viewMode: .sourceAccurate)
self.cognitiveCalculator = CognitiveComplexityCalculator(viewMode: .sourceAccurate)
self.functionDetector = FunctionDetector(viewMode: .sourceAccurate)
self.nominalTypeDetector = NominalTypeDetector(viewMode: .sourceAccurate)

// LCOM4: IndexStore-DB統合による高精度(90-95%)セマンティック解析
// IndexStoreが見つからない場合は構文解析ベースのフォールバック
if let projectRoot = projectRoot {
self.lcomCalculator = try SemanticLCOMCalculator(projectRoot: projectRoot)
self.enableLCOM4 = true
} else {
self.lcomCalculator = nil
self.enableLCOM4 = false
}
}

public func analyze(sourceFile: SourceFileSyntax, filePath: String) async throws
-> ComplexityResult
{
// 既存の関数複雑度計算
let functions = functionDetector.detectFunctions(in: sourceFile)
var functionComplexities: [FunctionComplexity] = []

Expand All @@ -37,10 +54,90 @@ public actor ComplexityAnalyzer: ComplexityAnalyzing {
functionComplexities.append(functionComplexity)
}

// LCOM4計算(有効な場合のみ)
var classCohesions: [ClassCohesion]? = nil

if enableLCOM4, let lcomCalculator = lcomCalculator {
let nominalTypes = nominalTypeDetector.detectTypes(in: sourceFile)
var cohesions: [ClassCohesion] = []

for detectedType in nominalTypes {
let lcom4Value = try await lcomCalculator.calculate(for: detectedType)

// メンバー数を計算
let (methods, properties) = extractMemberCounts(from: detectedType.members)

// NominalTypeKindをNominalTypeに変換
let nominalType: NominalType
switch detectedType.type {
case .class:
nominalType = .class
case .struct:
nominalType = .struct
case .actor:
nominalType = .actor
}

let cohesion = ClassCohesion(
name: detectedType.name,
type: nominalType,
lcom4: lcom4Value,
methodCount: methods,
propertyCount: properties,
location: detectedType.location
)

cohesions.append(cohesion)
}

classCohesions = cohesions.isEmpty ? nil : cohesions
}

return ComplexityResult(
filePath: filePath,
functions: functionComplexities
functions: functionComplexities,
classCohesions: classCohesions
)
}

// MARK: - Private Helpers

/// メンバー数をカウント(メソッドとプロパティ)
private func extractMemberCounts(from members: MemberBlockItemListSyntax) -> (
methods: Int, properties: Int
) {
var methodCount = 0
var propertyCount = 0

for member in members {
// メソッド検出
if let functionDecl = member.decl.as(FunctionDeclSyntax.self) {
let isStatic = functionDecl.modifiers.contains { $0.name.text == "static" }
if !isStatic {
methodCount += 1
}
} else if member.decl.is(InitializerDeclSyntax.self) {
methodCount += 1
} else if member.decl.is(DeinitializerDeclSyntax.self) {
methodCount += 1
}
// プロパティ検出
else if let variableDecl = member.decl.as(VariableDeclSyntax.self) {
let isStatic = variableDecl.modifiers.contains { $0.name.text == "static" }
if !isStatic {
for binding in variableDecl.bindings {
if binding.pattern.is(IdentifierPatternSyntax.self) {
// computed propertyは除外
if binding.accessorBlock == nil {
propertyCount += 1
}
}
}
}
}
}

return (methodCount, propertyCount)
}

}
Loading
Loading