Skip to content

Commit b43fae2

Browse files
authored
Allow non-final classes to act as suites. (#550)
Now that we require Swift 6, we can see the names of classes during `@Test` macro expansion. This was the primary blocker preventing us from allowing non-`final` classes, because we had to substitute `Self` for the class name and this was invalid if the suite type could be covariant. We can drop the constraint now. Note that test functions are _not_ heritable. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 133d65e commit b43fae2

File tree

6 files changed

+7
-57
lines changed

6 files changed

+7
-57
lines changed

Sources/Testing/Testing.docc/MigratingFromXCTest.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ concurrency safety:
8787
}
8888
}
8989

90-
If you use a class as a test suite, it must be declared `final`.
91-
9290
For more information about suites and how to declare and customize them, see
9391
<doc:OrganizingTests>.
9492

@@ -128,8 +126,8 @@ family of functions. When writing tests using the testing library, implement
128126
}
129127

130128
The use of `async` and `throws` is optional. If teardown is needed, declare your
131-
test suite as a `final` class or as an actor rather than as a structure and
132-
implement `deinit`:
129+
test suite as a class or as an actor rather than as a structure and implement
130+
`deinit`:
133131

134132
@Row {
135133
@Column {

Sources/Testing/Testing.docc/OrganizingTests.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,3 @@ _not_ be annotated with the `@available` attribute:
145145

146146
The compiler emits an error when presented with a test suite that doesn't
147147
meet this requirement.
148-
149-
#### Classes must be final
150-
151-
The testing library doesn't support inheritance between test suite
152-
types. When using a class as a test suite type, it may inherit from another
153-
class, but it must be declared `final`:
154-
155-
```swift
156-
@Suite final class FoodTruckTests { ... } // ✅ OK: The class is final.
157-
actor CashRegisterTests: NSObject { ... } // ✅ OK: The actors are implicitly final.
158-
class MenuItemTests { ... } // ❌ ERROR: This class isn't final.
159-
```

Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,6 @@ func diagnoseIssuesWithLexicalContext(
197197
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: lexicalContext.type, on: lexicalContext))
198198
}
199199

200-
// Suites that are classes must be final.
201-
if let classDecl = lexicalContext.as(ClassDeclSyntax.self) {
202-
if !classDecl.modifiers.lazy.map(\.name.tokenKind).contains(.keyword(.final)) {
203-
if Syntax(classDecl) == Syntax(decl) {
204-
diagnostics.append(.nonFinalClassNotSupported(classDecl, whenUsing: attribute))
205-
} else {
206-
diagnostics.append(.containingNodeUnsupported(classDecl, whenUsing: attribute, on: decl))
207-
}
208-
}
209-
}
210-
211200
// Suites cannot be protocols (there's nowhere to put most of the
212201
// declarations we generate.)
213202
if let protocolDecl = lexicalContext.as(ProtocolDeclSyntax.self) {

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -349,17 +349,10 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
349349
severity: .error
350350
)
351351
} else if let namedDecl = node.asProtocol((any NamedDeclSyntax).self) {
352-
// Special-case class declarations as implicitly non-final (since we would
353-
// only diagnose a class here if it were non-final.)
354-
let nonFinal = if node.is(ClassDeclSyntax.self) {
355-
" non-final"
356-
} else {
357-
""
358-
}
359352
let declName = namedDecl.name.textWithoutBackticks
360353
return Self(
361354
syntax: syntax,
362-
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic)\(nonFinal) \(_kindString(for: node)) '\(declName)'",
355+
message: "Attribute \(_macroName(attribute)) cannot be applied to \(_kindString(for: decl, includeA: true)) within\(generic) \(_kindString(for: node)) '\(declName)'",
363356
severity: .error
364357
)
365358
} else if let extensionDecl = node.as(ExtensionDeclSyntax.self) {
@@ -554,22 +547,6 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
554547
)
555548
}
556549

557-
/// Create a diagnostic message stating that `@Test` or `@Suite` is
558-
/// incompatible with a non-`final` class declaration.
559-
///
560-
/// - Parameters:
561-
/// - decl: The unsupported class declaration.
562-
/// - attribute: The `@Test` or `@Suite` attribute.
563-
///
564-
/// - Returns: A diagnostic message.
565-
static func nonFinalClassNotSupported(_ decl: ClassDeclSyntax, whenUsing attribute: AttributeSyntax) -> Self {
566-
Self(
567-
syntax: Syntax(decl),
568-
message: "Attribute \(_macroName(attribute)) cannot be applied to non-final class '\(decl.name.textWithoutBackticks)'",
569-
severity: .error
570-
)
571-
}
572-
573550
/// Create a diagnostic message stating that a parameter to a test function
574551
/// cannot be marked with the given specifier (such as `inout`).
575552
///

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ struct TestDeclarationMacroTests {
8484
"Attribute 'Suite' cannot be applied to a subclass of 'XCTestCase'",
8585
8686
// Unsupported inheritance
87-
"@Suite class C {}":
88-
"Attribute 'Suite' cannot be applied to non-final class 'C'",
8987
"@Suite protocol P {}":
9088
"Attribute 'Suite' cannot be applied to a protocol",
9189
@@ -110,10 +108,6 @@ struct TestDeclarationMacroTests {
110108
"Attribute 'Test' cannot be applied to a function within generic structure 'S'",
111109
"struct S<T> { @Suite struct S {} }":
112110
"Attribute 'Suite' cannot be applied to a structure within generic structure 'S'",
113-
"class C { @Test func f() {} }":
114-
"Attribute 'Test' cannot be applied to a function within non-final class 'C'",
115-
"class C { @Suite struct S {} }":
116-
"Attribute 'Suite' cannot be applied to a structure within non-final class 'C'",
117111
"protocol P { @Test func f() {} }":
118112
"Attribute 'Test' cannot be applied to a function within protocol 'P'",
119113
"protocol P { @Suite struct S {} }":

Tests/TestingTests/MiscellaneousTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ actor ActorTests {
151151
nonisolated func parameterizedNonisolated(i: Int) async throws {}
152152
}
153153

154+
@Suite(.hidden) class NonFinalClassTests {
155+
@Test(.hidden) func f() {}
156+
}
157+
154158
@Suite(.hidden)
155159
struct TestsWithStaticMemberAccessBySelfKeyword {
156160
static let x = 0 ..< 100

0 commit comments

Comments
 (0)