From 07994623f71f5e0644d9dd157ed2ff134f006bbd Mon Sep 17 00:00:00 2001 From: MarkoPejovic Date: Tue, 1 Oct 2019 18:14:21 -0700 Subject: [PATCH] Implement #2888 --- CHANGELOG.md | 4 + Rules.md | 100 ++++++++++++++ .../Models/MasterRuleList.swift | 1 + ...RawValueForCamelCasedCodableEnumRule.swift | 129 ++++++++++++++++++ SwiftLint.xcodeproj/project.pbxproj | 4 + Tests/LinuxMain.swift | 7 + .../AutomaticRuleTests.generated.swift | 6 + 7 files changed, 251 insertions(+) create mode 100644 Source/SwiftLintFramework/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 37218e6db5b..92d70d46012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ * Support building with Swift 5.1 on Linux. [Marcelo Fabri](https://github.com/marcelofabri) [#2874](https://github.com/realm/SwiftLint/issues/2874) +* Add `snake_case_raw_value_codable_string_enum` opt-in rule to enforce snake + cased raw values for camel cased Codable String enum cases. + [Marko Pejovic](https://github.com/00FA9A) + [#2888](https://github.com/realm/SwiftLint/issues/2888) #### Bug Fixes diff --git a/Rules.md b/Rules.md index 167044e8758..832d390eb3f 100644 --- a/Rules.md +++ b/Rules.md @@ -124,6 +124,7 @@ * [Quick Discouraged Call](#quick-discouraged-call) * [Quick Discouraged Focused Test](#quick-discouraged-focused-test) * [Quick Discouraged Pending Test](#quick-discouraged-pending-test) +* [Raw Value For Camel Cased Codable String Enum](#raw-alue-for-camel-cased-codable-enum) * [Reduce Boolean](#reduce-boolean) * [Reduce Into](#reduce-into) * [Redundant Discardable Let](#redundant-discardable-let) @@ -16873,6 +16874,105 @@ class TotoTests: QuickSpec { + +## Raw Value For Camel Cased Codable Enum + +Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version +--- | --- | --- | --- | --- | --- +`raw_value_for_camel_cased_codable_enum` | Disabled | No | lint | No | 3.0.0 + +Camel cased Codable String enum cases should have explicitly assigned raw value. + +### Examples + +
+Non Triggering Examples + + +```swift +enum Numbers: Codable { + case int(Int) + case short(Int16) +} +``` + +```swift +enum Numbers: Int, Codable { + case one = 1 + case two = 2 +} +``` + +```swift +enum Numbers: Double, Codable { + case one = 1.1 + case two = 2.2 +} +``` + +```swift +enum Numbers: String, Codable { + case one = "one" + case two = "two" +} +``` + +```swift +enum Status: String { + case ok + case notAcceptable + case maybeAcceptable = "maybe_acceptable" +} +``` + +```swift +enum Status: Int, Codable { + case ok + case notAcceptable + case maybeAcceptable = -1 +} +``` + +
+
+Triggering Examples + +```swift +enum Status: String, Codable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" +} +``` + +```swift +enum Status: String, Decodable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" +} +``` + +```swift +enum Status: String, Encodable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" +} +``` + +```swift +enum Status: String, Codable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" +} +``` + +
+ + + ## Reduce Boolean Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index b34c694029f..9e4a15016d5 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -125,6 +125,7 @@ public let masterRuleList = RuleList(rules: [ QuickDiscouragedCallRule.self, QuickDiscouragedFocusedTestRule.self, QuickDiscouragedPendingTestRule.self, + RawValueForCamelCasedCodableEnumRule.self, ReduceBooleanRule.self, ReduceIntoRule.self, RedundantDiscardableLetRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift b/Source/SwiftLintFramework/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift new file mode 100644 index 00000000000..c500c0b1648 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/RawValueForCamelCasedCodableEnumRule.swift @@ -0,0 +1,129 @@ +import SourceKittenFramework + +public struct RawValueForCamelCasedCodableEnumRule: ASTRule, OptInRule, ConfigurationProviderRule, +AutomaticTestableRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "raw_value_for_camel_cased_codable_enum", + name: "Raw Value For Camel Cased Codable Enum", + description: "Camel cased cases of Codable String enums should have raw value.", + kind: .lint, + nonTriggeringExamples: [ + """ + enum Numbers: Codable { + case int(Int) + case short(Int16) + } + """, + """ + enum Numbers: Int, Codable { + case one = 1 + case two = 2 + } + """, + """ + enum Numbers: Double, Codable { + case one = 1.1 + case two = 2.2 + } + """, + """ + enum Numbers: String, Codable { + case one = "one" + case two = "two" + } + """, + """ + enum Status: String { + case ok + case notAcceptable + case maybeAcceptable = "maybe_acceptable" + } + """, + """ + enum Status: Int, Codable { + case ok + case notAcceptable + case maybeAcceptable = -1 + } + """ + ], + triggeringExamples: [ + """ + enum Status: String, Codable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" + } + """, + """ + enum Status: String, Decodable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" + } + """, + """ + enum Status: String, Encodable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" + } + """, + """ + enum Status: String, Codable { + case ok + case ↓notAcceptable + case maybeAcceptable = "maybe_acceptable" + } + """ + ] + ) + + public func validate(file: File, + kind: SwiftDeclarationKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + guard kind == .enum else { return [] } + + let codableTypesSet = Set(["Codable", "Decodable", "Encodable"]) + let enumInheritedTypesSet = Set(dictionary.inheritedTypes) + + guard + enumInheritedTypesSet.contains("String"), + !enumInheritedTypesSet.isDisjoint(with: codableTypesSet) + else { return [] } + + let violations = violatingOffsetsForEnum(dictionary: dictionary) + return violations.map { + StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, byteOffset: $0)) + } + } + + private func violatingOffsetsForEnum(dictionary: [String: SourceKitRepresentable]) -> [Int] { + let locs = substructureElements(of: dictionary, matching: .enumcase) + .compactMap { substructureElements(of: $0, matching: .enumelement) } + .flatMap(camelCasedEnumCasesMissingRawValue) + .compactMap { $0.offset } + + return locs + } + + private func substructureElements(of dict: [String: SourceKitRepresentable], + matching kind: SwiftDeclarationKind) -> [[String: SourceKitRepresentable]] { + return dict.substructure.filter { $0.kind.flatMap(SwiftDeclarationKind.init) == kind } + } + + private func camelCasedEnumCasesMissingRawValue( + _ enumElements: [[String: SourceKitRepresentable]]) -> [[String: SourceKitRepresentable]] { + return enumElements + .filter { substructure in + guard let name = substructure.name, name != name.lowercased() else { return false } + return !substructure.elements.contains { $0.kind == "source.lang.swift.structure.elem.init_expr" } + } + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index ed7ef780947..44d4e324c22 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 7565E5F12262BA0900B0597C /* UnusedCaptureListRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7565E5F02262BA0900B0597C /* UnusedCaptureListRule.swift */; }; 756B585D2138ECD300D1A4E9 /* CollectionAlignmentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756B585C2138ECD300D1A4E9 /* CollectionAlignmentRule.swift */; }; 756C0779222EA4F400A111F4 /* ReduceIntoRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756C0777222EA49400A111F4 /* ReduceIntoRule.swift */; }; + 77DFF0E923442DE30041EEB4 /* RawValueForCamelCasedCodableEnumRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7723A4DE23442D7100F38590 /* RawValueForCamelCasedCodableEnumRule.swift */; }; 787CDE39208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787CDE38208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift */; }; 787CDE3B208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787CDE3A208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift */; }; 78F032461D7C877E00BE709A /* OverriddenSuperCallRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */; }; @@ -629,6 +630,7 @@ 756B585C2138ECD300D1A4E9 /* CollectionAlignmentRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionAlignmentRule.swift; sourceTree = ""; }; 756C0777222EA49400A111F4 /* ReduceIntoRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReduceIntoRule.swift; sourceTree = ""; }; 7578C915214173BE0080FEC9 /* CollectionAlignmentRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionAlignmentRuleTests.swift; sourceTree = ""; }; + 7723A4DE23442D7100F38590 /* RawValueForCamelCasedCodableEnumRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawValueForCamelCasedCodableEnumRule.swift; sourceTree = ""; }; 787CDE38208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCaseAlignmentConfiguration.swift; sourceTree = ""; }; 787CDE3A208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCaseAlignmentRuleTests.swift; sourceTree = ""; }; 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverriddenSuperCallRule.swift; sourceTree = ""; }; @@ -1112,6 +1114,7 @@ E315B83B1DFA4BC500621B44 /* DynamicInlineRule.swift */, 62A3E95B209E078000547A86 /* EmptyXCTestMethodRule.swift */, 626B01B420A1735900D2C42F /* EmptyXCTestMethodRuleExamples.swift */, + 7723A4DE23442D7100F38590 /* RawValueForCamelCasedCodableEnumRule.swift */, D4E92D1E2137B4C9002EDD48 /* IdenticalOperandsRule.swift */, D4441A27213279950020896F /* InertDeferRule.swift */, C26330352073DAA200D7B4FD /* LowerACLThanParentRule.swift */, @@ -2170,6 +2173,7 @@ 1E3C2D711EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift in Sources */, 188B3FF2207D61040073C2D6 /* ModifierOrderRule.swift in Sources */, 72EA17B61FD31F10009D5CE6 /* ExplicitACLRule.swift in Sources */, + 77DFF0E923442DE30041EEB4 /* RawValueForCamelCasedCodableEnumRule.swift in Sources */, B2902A0C1D66815600BFCCF7 /* PrivateUnitTestRule.swift in Sources */, D47A51101DB2DD4800A4CC21 /* AttributesRule.swift in Sources */, CE8178ED1EAC039D0063186E /* UnusedOptionalBindingConfiguration.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 5d48007bfd5..ab2a25a9b65 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1058,6 +1058,12 @@ extension QuickDiscouragedPendingTestRuleTests { ] } +extension RawValueForCamelCasedCodableEnumRuleTests { + static var allTests: [(String, (RawValueForCamelCasedCodableEnumRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension ReduceBooleanRuleTests { static var allTests: [(String, (ReduceBooleanRuleTests) -> () throws -> Void)] = [ ("testWithDefaultConfiguration", testWithDefaultConfiguration) @@ -1704,6 +1710,7 @@ XCTMain([ testCase(QuickDiscouragedCallRuleTests.allTests), testCase(QuickDiscouragedFocusedTestRuleTests.allTests), testCase(QuickDiscouragedPendingTestRuleTests.allTests), + testCase(RawValueForCamelCasedCodableEnumRuleTests.allTests), testCase(ReduceBooleanRuleTests.allTests), testCase(ReduceIntoRuleTests.allTests), testCase(RedundantDiscardableLetRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index 19a4d02c329..5b26328db8d 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -558,6 +558,12 @@ class QuickDiscouragedPendingTestRuleTests: XCTestCase { } } +class RawValueForCamelCasedCodableEnumRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(RawValueForCamelCasedCodableEnumRule.description) + } +} + class ReduceBooleanRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(ReduceBooleanRule.description)