From be35cef1eadbf95918b7dc049af8d730ef3ce25f Mon Sep 17 00:00:00 2001 From: John Sundell Date: Mon, 27 Aug 2018 00:10:08 +0200 Subject: [PATCH] Correctly handle static methods called on a generic type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch fixes syntax highlighting for the following scenario: ``` Type.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. --- .../Equatable/Equatable+AnyOf.swift | 2 +- Sources/Splash/Grammar/SwiftGrammar.swift | 26 ++++++++++++++----- Sources/Splash/Tokenizing/Segment.swift | 12 +++++---- Sources/Splash/Tokenizing/Tokenizer.swift | 4 +++ .../SplashTests/Tests/FunctionCallTests.swift | 16 +++++++++++- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Sources/Splash/Extensions/Equatable/Equatable+AnyOf.swift b/Sources/Splash/Extensions/Equatable/Equatable+AnyOf.swift index 7a1c7a3..ab6ba73 100644 --- a/Sources/Splash/Extensions/Equatable/Equatable+AnyOf.swift +++ b/Sources/Splash/Extensions/Equatable/Equatable+AnyOf.swift @@ -11,7 +11,7 @@ extension Equatable { return candidates.contains(self) } - func isAny(of candidates: [Self]) -> Bool { + func isAny(of candidates: S) -> Bool where S.Element == Self { return candidates.contains(self) } } diff --git a/Sources/Splash/Grammar/SwiftGrammar.swift b/Sources/Splash/Grammar/SwiftGrammar.swift index a6d4cad..e4266f3 100644 --- a/Sources/Splash/Grammar/SwiftGrammar.swift +++ b/Sources/Splash/Grammar/SwiftGrammar.swift @@ -214,12 +214,14 @@ private extension SwiftGrammar { struct TypeRule: SyntaxRule { var tokenType: TokenType { return .type } + private let declarationKeywords: Set = [ + "class", "struct", "enum", "func", + "protocol", "typealias", "import" + ] + func matches(_ segment: Segment) -> Bool { // Types should not be highlighted when declared if let previousToken = segment.tokens.previous { - let declarationKeywords = ["class", "struct", "enum", - "protocol", "typealias", "import"] - guard !previousToken.isAny(of: declarationKeywords) else { return false } @@ -234,9 +236,21 @@ private extension SwiftGrammar { } // In a generic declaration, only highlight constraints - if !segment.tokens.onSameLine.contains(anyOf: "var", "let") { - if !segment.tokens.containsBalancedOccurrences(of: "<", and: ">") { - return !segment.tokens.previous.isAny(of: "<", ",") + if segment.tokens.previous.isAny(of: "<", ",") { + // Since the declaration might be on another line, we have to walk + // 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 + } } } diff --git a/Sources/Splash/Tokenizing/Segment.swift b/Sources/Splash/Tokenizing/Segment.swift index f9850fc..0d156fb 100644 --- a/Sources/Splash/Tokenizing/Segment.swift +++ b/Sources/Splash/Tokenizing/Segment.swift @@ -24,16 +24,18 @@ public struct Segment { public extension Segment { /// A collection of tokens included in a code segment 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 - var counts: [String : Int] + public var counts: [String : Int] /// 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) - var previous: String? + public var previous: String? /// The current token which is currently being evaluated - var current: String + public var current: String /// Any upcoming token that will follow the current one - var next: String? + public var next: String? } } diff --git a/Sources/Splash/Tokenizing/Tokenizer.swift b/Sources/Splash/Tokenizing/Tokenizer.swift index 6560d97..52ebffd 100644 --- a/Sources/Splash/Tokenizing/Tokenizer.swift +++ b/Sources/Splash/Tokenizing/Tokenizer.swift @@ -43,6 +43,7 @@ private extension Tokenizer { private let delimiters: CharacterSet private var index: String.Index? private var tokenCounts = [String : Int]() + private var allTokens = [String]() private var lineTokens = [String]() 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 { let tokens = Segment.Tokens( + all: allTokens, counts: tokenCounts, onSameLine: lineTokens, previous: segments.current?.tokens.current, @@ -155,6 +157,8 @@ private extension Tokenizer { count += 1 tokenCounts[segment.tokens.current] = count + allTokens.append(segment.tokens.current) + if segment.isLastOnLine { lineTokens = [] } else { diff --git a/Tests/SplashTests/Tests/FunctionCallTests.swift b/Tests/SplashTests/Tests/FunctionCallTests.swift index f47ac4c..2060c74 100644 --- a/Tests/SplashTests/Tests/FunctionCallTests.swift +++ b/Tests/SplashTests/Tests/FunctionCallTests.swift @@ -75,6 +75,19 @@ final class FunctionCallTests: SyntaxHighlighterTestCase { ]) } + func testCallingStaticMethodOnGenericType() { + let components = highlighter.highlight("Array.call()") + + XCTAssertEqual(components, [ + .token("Array", .type), + .plainText("<"), + .token("String", .type), + .plainText(">."), + .token("call", .call), + .plainText("()") + ]) + } + func testAllTestsRunOnLinux() { XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests)) } @@ -87,7 +100,8 @@ extension FunctionCallTests { ("testImplicitInitializerCall", testImplicitInitializerCall), ("testExplicitInitializerCall", testExplicitInitializerCall), ("testAccessingPropertyAfterFunctionCallWithoutArguments", testAccessingPropertyAfterFunctionCallWithoutArguments), - ("testAccessingPropertyAfterFunctionCallWithArguments", testAccessingPropertyAfterFunctionCallWithArguments) + ("testAccessingPropertyAfterFunctionCallWithArguments", testAccessingPropertyAfterFunctionCallWithArguments), + ("testCallingStaticMethodOnGenericType", testCallingStaticMethodOnGenericType) ] } }