diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2560a83aa..3f7c30fc4 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 57e3cd13d..d58b93009 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 073f9a660..392964616 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 000000000..4eaf21a3b
--- /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 a06ae1f08..80e759aec 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 b6e303504..4b4984ede 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 f643394f9..d6f460055 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)
}