Skip to content

Commit

Permalink
Add convenience_type opt-in rule
Browse files Browse the repository at this point in the history
Fixes #1871
  • Loading branch information
marcelofabri committed Jun 17, 2018
1 parent aeb7040 commit 1d28025
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
[JP Simard](https://github.com/jpsim)
[#1420](https://github.com/realm/SwiftLint/issues/1420)

* Add `convenience_type` opt-in rule to validate that types hosting only static
members should be enums to avoid instantiation.
[Marcelo Fabri](https://github.com/marcelofabri)
[#1871](https://github.com/realm/SwiftLint/issues/1871)

#### Bug Fixes

* Update `LowerACLThanParent` rule to not lint extensions.
Expand Down
71 changes: 71 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* [Conditional Returns on Newline](#conditional-returns-on-newline)
* [Contains over first not nil](#contains-over-first-not-nil)
* [Control Statement](#control-statement)
* [Convenience Type](#convenience-type)
* [Custom Rules](#custom-rules)
* [Cyclomatic Complexity](#cyclomatic-complexity)
* [Discarded Notification Center Observer](#discarded-notification-center-observer)
Expand Down Expand Up @@ -1943,6 +1944,76 @@ do {



## Convenience Type

Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version
--- | --- | --- | --- | ---
`convenience_type` | Disabled | No | idiomatic | 4.1.0

Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation.

### Examples

<details>
<summary>Non Triggering Examples</summary>

```swift
enum Math { // enum
public static let pi = 3.14
}
```

```swift
// class with inheritance
class MathViewController: UIViewController {
public static let pi = 3.14
}
```

```swift
@objc class Math: NSObject { // class visible to Obj-C
public static let pi = 3.14
}
```

```swift
struct Math { // type with non-static declarations
public static let pi = 3.14
public let randomNumber = 2
}
```

```swift
class DummyClass {}
```

</details>
<details>
<summary>Triggering Examples</summary>

```swift
struct Math {
public static let pi = 3.14
}
```

```swift
class Math {
public static let pi = 3.14
}
```

```swift
struct Math {
public static let pi = 3.14
@available(*, unavailable) init() {}
}
```

</details>



## Custom Rules

Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public let masterRuleList = RuleList(rules: [
ConditionalReturnsOnNewlineRule.self,
ContainsOverFirstNotNilRule.self,
ControlStatementRule.self,
ConvenienceTypeRule.self,
CustomRules.self,
CyclomaticComplexityRule.self,
DiscardedNotificationCenterObserverRule.self,
Expand Down
108 changes: 108 additions & 0 deletions Source/SwiftLintFramework/Rules/ConvenienceTypeRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import SourceKittenFramework

public struct ConvenienceTypeRule: ASTRule, OptInRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)

public init() {}

public static let description = RuleDescription(
identifier: "convenience_type",
name: "Convenience Type",
description: "Types used for hosting only static members should be implemented as a caseless enum " +
"to avoid instantiation.",
kind: .idiomatic,
minSwiftVersion: .fourDotOne,
nonTriggeringExamples: [
"""
enum Math { // enum
public static let pi = 3.14
}
""",
"""
// class with inheritance
class MathViewController: UIViewController {
public static let pi = 3.14
}
""",
"""
@objc class Math: NSObject { // class visible to Obj-C
public static let pi = 3.14
}
""",
"""
struct Math { // type with non-static declarations
public static let pi = 3.14
public let randomNumber = 2
}
""",
"class DummyClass {}"
],
triggeringExamples: [
"""
↓struct Math {
public static let pi = 3.14
}
""",
"""
↓class Math {
public static let pi = 3.14
}
""",
"""
↓struct Math {
public static let pi = 3.14
@available(*, unavailable) init() {}
}
"""
]
)

public func validate(file: File, kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard let offset = dictionary.offset,
[.class, .struct].contains(kind),
dictionary.inheritedTypes.isEmpty,
!dictionary.substructure.isEmpty else {
return []
}

let containsInstanceDeclarations = dictionary.substructure.contains { dict in
guard let kind = dict.kind.flatMap(SwiftDeclarationKind.init(rawValue:)) else {
return false
}

let instanceKinds: Set<SwiftDeclarationKind> = [.varInstance, .functionSubscript, .functionMethodInstance]
guard instanceKinds.contains(kind), let name = dict.name else {
return false
}

if name.hasPrefix("init(") {
return !isFunctionUnavailable(file: file, dictionary: dict)
}

return true
}

guard !containsInstanceDeclarations else {
return []
}

return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}

private func isFunctionUnavailable(file: File, dictionary: [String: SourceKitRepresentable]) -> Bool {
return dictionary.swiftAttributes.contains { dict -> Bool in
guard dict.attribute.flatMap(SwiftDeclarationAttributeKind.init(rawValue:)) == .available,
let offset = dict.offset, let length = dict.length,
let contents = file.contents.bridge().substringWithByteRange(start: offset, length: length) else {
return false
}

return contents.contains("unavailable")
}
}
}
4 changes: 4 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
D4246D6F1F30DB260097E658 /* PrivateOverFilePrivateRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4246D6E1F30DB260097E658 /* PrivateOverFilePrivateRuleTests.swift */; };
D42B45D91F0AF5E30086B683 /* StrictFilePrivateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42B45D81F0AF5E30086B683 /* StrictFilePrivateRule.swift */; };
D42D2B381E09CC0D00CD7A2E /* FirstWhereRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42D2B371E09CC0D00CD7A2E /* FirstWhereRule.swift */; };
D42DEAAB20D5EE4400E86F31 /* ConvenienceTypeRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42DEAAA20D5EE4400E86F31 /* ConvenienceTypeRule.swift */; };
D4348EEA1C46122C007707FB /* FunctionBodyLengthRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4348EE91C46122C007707FB /* FunctionBodyLengthRuleTests.swift */; };
D43B04641E0620AB004016AF /* UnusedEnumeratedRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43B04631E0620AB004016AF /* UnusedEnumeratedRule.swift */; };
D43B04661E071ED3004016AF /* ColonRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43B04651E071ED3004016AF /* ColonRuleTests.swift */; };
Expand Down Expand Up @@ -590,6 +591,7 @@
D4246D6E1F30DB260097E658 /* PrivateOverFilePrivateRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOverFilePrivateRuleTests.swift; sourceTree = "<group>"; };
D42B45D81F0AF5E30086B683 /* StrictFilePrivateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrictFilePrivateRule.swift; sourceTree = "<group>"; };
D42D2B371E09CC0D00CD7A2E /* FirstWhereRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstWhereRule.swift; sourceTree = "<group>"; };
D42DEAAA20D5EE4400E86F31 /* ConvenienceTypeRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvenienceTypeRule.swift; sourceTree = "<group>"; };
D4348EE91C46122C007707FB /* FunctionBodyLengthRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionBodyLengthRuleTests.swift; sourceTree = "<group>"; };
D43B04631E0620AB004016AF /* UnusedEnumeratedRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedEnumeratedRule.swift; sourceTree = "<group>"; };
D43B04651E071ED3004016AF /* ColonRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColonRuleTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1110,6 +1112,7 @@
93E0C3CD1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift */,
29AD4C641F6EA16C009B66E1 /* ContainsOverFirstNotNilRule.swift */,
65454F451B14D73800319A6C /* ControlStatementRule.swift */,
D42DEAAA20D5EE4400E86F31 /* ConvenienceTypeRule.swift */,
3B1DF0111C5148140011BCED /* CustomRules.swift */,
2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */,
D4DABFD21E29B4A5009617B6 /* DiscardedNotificationCenterObserverRule.swift */,
Expand Down Expand Up @@ -1699,6 +1702,7 @@
3B1DF0121C5148140011BCED /* CustomRules.swift in Sources */,
2E5761AA1C573B83003271AF /* FunctionParameterCountRule.swift in Sources */,
E86396C91BADB2B9002C9E88 /* JSONReporter.swift in Sources */,
D42DEAAB20D5EE4400E86F31 /* ConvenienceTypeRule.swift in Sources */,
E881985A1BEA96EA00333A11 /* OperatorFunctionWhitespaceRule.swift in Sources */,
D44254201DB87CA200492EA4 /* ValidIBInspectableRule.swift in Sources */,
62640152201552FD005B9C4A /* DiscouragedOptionalBooleanRule.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Tests/SwiftLintFrameworkTests/RulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class RulesTests: XCTestCase {
verifyRule(ControlStatementRule.description)
}

func testConvenienceType() {
verifyRule(ConvenienceTypeRule.description)
}

func testCyclomaticComplexity() {
verifyRule(CyclomaticComplexityRule.description)
}
Expand Down

0 comments on commit 1d28025

Please sign in to comment.