Skip to content

Commit

Permalink
Merge pull request #809 from angelolloqui/feature/super_rule
Browse files Browse the repository at this point in the history
Implemented super call rule (#803)
  • Loading branch information
norio-nomura authored Oct 20, 2016
2 parents 3ecefe9 + 6161c9b commit 8b65811
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

##### Enhancements

* Add `SuperCallRule` Opt-In rule that warns about methods not calling to super.
[Angel G. Olloqui](https://github.com/angelolloqui)
[#803](https://github.com/realm/SwiftLint/issues/803)

* Add `RedundantNilCoalesingRule` Opt-In rule that warns against `?? nil`.
[Daniel Beard](https://github.com/daniel-beard)
[#764](https://github.com/realm/SwiftLint/issues/764)
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 @@ -71,6 +71,7 @@ public let masterRuleList = RuleList(rules:
RedundantNilCoalesingRule.self,
ReturnArrowWhitespaceRule.self,
StatementPositionRule.self,
OverridenSuperCallRule.self,
SwitchCaseOnNewlineRule.self,
TodoRule.self,
TrailingNewlineRule.self,
Expand Down
114 changes: 114 additions & 0 deletions Source/SwiftLintFramework/Rules/OverridenSuperCallRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// OverridenSuperCallRule.swift
// SwiftLint
//
// Created by Angel Garcia on 04/09/16.
// Copyright © 2016 Realm. All rights reserved.
//

import SourceKittenFramework

public struct OverridenSuperCallRule: ConfigurationProviderRule, ASTRule, OptInRule {
public var configuration = OverridenSuperCallConfiguration()

public init() { }

public static let description = RuleDescription(
identifier: "overridden_super_call",
name: "Overridden methods call super",
description: "Some Overridden methods should always call super",
nonTriggeringExamples: [
"class VC: UIViewController {\n" +
"\toverride func viewWillAppear(_ animated: Bool) {\n" +
"\t\tsuper.viewWillAppear(animated)\n" +
"\t}\n" +
"}\n",
"class VC: UIViewController {\n" +
"\toverride func viewWillAppear(_ animated: Bool) {\n" +
"\t\tself.method1()\n" +
"\t\tsuper.viewWillAppear(animated)\n" +
"\t\tself.method2()\n" +
"\t}\n" +
"}\n",
"class VC: UIViewController {\n" +
"\toverride func loadView() {\n" +
"\t}\n" +
"}\n",
"class Some {\n" +
"\tfunc viewWillAppear(_ animated: Bool) {\n" +
"\t}\n" +
"}\n"
],
triggeringExamples: [
"class VC: UIViewController {\n" +
"\toverride func viewWillAppear(_ animated: Bool) ↓{\n" +
"\t\t//Not calling to super\n" +
"\t\tself.method()\n" +
"\t}\n" +
"}\n",
"class VC: UIViewController {\n" +
"\toverride func viewWillAppear(_ animated: Bool) ↓{\n" +
"\t\tsuper.viewWillAppear(animated)\n" +
"\t\t//Other code\n" +
"\t\tsuper.viewWillAppear(animated)\n" +
"\t}\n" +
"}\n",
"class VC: UIViewController {\n" +
"\toverride func didReceiveMemoryWarning() ↓{\n" +
"\t}\n" +
"}\n"
]
)

public func validateFile(file: File,
kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {

guard let offset = dictionary["key.bodyoffset"] as? Int64,
let name = dictionary["key.name"] as? String
else { return [] }

let substructure = (dictionary["key.substructure"] as? [SourceKitRepresentable]) ?? []
guard kind == .FunctionMethodInstance &&
configuration.resolvedMethodNames.contains(name) &&
extractAttributes(dictionary).contains("source.decl.attribute.override")
else { return [] }

let callsToSuper = extractCallsToSuper(name, substructure: substructure)

if callsToSuper.isEmpty {
return [StyleViolation(ruleDescription: self.dynamicType.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: Int(offset)),
reason: "Method '\(name)' should call to super function")]
} else if callsToSuper.count > 1 {
return [StyleViolation(ruleDescription: self.dynamicType.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: Int(offset)),
reason: "Method '\(name)' should call to super only once")]
}
return []
}

private func extractAttributes(dictionary: [String: SourceKitRepresentable]) -> [String] {
guard let attributesDict = dictionary["key.attributes"] as? [SourceKitRepresentable]
else { return [] }
return attributesDict.flatMap {
($0 as? [String: SourceKitRepresentable])?["key.attribute"] as? String
}
}

private func extractCallsToSuper(name: String,
substructure: [SourceKitRepresentable]) -> [String] {
let superCall = "super.\(name)"
return substructure.flatMap {
guard let elems = $0 as? [String: SourceKitRepresentable],
type = elems["key.kind"] as? String,
name = elems["key.name"] as? String
where type == "source.lang.swift.expr.call" &&
superCall.containsString(name)
else { return nil }
return name
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// OverridenSuperCallConfiguration.swift
// SwiftLint
//
// Created by Angel Garcia on 05/09/16.
// Copyright © 2016 Realm. All rights reserved.
//

import Foundation

public struct OverridenSuperCallConfiguration: RuleConfiguration, Equatable {
var defaultIncluded = [
//NSObject
"awakeFromNib()",
"prepareForInterfaceBuilder()",
//UICollectionViewLayout
"invalidateLayout()",
"invalidateLayout(with:)",
"invalidateLayoutWithContext(_:)",
//UIView
"prepareForReuse()",
"updateConstraints()",
//UIViewController
"addChildViewController(_:)",
"decodeRestorableState(with:)",
"decodeRestorableStateWithCoder(_:)",
"didReceiveMemoryWarning()",
"encodeRestorableState(with:)",
"encodeRestorableStateWithCoder(_:)",
"removeFromParentViewController()",
"setEditing(_:animated:)",
"transition(from:to:duration:options:animations:completion:)",
"transitionCoordinator()",
"transitionFromViewController(_:toViewController:duration:options:animations:completion:)",
"viewDidAppear(_:)",
"viewDidDisappear(_:)",
"viewDidLoad()",
"viewWillAppear(_:)",
"viewWillDisappear(_:)",
//XCTestCase
"setUp()",
"tearDown()"
]

var severityConfiguration = SeverityConfiguration(.Warning)
var excluded: [String] = []
var included: [String] = ["*"]

public private(set) var resolvedMethodNames: [String]

init() {
resolvedMethodNames = defaultIncluded
}

public var consoleDescription: String {
return severityConfiguration.consoleDescription +
", excluded: [\(excluded)]" +
", included: [\(included)]"
}

public mutating func applyConfiguration(configuration: AnyObject) throws {
guard let configuration = configuration as? [String: AnyObject] else {
throw ConfigurationError.UnknownConfiguration
}

if let severityString = configuration["severity"] as? String {
try severityConfiguration.applyConfiguration(severityString)
}

if let excluded = [String].arrayOf(configuration["excluded"]) {
self.excluded = excluded
}

if let included = [String].arrayOf(configuration["included"]) {
self.included = included
}

self.resolvedMethodNames = calculateResolvedMethodNames()
}

public var severity: ViolationSeverity {
return severityConfiguration.severity
}

private func calculateResolvedMethodNames() -> [String] {
var names: [String] = []
if included.contains("*") && !excluded.contains("*") {
names += defaultIncluded
}
names += included.filter({ $0 != "*" })
names = names.filter { !excluded.contains($0) }
return names
}
}

public func == (lhs: OverridenSuperCallConfiguration,
rhs: OverridenSuperCallConfiguration) -> Bool {
return lhs.excluded == rhs.excluded &&
lhs.included == rhs.included &&
lhs.severityConfiguration == rhs.severityConfiguration
}
8 changes: 8 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
6CCFCF2F1CFEF73E003239EB /* SwiftyTextTable.framework in Embed Frameworks into SwiftLintFramework.framework */ = {isa = PBXBuildFile; fileRef = 3BBF2F9C1C640A0F006CD775 /* SwiftyTextTable.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
6CCFCF301CFEF742003239EB /* Yaml.framework in Embed Frameworks into SwiftLintFramework.framework */ = {isa = PBXBuildFile; fileRef = E89376AC1B8A701E0025708E /* Yaml.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7250948A1D0859260039B353 /* StatementPositionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 725094881D0855760039B353 /* StatementPositionConfiguration.swift */; };
78F032461D7C877E00BE709A /* OverridenSuperCallRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F032441D7C877800BE709A /* OverridenSuperCallRule.swift */; };
78F032481D7D614300BE709A /* OverridenSuperCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F032471D7D614300BE709A /* OverridenSuperCallConfiguration.swift */; };
83894F221B0C928A006214E1 /* RulesCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83894F211B0C928A006214E1 /* RulesCommand.swift */; };
83D71E281B131ECE000395DE /* RuleDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D71E261B131EB5000395DE /* RuleDescription.swift */; };
85DA81321D6B471000951BC4 /* MarkRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856651A61D6B395F005E6B29 /* MarkRule.swift */; };
Expand Down Expand Up @@ -223,6 +225,8 @@
6CB514E81C760C6900FA02C4 /* Structure+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Structure+SwiftLint.swift"; sourceTree = "<group>"; };
6CC4259A1C77046200AEA885 /* SyntaxMap+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SyntaxMap+SwiftLint.swift"; sourceTree = "<group>"; };
725094881D0855760039B353 /* StatementPositionConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementPositionConfiguration.swift; sourceTree = "<group>"; };
78F032441D7C877800BE709A /* OverridenSuperCallRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridenSuperCallRule.swift; sourceTree = "<group>"; };
78F032471D7D614300BE709A /* OverridenSuperCallConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridenSuperCallConfiguration.swift; sourceTree = "<group>"; };
83894F211B0C928A006214E1 /* RulesCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RulesCommand.swift; sourceTree = "<group>"; };
83D71E261B131EB5000395DE /* RuleDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleDescription.swift; sourceTree = "<group>"; };
856651A61D6B395F005E6B29 /* MarkRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkRule.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -370,6 +374,7 @@
isa = PBXGroup;
children = (
3BCC04D01C4F56D3006073C3 /* NameConfiguration.swift */,
78F032471D7D614300BE709A /* OverridenSuperCallConfiguration.swift */,
DAD3BE491D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift */,
B2902A0D1D6681F700BFCCF7 /* PrivateUnitTestConfiguration.swift */,
3BB47D821C514E8100AE6A10 /* RegexConfiguration.swift */,
Expand Down Expand Up @@ -623,6 +628,7 @@
E88DEA951B099CF200A66CB0 /* NestingRule.swift */,
692B1EB11BD7E00F00EAABFF /* OpeningBraceRule.swift */,
E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */,
78F032441D7C877800BE709A /* OverridenSuperCallRule.swift */,
B2902A0B1D66815600BFCCF7 /* PrivateUnitTestRule.swift */,
094385021D5D4F78009168CF /* PrivateOutletRule.swift */,
24B4DF0B1D6DFA370097803B /* RedundantNilCoalesingRule.swift */,
Expand Down Expand Up @@ -900,8 +906,10 @@
4DB7815E1CAD72BA00BC4723 /* LegacyCGGeometryFunctionsRule.swift in Sources */,
6CC4259B1C77046200AEA885 /* SyntaxMap+SwiftLint.swift in Sources */,
E881985C1BEA978500333A11 /* TrailingNewlineRule.swift in Sources */,
78F032481D7D614300BE709A /* OverridenSuperCallConfiguration.swift in Sources */,
E881985E1BEA982100333A11 /* TypeBodyLengthRule.swift in Sources */,
69F88BF71BDA38A6005E7CAE /* OpeningBraceRule.swift in Sources */,
78F032461D7C877E00BE709A /* OverridenSuperCallRule.swift in Sources */,
E849FF281BF9481A009AE999 /* MissingDocsRule.swift in Sources */,
E80E018D1B92C0F60078EB70 /* Command.swift in Sources */,
E88198571BEA953300333A11 /* ForceCastRule.swift in Sources */,
Expand Down
52 changes: 52 additions & 0 deletions Tests/SwiftLintFramework/RuleConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,56 @@ class RuleConfigurationsTests: XCTestCase {
XCTFail()
}
}

func testOverridenSuperCallConfigurationFromDictionary() {
var configuration = OverridenSuperCallConfiguration()
XCTAssertTrue(configuration.resolvedMethodNames.contains("viewWillAppear(_:)"))

let conf1 = [
"severity": "error",
"excluded": "viewWillAppear(_:)"
]
do {
try configuration.applyConfiguration(conf1)
XCTAssert(configuration.severityConfiguration.severity == .Error)
XCTAssertFalse(configuration.resolvedMethodNames.contains("*"))
XCTAssertFalse(configuration.resolvedMethodNames.contains("viewWillAppear(_:)"))
XCTAssertTrue(configuration.resolvedMethodNames.contains("viewWillDisappear(_:)"))
} catch {
XCTFail()
}

let conf2 = [
"severity": "error",
"excluded": "viewWillAppear(_:)",
"included": ["*", "testMethod1()", "testMethod2(_:)"]
]
do {
try configuration.applyConfiguration(conf2)
XCTAssert(configuration.severityConfiguration.severity == .Error)
XCTAssertFalse(configuration.resolvedMethodNames.contains("*"))
XCTAssertFalse(configuration.resolvedMethodNames.contains("viewWillAppear(_:)"))
XCTAssertTrue(configuration.resolvedMethodNames.contains("viewWillDisappear(_:)"))
XCTAssertTrue(configuration.resolvedMethodNames.contains("testMethod1()"))
XCTAssertTrue(configuration.resolvedMethodNames.contains("testMethod2(_:)"))
} catch {
XCTFail()
}

let conf3 = [
"severity": "warning",
"excluded": "*",
"included": ["testMethod1()", "testMethod2(_:)"]
]
do {
try configuration.applyConfiguration(conf3)
XCTAssert(configuration.severityConfiguration.severity == .Warning)
XCTAssert(configuration.resolvedMethodNames.count == 2)
XCTAssertFalse(configuration.resolvedMethodNames.contains("*"))
XCTAssertTrue(configuration.resolvedMethodNames.contains("testMethod1()"))
XCTAssertTrue(configuration.resolvedMethodNames.contains("testMethod2(_:)"))
} catch {
XCTFail()
}
}
}
5 changes: 5 additions & 0 deletions Tests/SwiftLintFramework/RulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,9 @@ class RulesTests: XCTestCase {
func testVariableName() {
verifyRule(VariableNameRule.description)
}

func testSuperCall() {
verifyRule(OverridenSuperCallRule.description)
}

}

0 comments on commit 8b65811

Please sign in to comment.