Skip to content

Commit bef63d8

Browse files
authored
Merge pull request #1916 from StevenWong12/warp_assert_classification
Add `assertClassification` for `ClassificationTests`
2 parents 2e8bff5 + 889fd0b commit bef63d8

File tree

3 files changed

+189
-165
lines changed

3 files changed

+189
-165
lines changed

Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public func assertIncrementalParse(
125125
}
126126
}
127127

128-
fileprivate func byteSourceRange(for substring: String, in sourceString: String, after: String.Index) -> ByteSourceRange? {
128+
public func byteSourceRange(for substring: String, in sourceString: String, after: String.Index) -> ByteSourceRange? {
129129
if let range = sourceString[after...].range(of: substring) {
130130
return ByteSourceRange(
131131
offset: sourceString.utf8.distance(from: sourceString.startIndex, to: range.lowerBound),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import XCTest
14+
import SwiftIDEUtils
15+
import SwiftParser
16+
import SwiftSyntax
17+
import _SwiftSyntaxTestSupport
18+
19+
/// Parse `source` and checks its `classifications` is the same as `expected`.
20+
///
21+
/// The `expected` classifications should only contain classifications that are not `none`. All uncovered ranges are expected to have no classification.
22+
///
23+
/// - Parameters:
24+
/// - range: An optional parameter to specify the ``ByteSourceRange`` in `source` that we want to test the `classifications` in.
25+
/// - expected: The element order should respect to the order of `ClassificationSpec.source` in `source`.
26+
func assertClassification(
27+
_ source: String,
28+
in range: ByteSourceRange? = nil,
29+
expected: [ClassificationSpec],
30+
file: StaticString = #file,
31+
line: UInt = #line
32+
) {
33+
let tree = Parser.parse(source: source)
34+
35+
var classifications: Array<SyntaxClassifiedRange>
36+
if let range {
37+
classifications = Array(tree.classifications(in: range))
38+
} else {
39+
classifications = Array(tree.classifications)
40+
}
41+
classifications = classifications.filter { $0.kind != .none }
42+
43+
if expected.count != classifications.count {
44+
XCTFail("Expected \(expected.count) re-used nodes but received \(classifications.count)", file: file, line: line)
45+
}
46+
47+
var lastRangeUpperBound = source.startIndex
48+
for (classification, spec) in zip(classifications, expected) {
49+
guard let range = byteSourceRange(for: spec.source, in: source, after: lastRangeUpperBound) else {
50+
XCTFail("Fail to find string in original source,", file: spec.file, line: spec.line)
51+
continue
52+
}
53+
54+
XCTAssertEqual(
55+
range,
56+
classification.range,
57+
"""
58+
Expected \(range) but received \(classification.range)
59+
""",
60+
file: spec.file,
61+
line: spec.line
62+
)
63+
64+
XCTAssertEqual(
65+
spec.kind,
66+
classification.kind,
67+
"""
68+
Expected \(spec.kind) syntax classification kind but received \(classification.kind)
69+
""",
70+
file: spec.file,
71+
line: spec.line
72+
)
73+
74+
lastRangeUpperBound = source.index(source.startIndex, offsetBy: range.endOffset)
75+
}
76+
}
77+
78+
/// An abstract data structure to describe a source code snippet and its ``SyntaxClassification``.
79+
public struct ClassificationSpec {
80+
/// Source code without any ``Trivia``
81+
let source: String
82+
/// The ``SyntaxClassification`` of the source code,
83+
let kind: SyntaxClassification
84+
/// The file and line at which this ``ClassificationSpec`` was created, so that assertion failures can be reported at its location.
85+
let file: StaticString
86+
let line: UInt
87+
88+
public init(
89+
source: String,
90+
kind: SyntaxClassification,
91+
file: StaticString = #file,
92+
line: UInt = #line
93+
) {
94+
self.source = source
95+
self.kind = kind
96+
self.file = file
97+
self.line = line
98+
}
99+
}

Tests/SwiftIDEUtilsTest/ClassificationTests.swift

Lines changed: 89 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -19,181 +19,106 @@ import _SwiftSyntaxTestSupport
1919
public class ClassificationTests: XCTestCase {
2020

2121
public func testClassification() {
22-
let source = """
22+
assertClassification(
23+
"""
2324
// blah.
2425
let x/*yo*/ = 0
26+
""",
27+
expected: [
28+
ClassificationSpec(source: "// blah.", kind: .lineComment),
29+
ClassificationSpec(source: "let", kind: .keyword),
30+
ClassificationSpec(source: "x", kind: .identifier),
31+
ClassificationSpec(source: "/*yo*/", kind: .blockComment),
32+
ClassificationSpec(source: "0", kind: .integerLiteral),
33+
]
34+
)
35+
36+
assertClassification(
37+
"x/*yo*/ ",
38+
in: ByteSourceRange(offset: 1, length: 6),
39+
expected: [
40+
ClassificationSpec(source: "x", kind: .identifier),
41+
ClassificationSpec(source: "/*yo*/", kind: .blockComment),
42+
]
43+
)
44+
}
45+
46+
public func testClassificationInCertainRange() {
47+
assertClassification(
2548
"""
26-
let tree = Parser.parse(source: source)
27-
do {
28-
let classif = Array(tree.classifications)
29-
XCTAssertEqual(classif.count, 8)
30-
guard classif.count == 8 else {
31-
return
32-
}
33-
XCTAssertEqual(classif[0].kind, .lineComment)
34-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 8))
35-
XCTAssertEqual(classif[1].kind, .none)
36-
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 8, length: 1))
37-
XCTAssertEqual(classif[2].kind, .keyword)
38-
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 9, length: 3))
39-
XCTAssertEqual(classif[3].kind, .none)
40-
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 12, length: 1))
41-
XCTAssertEqual(classif[4].kind, .identifier)
42-
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 13, length: 1))
43-
XCTAssertEqual(classif[5].kind, .blockComment)
44-
XCTAssertEqual(classif[5].range, ByteSourceRange(offset: 14, length: 6))
45-
XCTAssertEqual(classif[6].kind, .none)
46-
XCTAssertEqual(classif[6].range, ByteSourceRange(offset: 20, length: 3))
47-
XCTAssertEqual(classif[7].kind, .integerLiteral)
48-
XCTAssertEqual(classif[7].range, ByteSourceRange(offset: 23, length: 1))
49-
}
50-
do {
51-
let classif = Array(tree.classifications(in: ByteSourceRange(offset: 7, length: 8)))
52-
XCTAssertEqual(classif.count, 6)
53-
guard classif.count == 6 else {
54-
return
55-
}
56-
XCTAssertEqual(classif[0].kind, .lineComment)
57-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 8))
58-
XCTAssertEqual(classif[1].kind, .none)
59-
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 8, length: 1))
60-
XCTAssertEqual(classif[2].kind, .keyword)
61-
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 9, length: 3))
62-
XCTAssertEqual(classif[3].kind, .none)
63-
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 12, length: 1))
64-
XCTAssertEqual(classif[4].kind, .identifier)
65-
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 13, length: 1))
66-
XCTAssertEqual(classif[5].kind, .blockComment)
67-
XCTAssertEqual(classif[5].range, ByteSourceRange(offset: 14, length: 6))
68-
}
69-
do {
70-
let classif = Array(tree.classifications(in: ByteSourceRange(offset: 21, length: 1)))
71-
XCTAssertEqual(classif.count, 1)
72-
guard classif.count == 1 else {
73-
return
74-
}
75-
XCTAssertEqual(classif[0].kind, .none)
76-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 21, length: 2))
77-
}
78-
do {
79-
let pattern = (tree.statements[0].item.as(VariableDeclSyntax.self)!).bindings[0].pattern
80-
XCTAssertEqual(pattern.description, "x/*yo*/ ")
81-
// Classify with a relative range inside this node.
82-
let classif = Array(pattern.classifications(in: ByteSourceRange(offset: 5, length: 2)))
83-
XCTAssertEqual(classif.count, 2)
84-
guard classif.count == 2 else {
85-
return
86-
}
87-
XCTAssertEqual(classif[0].kind, .blockComment)
88-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 14, length: 6))
89-
XCTAssertEqual(classif[1].kind, .none)
90-
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 20, length: 1))
49+
// blah.
50+
let x/*yo*/ = 0
51+
""",
52+
in: ByteSourceRange(offset: 7, length: 8),
53+
expected: [
54+
ClassificationSpec(source: "// blah.", kind: .lineComment),
55+
ClassificationSpec(source: "let", kind: .keyword),
56+
ClassificationSpec(source: "x", kind: .identifier),
57+
ClassificationSpec(source: "/*yo*/", kind: .blockComment),
58+
]
59+
)
60+
}
9161

92-
do {
93-
let singleClassif = pattern.classification(at: 5)
94-
XCTAssertEqual(singleClassif, classif[0])
95-
}
96-
do {
97-
let singleClassif = pattern.classification(at: AbsolutePosition(utf8Offset: 19))
98-
XCTAssertEqual(singleClassif, classif[0])
99-
}
100-
}
62+
public func testClassificationInEmptyRange() {
63+
assertClassification(
64+
"""
65+
// blah.
66+
let x/*yo*/ = 0
67+
""",
68+
in: ByteSourceRange(offset: 21, length: 2),
69+
expected: []
70+
)
71+
}
72+
73+
public func testClassificationAt() throws {
74+
let tree = Parser.parse(source: "func foo() {}")
75+
let keyword = try XCTUnwrap(tree.classification(at: 3))
76+
let identifier = try XCTUnwrap(tree.classification(at: AbsolutePosition(utf8Offset: 6)))
77+
78+
XCTAssertEqual(keyword.kind, .keyword)
79+
XCTAssertEqual(keyword.range, ByteSourceRange(offset: 0, length: 4))
10180

102-
do {
103-
let source = "func foo() {}"
104-
let tree = Parser.parse(source: source)
105-
// For `classification(at:)` there's an initial walk to find the token that
106-
// the offset is contained in and the classified ranges are processed from that
107-
// token. That means that a `none` classified range would be restricted inside
108-
// the token range.
109-
let classif = tree.classification(at: 11)!
110-
XCTAssertEqual(classif.kind, .none)
111-
XCTAssertEqual(classif.range, ByteSourceRange(offset: 11, length: 1))
112-
}
81+
XCTAssertEqual(identifier.kind, .identifier)
82+
XCTAssertEqual(identifier.range, ByteSourceRange(offset: 5, length: 3))
11383
}
11484

11585
public func testTokenClassification() {
116-
let source = "let x: Int"
117-
let tree = Parser.parse(source: source)
118-
do {
119-
let tokens = Array(tree.tokens(viewMode: .sourceAccurate))
120-
XCTAssertEqual(tokens.count, 5)
121-
guard tokens.count == 5 else {
122-
return
123-
}
124-
let classif = tokens.map { $0.tokenClassification }
125-
XCTAssertEqual(classif[0].kind, .keyword)
126-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 3))
127-
XCTAssertEqual(classif[1].kind, .identifier)
128-
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 4, length: 1))
129-
XCTAssertEqual(classif[2].kind, .none)
130-
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 5, length: 1))
131-
XCTAssertEqual(classif[3].kind, .typeIdentifier)
132-
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 7, length: 3))
133-
XCTAssertEqual(classif[4].kind, .none)
134-
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 10, length: 0))
135-
}
136-
do {
137-
let tok = tree.lastToken(viewMode: .sourceAccurate)!.previousToken(viewMode: .sourceAccurate)!
138-
XCTAssertEqual("\(tok)", "Int")
139-
let classif = Array(tok.classifications).first!
140-
XCTAssertEqual(classif.kind, .typeIdentifier)
141-
}
86+
assertClassification(
87+
"""
88+
let x: Int
89+
""",
90+
expected: [
91+
ClassificationSpec(source: "let", kind: .keyword),
92+
ClassificationSpec(source: "x", kind: .identifier),
93+
ClassificationSpec(source: "Int", kind: .typeIdentifier),
94+
]
95+
)
14296
}
14397

14498
public func testOperatorTokenClassification() {
145-
do {
146-
let source = "let x: Int = 4 + 5 / 6"
147-
let tree = Parser.parse(source: source)
148-
149-
let tokens = Array(tree.tokens(viewMode: .sourceAccurate))
150-
XCTAssertEqual(tokens.count, 11)
151-
guard tokens.count == 11 else {
152-
return
153-
}
154-
let classif = tokens.map { $0.tokenClassification }
155-
XCTAssertEqual(classif[0].kind, .keyword)
156-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 3))
157-
XCTAssertEqual(classif[1].kind, .identifier)
158-
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 4, length: 1))
159-
XCTAssertEqual(classif[2].kind, .none)
160-
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 5, length: 1))
161-
XCTAssertEqual(classif[3].kind, .typeIdentifier)
162-
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 7, length: 3))
163-
XCTAssertEqual(classif[4].kind, .none)
164-
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 11, length: 1))
165-
XCTAssertEqual(classif[5].kind, .integerLiteral)
166-
XCTAssertEqual(classif[5].range, ByteSourceRange(offset: 13, length: 1))
167-
XCTAssertEqual(classif[6].kind, .operatorIdentifier)
168-
XCTAssertEqual(classif[6].range, ByteSourceRange(offset: 15, length: 1))
169-
XCTAssertEqual(classif[7].kind, .integerLiteral)
170-
XCTAssertEqual(classif[7].range, ByteSourceRange(offset: 17, length: 1))
171-
XCTAssertEqual(classif[8].kind, .operatorIdentifier)
172-
XCTAssertEqual(classif[8].range, ByteSourceRange(offset: 19, length: 1))
173-
XCTAssertEqual(classif[9].kind, .integerLiteral)
174-
XCTAssertEqual(classif[9].range, ByteSourceRange(offset: 21, length: 1))
175-
XCTAssertEqual(classif[10].kind, .none)
176-
XCTAssertEqual(classif[10].range, ByteSourceRange(offset: 22, length: 0))
177-
}
178-
179-
do {
180-
let source = "infix operator *--*"
181-
let tree = Parser.parse(source: source)
99+
assertClassification(
100+
"""
101+
let x: Int = 4 + 5 / 6
102+
""",
103+
expected: [
104+
ClassificationSpec(source: "let", kind: .keyword),
105+
ClassificationSpec(source: "x", kind: .identifier),
106+
ClassificationSpec(source: "Int", kind: .typeIdentifier),
107+
ClassificationSpec(source: "4", kind: .integerLiteral),
108+
ClassificationSpec(source: "+", kind: .operatorIdentifier),
109+
ClassificationSpec(source: "5", kind: .integerLiteral),
110+
ClassificationSpec(source: "/", kind: .operatorIdentifier),
111+
ClassificationSpec(source: "6", kind: .integerLiteral),
112+
]
113+
)
182114

183-
let tokens = Array(tree.tokens(viewMode: .sourceAccurate))
184-
XCTAssertEqual(tokens.count, 4)
185-
guard tokens.count == 4 else {
186-
return
187-
}
188-
let classif = tokens.map { $0.tokenClassification }
189-
XCTAssertEqual(classif[0].kind, .keyword)
190-
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 5))
191-
XCTAssertEqual(classif[1].kind, .keyword)
192-
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 6, length: 8))
193-
XCTAssertEqual(classif[2].kind, .operatorIdentifier)
194-
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 15, length: 4))
195-
XCTAssertEqual(classif[3].kind, .none)
196-
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 19, length: 0))
197-
}
115+
assertClassification(
116+
"infix operator *--*",
117+
expected: [
118+
ClassificationSpec(source: "infix", kind: .keyword),
119+
ClassificationSpec(source: "operator", kind: .keyword),
120+
ClassificationSpec(source: "*--*", kind: .operatorIdentifier),
121+
]
122+
)
198123
}
199124
}

0 commit comments

Comments
 (0)