Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add override_in_extension rule #1888

Merged
merged 3 commits into from
Oct 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

##### Enhancements

* None.
* Add `override_in_extension` opt-in rule that warns against overriding
declarations in an `extension`.
[Marcelo Fabri](https://github.com/marcelofabri)
[#1884](https://github.com/realm/SwiftLint/issues/1884)

##### Bug Fixes

Expand Down
75 changes: 75 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
* [Operator Usage Whitespace](#operator-usage-whitespace)
* [Operator Function Whitespace](#operator-function-whitespace)
* [Overridden methods call super](#overridden-methods-call-super)
* [Override in Extension](#override-in-extension)
* [Pattern Matching Keywords](#pattern-matching-keywords)
* [Private Outlets](#private-outlets)
* [Private over fileprivate](#private-over-fileprivate)
Expand Down Expand Up @@ -8484,6 +8485,80 @@ class VC: UIViewController {



## Override in Extension

Identifier | Enabled by default | Supports autocorrection | Kind
--- | --- | --- | ---
`override_in_extension` | Disabled | No | lint

Extensions shouldn't override declarations.

### Examples

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

```swift
extension Person {
var age: Int { return 42 }
}

```

```swift
extension Person {
func celebrateBirthday() {}
}

```

```swift
class Employee: Person {
override func celebrateBirthday() {}
}

```

```swift
class Foo: NSObject {}
extension Foo {
override var description: String { return "" }
}

```

```swift
struct Foo {
class Bar: NSObject {}
}
extension Foo.Bar {
override var description: String { return "" }
}

```

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

```swift
extension Person {
override ↓var age: Int { return 42 }
}

```

```swift
extension Person {
override ↓func celebrateBirthday() {}
}

```

</details>



## Pattern Matching Keywords

Identifier | Enabled by default | Supports autocorrection | Kind
Expand Down
64 changes: 64 additions & 0 deletions Source/SwiftLintFramework/Helpers/NamespaceCollector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// NamespaceCollector.swift
// SwiftLint
//
// Created by Marcelo Fabri on 10/07/17.
// Copyright © 2017 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

struct NamespaceCollector {
struct Element {
let name: String
let kind: SwiftDeclarationKind
let offset: Int
let dictionary: [String: SourceKitRepresentable]

init?(dictionary: [String: SourceKitRepresentable], namespace: [String]) {
guard let name = dictionary.name,
let kind = dictionary.kind.flatMap(SwiftDeclarationKind.init),
let offset = dictionary.offset else {
return nil
}

self.name = (namespace + [name]).joined(separator: ".")
self.kind = kind
self.offset = offset
self.dictionary = dictionary
}
}

private let dictionary: [String: SourceKitRepresentable]

init(dictionary: [String: SourceKitRepresentable]) {
self.dictionary = dictionary
}

func findAllElements(of types: Set<SwiftDeclarationKind>,
namespace: [String] = []) -> [Element] {
return findAllElements(in: dictionary, of: types, namespace: namespace)
}

private func findAllElements(in dictionary: [String: SourceKitRepresentable],
of types: Set<SwiftDeclarationKind>,
namespace: [String] = []) -> [Element] {

return dictionary.substructure.flatMap { subDict -> [Element] in

var elements: [Element] = []
guard let element = Element(dictionary: subDict, namespace: namespace) else {
return elements
}

if types.contains(element.kind) {
elements.append(element)
}

elements += findAllElements(in: subDict, of: types, namespace: [element.name])

return elements
}
}
}
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public let masterRuleList = RuleList(rules: [
OperatorFunctionWhitespaceRule.self,
OperatorUsageWhitespaceRule.self,
OverriddenSuperCallRule.self,
OverrideInExtensionRule.self,
PatternMatchingKeywordsRule.self,
PrivateOutletRule.self,
PrivateOverFilePrivateRule.self,
Expand Down
47 changes: 2 additions & 45 deletions Source/SwiftLintFramework/Rules/NoGroupingExtensionRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule {
)

public func validate(file: File) -> [StyleViolation] {

let elements = findAllElements(in: file.structure.dictionary,
of: [.class, .enum, .struct, .extension])
let collector = NamespaceCollector(dictionary: file.structure.dictionary)
let elements = collector.findAllElements(of: [.class, .enum, .struct, .extension])

let susceptibleNames = Set(elements.flatMap { $0.kind != .extension ? $0.name : nil })

Expand All @@ -46,46 +45,4 @@ public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule {
location: Location(file: file, byteOffset: $0.offset))
}
}

private func findAllElements(in dictionary: [String: SourceKitRepresentable],
of types: Set<SwiftDeclarationKind>,
namespace: [String] = []) -> [Element] {

return dictionary.substructure.flatMap { subDict -> [Element] in

var elements: [Element] = []
guard let element = Element(dictionary: subDict, namespace: namespace) else {
return elements
}

if types.contains(element.kind) {
elements.append(element)
}

elements += findAllElements(in: subDict, of: types, namespace: [element.name])

return elements
}
}
}

private struct Element {

let name: String
let kind: SwiftDeclarationKind
let offset: Int

init?(dictionary: [String: SourceKitRepresentable], namespace: [String]) {

guard let name = dictionary.name,
let kind = dictionary.kind.flatMap(SwiftDeclarationKind.init),
let offset = dictionary.offset
else {
return nil
}

self.name = (namespace + [name]).joined(separator: ".")
self.kind = kind
self.offset = offset
}
}
68 changes: 68 additions & 0 deletions Source/SwiftLintFramework/Rules/OverrideInExtensionRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// OverrideInExtensionRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 10/05/17.
// Copyright © 2017 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

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

public init() {}

public static let description = RuleDescription(
identifier: "override_in_extension",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds assertive (as in do this), instead of restrictive. "disallow_override_in_extensions" maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we have a super consistent naming convention. We already have force_try, force_cast, fallthrough and others that aren't restrictive.

name: "Override in Extension",
description: "Extensions shouldn't override declarations.",
kind: .lint,
nonTriggeringExamples: [
"extension Person {\n var age: Int { return 42 }\n}\n",
"extension Person {\n func celebrateBirthday() {}\n}\n",
"class Employee: Person {\n override func celebrateBirthday() {}\n}\n",
"class Foo: NSObject {}\n" +
"extension Foo {\n" +
" override var description: String { return \"\" }\n" +
"}\n",
"struct Foo {\n" +
" class Bar: NSObject {}\n" +
"}\n" +
"extension Foo.Bar {\n" +
" override var description: String { return \"\" }\n" +
"}\n"
],
triggeringExamples: [
"extension Person {\n override ↓var age: Int { return 42 }\n}\n",
"extension Person {\n override ↓func celebrateBirthday() {}\n}\n"
]
)

public func validate(file: File) -> [StyleViolation] {
let collector = NamespaceCollector(dictionary: file.structure.dictionary)
let elements = collector.findAllElements(of: [.class, .struct, .enum, .extension])

let susceptibleNames = Set(elements.flatMap { $0.kind == .class ? $0.name : nil })

return elements
.filter { $0.kind == .extension && !susceptibleNames.contains($0.name) }
.flatMap { element in
return element.dictionary.substructure.flatMap { element -> Int? in
guard element.kind.flatMap(SwiftDeclarationKind.init) != nil,
element.enclosedSwiftAttributes.contains("source.decl.attribute.override"),
let offset = element.offset else {
return nil
}

return offset
}
}
.map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0))
}
}
}
8 changes: 8 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
D40AD08A1E032F9700F48C30 /* UnusedClosureParameterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40AD0891E032F9700F48C30 /* UnusedClosureParameterRule.swift */; };
D40E041C1F46E3B30043BC4E /* SuperfluousDisableCommandRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40E041B1F46E3B30043BC4E /* SuperfluousDisableCommandRule.swift */; };
D40F83881DE9179200524C62 /* TrailingCommaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40F83871DE9179200524C62 /* TrailingCommaConfiguration.swift */; };
D40FE89D1F867BFF006433E2 /* OverrideInExtensionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40FE89C1F867BFF006433E2 /* OverrideInExtensionRule.swift */; };
D4130D971E16183F00242361 /* IdentifierNameRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */; };
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4130D981E16CC1300242361 /* TypeNameRuleExamples.swift */; };
D41B57781ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41B57771ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift */; };
Expand Down Expand Up @@ -180,6 +181,7 @@
D4998DE71DF191380006E05D /* AttributesRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4998DE61DF191380006E05D /* AttributesRuleTests.swift */; };
D4998DE91DF194F20006E05D /* FileHeaderRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4998DE81DF194F20006E05D /* FileHeaderRuleTests.swift */; };
D4A893351E15824100BF954D /* SwiftVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A893341E15824100BF954D /* SwiftVersion.swift */; };
D4AB0EA21F8993DD00CEC380 /* NamespaceCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AB0EA11F8993DD00CEC380 /* NamespaceCollector.swift */; };
D4B0226F1E0C75F9007E5297 /* VerticalParameterAlignmentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B0226E1E0C75F9007E5297 /* VerticalParameterAlignmentRule.swift */; };
D4B0228E1E0CC608007E5297 /* ClassDelegateProtocolRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */; };
D4B022961E0EF80C007E5297 /* RedundantOptionalInitializationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B022951E0EF80C007E5297 /* RedundantOptionalInitializationRule.swift */; };
Expand Down Expand Up @@ -485,6 +487,7 @@
D40AD0891E032F9700F48C30 /* UnusedClosureParameterRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedClosureParameterRule.swift; sourceTree = "<group>"; };
D40E041B1F46E3B30043BC4E /* SuperfluousDisableCommandRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuperfluousDisableCommandRule.swift; sourceTree = "<group>"; };
D40F83871DE9179200524C62 /* TrailingCommaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaConfiguration.swift; sourceTree = "<group>"; };
D40FE89C1F867BFF006433E2 /* OverrideInExtensionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideInExtensionRule.swift; sourceTree = "<group>"; };
D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifierNameRuleExamples.swift; sourceTree = "<group>"; };
D4130D981E16CC1300242361 /* TypeNameRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypeNameRuleExamples.swift; sourceTree = "<group>"; };
D41B57771ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionAccessModifierRule.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -528,6 +531,7 @@
D4998DE61DF191380006E05D /* AttributesRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesRuleTests.swift; sourceTree = "<group>"; };
D4998DE81DF194F20006E05D /* FileHeaderRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderRuleTests.swift; sourceTree = "<group>"; };
D4A893341E15824100BF954D /* SwiftVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftVersion.swift; sourceTree = "<group>"; };
D4AB0EA11F8993DD00CEC380 /* NamespaceCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamespaceCollector.swift; sourceTree = "<group>"; };
D4B0226E1E0C75F9007E5297 /* VerticalParameterAlignmentRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalParameterAlignmentRule.swift; sourceTree = "<group>"; };
D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassDelegateProtocolRule.swift; sourceTree = "<group>"; };
D4B022951E0EF80C007E5297 /* RedundantOptionalInitializationRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantOptionalInitializationRule.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -725,6 +729,7 @@
isa = PBXGroup;
children = (
4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */,
D4AB0EA11F8993DD00CEC380 /* NamespaceCollector.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1052,6 +1057,7 @@
692B1EB11BD7E00F00EAABFF /* OpeningBraceRule.swift */,
E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */,
D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */,
D40FE89C1F867BFF006433E2 /* OverrideInExtensionRule.swift */,
78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */,
D403A4A21F4DB5510020CA02 /* PatternMatchingKeywordsRule.swift */,
094385021D5D4F78009168CF /* PrivateOutletRule.swift */,
Expand Down Expand Up @@ -1509,6 +1515,7 @@
D4A893351E15824100BF954D /* SwiftVersion.swift in Sources */,
D4470D5D1EB8004B008A1B2E /* VerticalParameterAlignmentOnCallRule.swift in Sources */,
D4DABFD31E29B4A5009617B6 /* DiscardedNotificationCenterObserverRule.swift in Sources */,
D4AB0EA21F8993DD00CEC380 /* NamespaceCollector.swift in Sources */,
D4B022B21E10B613007E5297 /* RedundantVoidReturnRule.swift in Sources */,
3BCC04D21C4F56D3006073C3 /* NameConfiguration.swift in Sources */,
D4C27BFE1E12D53F00DF713E /* Version.swift in Sources */,
Expand Down Expand Up @@ -1547,6 +1554,7 @@
E881985B1BEA974E00333A11 /* StatementPositionRule.swift in Sources */,
B58AEED61C492C7B00E901FD /* ForceUnwrappingRule.swift in Sources */,
1EF115921EB2AD5900E30140 /* ExplicitTopLevelACLRule.swift in Sources */,
D40FE89D1F867BFF006433E2 /* OverrideInExtensionRule.swift in Sources */,
D41E7E0B1DF9DABB0065259A /* RedundantStringEnumValueRule.swift in Sources */,
E88DEA711B09847500A66CB0 /* ViolationSeverity.swift in Sources */,
1E3C2D711EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ extension RulesTests {
("testOpeningBrace", testOpeningBrace),
("testOperatorFunctionWhitespace", testOperatorFunctionWhitespace),
("testOperatorUsageWhitespace", testOperatorUsageWhitespace),
("testOverrideInExtension", testOverrideInExtension),
("testPatternMatchingKeywords", testPatternMatchingKeywords),
("testPrivateOutlet", testPrivateOutlet),
("testPrivateUnitTest", testPrivateUnitTest),
Expand Down
4 changes: 4 additions & 0 deletions Tests/SwiftLintFrameworkTests/RulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ class RulesTests: XCTestCase {
verifyRule(OperatorUsageWhitespaceRule.description)
}

func testOverrideInExtension() {
verifyRule(OverrideInExtensionRule.description)
}

func testPatternMatchingKeywords() {
verifyRule(PatternMatchingKeywordsRule.description)
}
Expand Down