Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Accessibility Font Size Rule #5436

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

#### Enhancements

* Add `Accessibility Font Size` rule to warn if a SwiftUI Text
has a fixed font size.
[MartijnAmbagtsheer](https://github.com/MartijnAmbagtsheer)

* Add new option `ignore_typealiases_and_associatedtypes` to
`nesting` rule. It excludes `typealias` and `associatedtype`
declarations from the analysis.
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

/// The rule list containing all available rules built into SwiftLint.
public let builtInRules: [any Rule.Type] = [
AccessibilityFontSizeRule.self,
AccessibilityLabelForImageRule.self,
AccessibilityTraitForButtonRule.self,
AnonymousArgumentInMultilineClosureRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import SourceKittenFramework

struct AccessibilityFontSizeRule: ASTRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)

static let description = RuleDescription(
identifier: "accessibility_font_size",
name: "Accessibility Font Size",
description: "Fonts may not have a fixed size",
kind: .lint,
minSwiftVersion: .fiveDotOne,
nonTriggeringExamples: AccessibilityFontSizeRuleExamples.nonTriggeringExamples,
triggeringExamples: AccessibilityFontSizeRuleExamples.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")) || kind == .extension,
dictionary.substructure.isNotEmpty else {
return []
}

return findFontViolations(file: file, substructure: dictionary.substructure)
}

/// Recursively check a file for font violations, and return all such violations.
private func findFontViolations(file: SwiftLintFile, substructure: [SourceKittenDictionary]) -> [StyleViolation] {
var violations = [StyleViolation]()
for dictionary in substructure {
guard let offset: ByteCount = dictionary.offset else {
continue
}

guard dictionary.isFontModifier(in: file) else {
if dictionary.substructure.isNotEmpty {
violations.append(
contentsOf: findFontViolations(
file: file,
substructure: dictionary.substructure
)
)
}

continue
}

if checkForViolations(dictionaries: [dictionary], in: file) {
violations.append(
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset)
)
)
}
}

return violations
}

private func checkForViolations(dictionaries: [SourceKittenDictionary], in file: SwiftLintFile) -> Bool {
for dictionary in dictionaries {
if dictionary.hasSystemFontModifier(in: file) || dictionary.hasCustomFontModifierWithFixedSize(in: file) {
return true
} else if dictionary.substructure.isNotEmpty {
if checkForViolations(dictionaries: dictionary.substructure, in: file) {
return true
}
}
}

return false
}
}

// MARK: SourceKittenDictionary extensions

private extension SourceKittenDictionary {
/// Whether or not the dictionary represents a SwiftUI Text.
/// Currently only accounts for SwiftUI text literals and not instance variables.
func isFontModifier(in file: SwiftLintFile) -> Bool {
// Text literals will be reported as calls to the initializer.
guard expressionKind == .call else {
return false
}

if hasModifier(
anyOf: [
SwiftUIModifier(
name: ".font",
arguments: []
)
],
in: file
) {
return true
}

return substructure.contains(where: { $0.isFontModifier(in: file) })
}

func hasCustomFontModifierWithFixedSize(in file: SwiftLintFile) -> Bool {
return hasModifier(
anyOf: [
SwiftUIModifier(
name: ".custom",
arguments: [
.init(
name: "fixedSize",
values: [],
matchType: .substring)
]
)
],
in: file
)
}

/// Whether or not the dictionary represents a SwiftUI View with an `font(.system())` modifier.
func hasSystemFontModifier(in file: SwiftLintFile) -> Bool {
return hasModifier(
anyOf: [
SwiftUIModifier(
name: "system",
arguments: [
.init(
name: "",
values: ["size"],
matchType: .substring)
]
)
],
in: file
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
internal struct AccessibilityFontSizeRuleExamples {
static let nonTriggeringExamples = [
Example("""
struct TestView: View {
var body: some View {
Text("Hello World!")
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
Text("Hello World!")
.font(.system(.largeTitle))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
TextField("Username", text: .constant(""))
.font(.system(.largeTitle))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
SecureField("Password", text: .constant(""))
.font(.system(.largeTitle))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
Button("Login") {}
.font(.system(.largeTitle))
}
}
""")
]

static let triggeringExamples = [
Example("""
struct TestView: View {
var body: some View {
↓Text("Hello World!")
.font(.system(size: 20))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓Text("Hello World!")
.italic()
.font(.system(size: 20))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓TextField("Username", text: .constant(""))
.font(.system(size: 15))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓SecureField("Password", text: .constant(""))
.font(.system(size: 15))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓Button("Login") {}
.font(.system(size: 15))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓Text("Hello World!")
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓Text("Hello World!")
.italic()
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓TextField("Username", text: .constant(""))
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓TextField("Username", text: .constant(""))
.italic()
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓SecureField("Password", text: .constant(""))
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓SecureField("Password", text: .constant(""))
.italic()
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓Button("Login") {}
.font(.custom("Havana", fixedSize: 16))
}
}
"""),
Example("""
struct TestView: View {
var body: some View {
↓Button("Login") {}
.italic()
.font(.custom("Havana", fixedSize: 16))
}
}
""")
]
}
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import SwiftLintTestHelpers
// swiftlint:disable:next blanket_disable_command
// swiftlint:disable file_length single_test_class type_name

class AccessibilityFontSizeRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(AccessibilityFontSizeRule.description)
}
}

class AccessibilityLabelForImageRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(AccessibilityLabelForImageRule.description)
Expand Down