204 lines
7.4 KiB
Swift
204 lines
7.4 KiB
Swift
import SourceKittenFramework
|
|
|
|
/// Allows for Enums that conform to a protocol to require that a specific case be present.
|
|
///
|
|
/// This is primarily for result enums where a specific case is common but cannot be inherited due to cases not being
|
|
/// inheritable.
|
|
///
|
|
/// For example: A result enum is used to define all of the responses a client must handle from a specific service call
|
|
/// in an API.
|
|
///
|
|
/// ````
|
|
/// enum MyServiceCallResponse: String {
|
|
/// case unauthorized
|
|
/// case unknownError
|
|
/// case accountCreated
|
|
/// }
|
|
///
|
|
/// // An exhaustive switch can be used so any new scenarios added cause compile errors.
|
|
/// switch response {
|
|
/// case unauthorized:
|
|
/// ...
|
|
/// case unknownError:
|
|
/// ...
|
|
/// case accountCreated:
|
|
/// ...
|
|
/// }
|
|
/// ````
|
|
///
|
|
/// If cases could be inherited you could put all of the common ones in an enum and then inherit from that enum:
|
|
///
|
|
/// ````
|
|
/// enum MyServiceResponse: String {
|
|
/// case unauthorized
|
|
/// case unknownError
|
|
/// }
|
|
///
|
|
/// enum MyServiceCallResponse: MyServiceResponse {
|
|
/// case accountCreated
|
|
/// }
|
|
/// ````
|
|
///
|
|
/// Which would result in MyServiceCallResponse having all of the cases when compiled:
|
|
///
|
|
/// ```
|
|
/// enum MyServiceCallResponse: MyServiceResponse {
|
|
/// case unauthorized
|
|
/// case unknownError
|
|
/// case accountCreated
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Since that cannot be done this rule allows you to define cases that should be present if conforming to a protocol.
|
|
///
|
|
/// `.swiftlint.yml`
|
|
/// ````
|
|
/// required_enum_case:
|
|
/// MyServiceResponse:
|
|
/// unauthorized: error
|
|
/// unknownError: error
|
|
/// ````
|
|
///
|
|
/// ````
|
|
/// protocol MyServiceResponse {}
|
|
///
|
|
/// // This will now have errors because `unauthorized` and `unknownError` are not present.
|
|
/// enum MyServiceCallResponse: String, MyServiceResponse {
|
|
/// case accountCreated
|
|
/// }
|
|
/// ````
|
|
public struct RequiredEnumCaseRule: ASTRule, OptInRule, ConfigurationProviderRule {
|
|
private typealias RequiredCase = RequiredEnumCaseRuleConfiguration.RequiredCase
|
|
|
|
/// Simple representation of parsed information from the SourceKitRepresentable dictionary.
|
|
private struct Enum {
|
|
let location: Location
|
|
let inheritedTypes: [String]
|
|
let cases: [String]
|
|
|
|
init(from dictionary: SourceKittenDictionary, in file: SwiftLintFile) {
|
|
location = Enum.location(from: dictionary, in: file)
|
|
inheritedTypes = dictionary.inheritedTypes
|
|
cases = Enum.cases(from: dictionary)
|
|
}
|
|
|
|
/// Determines the location of where the enum declaration starts.
|
|
///
|
|
/// - parameter dictionary: Parsed source for the enum.
|
|
/// - parameter file: `SwiftLintFile` that contains the enum.
|
|
///
|
|
/// - returns: Location of where the enum declaration starts.
|
|
static func location(from dictionary: SourceKittenDictionary, in file: SwiftLintFile) -> Location {
|
|
return Location(file: file, byteOffset: dictionary.offset ?? 0)
|
|
}
|
|
|
|
/// Determines the names of cases found in the enum.
|
|
///
|
|
/// - parameter dictionary: Parsed source for the enum.
|
|
/// - returns: Names of cases found in the enum.
|
|
static func cases(from dictionary: SourceKittenDictionary) -> [String] {
|
|
let caseSubstructures = dictionary.substructure.filter { dict in
|
|
return dict.declarationKind == .enumcase
|
|
}.flatMap { $0.substructure }
|
|
|
|
return caseSubstructures.compactMap { $0.name }.map { name in
|
|
if SwiftVersion.current > .fourDotOne,
|
|
let parenIndex = name.firstIndex(of: "("),
|
|
parenIndex > name.startIndex {
|
|
let index = name.index(before: parenIndex)
|
|
return String(name[...index])
|
|
} else {
|
|
return name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public var configuration = RequiredEnumCaseRuleConfiguration()
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "required_enum_case",
|
|
name: "Required Enum Case",
|
|
description: "Enums conforming to a specified protocol must implement a specific case(s).",
|
|
kind: .lint,
|
|
nonTriggeringExamples: [
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success, error, notConnected \n" +
|
|
"}",
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success, error, notConnected(error: Error) \n" +
|
|
"}",
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success\n" +
|
|
" case error\n" +
|
|
" case notConnected\n" +
|
|
"}",
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success\n" +
|
|
" case error\n" +
|
|
" case notConnected(error: Error)\n" +
|
|
"}"
|
|
],
|
|
triggeringExamples: [
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success, error \n" +
|
|
"}",
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success, error \n" +
|
|
"}",
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success\n" +
|
|
" case error\n" +
|
|
"}",
|
|
"enum MyNetworkResponse: String, NetworkResponsable {\n" +
|
|
" case success\n" +
|
|
" case error\n" +
|
|
"}"
|
|
]
|
|
)
|
|
|
|
public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
|
|
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
|
guard kind == .enum else {
|
|
return []
|
|
}
|
|
|
|
return violations(for: Enum(from: dictionary, in: file))
|
|
}
|
|
|
|
/// Iterates over all of the protocols in the configuration and creates violations for missing cases.
|
|
///
|
|
/// - parameter parsed: Enum information parsed from the SourceKitRepresentable dictionary.
|
|
/// - returns: Violations for missing cases.
|
|
private func violations(for parsed: Enum) -> [StyleViolation] {
|
|
var violations: [StyleViolation] = []
|
|
|
|
for (type, requiredCases) in configuration.protocols where parsed.inheritedTypes.contains(type) {
|
|
for requiredCase in requiredCases where !parsed.cases.contains(requiredCase.name) {
|
|
violations.append(create(violationIn: parsed, for: type, missing: requiredCase))
|
|
}
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
/// Creates the violation for a missing case.
|
|
///
|
|
/// - parameter parsed: Enum information parsed from the `SourceKitRepresentable` dictionary.
|
|
/// - parameter protocolName: Name of the protocol that is missing the case.
|
|
/// - parameter requiredCase: Information about the case and the severity of the violation.
|
|
///
|
|
/// - returns: Created violation.
|
|
private func create(violationIn parsed: Enum,
|
|
for protocolName: String,
|
|
missing requiredCase: RequiredCase) -> StyleViolation {
|
|
return StyleViolation(
|
|
ruleDescription: type(of: self).description,
|
|
severity: requiredCase.severity,
|
|
location: parsed.location,
|
|
reason: "Enums conforming to \"\(protocolName)\" must have a \"\(requiredCase.name)\" case")
|
|
}
|
|
}
|