diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b4653969..bdc5afee39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,11 @@ [#49](https://github.com/realm/SwiftLint/issues/49) [#956](https://github.com/realm/SwiftLint/issues/959) +* Add `ProhibitedSuperRule` opt-in rule that warns about methods calling + to super that should not, for example `UIViewController.loadView()`. + [Aaron McTavish](https://github.com/aamctustwo) + [#970](https://github.com/realm/SwiftLint/issues/970) + ##### Bug Fixes * Fix `weak_delegate` rule reporting a violation for variables containing diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 5eedf97ca3..08ca1bcb11 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -78,6 +78,7 @@ public let masterRuleList = RuleList(rules: OverriddenSuperCallRule.self, PrivateOutletRule.self, PrivateUnitTestRule.self, + ProhibitedSuperRule.self, RedundantNilCoalescingRule.self, RedundantStringEnumValueRule.self, ReturnArrowWhitespaceRule.self, diff --git a/Source/SwiftLintFramework/Rules/ProhibitedSuperRule.swift b/Source/SwiftLintFramework/Rules/ProhibitedSuperRule.swift new file mode 100644 index 0000000000..1e42857ced --- /dev/null +++ b/Source/SwiftLintFramework/Rules/ProhibitedSuperRule.swift @@ -0,0 +1,83 @@ +// +// ProhibitedSuperRule.swift +// SwiftLint +// +// Created by Aaron McTavish on 12/12/16. +// Copyright © 2016 Realm. All rights reserved. +// + +import SourceKittenFramework + +public struct ProhibitedSuperRule: ConfigurationProviderRule, ASTRule, OptInRule { + public var configuration = ProhibitedSuperConfiguration() + + public init() { } + + public static let description = RuleDescription( + identifier: "prohibited_super_call", + name: "Prohibited calls to super", + description: "Some methods should not call super", + nonTriggeringExamples: [ + "class VC: UIViewController {\n" + + "\toverride func loadView() {\n" + + "\t}\n" + + "}\n", + "class NSView {\n" + + "\tfunc updateLayer() {\n" + + "\t\tself.method1()" + + "\t}\n" + + "}\n" + ], + triggeringExamples: [ + "class VC: UIViewController {\n" + + "\toverride func loadView() ↓{\n" + + "\t\tsuper.loadView()\n" + + "\t}\n" + + "}\n", + "class VC: NSFileProviderExtension {\n" + + "\toverride func providePlaceholder(at url: URL," + + "completionHandler: @escaping (Error?) -> Void) ↓{\n" + + "\t\tself.method1()\n" + + "\t\tsuper.providePlaceholder(at:url, completionHandler: completionHandler)\n" + + "\t}\n" + + "}\n", + "class VC: NSView {\n" + + "\toverride func updateLayer() ↓{\n" + + "\t\tself.method1()\n" + + "\t\tsuper.updateLayer()\n" + + "\t\tself.method2()\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, + let substructure = (dictionary["key.substructure"] as? [SourceKitRepresentable]), + kind == .functionMethodInstance && + configuration.resolvedMethodNames.contains(name) && + dictionary.enclosedSwiftAttributes.contains("source.decl.attribute.override") && + !extractCallsToSuper(name, substructure: substructure).isEmpty + else { return [] } + + return [StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, byteOffset: Int(offset)), + reason: "Method '\(name)' should not call to super function")] + } + + private func extractCallsToSuper(_ name: String, + substructure: [SourceKitRepresentable]) -> [String] { + let superCall = "super.\(name)" + return substructure.flatMap { + guard let elems = $0 as? [String: SourceKitRepresentable], + let type = elems["key.kind"] as? String, + let name = elems["key.name"] as? String, + type == "source.lang.swift.expr.call" && superCall.contains(name) + else { return nil } + return name + } + } +} diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift new file mode 100644 index 0000000000..a3c6d9c312 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift @@ -0,0 +1,75 @@ +// +// ProhibitedSuperConfiguration.swift +// SwiftLint +// +// Created by Aaron McTavish on 12/12/16. +// Copyright © 2016 Realm. All rights reserved. +// + +import Foundation + +public struct ProhibitedSuperConfiguration: RuleConfiguration, Equatable { + var severityConfiguration = SeverityConfiguration(.warning) + var excluded = [String]() + var included = ["*"] + + private(set) var resolvedMethodNames = [ + // NSFileProviderExtension + "providePlaceholder(at:completionHandler:)", + // NSTextInput + "doCommand(by:)", + // NSView + "updateLayer()", + // UIViewController + "loadView()" + ] + + init() {} + + public var consoleDescription: String { + return severityConfiguration.consoleDescription + + ", excluded: [\(excluded)]" + + ", included: [\(included)]" + } + + public mutating func applyConfiguration(_ configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw ConfigurationError.unknownConfiguration + } + + if let severityString = configuration["severity"] as? String { + try severityConfiguration.applyConfiguration(severityString) + } + + if let excluded = [String].array(of: configuration["excluded"]) { + self.excluded = excluded + } + + if let included = [String].array(of: configuration["included"]) { + self.included = included + } + + resolvedMethodNames = calculateResolvedMethodNames() + } + + public var severity: ViolationSeverity { + return severityConfiguration.severity + } + + private func calculateResolvedMethodNames() -> [String] { + var names = [String]() + if included.contains("*") && !excluded.contains("*") { + names += resolvedMethodNames + } + names += included.filter { $0 != "*" } + names = names.filter { !excluded.contains($0) } + return names + } +} + +public func == (lhs: ProhibitedSuperConfiguration, + rhs: ProhibitedSuperConfiguration) -> Bool { + return lhs.excluded == rhs.excluded && + lhs.included == rhs.included && + lhs.severityConfiguration == rhs.severityConfiguration +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 69542f7058..c07dfa01f6 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 006ECFC41C44E99E00EF6364 /* LegacyConstantRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006ECFC31C44E99E00EF6364 /* LegacyConstantRule.swift */; }; + 009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 009E09271DFEE4C200B588A7 /* ProhibitedSuperRule.swift */; }; + 009E092A1DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 009E09291DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift */; }; 02FD8AEF1BFC18D60014BFFB /* ExtendedNSStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */; }; 094385011D5D2894009168CF /* WeakDelegateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094384FF1D5D2382009168CF /* WeakDelegateRule.swift */; }; 094385041D5D4F7C009168CF /* PrivateOutletRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094385021D5D4F78009168CF /* PrivateOutletRule.swift */; }; @@ -206,6 +208,8 @@ /* Begin PBXFileReference section */ 006ECFC31C44E99E00EF6364 /* LegacyConstantRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyConstantRule.swift; sourceTree = ""; }; + 009E09271DFEE4C200B588A7 /* ProhibitedSuperRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProhibitedSuperRule.swift; sourceTree = ""; }; + 009E09291DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProhibitedSuperConfiguration.swift; sourceTree = ""; }; 02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedNSStringTests.swift; sourceTree = ""; }; 094384FF1D5D2382009168CF /* WeakDelegateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDelegateRule.swift; sourceTree = ""; }; 094385021D5D4F78009168CF /* PrivateOutletRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOutletRule.swift; sourceTree = ""; }; @@ -425,6 +429,7 @@ 78F032471D7D614300BE709A /* OverridenSuperCallConfiguration.swift */, DAD3BE491D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift */, B2902A0D1D6681F700BFCCF7 /* PrivateUnitTestConfiguration.swift */, + 009E09291DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift */, 3BB47D821C514E8100AE6A10 /* RegexConfiguration.swift */, 3B0B14531C505D6300BE82F7 /* SeverityConfiguration.swift */, 3BCC04CF1C4F56D3006073C3 /* SeverityLevelsConfiguration.swift */, @@ -694,6 +699,7 @@ 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */, B2902A0B1D66815600BFCCF7 /* PrivateUnitTestRule.swift */, 094385021D5D4F78009168CF /* PrivateOutletRule.swift */, + 009E09271DFEE4C200B588A7 /* ProhibitedSuperRule.swift */, 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */, D41E7E0A1DF9DABB0065259A /* RedundantStringEnumValueRule.swift */, E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */, @@ -986,6 +992,7 @@ D44AD2761C0AA5350048F7B0 /* LegacyConstructorRule.swift in Sources */, 3BCC04CD1C4F5694006073C3 /* ConfigurationError.swift in Sources */, D4C4A34E1DEA877200E0E04C /* FileHeaderRule.swift in Sources */, + 009E092A1DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift in Sources */, BFF028AE1CBCF8A500B38A9D /* TrailingWhitespaceConfiguration.swift in Sources */, D4C4A34C1DEA4FF000E0E04C /* AttributesConfiguration.swift in Sources */, 83D71E281B131ECE000395DE /* RuleDescription.swift in Sources */, @@ -1023,6 +1030,7 @@ 1EC163521D5992D900DD2928 /* VerticalWhitespaceRule.swift in Sources */, 57ED827B1CF656E3002B3513 /* JUnitReporter.swift in Sources */, 24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */, + 009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */, E80E018F1B92C1350078EB70 /* Region.swift in Sources */, E88198581BEA956C00333A11 /* FunctionBodyLengthRule.swift in Sources */, E88DEA751B09852000A66CB0 /* File+SwiftLint.swift in Sources */, diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index e871b78355..37a94769b0 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -234,6 +234,10 @@ class RulesTests: XCTestCase { verifyRule(PrivateUnitTestRule.description) } + func testProhibitedSuper() { + verifyRule(ProhibitedSuperRule.description) + } + func testRedundantNilCoalescing() { verifyRule(RedundantNilCoalescingRule.description) } @@ -413,6 +417,7 @@ extension RulesTests { ("testOperatorFunctionWhitespace", testOperatorFunctionWhitespace), ("testPrivateOutlet", testPrivateOutlet), // ("testPrivateUnitTest", testPrivateUnitTest), + ("testProhibitedSuper", testProhibitedSuper), ("testRedundantNilCoalescing", testRedundantNilCoalescing), ("testRedundantStringEnumValue", testRedundantStringEnumValue), ("testReturnArrowWhitespace", testReturnArrowWhitespace),