diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef4c9deb..a2910d373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ * Add `implicitly_unwrapped_optional` rule that warns when using implicitly unwrapped optional, - except cases when this IUO is IBOutlet. + except cases when this IUO is IBOutlet. [Siarhei Fedartsou](https://github.com/SiarheiFedartsou/) * Performance improvements to `generic_type_name`, diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index 488150790..176f491c7 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -71,7 +71,7 @@ public struct CustomRules: Rule, ConfigurationProviderRule { } return configurations.flatMap { configuration -> [StyleViolation] in - let pattern = configuration.regex?.pattern ?? "" + let pattern = configuration.regex.pattern let excludingKinds = Array(Set(SyntaxKind.allKinds()).subtracting(configuration.matchKinds)) return file.match(pattern: pattern, excludingSyntaxKinds: excludingKinds).map({ StyleViolation(ruleDescription: configuration.description, diff --git a/Source/SwiftLintFramework/Rules/ImplicitlyUnwrappedOptionalRule.swift b/Source/SwiftLintFramework/Rules/ImplicitlyUnwrappedOptionalRule.swift index df3d68cbc..1eeac2ad8 100644 --- a/Source/SwiftLintFramework/Rules/ImplicitlyUnwrappedOptionalRule.swift +++ b/Source/SwiftLintFramework/Rules/ImplicitlyUnwrappedOptionalRule.swift @@ -10,7 +10,8 @@ import Foundation import SourceKittenFramework public struct ImplicitlyUnwrappedOptionalRule: ASTRule, ConfigurationProviderRule, OptInRule { - public var configuration = SeverityConfiguration(.warning) + public var configuration = ImplicitlyUnwrappedOptionalConfiguration(mode: .allExceptIBOutlets, + severity: SeverityConfiguration(.warning)) public init() {} @@ -21,6 +22,7 @@ public struct ImplicitlyUnwrappedOptionalRule: ASTRule, ConfigurationProviderRul nonTriggeringExamples: [ "@IBOutlet private var label: UILabel!", "@IBOutlet var label: UILabel!", + "@IBOutlet var label: [UILabel!]", "if !boolean {}", "let int: Int? = 42", "let int: Int? = nil" @@ -28,13 +30,22 @@ public struct ImplicitlyUnwrappedOptionalRule: ASTRule, ConfigurationProviderRul 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" + "var int: Int! = 42", + "let int: ImplicitlyUnwrappedOptional", + "let collection: AnyCollection", + "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 { @@ -42,10 +53,12 @@ public struct ImplicitlyUnwrappedOptionalRule: ASTRule, ConfigurationProviderRul } guard let typeName = dictionary.typeName else { return [] } - guard typeName.hasSuffix("!") else { return [] } + guard hasImplicitlyUnwrappedOptional(typeName) else { return [] } - let isOutlet = dictionary.enclosedSwiftAttributes.contains("source.decl.attribute.iboutlet") - if isOutlet { 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 { @@ -56,7 +69,7 @@ public struct ImplicitlyUnwrappedOptionalRule: ASTRule, ConfigurationProviderRul return [ StyleViolation(ruleDescription: type(of: self).description, - severity: configuration.severity, + severity: configuration.severity.severity, location: location) ] } diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift new file mode 100644 index 000000000..9d4033f6b --- /dev/null +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/ImplicitlyUnwrappedOptionalConfiguration.swift @@ -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 + } +} diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift index 0dfcbe8e2..10e7c2ba6 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/PrivateUnitTestConfiguration.swift @@ -13,7 +13,7 @@ public struct PrivateUnitTestConfiguration: RuleConfiguration, Equatable { public let identifier: String public var name: String? public var message = "Regex matched." - public var regex: NSRegularExpression? + public var regex: NSRegularExpression! public var included: NSRegularExpression? public var severityConfiguration = SeverityConfiguration(.warning) @@ -22,8 +22,7 @@ public struct PrivateUnitTestConfiguration: RuleConfiguration, Equatable { } public var consoleDescription: String { - let regexPattern = regex?.pattern ?? "" - return "\(severity.rawValue): \(regexPattern)" + return "\(severity.rawValue): \(regex.pattern)" } public var description: RuleDescription { diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift index b5344fcce..457398484 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift @@ -13,7 +13,7 @@ public struct RegexConfiguration: RuleConfiguration, Equatable { public let identifier: String public var name: String? public var message = "Regex matched." - public var regex: NSRegularExpression? + public var regex: NSRegularExpression! public var included: NSRegularExpression? public var matchKinds = Set(SyntaxKind.allKinds()) public var severityConfiguration = SeverityConfiguration(.warning) @@ -23,8 +23,7 @@ public struct RegexConfiguration: RuleConfiguration, Equatable { } public var consoleDescription: String { - let regexPattern = regex?.pattern ?? "" - return "\(severity.rawValue): \(regexPattern)" + return "\(severity.rawValue): \(regex.pattern)" } public var description: RuleDescription { diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 03cc4e9a2..f1a93300e 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -49,6 +49,9 @@ 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 */; }; @@ -312,6 +315,9 @@ 3BCC04D31C502BAB006073C3 /* RuleConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleConfigurationTests.swift; sourceTree = ""; }; 3BD9CD3C1C37175B009A5D25 /* YamlParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YamlParser.swift; sourceTree = ""; }; 3BDB224A1C345B4900473680 /* ProjectMock */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ProjectMock; sourceTree = ""; }; + 47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalConfiguration.swift; sourceTree = ""; }; + 47ACC8991E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalConfigurationTests.swift; sourceTree = ""; }; + 47ACC89B1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalRuleTests.swift; sourceTree = ""; }; 47FF3BDF1E7C745100187E6D /* ImplicitlyUnwrappedOptionalRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitlyUnwrappedOptionalRule.swift; sourceTree = ""; }; 4A9A3A391DC1D75F00DF5183 /* HTMLReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLReporter.swift; sourceTree = ""; }; 4DB7815C1CAD690100BC4723 /* LegacyCGGeometryFunctionsRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCGGeometryFunctionsRule.swift; sourceTree = ""; }; @@ -559,6 +565,7 @@ D43B04671E07228D004016AF /* ColonConfiguration.swift */, 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */, D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */, + 47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */, 3B034B6C1E0BE544005D49A9 /* LineLengthConfiguration.swift */, 3BCC04D01C4F56D3006073C3 /* NameConfiguration.swift */, D93DA3CF1E699E4E00809827 /* NestingConfiguration.swift */, @@ -761,6 +768,8 @@ 006204DD1E1E4E0A00FFFBE1 /* VerticalWhitespaceRuleTests.swift */, 67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */, 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */, + 47ACC8991E7DCCAD0088EEB2 /* ImplicitlyUnwrappedOptionalConfigurationTests.swift */, + 47ACC89B1E7DCFA00088EEB2 /* ImplicitlyUnwrappedOptionalRuleTests.swift */, ); name = SwiftLintFrameworkTests; path = Tests/SwiftLintFrameworkTests; @@ -1243,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 */, @@ -1333,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 */, @@ -1349,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 */, diff --git a/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift new file mode 100644 index 000000000..d85094f04 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalConfigurationTests.swift @@ -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) + ] + } +} diff --git a/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift new file mode 100644 index 000000000..2011ff450 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/ImplicitlyUnwrappedOptionalRuleTests.swift @@ -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) + ] + } +}