Correctly handle static methods called on a generic type

This patch fixes syntax highlighting for the following scenario:

```
Type<GenericType>.call()
```

Highlighting generic types is especially tricky, since we want them to
be highlighted when appearing at the call site, like above - but we don’t
want to highlight them when they are being declared. Hopefully with this
fix all/most edge cases are covered.
This commit is contained in:
John Sundell 2018-08-27 00:10:08 +02:00
parent aea685e8b4
commit be35cef1ea
5 changed files with 47 additions and 13 deletions

View File

@ -11,7 +11,7 @@ extension Equatable {
return candidates.contains(self) return candidates.contains(self)
} }
func isAny(of candidates: [Self]) -> Bool { func isAny<S: Sequence>(of candidates: S) -> Bool where S.Element == Self {
return candidates.contains(self) return candidates.contains(self)
} }
} }

View File

@ -214,12 +214,14 @@ private extension SwiftGrammar {
struct TypeRule: SyntaxRule { struct TypeRule: SyntaxRule {
var tokenType: TokenType { return .type } var tokenType: TokenType { return .type }
private let declarationKeywords: Set<String> = [
"class", "struct", "enum", "func",
"protocol", "typealias", "import"
]
func matches(_ segment: Segment) -> Bool { func matches(_ segment: Segment) -> Bool {
// Types should not be highlighted when declared // Types should not be highlighted when declared
if let previousToken = segment.tokens.previous { if let previousToken = segment.tokens.previous {
let declarationKeywords = ["class", "struct", "enum",
"protocol", "typealias", "import"]
guard !previousToken.isAny(of: declarationKeywords) else { guard !previousToken.isAny(of: declarationKeywords) else {
return false return false
} }
@ -234,9 +236,21 @@ private extension SwiftGrammar {
} }
// In a generic declaration, only highlight constraints // In a generic declaration, only highlight constraints
if !segment.tokens.onSameLine.contains(anyOf: "var", "let") { if segment.tokens.previous.isAny(of: "<", ",") {
if !segment.tokens.containsBalancedOccurrences(of: "<", and: ">") { // Since the declaration might be on another line, we have to walk
return !segment.tokens.previous.isAny(of: "<", ",") // backwards through all tokens until we've found enough information.
for token in segment.tokens.all.reversed() {
guard !declarationKeywords.contains(token) else {
return false
}
guard !keywords.contains(token) else {
return true
}
if token.isAny(of: ">", "=", "==", "(") {
return true
}
} }
} }

View File

@ -24,16 +24,18 @@ public struct Segment {
public extension Segment { public extension Segment {
/// A collection of tokens included in a code segment /// A collection of tokens included in a code segment
struct Tokens { struct Tokens {
/// All tokens that have been found so far (excluding the current one)
public var all: [String]
/// The number of times a given token has been found up until this point /// The number of times a given token has been found up until this point
var counts: [String : Int] public var counts: [String : Int]
/// The tokens that were previously found on the same line as the current one /// The tokens that were previously found on the same line as the current one
var onSameLine: [String] public var onSameLine: [String]
/// The token that was previously found (may be on a different line) /// The token that was previously found (may be on a different line)
var previous: String? public var previous: String?
/// The current token which is currently being evaluated /// The current token which is currently being evaluated
var current: String public var current: String
/// Any upcoming token that will follow the current one /// Any upcoming token that will follow the current one
var next: String? public var next: String?
} }
} }

View File

@ -43,6 +43,7 @@ private extension Tokenizer {
private let delimiters: CharacterSet private let delimiters: CharacterSet
private var index: String.Index? private var index: String.Index?
private var tokenCounts = [String : Int]() private var tokenCounts = [String : Int]()
private var allTokens = [String]()
private var lineTokens = [String]() private var lineTokens = [String]()
private var segments: (current: Segment?, previous: Segment?) private var segments: (current: Segment?, previous: Segment?)
@ -132,6 +133,7 @@ private extension Tokenizer {
private func makeSegment(with component: Component, at index: String.Index) -> Segment { private func makeSegment(with component: Component, at index: String.Index) -> Segment {
let tokens = Segment.Tokens( let tokens = Segment.Tokens(
all: allTokens,
counts: tokenCounts, counts: tokenCounts,
onSameLine: lineTokens, onSameLine: lineTokens,
previous: segments.current?.tokens.current, previous: segments.current?.tokens.current,
@ -155,6 +157,8 @@ private extension Tokenizer {
count += 1 count += 1
tokenCounts[segment.tokens.current] = count tokenCounts[segment.tokens.current] = count
allTokens.append(segment.tokens.current)
if segment.isLastOnLine { if segment.isLastOnLine {
lineTokens = [] lineTokens = []
} else { } else {

View File

@ -75,6 +75,19 @@ final class FunctionCallTests: SyntaxHighlighterTestCase {
]) ])
} }
func testCallingStaticMethodOnGenericType() {
let components = highlighter.highlight("Array<String>.call()")
XCTAssertEqual(components, [
.token("Array", .type),
.plainText("<"),
.token("String", .type),
.plainText(">."),
.token("call", .call),
.plainText("()")
])
}
func testAllTestsRunOnLinux() { func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests)) XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
} }
@ -87,7 +100,8 @@ extension FunctionCallTests {
("testImplicitInitializerCall", testImplicitInitializerCall), ("testImplicitInitializerCall", testImplicitInitializerCall),
("testExplicitInitializerCall", testExplicitInitializerCall), ("testExplicitInitializerCall", testExplicitInitializerCall),
("testAccessingPropertyAfterFunctionCallWithoutArguments", testAccessingPropertyAfterFunctionCallWithoutArguments), ("testAccessingPropertyAfterFunctionCallWithoutArguments", testAccessingPropertyAfterFunctionCallWithoutArguments),
("testAccessingPropertyAfterFunctionCallWithArguments", testAccessingPropertyAfterFunctionCallWithArguments) ("testAccessingPropertyAfterFunctionCallWithArguments", testAccessingPropertyAfterFunctionCallWithArguments),
("testCallingStaticMethodOnGenericType", testCallingStaticMethodOnGenericType)
] ]
} }
} }