Skip to content

Commit 30e5543

Browse files
committed
Fix false positive in accessibility_label_for_image for SwiftUI Label icon closures
Images inside a `Label`'s `icon:` closure are inherently labeled by the Label's text content and do not need a separate accessibility label. Adds `isInsideLabelIconClosure()` to detect this pattern via SwiftSyntax and exempts matching images from the rule, with three new non-triggering examples covering `systemName`, asset name, and `uiImage` variants. Fixes #6420
1 parent a7878fc commit 30e5543

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ private struct AccessibilityDeterminator {
7676
return true
7777
}
7878

79-
// 2. Check the parent hierarchy for exemptions
79+
// 2. Check if the Image is inside a Label's icon: closure.
80+
// SwiftUI Label provides accessibility through its text content, so the
81+
// icon image is inherently labeled and needs no separate accessibility label.
82+
if imageCall.isInsideLabelIconClosure() {
83+
return true
84+
}
85+
86+
// 3. Check the parent hierarchy for exemptions
8087
return imageCall.isExemptedByAncestors()
8188
}
8289
}
@@ -253,6 +260,27 @@ private extension FunctionCallExprSyntax {
253260
return false
254261
}
255262

263+
/// Check if this Image is inside the `icon:` closure argument of a SwiftUI Label.
264+
/// In SwiftUI, Label provides accessibility through its text content, so any Image
265+
/// in the icon closure is inherently labeled and does not need a separate label.
266+
func isInsideLabelIconClosure() -> Bool {
267+
var currentNode: Syntax? = Syntax(self)
268+
269+
while let node = currentNode {
270+
if let labeledExpr = node.as(LabeledExprSyntax.self),
271+
labeledExpr.label?.text == "icon",
272+
let argList = labeledExpr.parent?.as(LabeledExprListSyntax.self),
273+
let funcCall = argList.parent?.as(FunctionCallExprSyntax.self),
274+
let identifier = funcCall.calledExpression.as(DeclReferenceExprSyntax.self),
275+
identifier.baseName.text == "Label" {
276+
return true
277+
}
278+
currentNode = node.parent
279+
}
280+
281+
return false
282+
}
283+
256284
/// Check if this container has direct accessibility treatment
257285
private func hasDirectAccessibilityTreatment() -> Bool {
258286
var currentNode: Syntax? = Syntax(self)

Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,41 @@ internal struct AccessibilityLabelForImageRuleExamples {
258258
}
259259
}
260260
"""),
261+
// MARK: - Label icon closure exemptions
262+
// Images inside a Label's icon: closure are inherently labeled by the Label's text content.
263+
Example("""
264+
struct MyView: View {
265+
var body: some View {
266+
Label {
267+
Text("Connected")
268+
} icon: {
269+
Image(systemName: "checkmark.circle.fill")
270+
}
271+
}
272+
}
273+
"""),
274+
Example("""
275+
struct MyView: View {
276+
var body: some View {
277+
Label {
278+
Text("Download")
279+
} icon: {
280+
Image("custom-download-icon")
281+
}
282+
}
283+
}
284+
"""),
285+
Example("""
286+
struct MyView: View {
287+
var body: some View {
288+
Label {
289+
Text("Profile")
290+
} icon: {
291+
Image(uiImage: profileImage)
292+
}
293+
}
294+
}
295+
"""),
261296
]
262297

263298
static let triggeringExamples = [

0 commit comments

Comments
 (0)