Add support for Swift 5.5's concurrency features (#126)

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.
This commit is contained in:
John Sundell 2021-06-14 13:03:57 +02:00 committed by GitHub
parent 7f87f191b6
commit 7f4df436eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 297 additions and 13 deletions

View File

@ -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> = [
@ -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 {
@ -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
@ -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
@ -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
}
@ -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
}
}

View File

@ -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")
])
}
}

View File

@ -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("()")
])
}
}