mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
With the binding of configurations to their associated rule types "unknown configuration" errors can be made more specific mentioning also the rule's identifier in the printed message.
138 lines
6.2 KiB
Swift
138 lines
6.2 KiB
Swift
import SourceKittenFramework
|
|
|
|
/// The accessibility button and link traits are used to tell assistive technologies that an element is tappable. When
|
|
/// an element has one of these traits, VoiceOver will automatically read "button" or "link" after the element's label
|
|
/// to let the user know that they can activate it. When using a UIKit `UIButton` or SwiftUI `Button` or
|
|
/// `Link`, the button trait is added by default, but when you manually add a tap gesture recognizer to an
|
|
/// element, you need to explicitly add the button or link trait. In most cases the button trait should be used, but for
|
|
/// buttons that open a URL in an external browser we use the link trait instead. This rule attempts to catch uses of
|
|
/// the SwiftUI `.onTapGesture` modifier where the `.isButton` or `.isLink` trait is not explicitly applied.
|
|
struct AccessibilityTraitForButtonRule: ASTRule, ConfigurationProviderRule, OptInRule {
|
|
var configuration = SeverityConfiguration<Self>(.warning)
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "accessibility_trait_for_button",
|
|
name: "Accessibility Trait for Button",
|
|
description: "All views with tap gestures added should include the .isButton or the .isLink accessibility " +
|
|
"traits",
|
|
kind: .lint,
|
|
minSwiftVersion: .fiveDotOne,
|
|
nonTriggeringExamples: AccessibilityTraitForButtonRuleExamples.nonTriggeringExamples,
|
|
triggeringExamples: AccessibilityTraitForButtonRuleExamples.triggeringExamples
|
|
)
|
|
|
|
// MARK: AST Rule
|
|
|
|
func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
|
|
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
|
// Only proceed to check View structs.
|
|
guard kind == .struct,
|
|
dictionary.inheritedTypes.contains("View"),
|
|
dictionary.substructure.isNotEmpty else {
|
|
return []
|
|
}
|
|
|
|
return findButtonTraitViolations(file: file, substructure: dictionary.substructure)
|
|
}
|
|
|
|
/// Recursively check a file for image violations, and return all such violations.
|
|
private func findButtonTraitViolations(
|
|
file: SwiftLintFile,
|
|
substructure: [SourceKittenDictionary]
|
|
) -> [StyleViolation] {
|
|
var violations = [StyleViolation]()
|
|
for dictionary in substructure {
|
|
guard let offset: ByteCount = dictionary.offset else {
|
|
continue
|
|
}
|
|
|
|
// If it has a tap gesture and does not have a button or link trait, it's a violation.
|
|
// Also allowing ones that are hidden from accessibility, though that's not recommended.
|
|
if dictionary.hasOnSingleTapModifier(in: file) {
|
|
if dictionary.hasAccessibilityTrait(".isButton", in: file) ||
|
|
dictionary.hasAccessibilityTrait(".isLink", in: file) ||
|
|
dictionary.hasAccessibilityHiddenModifier(in: file) {
|
|
continue
|
|
}
|
|
|
|
violations.append(
|
|
StyleViolation(ruleDescription: Self.description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, byteOffset: offset))
|
|
)
|
|
}
|
|
|
|
// If dictionary did not represent a View with a tap gesture, recursively check substructure,
|
|
// unless it's a container that hides its children from accessibility.
|
|
else if dictionary.substructure.isNotEmpty {
|
|
if dictionary.hasAccessibilityHiddenModifier(in: file) ||
|
|
dictionary.hasAccessibilityElementChildrenIgnoreModifier(in: file) {
|
|
continue
|
|
}
|
|
|
|
violations.append(
|
|
contentsOf: findButtonTraitViolations(file: file, substructure: dictionary.substructure)
|
|
)
|
|
}
|
|
}
|
|
|
|
return violations
|
|
}
|
|
}
|
|
|
|
// MARK: SourceKittenDictionary extensions
|
|
|
|
private extension SourceKittenDictionary {
|
|
/// Whether or not the dictionary represents a SwiftUI View with a tap gesture where the `count` argument is 1.
|
|
/// A single tap can be represented by an `onTapGesture` modifier with a count of 1 (default value is 1),
|
|
/// or by a `gesture`, `simultaneousGesture`, or `highPriorityGesture` modifier with an argument
|
|
/// starting with a `TapGesture` object with a count of 1 (default value is 1).
|
|
func hasOnSingleTapModifier(in file: SwiftLintFile) -> Bool {
|
|
return hasModifier(
|
|
anyOf: [
|
|
SwiftUIModifier(
|
|
name: "onTapGesture",
|
|
arguments: [.init(name: "count", required: false, values: ["1"])]
|
|
),
|
|
SwiftUIModifier(
|
|
name: "gesture",
|
|
arguments: [
|
|
.init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix)
|
|
]
|
|
),
|
|
SwiftUIModifier(
|
|
name: "simultaneousGesture",
|
|
arguments: [
|
|
.init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix)
|
|
]
|
|
),
|
|
SwiftUIModifier(
|
|
name: "highPriorityGesture",
|
|
arguments: [
|
|
.init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix)
|
|
]
|
|
)
|
|
],
|
|
in: file
|
|
)
|
|
}
|
|
|
|
/// Whether or not the dictionary represents a SwiftUI View with an `accessibilityAddTraits()` or
|
|
/// `accessibility(addTraits:)` modifier with the specified trait (specify trait as a String).
|
|
func hasAccessibilityTrait(_ trait: String, in file: SwiftLintFile) -> Bool {
|
|
return hasModifier(
|
|
anyOf: [
|
|
SwiftUIModifier(
|
|
name: "accessibilityAddTraits",
|
|
arguments: [.init(name: "", values: [trait], matchType: .substring)]
|
|
),
|
|
SwiftUIModifier(
|
|
name: "accessibility",
|
|
arguments: [.init(name: "addTraits", values: [trait], matchType: .substring)]
|
|
)
|
|
],
|
|
in: file
|
|
)
|
|
}
|
|
}
|