diff --git a/CHANGELOG.md b/CHANGELOG.md index a32b74b58a..265d825810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ ##### Enhancements +* Added `OperatorFunctionWhitespaceRule` to make sure that + you use whitespace around operators when defining them. + [Akira Hirakawa](https://github.com/akirahrkw) + [#60](https://github.com/realm/SwiftLint/issues/60) + * Added `ReturnArrowWhitespaceRule` to make sure that you have 1 space before return arrow and return type. [Akira Hirakawa](https://github.com/akirahrkw) diff --git a/Source/SwiftLintFramework/Linter.swift b/Source/SwiftLintFramework/Linter.swift index 80c0e52c5e..e04e60c532 100644 --- a/Source/SwiftLintFramework/Linter.swift +++ b/Source/SwiftLintFramework/Linter.swift @@ -19,6 +19,7 @@ public struct Linter { TrailingWhitespaceRule(), ReturnArrowWhitespaceRule(), TrailingNewlineRule(), + OperatorFunctionWhitespaceRule(), ForceCastRule(), FileLengthRule(), TodoRule(), diff --git a/Source/SwiftLintFramework/Location.swift b/Source/SwiftLintFramework/Location.swift index 3640c04bcd..9cb786dce2 100644 --- a/Source/SwiftLintFramework/Location.swift +++ b/Source/SwiftLintFramework/Location.swift @@ -48,7 +48,7 @@ Returns true if `lhs` Location is equal to `rhs` Location. :returns: True if `lhs` Location is equal to `rhs` Location. */ -public func ==(lhs: Location, rhs: Location) -> Bool { +public func == (lhs: Location, rhs: Location) -> Bool { return lhs.file == rhs.file && lhs.line == rhs.line && lhs.character == rhs.character diff --git a/Source/SwiftLintFramework/Rules/OperatorFunctionWhitespaceRule.swift b/Source/SwiftLintFramework/Rules/OperatorFunctionWhitespaceRule.swift new file mode 100644 index 0000000000..931c9a52fe --- /dev/null +++ b/Source/SwiftLintFramework/Rules/OperatorFunctionWhitespaceRule.swift @@ -0,0 +1,87 @@ +// +// OperatorWhitespaceRule.swift +// SwiftLint +// +// Created by Akira Hirakawa on 8/6/15. +// Copyright (c) 2015 Realm. All rights reserved. +// + +import SourceKittenFramework +import SwiftXPC + +public struct OperatorFunctionWhitespaceRule: ASTRule { + public init() {} + + public let identifier = "operator_whitespace" + + public func validateFile(file: File) -> [StyleViolation] { + return validateFile(file, dictionary: file.structure.dictionary) + } + + public func validateFile(file: File, dictionary: XPCDictionary) -> [StyleViolation] { + return (dictionary["key.substructure"] as? XPCArray ?? []).flatMap { subItem in + var violations = [StyleViolation]() + if let subDict = subItem as? XPCDictionary, + let kindString = subDict["key.kind"] as? String, + let kind = flatMap(kindString, { SwiftDeclarationKind(rawValue: $0) }) { + violations.extend(validateFile(file, dictionary: subDict)) + violations.extend(validateFile(file, kind: kind, dictionary: subDict)) + } + return violations + } + } + + public func validateFile(file: File, + kind: SwiftDeclarationKind, + dictionary: XPCDictionary) -> [StyleViolation] { + let functionKinds: [SwiftDeclarationKind] = [ + .FunctionFree, + ] + if !contains(functionKinds, kind) { + return [] + } + var violations = [StyleViolation]() + if let nameOffset = flatMap(dictionary["key.nameoffset"] as? Int64, { Int($0) }), + let nameLength = flatMap(dictionary["key.namelength"] as? Int64, { Int($0) }), + let offset = flatMap(dictionary["key.offset"] as? Int64, { Int($0) }) { + + let location = Location(file: file, offset: offset) + let startAdvance = advance(file.contents.startIndex, nameOffset) + let endAdvance = advance(startAdvance, nameLength) + let range = Range(start: startAdvance, end: endAdvance) + let definition = file.contents.substringWithRange(range) + + let ope1 = ["/", "=", "-", "+", "!", "*", "|", "^", "~", "?", "."].map({"\\\($0)"}) + let ope2 = ["%", "<", ">", "&"] + let ope = "".join(ope1 + ope2) + let pattern = "^[\(ope)]+(<[A-Z]+>)?\\(" + + if let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil) { + let matchRange = NSRange(location: 0, length: count(definition.utf16)) + let matches = regex.matchesInString(definition, options: nil, range: matchRange) + + if matches.count > 0 { + violations.append(StyleViolation(type: .OperatorFunctionWhitespace, + location: location, + severity: .Medium, + reason: "Use whitespace around operators when defining them")) + } + } + } + return violations + } + + public let example = RuleExample( + ruleName: "Operator Function Whitespace Rule", + ruleDescription: "Use whitespace around operators when defining them.", + nonTriggeringExamples: [ + "func <| (lhs: Int, rhs: Int) -> Int {}\n", + "func <|< (lhs: A, rhs: A) -> A {}\n", + "func abc(lhs: Int, rhs: Int) -> Int {}\n" + ], + triggeringExamples: [ + "func <|(lhs: Int, rhs: Int) -> Int {}\n", + "func <|<(lhs: A, rhs: A) -> A {}\n" + ] + ) +} diff --git a/Source/SwiftLintFramework/StyleViolation.swift b/Source/SwiftLintFramework/StyleViolation.swift index 8bb046b325..1fe1dfb082 100644 --- a/Source/SwiftLintFramework/StyleViolation.swift +++ b/Source/SwiftLintFramework/StyleViolation.swift @@ -44,7 +44,7 @@ Returns true if `lhs` StyleViolation is equal to `rhs` StyleViolation. :returns: True if `lhs` StyleViolation is equal to `rhs` StyleViolation. */ -public func ==(lhs: StyleViolation, rhs: StyleViolation) -> Bool { +public func == (lhs: StyleViolation, rhs: StyleViolation) -> Bool { return lhs.type == rhs.type && lhs.location == rhs.location && lhs.severity == rhs.severity && diff --git a/Source/SwiftLintFramework/StyleViolationType.swift b/Source/SwiftLintFramework/StyleViolationType.swift index f98d2aac6b..0d7036e8ef 100644 --- a/Source/SwiftLintFramework/StyleViolationType.swift +++ b/Source/SwiftLintFramework/StyleViolationType.swift @@ -7,17 +7,18 @@ // public enum StyleViolationType: String, Printable { - case NameFormat = "Name Format" - case Length = "Length" - case TrailingNewline = "Trailing Newline" - case LeadingWhitespace = "Leading Whitespace" - case TrailingWhitespace = "Trailing Whitespace" - case ReturnArrowWhitespace = "Return Arrow Whitespace" - case ForceCast = "Force Cast" - case TODO = "TODO or FIXME" - case Colon = "Colon" - case Nesting = "Nesting" - case ControlStatement = "Control Statement Parentheses" + case NameFormat = "Name Format" + case Length = "Length" + case TrailingNewline = "Trailing Newline" + case LeadingWhitespace = "Leading Whitespace" + case TrailingWhitespace = "Trailing Whitespace" + case ReturnArrowWhitespace = "Return Arrow Whitespace" + case OperatorFunctionWhitespace = "Operator Function Whitespace" + case ForceCast = "Force Cast" + case TODO = "TODO or FIXME" + case Colon = "Colon" + case Nesting = "Nesting" + case ControlStatement = "Control Statement Parentheses" public var description: String { return rawValue } } diff --git a/Source/SwiftLintFrameworkTests/ASTRuleTests.swift b/Source/SwiftLintFrameworkTests/ASTRuleTests.swift index 3c6eb9f151..7ae87892b6 100644 --- a/Source/SwiftLintFrameworkTests/ASTRuleTests.swift +++ b/Source/SwiftLintFrameworkTests/ASTRuleTests.swift @@ -126,6 +126,10 @@ class ASTRuleTests: XCTestCase { } } + func testOperatorFunctionWhitespace() { + verifyRule(OperatorFunctionWhitespaceRule().example, type: .OperatorFunctionWhitespace) + } + func testNesting() { verifyRule(NestingRule().example, type: .Nesting, commentDoesntViolate: false) } diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 141a59aa70..a0bcc7866c 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ D0E7B65319E9C6AD00EDBA4D /* SwiftLintFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D1216D19E87B05005E4BAA /* SwiftLintFramework.framework */; }; D0E7B65619E9C76900EDBA4D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D1211B19E87861005E4BAA /* main.swift */; }; E57B23C11B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */; }; + E5A167C91B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */; }; E812249A1B04F85B001783D2 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81224991B04F85B001783D2 /* TestHelpers.swift */; }; E812249C1B04FADC001783D2 /* Linter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E812249B1B04FADC001783D2 /* Linter.swift */; }; E832F10B1B17E2F5003F265F /* NSFileManager+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832F10A1B17E2F5003F265F /* NSFileManager+SwiftLint.swift */; }; @@ -143,6 +144,7 @@ D0E7B63219E9C64500EDBA4D /* swiftlint.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = swiftlint.app; sourceTree = BUILT_PRODUCTS_DIR; }; D0E7B65819E9CA0800EDBA4D /* swiftlint */ = {isa = PBXFileReference; lastKnownFileType = text; path = swiftlint; sourceTree = BUILT_PRODUCTS_DIR; }; E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReturnArrowWhitespaceRule.swift; sourceTree = ""; }; + E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorFunctionWhitespaceRule.swift; sourceTree = ""; }; E81224991B04F85B001783D2 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; E812249B1B04FADC001783D2 /* Linter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Linter.swift; sourceTree = ""; }; E832F10A1B17E2F5003F265F /* NSFileManager+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSFileManager+SwiftLint.swift"; sourceTree = ""; }; @@ -415,6 +417,7 @@ E88DEA911B099B1F00A66CB0 /* TypeNameRule.swift */, E88DEA931B099C0900A66CB0 /* VariableNameRule.swift */, E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */, + E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */, ); path = Rules; sourceTree = ""; @@ -607,6 +610,7 @@ E88DEA901B099A3100A66CB0 /* FunctionBodyLengthRule.swift in Sources */, E88DEA6B1B0983FE00A66CB0 /* StyleViolation.swift in Sources */, E832F10B1B17E2F5003F265F /* NSFileManager+SwiftLint.swift in Sources */, + E5A167C91B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift in Sources */, E88DEA7E1B098F2A00A66CB0 /* LeadingWhitespaceRule.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;