From 86e27fe06bbffbb4ab2cf37665aef08ce9ff3e9d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 21 Aug 2024 09:05:40 -0400 Subject: [PATCH] Disallow `@Test` on member functions of `XCTestCase` subclasses. This PR adds a diagnostic if we detect that `@Test` has been used on a member function of an `XCTestCase` subclass. This was meant to be disallowed, but we neglected to add a check after adopting `lexicalContext`. --- Sources/TestingMacros/SuiteDeclarationMacro.swift | 6 +++++- Sources/TestingMacros/TestDeclarationMacro.swift | 10 ++++++++++ .../TestingMacrosTests/TestDeclarationMacroTests.swift | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Sources/TestingMacros/SuiteDeclarationMacro.swift b/Sources/TestingMacros/SuiteDeclarationMacro.swift index 5658123a0..a79255bc8 100644 --- a/Sources/TestingMacros/SuiteDeclarationMacro.swift +++ b/Sources/TestingMacros/SuiteDeclarationMacro.swift @@ -63,7 +63,11 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable { diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: suiteAttribute) diagnostics += diagnoseIssuesWithLexicalContext(declaration, containing: declaration, attribute: suiteAttribute) - // Suites inheriting from XCTestCase are not supported. + // Suites inheriting from XCTestCase are not supported. This check is + // duplicated in TestDeclarationMacro but is not part of + // diagnoseIssuesWithLexicalContext() because it doesn't need to recurse + // across the entire lexical context list, just the innermost type + // declaration. if let declaration = declaration.asProtocol((any DeclGroupSyntax).self), declaration.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") { diagnostics.append(.xcTestCaseNotSupported(declaration, whenUsing: suiteAttribute)) diff --git a/Sources/TestingMacros/TestDeclarationMacro.swift b/Sources/TestingMacros/TestDeclarationMacro.swift index 1f9025f08..334cc1dac 100644 --- a/Sources/TestingMacros/TestDeclarationMacro.swift +++ b/Sources/TestingMacros/TestDeclarationMacro.swift @@ -59,6 +59,16 @@ public struct TestDeclarationMacro: PeerMacro, Sendable { // Check if the lexical context is appropriate for a suite or test. diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: testAttribute) + // Suites inheriting from XCTestCase are not supported. We are a bit + // conservative here in this check and only check the immediate context. + // Presumably, if there's an intermediate lexical context that is *not* a + // type declaration, then it must be a function or closure (disallowed + // elsewhere) and thus the test function is not a member of any type. + if let containingTypeDecl = context.lexicalContext.first?.asProtocol((any DeclGroupSyntax).self), + containingTypeDecl.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") { + diagnostics.append(.containingNodeUnsupported(containingTypeDecl, whenUsing: testAttribute, on: declaration)) + } + // Only one @Test attribute is supported. let suiteAttributes = function.attributes(named: "Test") if suiteAttributes.count > 1 { diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 6330d3fcf..fffa06664 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -82,6 +82,10 @@ struct TestDeclarationMacroTests { "Attribute 'Suite' cannot be applied to a subclass of 'XCTestCase'", "@Suite final class C: XCTest.XCTestCase {}": "Attribute 'Suite' cannot be applied to a subclass of 'XCTestCase'", + "final class C: XCTestCase { @Test func f() {} }": + "Attribute 'Test' cannot be applied to a function within class 'C'", + "final class C: XCTest.XCTestCase { @Test func f() {} }": + "Attribute 'Test' cannot be applied to a function within class 'C'", // Unsupported inheritance "@Suite protocol P {}":