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 implicitly_unwrapped_optional rule #1362

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

##### Enhancements

* Add `implicitly_unwrapped_optional` rule
that warns when using implicitly unwrapped optional,
except cases when this IUO is IBOutlet.
[Siarhei Fedartsou](https://github.com/SiarheiFedartsou/)

* Performance improvements to `generic_type_name`,
`redundant_nil_coalescing`, `mark`, `first_where` and
`vertical_whitespace` rules.
Expand Down Expand Up @@ -270,7 +275,7 @@
[Marcelo Fabri](https://github.com/marcelofabri)
[#1109](https://github.com/realm/SwiftLint/issues/1109)

* Add `compiler_protocol_init` rule that flags usage of initializers
* Add `compiler_protocol_init` rule that flags usage of initializers
declared in protocols used by the compiler such as `ExpressibleByArrayLiteral`
that shouldn't be called directly. Instead, you should use a literal anywhere
a concrete type conforming to the protocol is expected by the context.
Expand All @@ -286,7 +291,7 @@
[Marcelo Fabri](https://github.com/marcelofabri)
[#51](https://github.com/realm/SwiftLint/issues/51)

* Update `vertical_whitespace` rule to allow configuration of the number of
* Update `vertical_whitespace` rule to allow configuration of the number of
consecutive empty lines before a violation using `max_empty_lines`.
The default value is still 1 line.
[Aaron McTavish](https://github.com/aamctustwo)
Expand All @@ -312,7 +317,7 @@
[Marcelo Fabri](https://github.com/marcelofabri)
[#973](https://github.com/realm/SwiftLint/issues/973)

* Add `unused_optional_binding` rule that will check for optional bindings
* Add `unused_optional_binding` rule that will check for optional bindings
not being used.
[Rafael Machado](https://github.com/rakaramos/)
[#1116](https://github.com/realm/SwiftLint/issues/1116)
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 @@ -105,6 +105,7 @@ public let masterRuleList = RuleList(rules:
GenericTypeNameRule.self,
IdentifierNameRule.self,
ImplicitGetterRule.self,
ImplicitlyUnwrappedOptionalRule.self,
LargeTupleRule.self,
LeadingWhitespaceRule.self,
LegacyCGGeometryFunctionsRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// ImplicitlyUnwrappedOptionalRule.swift
// SwiftLint
//
// Created by Siarhei Fedartsou on 17/03/17.
// Copyright © 2016 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

public struct ImplicitlyUnwrappedOptionalRule: ASTRule, ConfigurationProviderRule, OptInRule {
public var configuration = ImplicitlyUnwrappedOptionalConfiguration(mode: .allExceptIBOutlets,
severity: SeverityConfiguration(.warning))

public init() {}

public static let description = RuleDescription(
identifier: "implicitly_unwrapped_optional",
name: "Implicitly Unwrapped Optional",
description: "Implicitly unwrapped optionals should be avoided when possible.",
nonTriggeringExamples: [
"@IBOutlet private var label: UILabel!",
"@IBOutlet var label: UILabel!",
"@IBOutlet var label: [UILabel!]",
"if !boolean {}",
"let int: Int? = 42",
"let int: Int? = nil"
],
triggeringExamples: [
"let label: UILabel!",
"let IBOutlet: UILabel!",
"let labels: [UILabel!]",
"var ints: [Int!] = [42, nil, 42]",
"let label: IBOutlet!",
"let int: Int! = 42",
"let int: Int! = nil",
"var int: Int! = 42",
"let int: ImplicitlyUnwrappedOptional<Int>",
"let collection: AnyCollection<Int!>",
"func foo(int: Int!) {}"
]
)

private func hasImplicitlyUnwrappedOptional(_ typeName: String) -> Bool {
return typeName.range(of: "!") != nil || typeName.range(of: "ImplicitlyUnwrappedOptional<") != nil
}

public func validate(file: File, kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard SwiftDeclarationKind.variableKinds().contains(kind) else {
return []
}

guard let typeName = dictionary.typeName else { return [] }
guard hasImplicitlyUnwrappedOptional(typeName) else { return [] }

if configuration.mode == .allExceptIBOutlets {
let isOutlet = dictionary.enclosedSwiftAttributes.contains("source.decl.attribute.iboutlet")
if isOutlet { return [] }
}

let location: Location
if let offset = dictionary.offset {
location = Location(file: file, byteOffset: offset)
} else {
location = Location(file: file.path)
}

return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity.severity,
location: location)
]
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// ImplicitlyUnwrappedOptionalConfiguration.swift
// SwiftLint
//
// Created by Siarhei Fedartsou on 18/03/17.
// Copyright © 2017 Realm. All rights reserved.
//

import Foundation

// swiftlint:disable:next type_name
public enum ImplicitlyUnwrappedOptionalModeConfiguration: String {
case all = "all"
case allExceptIBOutlets = "all_except_iboutlets"

init(value: Any) throws {
if let string = (value as? String)?.lowercased(),
let value = ImplicitlyUnwrappedOptionalModeConfiguration(rawValue: string) {
self = value
} else {
throw ConfigurationError.unknownConfiguration
}
}
}

public struct ImplicitlyUnwrappedOptionalConfiguration: RuleConfiguration, Equatable {
private(set) var severity: SeverityConfiguration
private(set) var mode: ImplicitlyUnwrappedOptionalModeConfiguration

init(mode: ImplicitlyUnwrappedOptionalModeConfiguration, severity: SeverityConfiguration) {
self.mode = mode
self.severity = severity
}

public var consoleDescription: String {
return severity.consoleDescription +
", mode: \(mode)"
}

public mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}

if let modeString = configuration["mode"] {
try mode = ImplicitlyUnwrappedOptionalModeConfiguration(value: modeString)
}

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

public static func == (lhs: ImplicitlyUnwrappedOptionalConfiguration,
rhs: ImplicitlyUnwrappedOptionalConfiguration) -> Bool {
return lhs.severity == rhs.severity &&
lhs.mode == rhs.mode
}
}
16 changes: 16 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
3BCC04D41C502BAB006073C3 /* RuleConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCC04D31C502BAB006073C3 /* RuleConfigurationTests.swift */; };
3BD9CD3D1C37175B009A5D25 /* YamlParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD9CD3C1C37175B009A5D25 /* YamlParser.swift */; };
3BDB224B1C345B4900473680 /* ProjectMock in Resources */ = {isa = PBXBuildFile; fileRef = 3BDB224A1C345B4900473680 /* ProjectMock */; };
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */; };
47ACC89A1E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47ACC8991E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift */; };
47ACC89C1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47ACC89B1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift */; };
47FF3BE11E7C75B600187E6D /* ImplicitlyUnwrappedOptionalRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FF3BDF1E7C745100187E6D /* ImplicitlyUnwrappedOptionalRule.swift */; };
4A9A3A3A1DC1D75F00DF5183 /* HTMLReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */; };
4DB7815E1CAD72BA00BC4723 /* LegacyCGGeometryFunctionsRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB7815C1CAD690100BC4723 /* LegacyCGGeometryFunctionsRule.swift */; };
4DCB8E7F1CBE494E0070FCF0 /* RegexHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */; };
Expand Down Expand Up @@ -311,6 +315,10 @@
3BCC04D31C502BAB006073C3 /* RuleConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleConfigurationTests.swift; sourceTree = "<group>"; };
3BD9CD3C1C37175B009A5D25 /* YamlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YamlParser.swift; sourceTree = "<group>"; };
3BDB224A1C345B4900473680 /* ProjectMock */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ProjectMock; sourceTree = "<group>"; };
47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalConfiguration.swift; sourceTree = "<group>"; };
47ACC8991E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalConfigurationTests.swift; sourceTree = "<group>"; };
47ACC89B1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalRuleTests.swift; sourceTree = "<group>"; };
47FF3BDF1E7C745100187E6D /* ImplicitlyUnwrappedOptionalRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalRule.swift; sourceTree = "<group>"; };
4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLReporter.swift; sourceTree = "<group>"; };
4DB7815C1CAD690100BC4723 /* LegacyCGGeometryFunctionsRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCGGeometryFunctionsRule.swift; sourceTree = "<group>"; };
4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegexHelpers.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -557,6 +565,7 @@
D43B04671E07228D004016AF /* ColonConfiguration.swift */,
67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */,
D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */,
47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */,
3B034B6C1E0BE544005D49A9 /* LineLengthConfiguration.swift */,
3BCC04D01C4F56D3006073C3 /* NameConfiguration.swift */,
D93DA3CF1E699E4E00809827 /* NestingConfiguration.swift */,
Expand Down Expand Up @@ -759,6 +768,8 @@
006204DD1E1E4E0A00FFFBE1 /* VerticalWhitespaceRuleTests.swift */,
67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */,
67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */,
47ACC8991E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift */,
47ACC89B1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift */,
);
name = SwiftLintFrameworkTests;
path = Tests/SwiftLintFrameworkTests;
Expand Down Expand Up @@ -842,6 +853,7 @@
E88DEA931B099C0900A66CB0 /* IdentifierNameRule.swift */,
D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */,
D43DB1071DC573DA00281215 /* ImplicitGetterRule.swift */,
47FF3BDF1E7C745100187E6D /* ImplicitlyUnwrappedOptionalRule.swift */,
D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */,
E88DEA7D1B098F2A00A66CB0 /* LeadingWhitespaceRule.swift */,
4DB7815C1CAD690100BC4723 /* LegacyCGGeometryFunctionsRule.swift */,
Expand Down Expand Up @@ -1185,6 +1197,7 @@
3BCC04CD1C4F5694006073C3 /* ConfigurationError.swift in Sources */,
D4C4A34E1DEA877200E0E04C /* FileHeaderRule.swift in Sources */,
009E092A1DFEE4DD00B588A7 /* ProhibitedSuperConfiguration.swift in Sources */,
47FF3BE11E7C75B600187E6D /* ImplicitlyUnwrappedOptionalRule.swift in Sources */,
BFF028AE1CBCF8A500B38A9D /* TrailingWhitespaceConfiguration.swift in Sources */,
3B034B6E1E0BE549005D49A9 /* LineLengthConfiguration.swift in Sources */,
D4C4A34C1DEA4FF000E0E04C /* AttributesConfiguration.swift in Sources */,
Expand Down Expand Up @@ -1239,6 +1252,7 @@
D43B04691E072291004016AF /* ColonConfiguration.swift in Sources */,
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */,
E80E018F1B92C1350078EB70 /* Region.swift in Sources */,
E88198581BEA956C00333A11 /* FunctionBodyLengthRule.swift in Sources */,
Expand Down Expand Up @@ -1329,6 +1343,7 @@
E832F10D1B17E725003F265F /* IntegrationTests.swift in Sources */,
D4C27C001E12DFF500DF713E /* LinterCacheTests.swift in Sources */,
D4998DE91DF194F20006E05D /* FileHeaderRuleTests.swift in Sources */,
47ACC89C1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift in Sources */,
006204DE1E1E4E0A00FFFBE1 /* VerticalWhitespaceRuleTests.swift in Sources */,
02FD8AEF1BFC18D60014BFFB /* ExtendedNSStringTests.swift in Sources */,
D4CA758F1E2DEEA500A40E8A /* NumberSeparatorRuleTests.swift in Sources */,
Expand All @@ -1345,6 +1360,7 @@
3B30C4A11C3785B300E04027 /* YamlParserTests.swift in Sources */,
D4998DE71DF191380006E05D /* AttributesRuleTests.swift in Sources */,
E88198631BEA9A5400333A11 /* RulesTests.swift in Sources */,
47ACC89A1E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift in Sources */,
D46202211E16002A0027AAD1 /* Swift2RulesTests.swift in Sources */,
67932E2D1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift in Sources */,
C9802F2F1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// ImplicitlyUnwrappedOptionalConfigurationTests.swift
// SwiftLint
//
// Created by Siarhei Fedartsou on 18/03/17.
// Copyright © 2017 Realm. All rights reserved.
//

