Skip to content

Adopt lexicalContext from swift-syntax-6.0.0. #279

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 2 commits into from
Apr 1, 2024
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
22 changes: 17 additions & 5 deletions Sources/TestingMacros/SuiteDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
_diagnoseIssues(with: declaration, suiteAttribute: node, in: context)
guard _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) else {
return []
}
return _createTestContainerDecls(for: declaration, suiteAttribute: node, in: context)
}

Expand All @@ -33,7 +35,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
// The peer macro expansion of this macro is only used to diagnose misuses
// on symbols that are not decl groups.
if declaration.asProtocol((any DeclGroupSyntax).self) == nil {
_diagnoseIssues(with: declaration, suiteAttribute: node, in: context)
_ = _diagnoseIssues(with: declaration, suiteAttribute: node, in: context)
}
return []
}
Expand All @@ -44,23 +46,31 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
/// - declaration: The type declaration to diagnose.
/// - suiteAttribute: The `@Suite` attribute applied to `declaration`.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: Whether or not macro expansion should continue (i.e. stopping
/// if a fatal error was diagnosed.)
private static func _diagnoseIssues(
with declaration: some SyntaxProtocol,
suiteAttribute: AttributeSyntax,
in context: some MacroExpansionContext
) {
) -> Bool {
var diagnostics = [DiagnosticMessage]()
defer {
diagnostics.forEach(context.diagnose)
context.diagnose(diagnostics)
}

// The @Suite attribute is only supported on type declarations, all of which
// are DeclGroupSyntax types.
guard let declaration = declaration.asProtocol((any DeclGroupSyntax).self) else {
diagnostics.append(.attributeNotSupported(suiteAttribute, on: declaration))
return
return false
}

#if canImport(SwiftSyntax600)
// Check if the lexical context is appropriate for a suite or test.
diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: suiteAttribute, in: context)
#endif

// Generic suites are not supported.
if let genericClause = declaration.asProtocol((any WithGenericParametersSyntax).self)?.genericParameterClause {
diagnostics.append(.genericDeclarationNotSupported(declaration, whenUsing: suiteAttribute, becauseOf: genericClause))
Expand Down Expand Up @@ -115,6 +125,8 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
diagnostics.append(.availabilityAttributeNotSupported(noasyncAttribute, on: declaration, whenUsing: suiteAttribute))
}
}

return !diagnostics.lazy.map(\.severity).contains(.error)
}

/// Create a declaration for a type that conforms to the `__TestContainer`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,17 @@ extension FunctionDeclSyntax {
if signature.effectSpecifiers?.asyncSpecifier != nil {
selector += "WithCompletionHandler"
colonToken = .colonToken()
} else if signature.effectSpecifiers?.throwsSpecifier != nil {
selector += "AndReturnError"
colonToken = .colonToken()
} else {
let hasThrowsSpecifier: Bool
#if canImport(SwiftSyntax600)
hasThrowsSpecifier = signature.effectSpecifiers?.throwsClause != nil
#else
hasThrowsSpecifier = signature.effectSpecifiers?.throwsSpecifier != nil
#endif
if hasThrowsSpecifier {
selector += "AndReturnError"
colonToken = .colonToken()
}
}
return ObjCSelectorPieceListSyntax {
ObjCSelectorPieceSyntax(name: .identifier(selector), colon: colonToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,24 @@ func diagnoseIssuesWithTags(in traitExprs: [ExprSyntax], addedTo attribute: Attr
}
}
}

#if canImport(SwiftSyntax600)
/// Diagnose issues with the lexical context containing a declaration.
///
/// - Parameters:
/// - decl: The declaration to inspect.
/// - testAttribute: The `@Test` attribute applied to `decl`.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: An array of zero or more diagnostic messages related to the
/// lexical context containing `decl`.
func diagnoseIssuesWithLexicalContext(
containing decl: some DeclSyntaxProtocol,
attribute: AttributeSyntax,
in context: some MacroExpansionContext
) -> [DiagnosticMessage] {
context.lexicalContext
.filter { !$0.isProtocol((any DeclGroupSyntax).self) }
.map { .containingNodeUnsupported($0, whenUsing: attribute) }
}
#endif
37 changes: 36 additions & 1 deletion Sources/TestingMacros/Support/DiagnosticMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,13 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
result = ("subscript", "a")
case .enumCaseDecl:
result = ("enumeration case", "an")
#if canImport(SwiftSyntax600)
case .typeAliasDecl:
result = ("typealias", "a")
#else
case .typealiasDecl:
result = ("typealias", "a")
#endif
case .macroDecl:
result = ("macro", "a")
case .protocolDecl:
Expand Down Expand Up @@ -225,6 +230,27 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
)
}

