Skip to content

Commit

Permalink
Use SwiftSyntax visitor to parse commands (realm#3872)
Browse files Browse the repository at this point in the history
* Cache SwiftSyntax syntax trees
* Use SwiftSyntax visitor to parse commands
* Update changelog entry
* Cache commands
  • Loading branch information
jpsim authored and coffmark committed Apr 11, 2022
1 parent 253edc3 commit b7450b0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 23 deletions.
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@

#### Experimental

* The `force_cast` rule has been updated to use SwiftSyntax to find violations
instead of SourceKit. Please report any problems you encounter by opening a
GitHub issue. If this is successful, more rules may use Swift Syntax in the
future.
* The `force_cast` rule and the comment command parsing mechanism have been
updated to use SwiftSyntax instead of SourceKit. Please report any problems
you encounter by opening a GitHub issue. If this is successful, more rules may
use Swift Syntax in the future.
[JP Simard](https://github.com/jpsim)

#### Enhancements
Expand Down
23 changes: 23 additions & 0 deletions Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax

private typealias FileCacheKey = UUID
private var responseCache = Cache({ file -> [String: SourceKitRepresentable]? in
Expand All @@ -24,6 +25,20 @@ private var structureDictionaryCache = Cache({ file in
return structureCache.get(file).map { SourceKittenDictionary($0.dictionary) }
})

private var syntaxTreeCache = Cache({ file in
return try? SyntaxParser.parse(source: file.contents)
})

private var commandsCache = Cache({ file -> [Command] in
guard let tree = syntaxTreeCache.get(file) else {
return []
}
let locationConverter = SourceLocationConverter(file: file.path ?? "<nopath>", tree: tree)
let visitor = CommandVisitor(locationConverter: locationConverter)
visitor.walk(tree)
return visitor.commands
})

private var syntaxMapCache = Cache({ file in
responseCache.get(file).map { SwiftLintSyntaxMap(value: SyntaxMap(sourceKitResponse: $0)) }
})
Expand Down Expand Up @@ -177,6 +192,10 @@ extension SwiftLintFile {
return syntaxMap
}

internal var syntaxTree: SourceFileSyntax? { syntaxTreeCache.get(self) }

internal var commands: [Command] { commandsCache.get(self) }

internal var syntaxTokensByLines: [[SwiftLintSyntaxToken]] {
guard let syntaxTokensByLines = syntaxTokensByLinesCache.get(self) else {
if let handler = assertHandler {
Expand Down Expand Up @@ -208,6 +227,8 @@ extension SwiftLintFile {
syntaxMapCache.invalidate(self)
syntaxTokensByLinesCache.invalidate(self)
syntaxKindsByLinesCache.invalidate(self)
syntaxTreeCache.invalidate(self)
commandsCache.invalidate(self)
}

internal static func clearCaches() {
Expand All @@ -219,5 +240,7 @@ extension SwiftLintFile {
syntaxMapCache.clear()
syntaxTokensByLinesCache.clear()
syntaxKindsByLinesCache.clear()
syntaxTreeCache.clear()
commandsCache.clear()
}
}
30 changes: 12 additions & 18 deletions Source/SwiftLintFramework/Extensions/SwiftLintFile+Regex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,19 @@ extension SwiftLintFile {
}

internal func commands(in range: NSRange? = nil) -> [Command] {
if sourcekitdFailed {
return []
}
let contents = stringView
let range = range ?? stringView.range
let pattern = "swiftlint:(enable|disable)(:previous|:this|:next)?\\ [^\\n]+"
return match(pattern: pattern, range: range).filter { match in
return Set(match.1).isSubset(of: [.comment, .commentURL])
}.compactMap { match -> Command? in
let range = match.0
let actionString = contents.substring(with: range)
guard let lineAndCharacter = stringView.lineAndCharacter(forCharacterOffset: NSMaxRange(range))
else { return nil }
return Command(actionString: actionString,
line: lineAndCharacter.line,
character: lineAndCharacter.character)
}.flatMap { command in
return command.expand()
guard let range = range else {
return commands
.flatMap { $0.expand() }
}

let rangeStart = Location(file: self, characterOffset: range.location)
let rangeEnd = Location(file: self, characterOffset: NSMaxRange(range))
return commands
.filter { command in
let commandLocation = Location(file: path, line: command.line, character: command.character)
return rangeStart <= commandLocation && commandLocation <= rangeEnd
}
.flatMap { $0.expand() }
}

fileprivate func endOf(next command: Command?) -> Location {
Expand Down
51 changes: 51 additions & 0 deletions Source/SwiftLintFramework/Helpers/CommandVisitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import SwiftSyntax

// MARK: - CommandVisitor

/// Visits the source syntax tree to collect all SwiftLint-style comment commands.
final class CommandVisitor: SyntaxVisitor {
private(set) var commands: [Command] = []
let locationConverter: SourceLocationConverter

init(locationConverter: SourceLocationConverter) {
self.locationConverter = locationConverter
super.init()
}

override func visitPost(_ node: TokenSyntax) {
let leadingCommands = node.leadingTrivia.commands(offset: node.position,
locationConverter: locationConverter)
let trailingCommands = node.trailingTrivia.commands(offset: node.endPositionBeforeTrailingTrivia,
locationConverter: locationConverter)
self.commands.append(contentsOf: leadingCommands + trailingCommands)
}
}

// MARK: - Private Helpers

private extension Trivia {
func commands(offset: AbsolutePosition, locationConverter: SourceLocationConverter) -> [Command] {
var triviaOffset = SourceLength.zero
var results: [Command] = []
for trivia in self {
triviaOffset += trivia.sourceLength
switch trivia {
case .lineComment(let comment), .blockComment(let comment):
if
let lower = comment.range(of: "swiftlint:")?.lowerBound,
case let actionString = String(comment[lower...]),
case let end = locationConverter.location(for: offset + triviaOffset),
let line = end.line,
let column = end.column,
let command = Command(actionString: actionString, line: line, character: column)
{
results.append(command)
}
default:
break
}
}

return results
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct ForceCastRule: ConfigurationProviderRule, AutomaticTestableRule {
)

public func validate(file: SwiftLintFile) -> [StyleViolation] {
guard let tree = try? SyntaxParser.parse(source: file.contents) else {
guard let tree = file.syntaxTree else {
warnSyntaxParserFailureOnce()
return []
}
Expand Down

0 comments on commit b7450b0

Please sign in to comment.