Add shorthand_operator rule

Fixes #902
This commit is contained in:
Marcelo Fabri
2017-01-06 18:02:02 -02:00
parent 1aa69f8a4f
commit 5d6c2ec3e7
5 changed files with 117 additions and 0 deletions
+5
View File
@@ -103,6 +103,11 @@
[Javier Hernández](https://github.com/jaherhi)
[#384](https://github.com/realm/SwiftLint/issues/384)
* Add `shorthand_operator` rule that validates that shorthand operators should
be used when possible.
[Marcelo Fabri](https://github.com/marcelofabri)
[#902](https://github.com/realm/SwiftLint/issues/902)
##### Bug Fixes
* Ignore close parentheses on `vertical_parameter_alignment` rule.
@@ -122,6 +122,7 @@ public let masterRuleList = RuleList(rules:
RedundantStringEnumValueRule.self,
RedundantVoidReturnRule.self,
ReturnArrowWhitespaceRule.self,
ShorthandOperatorRule.self,
SortedImportsRule.self,
StatementPositionRule.self,
SwitchCaseOnNewlineRule.self,
@@ -0,0 +1,102 @@
//
// ShorthandOperatorRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 01/06/17.
// Copyright © 2017 Realm. All rights reserved.
//
import SourceKittenFramework
public struct ShorthandOperatorRule: ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.error)
public init() {}
public static let description = RuleDescription(
identifier: "shorthand_operator",
name: "Shorthand Operator",
description: "Prefer shorhand operators (+=, -=, *=, /=) over doing the operation and assigning.",
nonTriggeringExamples: allOperators.flatMap { operation in
[
"foo \(operation)= 1",
"foo \(operation)= variable",
"foo \(operation)= bar.method()"
]
},
triggeringExamples: allOperators.flatMap { operation in
[
"↓foo = foo \(operation) 1\n",
"↓foo = foo \(operation) aVariable\n",
"↓foo = foo \(operation) bar.method()\n",
"↓foo = 1 \(operation) foo\n",
"↓foo = aVariable \(operation) foo\n",
"↓foo = bar.method() \(operation) foo\n",
"↓foo = bar.method(param: 1, otherParam: 2) \(operation) foo\n"
]
}
)
private static let allOperators = ["+", "-", "/", "*"]
private static let pattern: String = {
let escapedOperators = allOperators.map { "\\\($0)" }.joined()
let operators = "[\(escapedOperators)]"
let operand = "[\\w\\d\\.]+?"
let spaces = "[^\\S\\r\\n]*?"
let otherOperand = "\(spaces).+?\(spaces)"
let pattern1 = "\\b(\(operand))\(spaces)=\(spaces)(\\1)\(spaces)\(operators)"
let pattern2 = "\\b(\(operand))\(spaces)=\(otherOperand)\(operators)\(spaces)(\\3)"
return "\(pattern1)|\(pattern2)"
}()
// swiftlint:disable:next force_try
private static let regex = try! NSRegularExpression(pattern: pattern, options: [])
public func validateFile(_ file: File) -> [StyleViolation] {
let contents = file.contents.bridge()
let range = NSRange(location: 0, length: contents.length)
let matches = ShorthandOperatorRule.regex.matches(in: file.contents, options: [], range: range)
return matches.flatMap { match -> StyleViolation? in
// byteRanges will have the ranges of captured groups
let byteRanges: [NSRange?] = (1..<match.numberOfRanges).map { rangeIdx in
let range = match.rangeAt(rangeIdx)
guard range.location != NSNotFound else {
return nil
}
return contents.NSRangeToByteRange(start: range.location, length: range.length)
}
guard byteRanges[0] != nil || byteRanges[2] != nil else {
return nil
}
let kindsInCaptureGroups = byteRanges.map { range in
range.flatMap { file.syntaxMap.tokensIn($0).flatMap { SyntaxKind(rawValue: $0.type) } } ?? []
}
let groupIndexes: [Int]
if byteRanges[0] != nil {
// it's a match from pattern1
groupIndexes = [0, 1]
} else {
// it's a match from pattern2
groupIndexes = [2, 3]
}
for idx in groupIndexes where kindsInCaptureGroups[idx] != [.identifier] {
return nil
}
return StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: match.range.location))
}
}
}
+4
View File
@@ -129,6 +129,7 @@
D4C4A34C1DEA4FF000E0E04C /* AttributesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34A1DEA4FD700E0E04C /* AttributesConfiguration.swift */; };
D4C4A34E1DEA877200E0E04C /* FileHeaderRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */; };
D4C4A3521DEFBBB700E0E04C /* FileHeaderConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */; };
D4D5A5FF1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */; };
D4DA1DF41E17511D0037413D /* CompilerProtocolInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */; };
D4DA1DFA1E18D6200037413D /* LargeTupleRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */; };
D4DA1DFE1E1A10DB0037413D /* NumberSeparatorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DFD1E1A10DB0037413D /* NumberSeparatorConfiguration.swift */; };
@@ -389,6 +390,7 @@
D4C4A34A1DEA4FD700E0E04C /* AttributesConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesConfiguration.swift; sourceTree = "<group>"; };
D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderRule.swift; sourceTree = "<group>"; };
D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderConfiguration.swift; sourceTree = "<group>"; };
D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShorthandOperatorRule.swift; sourceTree = "<group>"; };
D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompilerProtocolInitRule.swift; sourceTree = "<group>"; };
D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeTupleRule.swift; sourceTree = "<group>"; };
D4DA1DFD1E1A10DB0037413D /* NumberSeparatorConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorConfiguration.swift; sourceTree = "<group>"; };
@@ -820,6 +822,7 @@
D4B022B11E10B613007E5297 /* RedundantVoidReturnRule.swift */,
E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */,
3BCC04CE1C4F56D3006073C3 /* RuleConfigurations */,
D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */,
D286EC001E02DA190003CF72 /* SortedImportsRule.swift */,
692B60AB1BD8F2E700C7AA22 /* StatementPositionRule.swift */,
D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */,
@@ -1238,6 +1241,7 @@
E832F10B1B17E2F5003F265F /* NSFileManager+SwiftLint.swift in Sources */,
E816194C1BFBF35D00946723 /* SwiftDeclarationKind+SwiftLint.swift in Sources */,
3BA79C9B1C4767910057E705 /* NSRange+SwiftLint.swift in Sources */,
D4D5A5FF1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift in Sources */,
4DCB8E7F1CBE494E0070FCF0 /* RegexHelpers.swift in Sources */,
E86396C21BADAAE5002C9E88 /* Reporter.swift in Sources */,
D4B0228E1E0CC608007E5297 /* ClassDelegateProtocolRule.swift in Sources */,
@@ -217,6 +217,10 @@ class RulesTests: XCTestCase {
verifyRule(ReturnArrowWhitespaceRule.description)
}
func testShorthandOperator() {
verifyRule(ShorthandOperatorRule.description)
}
func testSortedImports() {
verifyRule(SortedImportsRule.description)
}
@@ -381,6 +385,7 @@ extension RulesTests {
("testRedundantStringEnumValue", testRedundantStringEnumValue),
("testRedundantVoidReturn", testRedundantVoidReturn),
("testReturnArrowWhitespace", testReturnArrowWhitespace),
("testShorthandOperator", testShorthandOperator),
("testSortedImports", testSortedImports),
("testStatementPosition", testStatementPosition),
("testStatementPositionUncuddled", testStatementPositionUncuddled),