#if canImport(SwiftSyntax600)
/// Create a diagnostic message stating that the given attribute cannot be
/// used within a lexical context.
///
/// - Parameters:
/// - node: The lexical context preventing the use of `attribute`.
/// - attribute: The `@Test` or `@Suite` attribute.
///
/// - Returns: A diagnostic message.
static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax) -> Self {
// It would be great if the diagnostic pointed to the containing lexical
// context that was unsupported, but that node may be synthesized and does
// not have reliable location information.
Self(
syntax: Syntax(attribute),
message: "The @\(attribute.attributeNameText) attribute cannot be applied within \(_kindString(for: node, includeA: true)).",
severity: .error
)
}
#endif

/// Create a diagnostic message stating that the given attribute has no effect
/// when applied to the given extension declaration.
///
Expand Down Expand Up @@ -406,7 +432,6 @@ extension MacroExpansionContext {
/// - message: The diagnostic message to emit. The `node` and `position`
/// arguments to `Diagnostic.init()` are derived from the message's
/// `syntax` property.
/// - fixIts: Any Fix-Its to apply.
func diagnose(_ message: DiagnosticMessage) {
diagnose(
Diagnostic(
Expand All @@ -418,6 +443,16 @@ extension MacroExpansionContext {
)
}

/// Emit a sequence of diagnostic messages.
///
/// - Parameters:
/// - messages: The diagnostic messages to emit.
func diagnose(_ messages: some Sequence<DiagnosticMessage>) {
for message in messages {
diagnose(message)
}
}

/// Emit a diagnostic message for debugging purposes during development of the
/// testing library.
///
Expand Down
107 changes: 83 additions & 24 deletions Sources/TestingMacros/TestDeclarationMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
_diagnoseIssues(with: declaration, testAttribute: node, in: context)
guard _diagnoseIssues(with: declaration, testAttribute: node, in: context) else {
return []
}

guard let function = declaration.as(FunctionDeclSyntax.self) else {
return []
Expand All @@ -45,6 +47,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
testAttribute: AttributeSyntax,
in context: some MacroExpansionContext
) -> TypeSyntax? {
#if canImport(SwiftSyntax600)
let types = context.lexicalContext
.compactMap { $0.asProtocol((any DeclGroupSyntax).self) }
.map(\.type)
.reversed()
if types.isEmpty {
return nil
}
let typeName = types.map(\.trimmedDescription).joined(separator: ".")
return "\(raw: typeName)"
#else
// Find the beginning of the first attribute on the declaration, including
// those embedded in #if statements, to account for patterns like
// `@MainActor @Test func` where there's a space ahead of @Test, but the
Expand Down Expand Up @@ -79,6 +92,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
return TypeSyntax(IdentifierTypeSyntax(name: .keyword(.Self)))
}
return nil
#endif
}

/// Diagnose issues with a `@Test` declaration.
Expand All @@ -87,22 +101,30 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
/// - declaration: The function declaration to diagnose.
/// - testAttribute: The `@Test` attribute applied to `declaration`.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: Whether or not macro expansion should continue (i.e. stopping
/// if a fatal error was diagnosed.)
private static func _diagnoseIssues(
with declaration: some DeclSyntaxProtocol,
testAttribute: AttributeSyntax,
in context: some MacroExpansionContext
) {
) -> Bool {
var diagnostics = [DiagnosticMessage]()
defer {
diagnostics.forEach(context.diagnose)
context.diagnose(diagnostics)
}

// The @Test attribute is only supported on function declarations.
guard let function = declaration.as(FunctionDeclSyntax.self) else {
diagnostics.append(.attributeNotSupported(testAttribute, on: declaration))
return
return false
}

#if canImport(SwiftSyntax600)
// Check if the lexical context is appropriate for a suite or test.
diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: testAttribute, in: context)
#endif

// Only one @Test attribute is supported.
let suiteAttributes = function.attributes(named: "Test", in: context)
if suiteAttributes.count > 1 {
Expand All @@ -113,13 +135,22 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {

// We don't support inout, isolated, or _const parameters on test functions.
for parameter in parameterList {
if let specifier = parameter.type.as(AttributedTypeSyntax.self)?.specifier {
switch specifier.tokenKind {
case .keyword(.inout), .keyword(.isolated), .keyword(._const):
let invalidSpecifierKeywords: [TokenKind] = [.keyword(.inout), .keyword(.isolated), .keyword(._const),]
if let parameterType = parameter.type.as(AttributedTypeSyntax.self) {
#if canImport(SwiftSyntax600)
for specifier in parameterType.specifiers {
guard case let .simpleTypeSpecifier(specifier) = specifier else {
continue
}
if invalidSpecifierKeywords.contains(specifier.specifier.tokenKind) {
diagnostics.append(.specifierNotSupported(specifier.specifier, on: parameter, whenUsing: testAttribute))
}
}
#else
if let specifier = parameterType.specifier, invalidSpecifierKeywords.contains(specifier.tokenKind) {
diagnostics.append(.specifierNotSupported(specifier, on: parameter, whenUsing: testAttribute))
default:
break
}
#endif
}
}

Expand All @@ -144,6 +175,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
}
}
}

return !diagnostics.lazy.map(\.severity).contains(.error)
}

/// Create a function call parameter list used to call a function from its
Expand Down Expand Up @@ -220,21 +253,41 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
private static func _createCaptureListExpr(
from parametersWithLabels: some Sequence<(DeclReferenceExprSyntax, FunctionParameterSyntax)>
) -> ClosureCaptureClauseSyntax {
ClosureCaptureClauseSyntax {
for (label, parameter) in parametersWithLabels {
if case let .keyword(specifierKeyword) = parameter.type.as(AttributedTypeSyntax.self)?.specifier?.tokenKind,
specifierKeyword == .borrowing || specifierKeyword == .consuming {
ClosureCaptureSyntax(
name: label.baseName,
equal: .equalToken(),
expression: CopyExprSyntax(
copyKeyword: .keyword(.copy).with(\.trailingTrivia, .space),
expression: label
)
)
} else {
ClosureCaptureSyntax(expression: label)
let specifierKeywordsNeedingCopy: [TokenKind] = [.keyword(.borrowing), .keyword(.consuming),]
let closureCaptures = parametersWithLabels.lazy.map { label, parameter in
var needsCopy = false
if let parameterType = parameter.type.as(AttributedTypeSyntax.self) {
#if canImport(SwiftSyntax600)
needsCopy = parameterType.specifiers.contains { specifier in
guard case let .simpleTypeSpecifier(specifier) = specifier else {
return false
}
return specifierKeywordsNeedingCopy.contains(specifier.specifier.tokenKind)
}
#else
if let specifier = parameterType.specifier {
needsCopy = specifierKeywordsNeedingCopy.contains(specifier.tokenKind)
}
#endif
}

if needsCopy {
return ClosureCaptureSyntax(
name: label.baseName,
equal: .equalToken(),
expression: CopyExprSyntax(
copyKeyword: .keyword(.copy).with(\.trailingTrivia, .space),
expression: label
)
)
} else {
return ClosureCaptureSyntax(expression: label)
}
}

return ClosureCaptureClauseSyntax {
for closureCapture in closureCaptures {
closureCapture
}
}
}
Expand Down Expand Up @@ -406,6 +459,11 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
) -> [DeclSyntax] {
var result = [DeclSyntax]()

#if canImport(SwiftSyntax600)
// Get the name of the type containing the function for passing to the test
// factory function later.
let typealiasExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil"
#else
// We cannot directly refer to Self here because it will end up being
// resolved as the __TestContainer type we generate. Create a uniquely-named
// reference to Self outside the context of the generated type, and use it
Expand All @@ -415,7 +473,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
// inside a static computed property instead of a typealias (where covariant
// Self is disallowed.)
//
// This "typealias" will not be necessary when rdar://105470382 is resolved.
// This "typealias" is not necessary when swift-syntax-6.0.0 is available.
var typealiasExpr: ExprSyntax = "nil"
if let typeName {
let typealiasName = context.makeUniqueName(thunking: functionDecl)
Expand All @@ -430,6 +488,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {

typealiasExpr = "\(typealiasName)"
}
#endif

// Parse the @Test attribute.
let attributeInfo = AttributeInfo(byParsing: testAttribute, on: functionDecl, in: context)
Expand Down
Loading