From 428379ce8fc66f5b772cd7a0bdc48344dc4c6e30 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Mon, 9 Apr 2018 00:17:51 -0700 Subject: [PATCH] Add unavailable_function --- CHANGELOG.md | 6 ++ Rules.md | 61 +++++++++++ .../Models/MasterRuleList.swift | 1 + .../Rules/UnavailableFunctionRule.swift | 100 ++++++++++++++++++ SwiftLint.xcodeproj/project.pbxproj | 4 + Tests/LinuxMain.swift | 1 + .../SwiftLintFrameworkTests/RulesTests.swift | 4 + 7 files changed, 177 insertions(+) create mode 100644 Source/SwiftLintFramework/Rules/UnavailableFunctionRule.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 2560a83aad..3f7c30fc41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ [Marcelo Fabri](https://github.com/marcelofabri) [#898](https://github.com/realm/SwiftLint/issues/898) +* Add `unavailable_function` opt-in rule to validate that functions that are + currently unimplemented (using a placeholder `fatalError`) are marked with + `@available(*, unavailable)`. + [Marcelo Fabri](https://github.com/marcelofabri) + [#2127](https://github.com/realm/SwiftLint/issues/2127) + #### Bug Fixes * None. diff --git a/Rules.md b/Rules.md index 57e3cd13d8..d58b93009b 100644 --- a/Rules.md +++ b/Rules.md @@ -115,6 +115,7 @@ * [Trailing Whitespace](#trailing-whitespace) * [Type Body Length](#type-body-length) * [Type Name](#type-name) +* [Unavailable Function](#unavailable-function) * [Unneeded Break in Switch](#unneeded-break-in-switch) * [Unneeded Parentheses in Closure Argument](#unneeded-parentheses-in-closure-argument) * [Untyped Error in Catch](#untyped-error-in-catch) @@ -17389,6 +17390,66 @@ protocol Foo { +## Unavailable Function + +Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version +--- | --- | --- | --- | --- +`unavailable_function` | Disabled | No | idiomatic | 4.1.0 + +Unimplemented functions should be marked as unavailable. + +### Examples + +
+Non Triggering Examples + +```swift +class ViewController: UIViewController { + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +``` + +```swift +func jsonValue(_ jsonString: String) -> NSObject { + let data = jsonString.data(using: .utf8)! + let result = try! JSONSerialization.jsonObject(with: data, options: []) + if let dict = (result as? [String: Any])?.bridge() { + return dict + } else if let array = (result as? [Any])?.bridge() { + return array + } + fatalError() +} +``` + +
+
+Triggering Examples + +```swift +class ViewController: UIViewController { + public required ↓init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +``` + +```swift +class ViewController: UIViewController { + public required ↓init?(coder aDecoder: NSCoder) { + let reason = "init(coder:) has not been implemented" + fatalError(reason) + } +} +``` + +
+ + + ## Unneeded Break in Switch Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 073f9a6601..3929646169 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -123,6 +123,7 @@ public let masterRuleList = RuleList(rules: [ TrailingWhitespaceRule.self, TypeBodyLengthRule.self, TypeNameRule.self, + UnavailableFunctionRule.self, UnneededBreakInSwitchRule.self, UnneededParenthesesInClosureArgumentRule.self, UntypedErrorInCatchRule.self, diff --git a/Source/SwiftLintFramework/Rules/UnavailableFunctionRule.swift b/Source/SwiftLintFramework/Rules/UnavailableFunctionRule.swift new file mode 100644 index 0000000000..4eaf21a3bb --- /dev/null +++ b/Source/SwiftLintFramework/Rules/UnavailableFunctionRule.swift @@ -0,0 +1,100 @@ +// +// UnavailableFunctionRule.swift +// SwiftLint +// +// Created by Marcelo Fabri on 04/09/18. +// Copyright © 2018 Realm. All rights reserved. +// + +import Foundation +import SourceKittenFramework + +public struct UnavailableFunctionRule: ASTRule, ConfigurationProviderRule, OptInRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "unavailable_function", + name: "Unavailable Function", + description: "Unimplemented functions should be marked as unavailable.", + kind: .idiomatic, + minSwiftVersion: .fourDotOne, + nonTriggeringExamples: [ + """ + class ViewController: UIViewController { + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + """, + """ + func jsonValue(_ jsonString: String) -> NSObject { + let data = jsonString.data(using: .utf8)! + let result = try! JSONSerialization.jsonObject(with: data, options: []) + if let dict = (result as? [String: Any])?.bridge() { + return dict + } else if let array = (result as? [Any])?.bridge() { + return array + } + fatalError() + } + """ + ], + triggeringExamples: [ + """ + class ViewController: UIViewController { + public required ↓init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + """, + """ + class ViewController: UIViewController { + public required ↓init?(coder aDecoder: NSCoder) { + let reason = "init(coder:) has not been implemented" + fatalError(reason) + } + } + """ + ] + ) + + public func validate(file: File, kind: SwiftDeclarationKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + guard SwiftDeclarationKind.functionKinds.contains(kind) else { + return [] + } + + let containsFatalError = dictionary.substructure.contains { dict -> Bool in + return dict.kind.flatMap(SwiftExpressionKind.init(rawValue:)) == .call && dict.name == "fatalError" + } + + guard let offset = dictionary.offset, containsFatalError, + !isFunctionUnavailable(file: file, dictionary: dictionary), + let bodyOffset = dictionary.bodyOffset, let bodyLength = dictionary.bodyLength, + let range = file.contents.bridge().byteRangeToNSRange(start: bodyOffset, length: bodyLength), + file.match(pattern: "\\breturn\\b", with: [.keyword], range: range).isEmpty 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") + } + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index a06ae1f08e..80e759aec3 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ D4DABFD91E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DABFD81E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift */; }; D4DAE8BC1DE14E8F00B0AE7A /* NimbleOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */; }; D4DB92251E628898005DE9C1 /* TodoRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB92241E628898005DE9C1 /* TodoRuleTests.swift */; }; + D4DE9133207B4750000FFAA8 /* UnavailableFunctionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DE9131207B4731000FFAA8 /* UnavailableFunctionRule.swift */; }; D4E2BA851F6CD77B00E8E184 /* ArrayInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E2BA841F6CD77B00E8E184 /* ArrayInitRule.swift */; }; D4EA77C81F817FD200C315FB /* UnneededBreakInSwitchRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EA77C71F817FD200C315FB /* UnneededBreakInSwitchRule.swift */; }; D4EA77CA1F81FACC00C315FB /* LiteralExpressionEndIdentationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EA77C91F81FACC00C315FB /* LiteralExpressionEndIdentationRule.swift */; }; @@ -622,6 +623,7 @@ D4DABFD81E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationCenterDetachmentRuleExamples.swift; sourceTree = ""; }; D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleOperatorRule.swift; sourceTree = ""; }; D4DB92241E628898005DE9C1 /* TodoRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoRuleTests.swift; sourceTree = ""; }; + D4DE9131207B4731000FFAA8 /* UnavailableFunctionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnavailableFunctionRule.swift; sourceTree = ""; }; D4E2BA841F6CD77B00E8E184 /* ArrayInitRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayInitRule.swift; sourceTree = ""; }; D4EA77C71F817FD200C315FB /* UnneededBreakInSwitchRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnneededBreakInSwitchRule.swift; sourceTree = ""; }; D4EA77C91F81FACC00C315FB /* LiteralExpressionEndIdentationRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiteralExpressionEndIdentationRule.swift; sourceTree = ""; }; @@ -1179,6 +1181,7 @@ E88DEA8D1B0999CD00A66CB0 /* TypeBodyLengthRule.swift */, E88DEA911B099B1F00A66CB0 /* TypeNameRule.swift */, D4130D981E16CC1300242361 /* TypeNameRuleExamples.swift */, + D4DE9131207B4731000FFAA8 /* UnavailableFunctionRule.swift */, D4EA77C71F817FD200C315FB /* UnneededBreakInSwitchRule.swift */, D46A317E1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift */, 181D9E162038343D001F6887 /* UntypedErrorInCatchRule.swift */, @@ -1648,6 +1651,7 @@ 3BCC04D21C4F56D3006073C3 /* NameConfiguration.swift in Sources */, D4C27BFE1E12D53F00DF713E /* Version.swift in Sources */, B2902A0E1D6681F700BFCCF7 /* PrivateUnitTestConfiguration.swift in Sources */, + D4DE9133207B4750000FFAA8 /* UnavailableFunctionRule.swift in Sources */, D47A510E1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift in Sources */, D462021F1E15F52D0027AAD1 /* NumberSeparatorRuleExamples.swift in Sources */, D4DA1DF41E17511D0037413D /* CompilerProtocolInitRule.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index b6e3035047..4b4984ede8 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -513,6 +513,7 @@ extension RulesTests { ("testTrailingSemicolon", testTrailingSemicolon), ("testTrailingWhitespace", testTrailingWhitespace), ("testTypeBodyLength", testTypeBodyLength), + ("testUnavailableFunction", testUnavailableFunction), ("testUnneededBreakInSwitch", testUnneededBreakInSwitch), ("testUnneededParenthesesInClosureArgument", testUnneededParenthesesInClosureArgument), ("testUntypedErrorInCatch", testUntypedErrorInCatch), diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index f643394f98..d6f460055c 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -433,6 +433,10 @@ class RulesTests: XCTestCase { verifyRule(TypeBodyLengthRule.description) } + func testUnavailableFunction() { + verifyRule(UnavailableFunctionRule.description) + } + func testUnneededBreakInSwitch() { verifyRule(UnneededBreakInSwitchRule.description) }