From dc7dfba2170edcf69bd1dd4c2fe7e1281c611092 Mon Sep 17 00:00:00 2001 From: Matthew Thompson Date: Wed, 15 Feb 2023 20:45:00 -0600 Subject: [PATCH] Create rule that requires SwiftUI state properties to be private Make private state property rule opt-in Update CHANGELOG.md --- CHANGELOG.md | 5 ++ .../Extensions/SwiftSyntax+SwiftLint.swift | 15 ++++ .../Models/PrimaryRuleList.swift | 3 +- .../PrivateSwiftUIStatePropertyRule.swift | 74 +++++++++++++++++++ Tests/GeneratedTests/GeneratedTests.swift | 8 +- 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 Source/SwiftLintFramework/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 06179da4750..ba15babd273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Source/SwiftLintFramework/Extensions/SwiftSyntax+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/SwiftSyntax+SwiftLint.swift index 581ed622794..044e424eb4b 100644 --- a/Source/SwiftLintFramework/Extensions/SwiftSyntax+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/SwiftSyntax+SwiftLint.swift @@ -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")) diff --git a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift index a466c9fe07d..a9870d8da15 100644 --- a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift +++ b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift @@ -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. @@ -149,6 +149,7 @@ let builtInRules: [Rule.Type] = [ PrivateOutletRule.self, PrivateOverFilePrivateRule.self, PrivateSubjectRule.self, + PrivateSwiftUIStatePropertyRule.self, PrivateUnitTestRule.self, ProhibitedInterfaceBuilderRule.self, ProhibitedSuperRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift b/Source/SwiftLintFramework/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift new file mode 100644 index 00000000000..44899378b57 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift @@ -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) + } + } +} diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 1d1e402c289..99038f124a7 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -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 @@ -877,6 +877,12 @@ class PrivateSubjectRuleGeneratedTests: XCTestCase { } } +class PrivateSwiftUIStatePropertyRuleGeneratedTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(PrivateSwiftUIStatePropertyRule.description) + } +} + class PrivateUnitTestRuleGeneratedTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateUnitTestRule.description)