import SourceKittenFramework
@testable import SwiftLintFramework
import XCTest

// swiftlint:disable:next type_name
class ImplicitlyUnwrappedOptionalConfigurationTests: XCTestCase {

func testImplicitlyUnwrappedOptionalConfigurationProperlyAppliesConfigurationFromDictionary() throws {
var configuration = ImplicitlyUnwrappedOptionalConfiguration(mode: .allExceptIBOutlets,
severity: SeverityConfiguration(.warning))

try configuration.apply(configuration: ["mode": "all", "severity": "error"])
XCTAssertEqual(configuration.mode, .all)
XCTAssertEqual(configuration.severity.severity, .error)

try configuration.apply(configuration: ["mode": "all_except_iboutlets"])
XCTAssertEqual(configuration.mode, .allExceptIBOutlets)
XCTAssertEqual(configuration.severity.severity, .error)

try configuration.apply(configuration: ["severity": "warning"])
XCTAssertEqual(configuration.mode, .allExceptIBOutlets)
XCTAssertEqual(configuration.severity.severity, .warning)

try configuration.apply(configuration: ["mode": "all", "severity": "warning"])
XCTAssertEqual(configuration.mode, .all)
XCTAssertEqual(configuration.severity.severity, .warning)
}

func testImplicitlyUnwrappedOptionalConfigurationThrowsOnBadConfig() {
let badConfigs: [[String: Any]] = [
["mode": "everything"],
["mode": false],
["mode": 42]
]

for badConfig in badConfigs {
var configuration = ImplicitlyUnwrappedOptionalConfiguration(mode: .allExceptIBOutlets,
severity: SeverityConfiguration(.warning))
checkError(ConfigurationError.unknownConfiguration) {
try configuration.apply(configuration: badConfig)
}
}
}

}

