diff --git a/CHANGELOG.md b/CHANGELOG.md
index faeb7b0cc3a..149f52bc42e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,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)
* Speedup `LetVarWhiteSpacesRule`.
[PaulTaykalo](https://github.com/PaulTaykalo)
diff --git a/Rules.md b/Rules.md
index 29fe3ee6299..f00f09bffad 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)