diff --git a/CHANGELOG.md b/CHANGELOG.md index ad047ddbfaf..87c63001b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ * Adds `untyped-error-in-catch-rule` opt-in rule to warn against declaring errors without type check in catch statements. [Daniel Metzing](https://github.com/dirtydanee) [#2045](https://github.com/realm/SwiftLint/issues/2045) +* Add a new `excluded` config parameter to the `explicit_type_interface` rule + to exempt certain types of variables from the rule. + [Rounak Jain](https://github.com/rounak) + [#2028](https://github.com/realm/SwiftLint/issues/2028) #### Bug Fixes diff --git a/Source/SwiftLintFramework/Rules/ExplicitTypeInterfaceRule.swift b/Source/SwiftLintFramework/Rules/ExplicitTypeInterfaceRule.swift index ef0b58dd020..91625db71cd 100644 --- a/Source/SwiftLintFramework/Rules/ExplicitTypeInterfaceRule.swift +++ b/Source/SwiftLintFramework/Rules/ExplicitTypeInterfaceRule.swift @@ -10,7 +10,7 @@ import Foundation import SourceKittenFramework public struct ExplicitTypeInterfaceRule: ASTRule, OptInRule, ConfigurationProviderRule { - public var configuration = SeverityConfiguration(.warning) + public var configuration = ExplicitTypeInterfaceConfiguration() public init() {} @@ -33,13 +33,10 @@ public struct ExplicitTypeInterfaceRule: ASTRule, OptInRule, ConfigurationProvid ] ) - private static let allowedKinds: Set = [.varInstance, .varLocal, - .varStatic, .varClass] - public func validate(file: File, kind: SwiftDeclarationKind, dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { - guard ExplicitTypeInterfaceRule.allowedKinds.contains(kind), + guard configuration.allowedKinds.contains(kind), !containsType(dictionary: dictionary), let offset = dictionary.offset else { return [] @@ -47,7 +44,7 @@ public struct ExplicitTypeInterfaceRule: ASTRule, OptInRule, ConfigurationProvid return [ StyleViolation(ruleDescription: type(of: self).description, - severity: configuration.severity, + severity: configuration.severityConfiguration.severity, location: Location(file: file, byteOffset: offset)) ] } diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift new file mode 100644 index 00000000000..ee1e7da037c --- /dev/null +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/ExplicitTypeInterfaceConfiguration.swift @@ -0,0 +1,89 @@ +// +// ExplicitTypeInterfaceConfiguration.swift +// SwiftLint +// +// Created by Rounak Jain on 2/18/18. +// Copyright © 2018 Realm. All rights reserved. +// + +import Foundation +import SourceKittenFramework + +private enum VariableKind: String { + case instance + case local + case `static` + case `class` +} + +private extension SwiftDeclarationKind { + init(variableKind: VariableKind) { + switch variableKind { + case .instance: + self = .varInstance + case .local: + self = .varLocal + case .static: + self = .varStatic + case .class: + self = .varClass + } + } + + var variableKind: VariableKind? { + switch self { + case .varInstance: + return .instance + case .varLocal: + return .local + case .varStatic: + return .static + case .varClass: + return .class + default: + return nil + } + } +} + +public struct ExplicitTypeInterfaceConfiguration: RuleConfiguration, Equatable { + + private static let variableKinds: Set = [.varInstance, + .varLocal, + .varStatic, + .varClass] + + public var severityConfiguration = SeverityConfiguration(.warning) + + public var allowedKinds = ExplicitTypeInterfaceConfiguration.variableKinds + + public var consoleDescription: String { + let excludedKinds = ExplicitTypeInterfaceConfiguration.variableKinds.subtracting(allowedKinds) + let simplifiedExcludedKinds = excludedKinds.flatMap { $0.variableKind?.rawValue }.sorted() + return severityConfiguration.consoleDescription + ", excluded: \(simplifiedExcludedKinds)" + } + + public init() {} + + public mutating func apply(configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw ConfigurationError.unknownConfiguration + } + for (key, value) in configuration { + switch (key, value) { + case ("severity", let severityString as String): + try severityConfiguration.apply(configuration: severityString) + case ("excluded", let excludedStrings as [String]): + let excludedKinds = excludedStrings.flatMap(VariableKind.init(rawValue:)) + allowedKinds.subtract(excludedKinds.map(SwiftDeclarationKind.init(variableKind:))) + default: + throw ConfigurationError.unknownConfiguration + } + } + } + + public static func == (lhs: ExplicitTypeInterfaceConfiguration, rhs: ExplicitTypeInterfaceConfiguration) -> Bool { + return lhs.allowedKinds == rhs.allowedKinds && lhs.severityConfiguration == rhs.severityConfiguration + } + +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 940d3a58a32..990fb36da6d 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 094385011D5D2894009168CF /* WeakDelegateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094384FF1D5D2382009168CF /* WeakDelegateRule.swift */; }; 094385041D5D4F7C009168CF /* PrivateOutletRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094385021D5D4F78009168CF /* PrivateOutletRule.swift */; }; 181D9E172038343D001F6887 /* UntypedErrorInCatchRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181D9E162038343D001F6887 /* UntypedErrorInCatchRule.swift */; }; + 125AAC78203AA82D0004BCE0 /* ExplicitTypeInterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125AAC77203AA82D0004BCE0 /* ExplicitTypeInterfaceConfiguration.swift */; }; + 125CE52F20425EFD001635E5 /* ExplicitTypeInterfaceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125CE52E20425EFD001635E5 /* ExplicitTypeInterfaceConfigurationTests.swift */; }; + 12E3D4DC2042729300B3E30E /* ExplicitTypeInterfaceRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E3D4DB2042729300B3E30E /* ExplicitTypeInterfaceRuleTests.swift */; }; 187290721FC37CA50016BEA2 /* YodaConditionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */; }; 1E18574B1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E18574A1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift */; }; 1E3C2D711EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */; }; @@ -367,6 +370,10 @@ 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 = ""; }; 181D9E162038343D001F6887 /* UntypedErrorInCatchRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UntypedErrorInCatchRule.swift; sourceTree = ""; }; + 125AAC77203AA82D0004BCE0 /* ExplicitTypeInterfaceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplicitTypeInterfaceConfiguration.swift; sourceTree = ""; }; + 125CE52E20425EFD001635E5 /* ExplicitTypeInterfaceConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplicitTypeInterfaceConfigurationTests.swift; sourceTree = ""; }; + 12E3D4DB2042729300B3E30E /* ExplicitTypeInterfaceRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplicitTypeInterfaceRuleTests.swift; sourceTree = ""; }; +>>>>>>> upstream-master 1872906F1FC37A9B0016BEA2 /* YodaConditionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YodaConditionRule.swift; sourceTree = ""; }; 1E18574A1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoExtensionAccessModifierRule.swift; sourceTree = ""; }; 1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOverFilePrivateRule.swift; sourceTree = ""; }; @@ -736,6 +743,7 @@ D43B04671E07228D004016AF /* ColonConfiguration.swift */, 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */, 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */, + 125AAC77203AA82D0004BCE0 /* ExplicitTypeInterfaceConfiguration.swift */, D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */, 29FFC3781F1574FD007E4825 /* FileLengthRuleConfiguration.swift */, 47ACC8971E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift */, @@ -928,6 +936,8 @@ 67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */, 62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */, D48B51221F4F5E4B0068AB98 /* DocumentationTests.swift */, + 125CE52E20425EFD001635E5 /* ExplicitTypeInterfaceConfigurationTests.swift */, + 12E3D4DB2042729300B3E30E /* ExplicitTypeInterfaceRuleTests.swift */, 02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */, D4998DE81DF194F20006E05D /* FileHeaderRuleTests.swift */, 29FFC37B1F157BA8007E4825 /* FileLengthRuleTests.swift */, @@ -1543,6 +1553,7 @@ E847F0A91BFBBABD00EA9363 /* EmptyCountRule.swift in Sources */, D46252541DF63FB200BE2CA1 /* NumberSeparatorRule.swift in Sources */, E315B83C1DFA4BC500621B44 /* DynamicInlineRule.swift in Sources */, + 125AAC78203AA82D0004BCE0 /* ExplicitTypeInterfaceConfiguration.swift in Sources */, 1E18574B1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift in Sources */, D42D2B381E09CC0D00CD7A2E /* FirstWhereRule.swift in Sources */, D4B0226F1E0C75F9007E5297 /* VerticalParameterAlignmentRule.swift in Sources */, @@ -1715,6 +1726,7 @@ 29FFC37D1F157BDE007E4825 /* FileLengthRuleTests.swift in Sources */, 006204DE1E1E4E0A00FFFBE1 /* VerticalWhitespaceRuleTests.swift in Sources */, 02FD8AEF1BFC18D60014BFFB /* ExtendedNSStringTests.swift in Sources */, + 12E3D4DC2042729300B3E30E /* ExplicitTypeInterfaceRuleTests.swift in Sources */, D48B51231F4F5E4B0068AB98 /* DocumentationTests.swift in Sources */, D4CA758F1E2DEEA500A40E8A /* NumberSeparatorRuleTests.swift in Sources */, D4DB92251E628898005DE9C1 /* TodoRuleTests.swift in Sources */, @@ -1732,6 +1744,7 @@ E86396C71BADAFE6002C9E88 /* ReporterTests.swift in Sources */, D43B04661E071ED3004016AF /* ColonRuleTests.swift in Sources */, 3B12C9C71C3361CB000B423F /* RuleTests.swift in Sources */, + 125CE52F20425EFD001635E5 /* ExplicitTypeInterfaceConfigurationTests.swift in Sources */, 67EB4DFC1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift in Sources */, 3B30C4A11C3785B300E04027 /* YamlParserTests.swift in Sources */, 3B20CD0A1EB699380069EF2E /* GenericTypeNameRuleTests.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 368f5fcaa3c..14ee5fb0557 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -139,6 +139,25 @@ extension DocumentationTests { ] } +extension ExplicitTypeInterfaceConfigurationTests { + static var allTests: [(String, (ExplicitTypeInterfaceConfigurationTests) -> () throws -> Void)] = [ + ("testDefaultConfiguration", testDefaultConfiguration), + ("testApplyingCustomConfiguration", testApplyingCustomConfiguration), + ("testInvalidKeyInCustomConfiguration", testInvalidKeyInCustomConfiguration), + ("testInvalidTypeOfCustomConfiguration", testInvalidTypeOfCustomConfiguration), + ("testInvalidTypeOfValueInCustomConfiguration", testInvalidTypeOfValueInCustomConfiguration), + ("testConsoleDescription", testConsoleDescription) + ] +} + +extension ExplicitTypeInterfaceRuleTests { + static var allTests: [(String, (ExplicitTypeInterfaceRuleTests) -> () throws -> Void)] = [ + ("testExplicitTypeInterface", testExplicitTypeInterface), + ("testExcludeLocalVars", testExcludeLocalVars), + ("testExcludeClassVars", testExcludeClassVars) + ] +} + extension ExtendedNSStringTests { static var allTests: [(String, (ExtendedNSStringTests) -> () throws -> Void)] = [ ("testLineAndCharacterForByteOffset_forContentsContainingMultibyteCharacters", testLineAndCharacterForByteOffset_forContentsContainingMultibyteCharacters) @@ -403,7 +422,6 @@ extension RulesTests { ("testExplicitEnumRawValue", testExplicitEnumRawValue), ("testExplicitInit", testExplicitInit), ("testExplicitTopLevelACL", testExplicitTopLevelACL), - ("testExplicitTypeInterface", testExplicitTypeInterface), ("testExtensionAccessModifier", testExtensionAccessModifier), ("testFallthrough", testFallthrough), ("testFatalErrorMessage", testFatalErrorMessage), @@ -564,6 +582,8 @@ XCTMain([ testCase(CyclomaticComplexityRuleTests.allTests), testCase(DiscouragedDirectInitRuleTests.allTests), testCase(DocumentationTests.allTests), + testCase(ExplicitTypeInterfaceConfigurationTests.allTests), + testCase(ExplicitTypeInterfaceRuleTests.allTests), testCase(ExtendedNSStringTests.allTests), testCase(FileHeaderRuleTests.allTests), testCase(FileLengthRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift new file mode 100644 index 00000000000..4d45b7d01f2 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceConfigurationTests.swift @@ -0,0 +1,54 @@ +// +// ExplicitTypeInterfaceConfigurationTests.swift +// SwiftLint +// +// Created by Rounak Jain on 2/24/18. +// Copyright © 2018 Realm. All rights reserved. +// + +@testable import SwiftLintFramework +import XCTest + +class ExplicitTypeInterfaceConfigurationTests: XCTestCase { + + func testDefaultConfiguration() { + let config = ExplicitTypeInterfaceConfiguration() + XCTAssertEqual(config.severityConfiguration.severity, .warning) + XCTAssertEqual(config.allowedKinds, Set([.varInstance, .varClass, .varStatic, .varLocal])) + } + + func testApplyingCustomConfiguration() throws { + var config = ExplicitTypeInterfaceConfiguration() + try config.apply(configuration: ["severity": "error", + "excluded": ["local"]]) + XCTAssertEqual(config.severityConfiguration.severity, .error) + XCTAssertEqual(config.allowedKinds, Set([.varInstance, .varClass, .varStatic])) + } + + func testInvalidKeyInCustomConfiguration() { + var config = ExplicitTypeInterfaceConfiguration() + checkError(ConfigurationError.unknownConfiguration) { + try config.apply(configuration: ["invalidKey": "error"]) + } + } + + func testInvalidTypeOfCustomConfiguration() { + var config = ExplicitTypeInterfaceConfiguration() + checkError(ConfigurationError.unknownConfiguration) { + try config.apply(configuration: "invalidKey") + } + } + + func testInvalidTypeOfValueInCustomConfiguration() { + var config = ExplicitTypeInterfaceConfiguration() + checkError(ConfigurationError.unknownConfiguration) { + try config.apply(configuration: ["severity": 1]) + } + } + + func testConsoleDescription() throws { + var config = ExplicitTypeInterfaceConfiguration() + try config.apply(configuration: ["excluded": ["class", "instance"]]) + XCTAssertEqual(config.consoleDescription, "warning, excluded: [\"class\", \"instance\"]") + } +} diff --git a/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift new file mode 100644 index 00000000000..16f2073126c --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift @@ -0,0 +1,47 @@ +// +// ExplicitTypeInterfaceRuleTests.swift +// SwiftLint +// +// Created by Rounak Jain on 2/24/18. +// Copyright © 2018 Realm. All rights reserved. +// + +import SwiftLintFramework +import XCTest + +class ExplicitTypeInterfaceRuleTests: XCTestCase { + + func testExplicitTypeInterface() { + verifyRule(ExplicitTypeInterfaceRule.description) + } + + func testExcludeLocalVars() { + let nonTriggeringExamples = ExplicitTypeInterfaceRule.description.nonTriggeringExamples + [ + "func foo() {\nlet intVal = 1\n}" + ] + let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples + let description = ExplicitTypeInterfaceRule.description + .with(triggeringExamples: triggeringExamples) + .with(nonTriggeringExamples: nonTriggeringExamples) + + verifyRule(description, ruleConfiguration: ["excluded": ["local"]]) + } + + func testExcludeClassVars() { + let nonTriggeringExamples = ExplicitTypeInterfaceRule.description.nonTriggeringExamples + [ + "class Foo {\n static var myStaticVar = 0\n}\n", + "class Foo {\n static let myStaticLet = 0\n}\n" + ] + let triggeringExamples = [ + "class Foo {\n ↓var myVar = 0\n\n}\n", + "class Foo {\n ↓let mylet = 0\n\n}\n", + "class Foo {\n ↓class var myClassVar = 0\n}\n" + ] + let description = ExplicitTypeInterfaceRule.description + .with(triggeringExamples: triggeringExamples) + .with(nonTriggeringExamples: nonTriggeringExamples) + + verifyRule(description, ruleConfiguration: ["excluded": ["static"]]) + } + +} diff --git a/Tests/SwiftLintFrameworkTests/ReporterTests.swift b/Tests/SwiftLintFrameworkTests/ReporterTests.swift index 8f1ec12633d..6ee829d77dd 100644 --- a/Tests/SwiftLintFrameworkTests/ReporterTests.swift +++ b/Tests/SwiftLintFrameworkTests/ReporterTests.swift @@ -81,7 +81,7 @@ class ReporterTests: XCTestCase { } else if let array = (result as? [Any])?.bridge() { return array } - fatalError("Unexpected value in JSON: \(result)") + queuedFatalError("Unexpected value in JSON: \(result)") } XCTAssertEqual(try jsonValue(result), try jsonValue(expectedOutput)) } diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index c7d80b71e4c..18bc82c6ab9 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -114,10 +114,6 @@ class RulesTests: XCTestCase { verifyRule(ExplicitTopLevelACLRule.description) } - func testExplicitTypeInterface() { - verifyRule(ExplicitTypeInterfaceRule.description) - } - func testExtensionAccessModifier() { verifyRule(ExtensionAccessModifierRule.description) } diff --git a/circle.yml b/circle.yml index 66f20815555..b94e55857d2 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ jobs: tsan: macos: - xcode: "9.0" + xcode: "9.2.0" steps: - checkout # Pre-cache @@ -13,22 +13,23 @@ jobs: osscheck: macos: - xcode: "9.0" + xcode: "9.2.0" steps: - checkout + - run: brew install ruby - run: bundle install - run: bundle exec danger --verbose swiftpm_4: macos: - xcode: "9.0" + xcode: "9.2.0" steps: - checkout - run: swift test --parallel xcode_9: macos: - xcode: "9.0" + xcode: "9.2.0" steps: - checkout - run: set -o pipefail && script/cibuild | xcpretty -r junit @@ -38,11 +39,13 @@ jobs: cocoapods: macos: - xcode: "9.0" + xcode: "9.2.0" steps: - checkout + - run: brew install ruby + - run: bundle install - run: curl -sS https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash - - run: echo "4.0" > .swift-version; pod lib lint SwiftLintFramework.podspec + - run: echo "4.0" > .swift-version; bundle exec pod lib lint SwiftLintFramework.podspec linux_swift_4: docker: