Skip to content

Commit

Permalink
Add support for Swift 5.5's concurrency features (#126)
Browse files Browse the repository at this point in the history
This patch adds syntax highlighting support for the new concurrency keywords
introduced in Swift 5.5 - `actor`, `async`, and `await`. It also includes supporting
changes to make sure that usages of these new features/keywords are highlighted
correctly, and to protect against regressions within existing Splash-highlighted code.
  • Loading branch information
JohnSundell authored Jun 14, 2021
1 parent 7f87f19 commit 7f4df43
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 13 deletions.
43 changes: 30 additions & 13 deletions Sources/Splash/Grammar/SwiftGrammar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ private extension SwiftGrammar {
"lazy", "subscript", "defer", "inout", "while",
"continue", "fallthrough", "repeat", "indirect",
"deinit", "is", "#file", "#line", "#function",
"dynamic", "some", "#available", "convenience", "unowned"
"dynamic", "some", "#available", "convenience", "unowned",
"async", "await", "actor"
] as Set<String>).union(accessControlKeywords)

static let accessControlKeywords: Set<String> = [
Expand All @@ -91,7 +92,8 @@ private extension SwiftGrammar {
static let declarationKeywords: Set<String> = [
"class", "struct", "enum", "func",
"protocol", "typealias", "import",
"associatedtype", "subscript", "init"
"associatedtype", "subscript", "init",
"actor"
]

struct PreprocessingRule: SyntaxRule {
Expand Down Expand Up @@ -252,6 +254,7 @@ private extension SwiftGrammar {
keywordsToAvoid.remove("throw")
keywordsToAvoid.remove("if")
keywordsToAvoid.remove("in")
keywordsToAvoid.remove("await")
self.keywordsToAvoid = keywordsToAvoid
var callLikeKeywords = accessControlKeywords
Expand Down Expand Up @@ -351,14 +354,32 @@ private extension SwiftGrammar {
}
}
if let previousToken = segment.tokens.previous {
// Don't highlight variables with the same name as a keyword
// when used in optional binding, such as if let, guard let:
if !segment.tokens.onSameLine.isEmpty, segment.tokens.current != "self" {
guard !previousToken.isAny(of: "let", "var") else {
if segment.trailingWhitespace == nil {
if !segment.tokens.current.isAny(of: "self", "super") {
guard segment.tokens.next != "." else {
return false
}
}
}
if let previousToken = segment.tokens.previous {
if !segment.tokens.onSameLine.isEmpty {
// Don't highlight variables with the same name as a keyword
// when used in optional binding, such as if let, guard let:
if segment.tokens.current != "self" {
guard !previousToken.isAny(of: "let", "var") else {
return false
}
if segment.tokens.current == "actor" {
if accessControlKeywords.contains(previousToken) {
return true
}
return previousToken.first == "@"
}
}
}
if !declarationKeywords.contains(segment.tokens.current) {
// Highlight the '(set)' part of setter access modifiers
Expand All @@ -376,7 +397,7 @@ private extension SwiftGrammar {
}
// Don't highlight most keywords when used as a parameter label
if !segment.tokens.current.isAny(of: "self", "let", "var", "true", "false", "inout", "nil", "try") {
if !segment.tokens.current.isAny(of: "self", "let", "var", "true", "false", "inout", "nil", "try", "actor") {
guard !previousToken.isAny(of: "(", ",", ">(") else {
return false
}
Expand Down Expand Up @@ -451,11 +472,7 @@ private extension SwiftGrammar {
return !foundOpeningBracket
}

guard !keywords.contains(token) else {
return true
}

if token.isAny(of: "=", "==", "(") {
if token.isAny(of: "=", "==", "(", "_", "@escaping") {
return true
}
}
Expand Down
179 changes: 179 additions & 0 deletions Tests/SplashTests/Tests/DeclarationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1326,4 +1326,183 @@ final class DeclarationTests: SyntaxHighlighterTestCase {
.plainText("}")
])
}

func testNonThrowingAsyncFunctionDeclaration() {
let components = highlighter.highlight("func test() async {}")

XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("test()"),
.whitespace(" "),
.token("async", .keyword),
.whitespace(" "),
.plainText("{}")
])
}

func testNonThrowingAsyncFunctionDeclarationWithReturnValue() {
let components = highlighter.highlight("func test() async -> Int { 0 }")

XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("test()"),
.whitespace(" "),
.token("async", .keyword),
.whitespace(" "),
.plainText("->"),
.whitespace(" "),
.token("Int", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("0", .number),
.whitespace(" "),
.plainText("}")
])
}

func testThrowingAsyncFunctionDeclaration() {
let components = highlighter.highlight("func test() async throws {}")

XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("test()"),
.whitespace(" "),
.token("async", .keyword),
.whitespace(" "),
.token("throws", .keyword),
.whitespace(" "),
.plainText("{}")
])
}

func testDeclaringGenericFunctionNamedAwait() {
let components = highlighter.highlight("""
func await<T>(_ function: () -> T) {}
""")

XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("await<T>("),
.token("_", .keyword),
.whitespace(" "),
.plainText("function:"),
.whitespace(" "),
.plainText("()"),
.whitespace(" "),
.plainText("->"),
.whitespace(" "),
.token("T", .type),
.plainText(")"),
.whitespace(" "),
.plainText("{}")
])
}

func testActorDeclaration() {
let components = highlighter.highlight("""
actor MyActor {
var value = 0
func action() {}
}
""")

XCTAssertEqual(components, [
.token("actor", .keyword),
.whitespace(" "),
.plainText("MyActor"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n "),
.token("var", .keyword),
.whitespace(" "),
.plainText("value"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("0", .number),
.whitespace("\n "),
.token("func", .keyword),
.whitespace(" "),
.plainText("action()"),
.whitespace(" "),
.plainText("{}"),
.whitespace("\n"),
.plainText("}")
])
}

func testPublicActorDeclaration() {
let components = highlighter.highlight("public actor MyActor {}")

XCTAssertEqual(components, [
.token("public", .keyword),
.whitespace(" "),
.token("actor", .keyword),
.whitespace(" "),
.plainText("MyActor"),
.whitespace(" "),
.plainText("{}")
])
}

func testDeclaringAndMutatingLocalVariableNamedActor() {
let components = highlighter.highlight("""
let actor = Actor()
actor.position = scene.center
""")

XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("actor"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("Actor", .type),
.plainText("()"),
.whitespace("\n"),
.plainText("actor."),
.token("position", .property),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.plainText("scene."),
.token("center", .property)
])
}

func testPassingAndReferencingLocalVariableNamedActor() {
let components = highlighter.highlight("""
prepare(actor: actor)
scene.add(actor)
latestActor = actor
return actor
""")

XCTAssertEqual(components, [
.token("prepare", .call),
.plainText("(actor:"),
.whitespace(" "),
.plainText("actor)"),
.whitespace("\n"),
.plainText("scene."),
.token("add", .call),
.plainText("(actor)"),
.whitespace("\n"),
.plainText("latestActor"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.plainText("actor"),
.whitespace("\n"),
.token("return", .keyword),
.whitespace(" "),
.plainText("actor")
])
}
}
88 changes: 88 additions & 0 deletions Tests/SplashTests/Tests/StatementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,92 @@ final class StatementTests: SyntaxHighlighterTestCase {
.plainText("queryItems")
])
}

