Skip to content

Commit

Permalink
Add new private_swiftui_state_property rule (#4769)
Browse files Browse the repository at this point in the history
  • Loading branch information
mt00chikin authored Jul 28, 2023
1 parent b3189aa commit 0537f3a
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
* Rewrite `control_statement` rule using SwiftSyntax.
[SimplyDanny](https://github.com/SimplyDanny)

* 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 `unneeded_override` rule to remove function overrides that only
call super.
[keith](https://github.com/keith)
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 @@ -151,6 +151,7 @@ public 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,246 @@
import SwiftSyntax

/// Require that any state properties in SwiftUI be declared as private
///
/// State properties should only be accessible from inside a SwiftUI App, View, or Scene, or from methods called by it
struct PrivateSwiftUIStatePropertyRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)

static let description = RuleDescription(
identifier: "private_swiftui_state",
name: "Private SwiftUI @State Properties",
description: "SwiftUI state properties should be private",
kind: .lint,
nonTriggeringExamples: [
Example("""
struct MyApp: App {
@State private var isPlaying: Bool = false
}
"""),
Example("""
struct MyScene: Scene {
@State private var isPlaying: Bool = false
}
"""),
Example("""
struct ContentView: View {
@State private var isPlaying: Bool = false
}
"""),
Example("""
struct ContentView: View {
@State fileprivate var isPlaying: Bool = false
}
"""),
Example("""
struct ContentView: View {
@State private var isPlaying: Bool = false
struct InnerView: View {
@State private var showsIndicator: Bool = false
}
}
"""),
Example("""
struct MyStruct {
struct ContentView: View {
@State private var isPlaying: Bool = false
}
}
"""),
Example("""
struct MyStruct {
struct ContentView: View {
@State private var isPlaying: Bool = false
}
@State var nonTriggeringState: Bool = false
}
"""),

Example("""
struct ContentView: View {
var isPlaying = false
}
"""),
Example("""
struct ContentView: View {
@StateObject var foo = Foo()
}
"""),
Example("""
struct Foo {
@State var bar = false
}
"""),
Example("""
class Foo: ObservableObject {
@State var bar = Bar()
}
"""),
Example("""
extension MyObject {
struct ContentView: View {
@State private var isPlaying: Bool = false
}
}
"""),
Example("""
actor ContentView: View {
@State private var isPlaying: Bool = false
}
""")
],
triggeringExamples: [
Example("""
struct MyApp: App {
@State ↓var isPlaying: Bool = false
}
"""),
Example("""
struct MyScene: Scene {
@State ↓var isPlaying: Bool = false
}
"""),
Example("""
struct ContentView: View {
@State ↓var isPlaying: Bool = false
}
"""),
Example("""
struct ContentView: View {
struct InnerView: View {
@State private var showsIndicator: Bool = false
}
@State ↓var isPlaying: Bool = false
}
"""),
Example("""
struct MyStruct {
struct ContentView: View {
@State ↓var isPlaying: Bool = false
}
}
"""),
Example("""
struct MyStruct {
struct ContentView: View {
@State ↓var isPlaying: Bool = false
}
@State var isPlaying: Bool = false
}
"""),
Example("""
final class ContentView: View {
@State ↓var isPlaying: Bool = false
}
"""),
Example("""
extension MyObject {
struct ContentView: View {
@State ↓var isPlaying: Bool = false
}
}
"""),
Example("""
actor ContentView: View {
@State ↓var isPlaying: Bool = false
}
""")
]
)

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

private extension PrivateSwiftUIStatePropertyRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
[ProtocolDeclSyntax.self]
}

/// LIFO stack that stores type inheritance clauses for each visited node
/// The last value is the inheritance clause for the most recently visited node
/// A nil value indicates that the node does not provide any inheritance clause
private var visitedTypeInheritances = Stack<TypeInheritanceClauseSyntax?>()

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
visitedTypeInheritances.push(node.inheritanceClause)
return .visitChildren
}

override func visitPost(_ node: ClassDeclSyntax) {
visitedTypeInheritances.pop()
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
visitedTypeInheritances.push(node.inheritanceClause)
return .visitChildren
}

override func visitPost(_ node: StructDeclSyntax) {
visitedTypeInheritances.pop()
}

override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
visitedTypeInheritances.push(node.inheritanceClause)
return .visitChildren
}

override func visitPost(_ node: ActorDeclSyntax) {
visitedTypeInheritances.pop()
}

override func visitPost(_ node: MemberDeclListItemSyntax) {
guard
let decl = node.decl.as(VariableDeclSyntax.self),
let inheritanceClause = visitedTypeInheritances.peek() as? TypeInheritanceClauseSyntax,
inheritanceClause.conformsToApplicableSwiftUIProtocol,
decl.attributes.hasStateAttribute,
!decl.modifiers.isPrivateOrFileprivate
else {
return
}

violations.append(decl.bindingKeyword.positionAfterSkippingLeadingTrivia)
}
}
}

private extension TypeInheritanceClauseSyntax {
static let applicableSwiftUIProtocols: Set<String> = ["View", "App", "Scene"]

var conformsToApplicableSwiftUIProtocol: Bool {
inheritedTypeCollection.containsInheritedType(inheritedTypes: Self.applicableSwiftUIProtocols)
}
}

private extension InheritedTypeListSyntax {
func containsInheritedType(inheritedTypes: Set<String>) -> Bool {
contains {
guard let simpleType = $0.typeName.as(SimpleTypeIdentifierSyntax.self) else { return false }

return inheritedTypes.contains(simpleType.name.text)
}
}
}

private extension AttributeListSyntax? {
/// Returns `true` if the attribute's identifier is equal to "State"
var hasStateAttribute: Bool {
guard let attributes = self else { return false }

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

return identifier.name.text == "State"
}
}
}
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,12 @@ class PrivateSubjectRuleGeneratedTests: SwiftLintTestCase {
}
}

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

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

0 comments on commit 0537f3a

Please sign in to comment.