Skip to content

Commit

Permalink
Merge pull request #111 from realm/jp-comment-commands
Browse files Browse the repository at this point in the history
Disable/re-enable rules from within source code comments. Fixes #4.
  • Loading branch information
jpsim committed Sep 1, 2015
2 parents 86a0e50 + 7d85fe3 commit 56fe7b0
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 37 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
names to start with an underscore.
[JP Simard](https://github.com/jpsim)

* Disable and re-enable rules from within source code comments using
`// swiftlint:disable $IDENTIFIER` and `// swiftlint:enable $IDENTIFIER`.
[JP Simard](https://github.com/jpsim)
[#4](https://github.com/realm/SwiftLint/issues/4)

##### Bug Fixes

* None.
Expand Down
45 changes: 45 additions & 0 deletions Source/SwiftLintFramework/Command.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Command.swift
// SwiftLint
//
// Created by JP Simard on 8/29/15.
// Copyright © 2015 Realm. All rights reserved.
//

import Foundation

public enum CommandAction: String {
case Enable = "enable"
case Disable = "disable"
}

public struct Command {
let action: CommandAction
let ruleIdentifier: String
let line: Int
let character: Int

public init(action: CommandAction, ruleIdentifier: String, line: Int, character: Int) {
self.action = action
self.ruleIdentifier = ruleIdentifier
self.line = line
self.character = character
}

public init?(string: NSString, range: NSRange) {
let scanner = NSScanner(string: string.substringWithRange(range))
scanner.scanString("swiftlint:", intoString: nil)
var actionNSString: NSString? = nil
scanner.scanUpToString(" ", intoString: &actionNSString)
guard let actionString = actionNSString as String?,
action = CommandAction(rawValue: actionString),
lineAndCharacter = string.lineAndCharacterForByteOffset(NSMaxRange(range)) else {
return nil
}
self.action = action
let ruleStart = scanner.string.startIndex.advancedBy(scanner.scanLocation + 1)
ruleIdentifier = scanner.string.substringFromIndex(ruleStart)
line = lineAndCharacter.line
character = lineAndCharacter.character
}
}
41 changes: 39 additions & 2 deletions Source/SwiftLintFramework/File+SwiftLint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,44 @@
import SourceKittenFramework
import SwiftXPC

typealias Line = (index: Int, content: String)
internal typealias Line = (index: Int, content: String)

extension File {
public func regions() -> [Region] {
let nsStringContents = contents as NSString
let commands = matchPattern("swiftlint:(enable|disable)\\ [^\\s]+",
withSyntaxKinds: [.Comment]).flatMap { Command(string: nsStringContents, range: $0) }
let lines = contents.lines()
let totalNumberOfLines = lines.count
let numberOfCharactersInLastLine = lines.last?.content.characters.count
let firstRegion = Region(start:
Location(file: path, line: 1, character: 0),
end: Location(file: path,
line: commands.first?.line ?? totalNumberOfLines,
character: commands.first?.character ?? numberOfCharactersInLastLine),
disabledRuleIdentifiers: [])
var regions = [firstRegion]
var disabledRules = Set<String>()
let commandPairs = zip(commands, Array(commands.dropFirst().map({Optional($0)})) + [nil])
for (command, nextCommand) in commandPairs {
switch command.action {
case .Disable: disabledRules.insert(command.ruleIdentifier)
case .Enable: disabledRules.remove(command.ruleIdentifier)
}
regions.append(
Region(
start: Location(file: path,
line: command.line,
character: command.character),
end: Location(file: path,
line: nextCommand?.line ?? totalNumberOfLines,
character: nextCommand?.character ?? numberOfCharactersInLastLine),
disabledRuleIdentifiers: disabledRules)
)
}
return regions
}

public func matchPattern(pattern: String,
withSyntaxKinds syntaxKinds: [SyntaxKind]) -> [NSRange] {
return matchPattern(pattern).filter { _, kindsInRange in
Expand All @@ -27,7 +62,9 @@ extension File {
let matches = regex.matchesInString(contents, options: [], range: range)
return matches.map { match in
let tokensInRange = syntax.tokens.filter {
NSLocationInRange($0.offset, match.range)
NSLocationInRange($0.offset, match.range) ||
NSLocationInRange(match.range.location,
NSRange(location: $0.offset, length: $0.length))
}
let kindsInRange = tokensInRange.flatMap {
SyntaxKind(rawValue: $0.type)
Expand Down
12 changes: 11 additions & 1 deletion Source/SwiftLintFramework/Linter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ public struct Linter {
private let rules: [Rule]

public var styleViolations: [StyleViolation] {
return rules.flatMap { $0.validateFile(self.file) }
let regions = file.regions()
return rules.flatMap { rule in
return rule.validateFile(self.file).filter { styleViolation in
guard let violationRegion = regions.filter({
$0.start <= styleViolation.location && $0.end >= styleViolation.location
}).first else {
return true
}
return !violationRegion.disabledRuleIdentifiers.contains(rule.identifier)
}
}
}

public var ruleExamples: [RuleExample] {
Expand Down
22 changes: 12 additions & 10 deletions Source/SwiftLintFramework/Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import SourceKittenFramework

public struct Location: CustomStringConvertible, Equatable {
public struct Location: CustomStringConvertible, Comparable {
public let file: String?
public let line: Int?
public let character: Int?
Expand Down Expand Up @@ -38,18 +38,20 @@ public struct Location: CustomStringConvertible, Equatable {
}
}

// MARK: Equatable
// MARK: Comparable

/**
Returns true if `lhs` Location is equal to `rhs` Location.

:param: lhs Location to compare to `rhs`.
:param: rhs Location to compare to `lhs`.

:returns: True if `lhs` Location is equal to `rhs` Location.
*/
public func == (lhs: Location, rhs: Location) -> Bool {
return lhs.file == rhs.file &&
lhs.line == rhs.line &&
lhs.character == rhs.character
}

public func < (lhs: Location, rhs: Location) -> Bool {
if lhs.file != rhs.file {
return lhs.file < rhs.file
}
if lhs.line != rhs.line {
return lhs.line < rhs.line
}
return lhs.character < rhs.character
}
22 changes: 22 additions & 0 deletions Source/SwiftLintFramework/Region.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Region.swift
// SwiftLint
//
// Created by JP Simard on 8/29/15.
// Copyright © 2015 Realm. All rights reserved.
//

import Foundation
import SourceKittenFramework

public struct Region {
let start: Location
let end: Location
let disabledRuleIdentifiers: Set<String>

public init(start: Location, end: Location, disabledRuleIdentifiers: Set<String>) {
self.start = start
self.end = end
self.disabledRuleIdentifiers = disabledRuleIdentifiers
}
}
8 changes: 4 additions & 4 deletions Source/SwiftLintFrameworkTests/ASTRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,18 @@ class ASTRuleTests: XCTestCase {
}

func testTypeNamesVerifyRule() {
verifyRule(TypeNameRule().example, type: .NameFormat)
verifyRule(TypeNameRule(), type: .NameFormat)
}

func testVariableNamesVerifyRule() {
verifyRule(VariableNameRule().example, type: .NameFormat)
verifyRule(VariableNameRule(), type: .NameFormat)
}

func testNesting() {
verifyRule(NestingRule().example, type: .Nesting, commentDoesntViolate: false)
verifyRule(NestingRule(), type: .Nesting, commentDoesntViolate: false)
}

func testControlStatements() {
verifyRule(ControlStatementRule().example, type: .ControlStatement)
verifyRule(ControlStatementRule(), type: .ControlStatement)
}
}
14 changes: 7 additions & 7 deletions Source/SwiftLintFrameworkTests/StringRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,35 @@ class StringRuleTests: XCTestCase {
}

func testFileShouldntStartWithWhitespace() {
verifyRule(LeadingWhitespaceRule().example,
verifyRule(LeadingWhitespaceRule(),
type: .LeadingWhitespace,
commentDoesntViolate: false)
}

func testLinesShouldntContainTrailingWhitespace() {
verifyRule(TrailingWhitespaceRule().example,
verifyRule(TrailingWhitespaceRule(),
type: .TrailingWhitespace,
commentDoesntViolate: false)
}

func testLinesShouldContainReturnArrowWhitespace() {
verifyRule(ReturnArrowWhitespaceRule().example,
verifyRule(ReturnArrowWhitespaceRule(),
type: .ReturnArrowWhitespace)
}

func testForceCasting() {
verifyRule(ForceCastRule().example, type: .ForceCast)
verifyRule(ForceCastRule(), type: .ForceCast)
}

func testOperatorFunctionWhitespace() {
verifyRule(OperatorFunctionWhitespaceRule().example, type: .OperatorFunctionWhitespace)
verifyRule(OperatorFunctionWhitespaceRule(), type: .OperatorFunctionWhitespace)
}

func testTodoOrFIXME() {
verifyRule(TodoRule().example, type: .TODO)
verifyRule(TodoRule(), type: .TODO, commentDoesntViolate: false)
}

func testColon() {
verifyRule(ColonRule().example, type: .Colon)
verifyRule(ColonRule(), type: .Colon)
}
}
14 changes: 9 additions & 5 deletions Source/SwiftLintFrameworkTests/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ private func violations(string: String, _ type: StyleViolationType) -> [StyleVio
}

extension XCTestCase {
func verifyRule(rule: RuleExample,
func verifyRule(rule: Rule,
type: StyleViolationType,
commentDoesntViolate: Bool = true) {
XCTAssertEqual(rule.nonTriggeringExamples.flatMap({violations($0, type)}), [])
XCTAssertEqual(rule.triggeringExamples.flatMap({violations($0, type).map({$0.type})}),
Array(count: rule.triggeringExamples.count, repeatedValue: type))
let example = rule.example
XCTAssertEqual(example.nonTriggeringExamples.flatMap({violations($0, type)}), [])
XCTAssertEqual(example.triggeringExamples.flatMap({violations($0, type).map({$0.type})}),
Array(count: example.triggeringExamples.count, repeatedValue: type))

if commentDoesntViolate {
XCTAssertEqual(rule.triggeringExamples.flatMap({violations("// " + $0, type)}), [])
XCTAssertEqual(example.triggeringExamples.flatMap({violations("// " + $0, type)}), [])
}

let command = "// swiftlint:disable \(rule.identifier)\n"
XCTAssertEqual(example.triggeringExamples.flatMap({violations(command + $0, type)}), [])
}
}
Loading

0 comments on commit 56fe7b0

Please sign in to comment.