func testAwaitingFunctionCall() {
let components = highlighter.highlight("let result = await call()")

XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("result"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("await", .keyword),
.whitespace(" "),
.token("call", .call),
.plainText("()")
])
}

func testAwaitingVariable() {
let components = highlighter.highlight("let result = await value")

XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("result"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("await", .keyword),
.whitespace(" "),
.plainText("value")
])
}

func testAwaitingAsyncSequenceElement() {
let components = highlighter.highlight("for await value in sequence {}")

XCTAssertEqual(components, [
.token("for", .keyword),
.whitespace(" "),
.token("await", .keyword),
.whitespace(" "),
.plainText("value"),
.whitespace(" "),
.token("in", .keyword),
.whitespace(" "),
.plainText("sequence"),
.whitespace(" "),
.plainText("{}")
])
}

func testAwaitingThrowingAsyncSequenceElement() {
let components = highlighter.highlight("for try await value in sequence {}")

XCTAssertEqual(components, [
.token("for", .keyword),
.whitespace(" "),
.token("try", .keyword),
.whitespace(" "),
.token("await", .keyword),
.whitespace(" "),
.plainText("value"),
.whitespace(" "),
.token("in", .keyword),
.whitespace(" "),
.plainText("sequence"),
.whitespace(" "),
.plainText("{}")
])
}

func testAsyncLetExpression() {
let components = highlighter.highlight("async let result = call()")

XCTAssertEqual(components, [
.token("async", .keyword),
.whitespace(" "),
.token("let", .keyword),
.whitespace(" "),
.plainText("result"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("call", .call),
.plainText("()")
])
}
}

0 comments on commit 7f4df43

Please sign in to comment.