Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Fix indexing of xib/storyboard files in SPM projects.
- Fix types conforming to App Intents protocols being reported as unused.
- Fix superclass initializer reported as unused when called on subclass.
- Fix unused parameter false-positive for parameters used in closure capture lists.

## 3.3.0 (2025-12-13)

Expand Down
8 changes: 6 additions & 2 deletions Sources/SyntaxAnalysis/UnusedParameterParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,12 @@ struct UnusedParameterParser {
let signature = syntax.children(viewMode: .sourceAccurate).mapFirst { $0.as(ClosureSignatureSyntax.self) }
let rawParams = signature?.parameterClause?.children(viewMode: .sourceAccurate).compactMap { $0.as(ClosureShorthandParameterSyntax.self) }
let params = rawParams?.map(\.name.text) ?? []
let items = syntax.statements.compactMap { parse(node: $0.item, collector) }
return Closure(params: params, items: items)

// Parse capture list expressions (e.g., [captured = state.someProperty])
let captureItems = signature?.capture?.items.compactMap { parse(node: $0.initializer?.value, collector) } ?? []

let bodyItems = syntax.statements.compactMap { parse(node: $0.item, collector) }
return Closure(params: params, items: captureItems + bodyItems)
}

private func parse(variableDecl syntax: VariableDeclSyntax, _ collector: Collector<some Any>?) -> Variable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

struct CaptureListFixtureState {
var someProperty: String
}

class CaptureListFixture {
// Parameter used in capture list should be considered used
func functionWithCaptureList(_ state: CaptureListFixtureState) -> () -> String {
return { [someProperty = state.someProperty] in
return someProperty
}
}

// Multiple parameters used in capture list
func multipleParamsInCaptureList(param1: String, param2: Int, unusedParam: Bool) -> () -> String {
return { [captured1 = param1, captured2 = param2] in
return "\(captured1) \(captured2)"
}
}

// Parameter used both in capture list and directly - should be used
func paramUsedInCaptureListAndDirectly(param: String) -> () -> String {
print(param)
return { [captured = param] in
return captured
}
}

// Weak capture (common pattern for avoiding retain cycles)
func weakCapture(object: AnyObject) -> () -> Void {
return { [weak object] in
_ = object
}
}

// Nested closure with capture list
func nestedClosureWithCaptureList(outerParam: String) -> () -> () -> String {
return {
return { [captured = outerParam] in
return captured
}
}
}

// Shorthand capture (just [param] without assignment)
func shorthandCapture(param: String) -> () -> String {
return { [param] in
return param
}
}

// Capture list only - parameter not used in body, only in capture
func captureListOnly(param: String) -> () -> String {
return { [captured = param] in
return "constant"
}
}
}

27 changes: 27 additions & 0 deletions Tests/PeripheryTests/Syntax/UnusedParameterTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,33 @@ final class UnusedParameterTest: XCTestCase {
assertUnused(label: "otherUnused", name: "otherUnused", in: "myFunc(class:func:otherUsed:otherUnused:)")
}

// https://github.com/peripheryapp/periphery/issues/994
func testCaptureListUsage() {
analyze()
// Parameter used in capture list should be considered used
assertUsed(label: "_", name: "state", in: "functionWithCaptureList(_:)")

// Multiple parameters - some used in capture list, one unused
assertUsed(label: "param1", name: "param1", in: "multipleParamsInCaptureList(param1:param2:unusedParam:)")
assertUsed(label: "param2", name: "param2", in: "multipleParamsInCaptureList(param1:param2:unusedParam:)")
assertUnused(label: "unusedParam", name: "unusedParam", in: "multipleParamsInCaptureList(param1:param2:unusedParam:)")

// Parameter used both in capture list and directly
assertUsed(label: "param", name: "param", in: "paramUsedInCaptureListAndDirectly(param:)")

// Weak capture (common pattern for avoiding retain cycles)
assertUsed(label: "object", name: "object", in: "weakCapture(object:)")

// Nested closure with capture list
assertUsed(label: "outerParam", name: "outerParam", in: "nestedClosureWithCaptureList(outerParam:)")

// Shorthand capture (just [param] without assignment)
assertUsed(label: "param", name: "param", in: "shorthandCapture(param:)")

// Capture list only - parameter not used in body, only in capture
assertUsed(label: "param", name: "param", in: "captureListOnly(param:)")
}

// MARK: - Private

private var unusedParamsByFunction: [(Function, Set<Parameter>)] = []
Expand Down
Loading