1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 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 LanguageServerProtocol
import SwiftSyntax
/// Scans a source file for `XCTestCase` classes and test methods.
///
/// The syntax visitor scans from class and extension declarations that could be `XCTestCase` classes or extensions
/// thereof. It then calls into `findTestMethods` to find the actual test methods.
final class SyntacticSwiftXCTestScanner: SyntaxVisitor {
/// The document snapshot of the syntax tree that is being walked.
private var snapshot: DocumentSnapshot
/// The workspace symbols representing the found `XCTestCase` subclasses and test methods.
private var result: [AnnotatedTestItem] = []
private init(snapshot: DocumentSnapshot) {
self.snapshot = snapshot
super.init(viewMode: .fixedUp)
}
public static func findTestSymbols(
in snapshot: DocumentSnapshot,
syntaxTreeManager: SyntaxTreeManager
) async -> [AnnotatedTestItem] {
guard snapshot.text.contains("XCTestCase") || snapshot.text.contains("test") else {
// If the file contains tests that can be discovered syntactically, it needs to have a class inheriting from
// `XCTestCase` or a function starting with `test`.
// This is intended to filter out files that obviously do not contain tests.
return []
}
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
let visitor = SyntacticSwiftXCTestScanner(snapshot: snapshot)
visitor.walk(syntaxTree)
return visitor.result
}
private func findTestMethods(in members: MemberBlockItemListSyntax, containerName: String) -> [TestItem] {
return members.compactMap { (member) -> TestItem? in
guard let function = member.decl.as(FunctionDeclSyntax.self) else {
return nil
}
guard function.name.text.starts(with: "test") else {
return nil
}
guard function.modifiers.map(\.name.tokenKind).allSatisfy({ $0 != .keyword(.static) && $0 != .keyword(.class) })
else {
// Test methods can't be static.
return nil
}
guard function.signature.returnClause == nil, function.signature.parameterClause.parameters.isEmpty else {
// Test methods can't have a return type or have parameters.
// Technically we are also filtering out functions that have an explicit `Void` return type here but such
// declarations are probably less common than helper functions that start with `test` and have a return type.
return nil
}
let range = snapshot.range(
of: function.positionAfterSkippingLeadingTrivia..<function.endPositionBeforeTrailingTrivia
)
return TestItem(
id: "\(containerName)/\(function.name.text)()",
label: "\(function.name.text)()",
disabled: false,
style: TestStyle.xcTest,
location: Location(uri: snapshot.uri, range: range),
children: [],
tags: []
)
}
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
guard let inheritedTypes = node.inheritanceClause?.inheritedTypes, let superclass = inheritedTypes.first else {
// The class has no superclass and thus can't inherit from XCTestCase.
// Continue scanning its children in case it has a nested subclass that inherits from XCTestCase.
return .visitChildren
}
let superclassName = superclass.type.as(IdentifierTypeSyntax.self)?.name.text
if superclassName == "NSObject" {
// We know that the class can't be an subclass of `XCTestCase` so don't visit it.
// We can't explicitly check for the `XCTestCase` superclass because the class might inherit from a class that in
// turn inherits from `XCTestCase`. Resolving that inheritance hierarchy would be semantic.
return .visitChildren
}
let testMethods = findTestMethods(in: node.memberBlock.members, containerName: node.name.text)
guard !testMethods.isEmpty || superclassName == "XCTestCase" else {
// Don't report a test class if it doesn't contain any test methods.
return .visitChildren
}
let range = snapshot.range(of: node.positionAfterSkippingLeadingTrivia..<node.endPositionBeforeTrailingTrivia)
let testItem = AnnotatedTestItem(
testItem: TestItem(
id: node.name.text,
label: node.name.text,
disabled: false,
style: TestStyle.xcTest,
location: Location(uri: snapshot.uri, range: range),
children: testMethods,
tags: []
),
isExtension: false
)
result.append(testItem)
return .visitChildren
}
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
result += findTestMethods(in: node.memberBlock.members, containerName: node.extendedType.trimmedDescription)
.map { AnnotatedTestItem(testItem: $0, isExtension: true) }
return .visitChildren
}
}
|