Skip to content

Commit

Permalink
Create rule that requires SwiftUI state properties to be private
Browse files Browse the repository at this point in the history
Make private state property rule opt-in

Update CHANGELOG.md
  • Loading branch information
thompsonmatthew-heb committed Feb 20, 2023
1 parent 393318d commit dc7dfba
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

#### Enhancements

* Add new `private_swiftui_state_property` opt-in rule to encourage setting
SwiftUI `@State` properties to private.
[mt00chikin](https://github.com/mt00chikin)
[#3173](https://github.com/realm/SwiftLint/issues/3173)

* Add local links to rule descriptions to every rule listed
in `Rule Directory.md`.
[kattouf](https://github.com/kattouf)
Expand Down
15 changes: 15 additions & 0 deletions Source/SwiftLintFramework/Extensions/SwiftSyntax+SwiftLint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,21 @@ extension TokenKind {
}
}

extension AttributeListSyntax? {
var hasStateAttribute: Bool {
guard let attributes = self else { return false }

return attributes.contains { attr in
guard let stateAttr = attr.as(CustomAttributeSyntax.self),
let identifier = stateAttr.attributeName.as(SimpleTypeIdentifierSyntax.self) else {
return false
}

return identifier.name.text == "State"
}
}
}

extension ModifierListSyntax? {
var containsLazy: Bool {
contains(tokenKind: .contextualKeyword("lazy"))
Expand Down
3 changes: 2 additions & 1 deletion Source/SwiftLintFramework/Models/PrimaryRuleList.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated using Sourcery 2.0.0 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.0.1 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

/// The rule list containing all available rules built into SwiftLint.
Expand Down Expand Up @@ -149,6 +149,7 @@ let builtInRules: [Rule.Type] = [
PrivateOutletRule.self,
PrivateOverFilePrivateRule.self,
PrivateSubjectRule.self,
PrivateSwiftUIStatePropertyRule.self,
PrivateUnitTestRule.self,
ProhibitedInterfaceBuilderRule.self,
ProhibitedSuperRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import SwiftSyntax

/// Rule to require that any state properties in SwiftUI be declared as private.
/// State properties should only be accessible from inside the View's body, or from methods called by it
struct PrivateSwiftUIStatePropertyRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration(.warning)

static let description = RuleDescription(
identifier: "private_swiftui_state",
name: "Private SwiftUI @State Properties",
description: "SwiftUI's state properties should be private",
kind: .lint,
nonTriggeringExamples: [
Example(
"""
struct ContentView: View {
@State private var isPlaying: Bool = false
}
"""
),
Example(
"""
struct ContentView: View {
@State fileprivate var isPlaying: Bool = false
}
"""
),
Example(
"""
struct ContentView: View {
var isPlaying = false
}
"""
),
Example(
"""
struct ContentView: View {
@StateObject var foo = Foo()
}
"""
)
],
triggeringExamples: [
Example(
"""
struct ContentView: View {
@State var isPlaying: Bool = false
}
"""
)
])

init() {}

func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}

private extension PrivateSwiftUIStatePropertyRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: MemberDeclListItemSyntax) {
guard
let decl = node.decl.as(VariableDeclSyntax.self),
decl.attributes.hasStateAttribute,
!decl.modifiers.isPrivateOrFileprivate
else {
return
}

violations.append(decl.letOrVarKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
8 changes: 7 additions & 1 deletion Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated using Sourcery 2.0.0 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.0.1 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
@_spi(TestHelper)
@testable import SwiftLintFramework
Expand Down Expand Up @@ -877,6 +877,12 @@ class PrivateSubjectRuleGeneratedTests: XCTestCase {
}
}

class PrivateSwiftUIStatePropertyRuleGeneratedTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(PrivateSwiftUIStatePropertyRule.description)
}
}

class PrivateUnitTestRuleGeneratedTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(PrivateUnitTestRule.description)
Expand Down

0 comments on commit dc7dfba

Please sign in to comment.