extension ImplicitlyUnwrappedOptionalConfigurationTests {
static var allTests: [(String, (ImplicitlyUnwrappedOptionalConfigurationTests) -> () throws -> Void)] {
return [
("testImplicitlyUnwrappedOptionalConfigurationProperlyAppliesConfigurationFromDictionary",
testImplicitlyUnwrappedOptionalConfigurationProperlyAppliesConfigurationFromDictionary),
("testImplicitlyUnwrappedOptionalConfigurationThrowsOnBadConfig",
testImplicitlyUnwrappedOptionalConfigurationThrowsOnBadConfig)
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// ImplicitlyUnwrappedOptionalRuleTests.swift
// SwiftLint
//
// Created by Siarhei Fedartsou on 18/03/17.
// Copyright © 2017 Realm. All rights reserved.
//

import Foundation
@testable import SwiftLintFramework
import XCTest

class ImplicitlyUnwrappedOptionalRuleTests: XCTestCase {

func testImplicitlyUnwrappedOptionalRuleDefaultConfiguration() {
let rule = ImplicitlyUnwrappedOptionalRule()
XCTAssertEqual(rule.configuration.mode, .allExceptIBOutlets)
XCTAssertEqual(rule.configuration.severity.severity, .warning)
}

func testImplicitlyUnwrappedOptionalRuleWarnsOnOutletsInAllMode() {
let baseDescription = ImplicitlyUnwrappedOptionalRule.description
let triggeringExamples = [
"@IBOutlet private var label: UILabel!",
"@IBOutlet var label: UILabel!",
"let int: Int!"
]

let nonTriggeringExamples = ["if !boolean {}"]
let description = RuleDescription(identifier: baseDescription.identifier,
name: baseDescription.name,
description: baseDescription.description,
nonTriggeringExamples: nonTriggeringExamples,
triggeringExamples: triggeringExamples,
corrections: baseDescription.corrections)
verifyRule(description, ruleConfiguration: ["mode": "all"],
commentDoesntViolate: true, stringDoesntViolate: true)
}
}

extension ImplicitlyUnwrappedOptionalRuleTests {
static var allTests: [(String, (ImplicitlyUnwrappedOptionalRuleTests) -> () throws -> Void)] {
return [
("testImplicitlyUnwrappedOptionalRuleDefaultConfiguration",
testImplicitlyUnwrappedOptionalRuleDefaultConfiguration),
("testImplicitlyUnwrappedOptionalRuleWarnsOnOutletsInAllMode",
testImplicitlyUnwrappedOptionalRuleWarnsOnOutletsInAllMode)
]
}
}
5 changes: 5 additions & 0 deletions Tests/SwiftLintFrameworkTests/RulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ class RulesTests: XCTestCase {
verifyRule(ImplicitGetterRule.description)
}

func testImplicitlyUnwrappedOptional() {
verifyRule(ImplicitlyUnwrappedOptionalRule.description)
}

func testLargeTuple() {
verifyRule(LargeTupleRule.description)
}
Expand Down Expand Up @@ -378,6 +382,7 @@ extension RulesTests {
("testGenericTypeName", testGenericTypeName),
("testIdentifierName", testIdentifierName),
("testImplicitGetter", testImplicitGetter),
("testImplicitlyUnwrappedOptional", testImplicitlyUnwrappedOptional),
("testLargeTuple", testLargeTuple),
("testLeadingWhitespace", testLeadingWhitespace),
("testLegacyCGGeometryFunctions", testLegacyCGGeometryFunctions),
Expand Down