Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift
T
Danny Mösch 58928b7e40 Enforce any on existential types (#5273)
This makes syntactically clear which types are rather expensive.
2023-10-12 08:37:23 +02:00

283 lines
9.0 KiB
Swift

import SwiftSyntax
/// Require that any state properties in SwiftUI be declared as private
///
/// State and StateObject properties should only be accessible
/// from inside a SwiftUI App, View, or Scene, or from methods called by it.
///
/// Per Apple's documentation on [State](https://developer.apple.com/documentation/swiftui/state)
/// and [StateObject](https://developer.apple.com/documentation/swiftui/stateobject)
///
/// Declare state and state objects as private to prevent setting them from a memberwise initializer,
/// which can conflict with the storage management that SwiftUI provides:
@SwiftSyntaxRule
struct PrivateSwiftUIStatePropertyRule: 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 MyApp: App {
@StateObject private var model = DataModel()
}
"""),
Example("""
struct MyScene: Scene {
@StateObject private var model = DataModel()
}
"""),
Example("""
struct ContentView: View {
@StateObject private var model = DataModel()
}
"""),
Example("""
struct MyStruct {
struct ContentView: View {
@StateObject private var dataModel = DataModel()
}
@StateObject var nonTriggeringObject = MyModel()
}
"""),
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
}
"""),
Example("""
struct MyApp: App {
@StateObject ↓var model = DataModel()
}
"""),
Example("""
struct MyScene: Scene {
@StateObject ↓var model = DataModel()
}
"""),
Example("""
struct ContentView: View {
@StateObject ↓var model = DataModel()
}
""")
]
)
}
private extension PrivateSwiftUIStatePropertyRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [any 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<InheritanceClauseSyntax?>()
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: MemberBlockItemSyntax) {
guard
let decl = node.decl.as(VariableDeclSyntax.self),
let inheritanceClause = visitedTypeInheritances.peek() as? InheritanceClauseSyntax,
inheritanceClause.conformsToApplicableSwiftUIProtocol,
decl.attributes.hasStateAttribute,
!decl.modifiers.containsPrivateOrFileprivate()
else {
return
}
violations.append(decl.bindingSpecifier.positionAfterSkippingLeadingTrivia)
}
}
}
private extension InheritanceClauseSyntax {
static let applicableSwiftUIProtocols: Set<String> = ["View", "App", "Scene"]
var conformsToApplicableSwiftUIProtocol: Bool {
inheritedTypes.containsInheritedType(inheritedTypes: Self.applicableSwiftUIProtocols)
}
}
private extension InheritedTypeListSyntax {
func containsInheritedType(inheritedTypes: Set<String>) -> Bool {
contains {
guard let simpleType = $0.type.as(IdentifierTypeSyntax.self) else { return false }
return inheritedTypes.contains(simpleType.name.text)
}
}
}
private extension AttributeListSyntax {
/// Returns `true` if the attribute's identifier is equal to `State` or `StateObject`
var hasStateAttribute: Bool {
contains { attr in
guard let stateAttr = attr.as(AttributeSyntax.self),
let identifier = stateAttr.attributeName.as(IdentifierTypeSyntax.self) else {
return false
}
return identifier.name.text == "State" || identifier.name.text == "StateObject"
}
}
}