Skip to content

Add assertClassification for ClassificationTests #1916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public func assertIncrementalParse(
}
}

fileprivate func byteSourceRange(for substring: String, in sourceString: String, after: String.Index) -> ByteSourceRange? {
public func byteSourceRange(for substring: String, in sourceString: String, after: String.Index) -> ByteSourceRange? {
if let range = sourceString[after...].range(of: substring) {
return ByteSourceRange(
offset: sourceString.utf8.distance(from: sourceString.startIndex, to: range.lowerBound),
Expand Down
99 changes: 99 additions & 0 deletions Tests/SwiftIDEUtilsTest/Assertions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import XCTest
import SwiftIDEUtils
import SwiftParser
import SwiftSyntax
import _SwiftSyntaxTestSupport

/// Parse `source` and checks its `classifications` is the same as `expected`.
///
/// The `expected` classifications should only contain classifications that are not `none`. All uncovered ranges are expected to have no classification.
///
/// - Parameters:
/// - range: An optional parameter to specify the ``ByteSourceRange`` in `source` that we want to test the `classifications` in.
/// - expected: The element order should respect to the order of `ClassificationSpec.source` in `source`.
func assertClassification(
_ source: String,
in range: ByteSourceRange? = nil,
expected: [ClassificationSpec],
file: StaticString = #file,
line: UInt = #line
) {
let tree = Parser.parse(source: source)

var classifications: Array<SyntaxClassifiedRange>
if let range {
classifications = Array(tree.classifications(in: range))
} else {
classifications = Array(tree.classifications)
}
classifications = classifications.filter { $0.kind != .none }

if expected.count != classifications.count {
XCTFail("Expected \(expected.count) re-used nodes but received \(classifications.count)", file: file, line: line)
}

var lastRangeUpperBound = source.startIndex
for (classification, spec) in zip(classifications, expected) {
guard let range = byteSourceRange(for: spec.source, in: source, after: lastRangeUpperBound) else {
XCTFail("Fail to find string in original source,", file: spec.file, line: spec.line)
continue
}

XCTAssertEqual(
range,
classification.range,
"""
Expected \(range) but received \(classification.range)
""",
file: spec.file,
line: spec.line
)

XCTAssertEqual(
spec.kind,
classification.kind,
"""
Expected \(spec.kind) syntax classification kind but received \(classification.kind)
""",
file: spec.file,
line: spec.line
)

lastRangeUpperBound = source.index(source.startIndex, offsetBy: range.endOffset)
}
}

/// An abstract data structure to describe a source code snippet and its ``SyntaxClassification``.
public struct ClassificationSpec {
/// Source code without any ``Trivia``
let source: String
/// The ``SyntaxClassification`` of the source code,
let kind: SyntaxClassification
/// The file and line at which this ``ClassificationSpec`` was created, so that assertion failures can be reported at its location.
let file: StaticString
let line: UInt

public init(
source: String,
kind: SyntaxClassification,
file: StaticString = #file,
line: UInt = #line
) {
self.source = source
self.kind = kind
self.file = file
self.line = line
}
}
253 changes: 89 additions & 164 deletions Tests/SwiftIDEUtilsTest/ClassificationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,181 +19,106 @@ import _SwiftSyntaxTestSupport
public class ClassificationTests: XCTestCase {

public func testClassification() {
let source = """
assertClassification(
"""
// blah.
let x/*yo*/ = 0
""",
expected: [
ClassificationSpec(source: "// blah.", kind: .lineComment),
ClassificationSpec(source: "let", kind: .keyword),
ClassificationSpec(source: "x", kind: .identifier),
ClassificationSpec(source: "/*yo*/", kind: .blockComment),
ClassificationSpec(source: "0", kind: .integerLiteral),
]
)

assertClassification(
"x/*yo*/ ",
in: ByteSourceRange(offset: 1, length: 6),
expected: [
ClassificationSpec(source: "x", kind: .identifier),
ClassificationSpec(source: "/*yo*/", kind: .blockComment),
]
)
}

public func testClassificationInCertainRange() {
assertClassification(
"""
let tree = Parser.parse(source: source)
do {
let classif = Array(tree.classifications)
XCTAssertEqual(classif.count, 8)
guard classif.count == 8 else {
return
}
XCTAssertEqual(classif[0].kind, .lineComment)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 8))
XCTAssertEqual(classif[1].kind, .none)
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 8, length: 1))
XCTAssertEqual(classif[2].kind, .keyword)
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 9, length: 3))
XCTAssertEqual(classif[3].kind, .none)
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 12, length: 1))
XCTAssertEqual(classif[4].kind, .identifier)
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 13, length: 1))
XCTAssertEqual(classif[5].kind, .blockComment)
XCTAssertEqual(classif[5].range, ByteSourceRange(offset: 14, length: 6))
XCTAssertEqual(classif[6].kind, .none)
XCTAssertEqual(classif[6].range, ByteSourceRange(offset: 20, length: 3))
XCTAssertEqual(classif[7].kind, .integerLiteral)
XCTAssertEqual(classif[7].range, ByteSourceRange(offset: 23, length: 1))
}
do {
let classif = Array(tree.classifications(in: ByteSourceRange(offset: 7, length: 8)))
XCTAssertEqual(classif.count, 6)
guard classif.count == 6 else {
return
}
XCTAssertEqual(classif[0].kind, .lineComment)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 8))
XCTAssertEqual(classif[1].kind, .none)
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 8, length: 1))
XCTAssertEqual(classif[2].kind, .keyword)
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 9, length: 3))
XCTAssertEqual(classif[3].kind, .none)
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 12, length: 1))
XCTAssertEqual(classif[4].kind, .identifier)
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 13, length: 1))
XCTAssertEqual(classif[5].kind, .blockComment)
XCTAssertEqual(classif[5].range, ByteSourceRange(offset: 14, length: 6))
}
do {
let classif = Array(tree.classifications(in: ByteSourceRange(offset: 21, length: 1)))
XCTAssertEqual(classif.count, 1)
guard classif.count == 1 else {
return
}
XCTAssertEqual(classif[0].kind, .none)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 21, length: 2))
}
do {
let pattern = (tree.statements[0].item.as(VariableDeclSyntax.self)!).bindings[0].pattern
XCTAssertEqual(pattern.description, "x/*yo*/ ")
// Classify with a relative range inside this node.
let classif = Array(pattern.classifications(in: ByteSourceRange(offset: 5, length: 2)))
XCTAssertEqual(classif.count, 2)
guard classif.count == 2 else {
return
}
XCTAssertEqual(classif[0].kind, .blockComment)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 14, length: 6))
XCTAssertEqual(classif[1].kind, .none)
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 20, length: 1))
// blah.
let x/*yo*/ = 0
""",
in: ByteSourceRange(offset: 7, length: 8),
expected: [
ClassificationSpec(source: "// blah.", kind: .lineComment),
ClassificationSpec(source: "let", kind: .keyword),
ClassificationSpec(source: "x", kind: .identifier),
ClassificationSpec(source: "/*yo*/", kind: .blockComment),
]
)
}

do {
let singleClassif = pattern.classification(at: 5)
XCTAssertEqual(singleClassif, classif[0])
}
do {
let singleClassif = pattern.classification(at: AbsolutePosition(utf8Offset: 19))
XCTAssertEqual(singleClassif, classif[0])
}
}
public func testClassificationInEmptyRange() {
assertClassification(
"""
// blah.
let x/*yo*/ = 0
""",
in: ByteSourceRange(offset: 21, length: 2),
expected: []
)
}

public func testClassificationAt() throws {
let tree = Parser.parse(source: "func foo() {}")
let keyword = try XCTUnwrap(tree.classification(at: 3))
let identifier = try XCTUnwrap(tree.classification(at: AbsolutePosition(utf8Offset: 6)))

XCTAssertEqual(keyword.kind, .keyword)
XCTAssertEqual(keyword.range, ByteSourceRange(offset: 0, length: 4))

do {
let source = "func foo() {}"
let tree = Parser.parse(source: source)
// For `classification(at:)` there's an initial walk to find the token that
// the offset is contained in and the classified ranges are processed from that
// token. That means that a `none` classified range would be restricted inside
// the token range.
let classif = tree.classification(at: 11)!
XCTAssertEqual(classif.kind, .none)
XCTAssertEqual(classif.range, ByteSourceRange(offset: 11, length: 1))
}
XCTAssertEqual(identifier.kind, .identifier)
XCTAssertEqual(identifier.range, ByteSourceRange(offset: 5, length: 3))
}

public func testTokenClassification() {
let source = "let x: Int"
let tree = Parser.parse(source: source)
do {
let tokens = Array(tree.tokens(viewMode: .sourceAccurate))
XCTAssertEqual(tokens.count, 5)
guard tokens.count == 5 else {
return
}
let classif = tokens.map { $0.tokenClassification }
XCTAssertEqual(classif[0].kind, .keyword)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 3))
XCTAssertEqual(classif[1].kind, .identifier)
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 4, length: 1))
XCTAssertEqual(classif[2].kind, .none)
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 5, length: 1))
XCTAssertEqual(classif[3].kind, .typeIdentifier)
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 7, length: 3))
XCTAssertEqual(classif[4].kind, .none)
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 10, length: 0))
}
do {
let tok = tree.lastToken(viewMode: .sourceAccurate)!.previousToken(viewMode: .sourceAccurate)!
XCTAssertEqual("\(tok)", "Int")
let classif = Array(tok.classifications).first!
XCTAssertEqual(classif.kind, .typeIdentifier)
}
assertClassification(
"""
let x: Int
""",
expected: [
ClassificationSpec(source: "let", kind: .keyword),
ClassificationSpec(source: "x", kind: .identifier),
ClassificationSpec(source: "Int", kind: .typeIdentifier),
]
)
}

public func testOperatorTokenClassification() {
do {
let source = "let x: Int = 4 + 5 / 6"
let tree = Parser.parse(source: source)

let tokens = Array(tree.tokens(viewMode: .sourceAccurate))
XCTAssertEqual(tokens.count, 11)
guard tokens.count == 11 else {
return
}
let classif = tokens.map { $0.tokenClassification }
XCTAssertEqual(classif[0].kind, .keyword)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 3))
XCTAssertEqual(classif[1].kind, .identifier)
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 4, length: 1))
XCTAssertEqual(classif[2].kind, .none)
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 5, length: 1))
XCTAssertEqual(classif[3].kind, .typeIdentifier)
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 7, length: 3))
XCTAssertEqual(classif[4].kind, .none)
XCTAssertEqual(classif[4].range, ByteSourceRange(offset: 11, length: 1))
XCTAssertEqual(classif[5].kind, .integerLiteral)
XCTAssertEqual(classif[5].range, ByteSourceRange(offset: 13, length: 1))
XCTAssertEqual(classif[6].kind, .operatorIdentifier)
XCTAssertEqual(classif[6].range, ByteSourceRange(offset: 15, length: 1))
XCTAssertEqual(classif[7].kind, .integerLiteral)
XCTAssertEqual(classif[7].range, ByteSourceRange(offset: 17, length: 1))
XCTAssertEqual(classif[8].kind, .operatorIdentifier)
XCTAssertEqual(classif[8].range, ByteSourceRange(offset: 19, length: 1))
XCTAssertEqual(classif[9].kind, .integerLiteral)
XCTAssertEqual(classif[9].range, ByteSourceRange(offset: 21, length: 1))
XCTAssertEqual(classif[10].kind, .none)
XCTAssertEqual(classif[10].range, ByteSourceRange(offset: 22, length: 0))
}

do {
let source = "infix operator *--*"
let tree = Parser.parse(source: source)
assertClassification(
"""
let x: Int = 4 + 5 / 6
""",
expected: [
ClassificationSpec(source: "let", kind: .keyword),
ClassificationSpec(source: "x", kind: .identifier),
ClassificationSpec(source: "Int", kind: .typeIdentifier),
ClassificationSpec(source: "4", kind: .integerLiteral),
ClassificationSpec(source: "+", kind: .operatorIdentifier),
ClassificationSpec(source: "5", kind: .integerLiteral),
ClassificationSpec(source: "/", kind: .operatorIdentifier),
ClassificationSpec(source: "6", kind: .integerLiteral),
]
)

let tokens = Array(tree.tokens(viewMode: .sourceAccurate))
XCTAssertEqual(tokens.count, 4)
guard tokens.count == 4 else {
return
}
let classif = tokens.map { $0.tokenClassification }
XCTAssertEqual(classif[0].kind, .keyword)
XCTAssertEqual(classif[0].range, ByteSourceRange(offset: 0, length: 5))
XCTAssertEqual(classif[1].kind, .keyword)
XCTAssertEqual(classif[1].range, ByteSourceRange(offset: 6, length: 8))
XCTAssertEqual(classif[2].kind, .operatorIdentifier)
XCTAssertEqual(classif[2].range, ByteSourceRange(offset: 15, length: 4))
XCTAssertEqual(classif[3].kind, .none)
XCTAssertEqual(classif[3].range, ByteSourceRange(offset: 19, length: 0))
}
assertClassification(
"infix operator *--*",
expected: [
ClassificationSpec(source: "infix", kind: .keyword),
ClassificationSpec(source: "operator", kind: .keyword),
ClassificationSpec(source: "*--*", kind: .operatorIdentifier),
]
)
}
}