Skip to content

Commit

Permalink
Add capture_group option to custom_rules
Browse files Browse the repository at this point in the history
This option allows for more fine-grained placement of the location
marker for code violating a custom rule, e.g.:

```swift
print("Hello world.")
             ^~~~~
```

for this `.swiftlint.yml`:

```yaml
custom_rules:
  world_preceded_by_hello:
    name: "World Preceded by Hello"
    included: ".+\\.swift"
    message: "The word World predeced by the word hello should be capitalised."
    severity: warning
    regex: "(?i:hello)\\s+(world)"
    match_kinds: [string]
    capture_group: 1
```
  • Loading branch information
pyrtsa authored and jpsim committed Jan 5, 2020
1 parent 8fb9006 commit 805b9ab
Show file tree
Hide file tree
Showing 6 changed files with 31 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
full configuration details regardless of your terminal width.
[Optional Endeavors](https://github.com/optionalendeavors)

* Add `capture_group` option to `custom_rules` for more fine-grained placement
of the location marker for violating code.
[pyrtsa](https://github.com/pyrtsa)

#### Bug Fixes

* None.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ custom_rules:
excluded: ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
name: "Pirates Beat Ninjas" # rule name. optional.
regex: "([n,N]inja)" # matching pattern
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
match_kinds: # SyntaxKinds to match. optional.
- comment
- identifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ extension SwiftLintFile {
return matchesAndTokens(matching: pattern, range: range).map { ($0.0.range, $0.1) }
}

internal func match(pattern: String, range: NSRange? = nil) -> [(NSRange, [SyntaxKind])] {
internal func match(pattern: String, range: NSRange? = nil, captureGroup: Int = 0) -> [(NSRange, [SyntaxKind])] {
return matchesAndSyntaxKinds(matching: pattern, range: range).map { textCheckingResult, syntaxKinds in
(textCheckingResult.range, syntaxKinds)
(textCheckingResult.range(at: captureGroup), syntaxKinds)
}
}

Expand Down Expand Up @@ -208,8 +208,9 @@ extension SwiftLintFile {
*/
internal func match(pattern: String,
excludingSyntaxKinds syntaxKinds: Set<SyntaxKind>,
range: NSRange? = nil) -> [NSRange] {
return match(pattern: pattern, range: range)
range: NSRange? = nil,
captureGroup: Int = 0) -> [NSRange] {
return match(pattern: pattern, range: range, captureGroup: captureGroup)
.filter { syntaxKinds.isDisjoint(with: $0.1) }
.map { $0.0 }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public struct RegexConfiguration: RuleConfiguration, Hashable, CacheDescriptionP
public var excluded: NSRegularExpression?
public var matchKinds = SyntaxKind.allKinds
public var severityConfiguration = SeverityConfiguration(.warning)
public var captureGroup: Int = 0

public var severity: ViolationSeverity {
return severityConfiguration.severity
Expand Down Expand Up @@ -74,6 +75,12 @@ public struct RegexConfiguration: RuleConfiguration, Hashable, CacheDescriptionP
if let severityString = configurationDict["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
if let captureGroup = configurationDict["capture_group"] as? Int {
guard (0 ... regex.numberOfCaptureGroups).contains(captureGroup) else {
throw ConfigurationError.unknownConfiguration
}
self.captureGroup = captureGroup
}
}

public func hash(into hasher: inout Hasher) {
Expand Down
3 changes: 2 additions & 1 deletion Source/SwiftLintFramework/Rules/Style/CustomRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ public struct CustomRules: Rule, ConfigurationProviderRule, CacheDescriptionProv

return configurations.flatMap { configuration -> [StyleViolation] in
let pattern = configuration.regex.pattern
let captureGroup = configuration.captureGroup
let excludingKinds = SyntaxKind.allKinds.subtracting(configuration.matchKinds)
return file.match(pattern: pattern, excludingSyntaxKinds: excludingKinds).map({
return file.match(pattern: pattern, excludingSyntaxKinds: excludingKinds, captureGroup: captureGroup).map({
StyleViolation(ruleDescription: configuration.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location),
Expand Down
15 changes: 12 additions & 3 deletions Tests/SwiftLintFrameworkTests/CustomRulesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,18 @@ class CustomRulesTests: XCTestCase {
XCTAssertEqual(violations.count, 0)
}

private func getCustomRules(_ extraConfig: [String: String] = [:]) -> (RegexConfiguration, CustomRules) {
var config = ["regex": "pattern",
"match_kinds": "comment"]
func testCustomRulesCaptureGroup() {
let (_, customRules) = getCustomRules(["regex": #"\ba\s+(\w+)"#,
"capture_group": 1])
let violations = customRules.validate(file: getTestTextFile())
XCTAssertEqual(violations.count, 1)
XCTAssertEqual(violations[0].location.line, 2)
XCTAssertEqual(violations[0].location.character, 6)
}

private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (RegexConfiguration, CustomRules) {
var config: [String: Any] = ["regex": "pattern",
"match_kinds": "comment"]
extraConfig.forEach { config[$0] = $1 }

var regexConfig = RegexConfiguration(identifier: "custom")
Expand Down

0 comments on commit 805b9ab

Please sign in to comment.