diff --git a/CHANGELOG.md b/CHANGELOG.md index 04322b84c..dc69cf820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ with redundant value assignments. [Marcelo Fabri](https://github.com/marcelofabri) [#946](https://github.com/realm/SwiftLint/issues/946) + +* Add `empty_parentheses_with_trailing_closure` rule that checks for + empty parentheses after method call when using trailing closures. + [Marcelo Fabri](https://github.com/marcelofabri) + [#885](https://github.com/realm/SwiftLint/issues/885) ##### Bug Fixes diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 554ccd243..b36698690 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -50,6 +50,7 @@ public let masterRuleList = RuleList(rules: CustomRules.self, CyclomaticComplexityRule.self, EmptyCountRule.self, + EmptyParenthesesWithTrailingClosureRule.self, ExplicitInitRule.self, FileHeaderRule.self, FileLengthRule.self, diff --git a/Source/SwiftLintFramework/Rules/EmptyParenthesesWithTrailingClosureRule.swift b/Source/SwiftLintFramework/Rules/EmptyParenthesesWithTrailingClosureRule.swift new file mode 100644 index 000000000..e6dcea212 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/EmptyParenthesesWithTrailingClosureRule.swift @@ -0,0 +1,84 @@ +// +// EmptyParenthesesWithTrailingClosureRule.swift +// SwiftLint +// +// Created by Marcelo Fabri on 11/12/16. +// Copyright © 2016 Realm. All rights reserved. +// + +import Foundation +import SourceKittenFramework + +public struct EmptyParenthesesWithTrailingClosureRule: ASTRule, ConfigurationProviderRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "empty_parentheses_with_trailing_closure", + name: "Empty Parentheses with Trailing Closure", + description: "When using trailing closures, empty parentheses should be avoided " + + "after the method call.", + nonTriggeringExamples: [ + "[1, 2].map { $0 + 1 }\n", + "[1, 2].map({ $0 + 1 })\n", + "[1, 2].reduce(0) { $0 + $1 }", + "[1, 2].map { number in\n number + 1 \n}\n", + "let isEmpty = [1, 2].map.isEmpty()\n" + ], + triggeringExamples: [ + "[1, 2].map↓() { $0 + 1 }", + "[1, 2].map↓( ) { $0 + 1 }\n", + "[1, 2].map↓() { number in\n number + 1 \n}\n", + "[1, 2].map↓( ) { number in\n number + 1 \n}\n" + ] + ) + + public enum Kind: String { + case exprCall = "source.lang.swift.expr.call" + case other + public init?(rawValue: String) { + switch rawValue { + case Kind.exprCall.rawValue: + self = .exprCall + default: + self = .other + } + } + } + + private static let emptyParenthesesRegex = regex("^\\s*\\(\\s*\\)") + + public func validateFile(_ file: File, + kind: Kind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + guard kind == .exprCall else { + return [] + } + + guard let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }), + let length = (dictionary["key.length"] as? Int64).flatMap({ Int($0) }), + let nameOffset = (dictionary["key.nameoffset"] as? Int64).flatMap({ Int($0) }), + let nameLength = (dictionary["key.namelength"] as? Int64).flatMap({ Int($0) }), + let bodyLength = (dictionary["key.bodylength"] as? Int64).flatMap({ Int($0) }), + bodyLength > 0 else { + return [] + } + + let rangeStart = nameOffset + nameLength + let rangeLength = (offset + length) - (nameOffset + nameLength) + let regex = EmptyParenthesesWithTrailingClosureRule.emptyParenthesesRegex + + guard let range = file.contents.byteRangeToNSRange(start: rangeStart, length: rangeLength), + let match = regex.firstMatch(in: file.contents, options: [], range: range), + match.range.location != NSNotFound else { + return [] + } + + return [ + StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, byteOffset: rangeStart)) + ] + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 90fd74310..00e5957b5 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ D44AD2761C0AA5350048F7B0 /* LegacyConstructorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44AD2741C0AA3730048F7B0 /* LegacyConstructorRule.swift */; }; D46252541DF63FB200BE2CA1 /* NumberSeparatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */; }; D46E041D1DE3712C00728374 /* TrailingCommaRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */; }; + D47079A71DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */; }; D47A510E1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */; }; D4998DE91DF194F20006E05D /* FileHeaderRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4998DE81DF194F20006E05D /* FileHeaderRuleTests.swift */; }; D4C4A34E1DEA877200E0E04C /* FileHeaderRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */; }; @@ -289,6 +290,7 @@ D44AD2741C0AA3730048F7B0 /* LegacyConstructorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyConstructorRule.swift; sourceTree = ""; }; D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorRule.swift; sourceTree = ""; }; D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaRule.swift; sourceTree = ""; }; + D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyParenthesesWithTrailingClosureRule.swift; sourceTree = ""; }; D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCaseOnNewlineRule.swift; sourceTree = ""; }; D4998DE81DF194F20006E05D /* FileHeaderRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderRuleTests.swift; sourceTree = ""; }; D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderRule.swift; sourceTree = ""; }; @@ -649,6 +651,7 @@ 3B1DF0111C5148140011BCED /* CustomRules.swift */, 2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */, E847F0A81BFBBABD00EA9363 /* EmptyCountRule.swift */, + D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */, 7C0C2E791D2866CB0076435A /* ExplicitInitRule.swift */, D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */, E88DEA891B0992B300A66CB0 /* FileLengthRule.swift */, @@ -955,6 +958,7 @@ 6CC4259B1C77046200AEA885 /* SyntaxMap+SwiftLint.swift in Sources */, E881985C1BEA978500333A11 /* TrailingNewlineRule.swift in Sources */, 78F032481D7D614300BE709A /* OverridenSuperCallConfiguration.swift in Sources */, + D47079A71DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift in Sources */, E881985E1BEA982100333A11 /* TypeBodyLengthRule.swift in Sources */, 69F88BF71BDA38A6005E7CAE /* OpeningBraceRule.swift in Sources */, 78F032461D7C877E00BE709A /* OverriddenSuperCallRule.swift in Sources */, diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index f0b0af847..2abb8eec6 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -113,6 +113,10 @@ class RulesTests: XCTestCase { verifyRule(EmptyCountRule.description) } + func testEmptyParenthesesWithTrailingClosure() { + verifyRule(EmptyParenthesesWithTrailingClosureRule.description) + } + func testExplicitInit() { verifyRule(ExplicitInitRule.description) }