Skip to content

Improve assertMacroExpansion highlight verification #2213

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
Nov 29, 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
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ let package = Package(
dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftParser", "SwiftSyntaxMacros", "SwiftSyntaxMacroExpansion"]
),

.testTarget(
name: "SwiftSyntaxMacrosTestSupportTests",
dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftSyntaxMacros", "SwiftSyntaxMacrosTestSupport"]
),

// MARK: SwiftParser

.target(
Expand Down
6 changes: 6 additions & 0 deletions Release Notes/510.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
- Issue: https://github.com/apple/swift-syntax/issues/2261
- Pull Request: https://github.com/apple/swift-syntax/pull/2264

- `DiagnosticSpec.highlight` replaced by `highlights`
- Description: The use of a single string `highlight` prevented users from asserting that a macro highlighted exactly the expected set of syntax nodes. Use of `DiagnosticSpec.init(...highlight:...)` is deprecated and forwards to `DiagnosticSpec.init(...highlights:...)`. Migrating from `highlight` to `highlights` is straightforward; any uses of `DiagnosticSpec.init` which do not specify a `highlight` do not need to change, otherwise:
- If the diagnostic highlights a single node, the `highlight` string should be replaced with a single element array containing the same string without any trailing trivia, e.g., `highlight: "let "` -> `highlights: ["let"]`.
- If the diagnostic highlights multiple nodes, the `highlight` string should be replaced with an array containing an element for each highlighted node, e.g., `highlight: "struct {}"` -> `highlights: ["struct", "{}"]`.
- Pull Request: https://github.com/apple/swift-syntax/pull/2213

## Template

- *Affected API or two word description*
Expand Down
84 changes: 62 additions & 22 deletions Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ public struct DiagnosticSpec {
/// The expected severity of the diagnostic
public let severity: DiagnosticSeverity

/// If not `nil`, the text the diagnostic is expected to highlight
public let highlight: String?
/// If not `nil`, the text fragments the diagnostic is expected to highlight
public let highlights: [String]?
Comment on lines -131 to +132
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain API-compatibility can you add

@available(*, deprecated, message: "Use highlights instead")
public var highlight: String? {
  guard let highlights else {
    return nil
  }
  return highlights.joined()
}


/// The notes that are expected to be attached to the diagnostic
public let notes: [NoteSpec]
Expand All @@ -141,15 +141,15 @@ public struct DiagnosticSpec {
internal let originatorFile: StaticString
internal let originatorLine: UInt

/// Creates a new ``DiagnosticSpec`` that describes a diagnsotic tests are expecting to be generated by a macro expansion.
/// Creates a new ``DiagnosticSpec`` that describes a diagnostic tests are expecting to be generated by a macro expansion.
///
/// - Parameters:
/// - id: If not `nil`, the ID, which the diagnostic is expected to have.
/// - message: The expected message of the diagnostic
/// - line: The line to which the diagnostic is expected to point
/// - column: The column to which the diagnostic is expected to point
/// - severity: The expected severity of the diagnostic
/// - highlight: If not `nil`, the text the diagnostic is expected to highlight
/// - highlights: If not empty, the text fragments the diagnostic is expected to highlight
/// - notes: The notes that are expected to be attached to the diagnostic
/// - fixIts: The messages of the Fix-Its the diagnostic is expected to produce
/// - originatorFile: The file at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location.
Expand All @@ -160,7 +160,7 @@ public struct DiagnosticSpec {
line: Int,
column: Int,
severity: DiagnosticSeverity = .error,
highlight: String? = nil,
highlights: [String]? = nil,
notes: [NoteSpec] = [],
fixIts: [FixItSpec] = [],
originatorFile: StaticString = #file,
Expand All @@ -171,14 +171,50 @@ public struct DiagnosticSpec {
self.line = line
self.column = column
self.severity = severity
self.highlight = highlight
self.highlights = highlights
self.notes = notes
self.fixIts = fixIts
self.originatorFile = originatorFile
self.originatorLine = originatorLine
}
}

extension DiagnosticSpec {
@available(*, deprecated, message: "Use highlights instead")
public var highlight: String? {
guard let highlights else {
return nil
}
return highlights.joined(separator: " ")
}

@_disfavoredOverload
@available(*, deprecated, message: "Use init(id:message:line:column:severity:highlights:notes:fixIts:originatorFile:originatorLine:) instead")
public init(
id: MessageID? = nil,
message: String,
line: Int,
column: Int,
severity: DiagnosticSeverity = .error,
highlight: String? = nil,
notes: [NoteSpec] = [],
fixIts: [FixItSpec] = [],
originatorFile: StaticString = #file,
originatorLine: UInt = #line
) {
self.init(
id: id,
message: message,
line: line,
column: column,
severity: severity,
highlights: highlight.map { [$0] },
notes: notes,
fixIts: fixIts
)
}
}

func assertDiagnostic(
_ diag: Diagnostic,
in expansionContext: BasicMacroExpansionContext,
Expand All @@ -194,23 +230,27 @@ func assertDiagnostic(

XCTAssertEqual(spec.severity, diag.diagMessage.severity, "severity does not match", file: spec.originatorFile, line: spec.originatorLine)

if let highlight = spec.highlight {
var highlightedCode = ""
highlightedCode.append(diag.highlights.first?.with(\.leadingTrivia, []).description ?? "")
for highlight in diag.highlights.dropFirst().dropLast() {
highlightedCode.append(highlight.description)
}
if diag.highlights.count > 1 {
highlightedCode.append(diag.highlights.last?.with(\.trailingTrivia, []).description ?? "")
if let highlights = spec.highlights {
if diag.highlights.count != highlights.count {
XCTFail(
"""
Expected \(highlights.count) highlights but received \(diag.highlights.count):
\(diag.highlights.map(\.trimmedDescription).joined(separator: "\n"))
""",
file: spec.originatorFile,
line: spec.originatorLine
)
} else {
for (actual, expected) in zip(diag.highlights, highlights) {
assertStringsEqualWithDiff(
actual.trimmedDescription,
expected,
"highlight does not match",
file: spec.originatorFile,
line: spec.originatorLine
)
}
}

assertStringsEqualWithDiff(
highlightedCode,
highlight,
"highlight does not match",
file: spec.originatorFile,
line: spec.originatorLine
)
}
if diag.notes.count != spec.notes.count {
XCTFail(
Expand Down
101 changes: 101 additions & 0 deletions Tests/SwiftSyntaxMacrosTestSupportTests/AssertionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// 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 SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest

final class AssertionsTests: XCTestCase {
struct OnlyStruct: DiagnosticMessage {
var message = "'@NoStruct' cannot be applied to struct types"
var diagnosticID = MessageID(domain: "\(AssertionsTests.self)", id: "\(OnlyStruct.self)")
var severity = DiagnosticSeverity.error
}

struct NoStruct: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
if let structDecl = declaration.as(StructDeclSyntax.self) {
context.diagnose(
.init(
node: structDecl.structKeyword,
message: OnlyStruct()
)
)
}
return []
}
}

struct NoStructMultiHighlight: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
if let structDecl = declaration.as(StructDeclSyntax.self) {
context.diagnose(
.init(
node: structDecl.structKeyword,
message: OnlyStruct(),
highlights: [
Syntax(structDecl.structKeyword),
Syntax(structDecl.name),
]
)
)
}
return []
}
}

func testAssertMacroExpansionIgnoresHighlightMatchingIfNil() {
assertMacroExpansion(
"@NoStruct struct S { }",
expandedSource: "struct S { }",
diagnostics: [
.init(message: OnlyStruct().message, line: 1, column: 11)
],
macros: ["NoStruct": NoStruct.self]
)
}

@available(*, deprecated)
func testAssertMacroExpansionMatchesSingleStringHighlight() {
assertMacroExpansion(
"@NoStruct struct S { }",
expandedSource: "struct S { }",
diagnostics: [
.init(message: OnlyStruct().message, line: 1, column: 11, highlight: "struct")
],
macros: ["NoStruct": NoStruct.self]
)
}

func testAssertMacroExpansionMatchesArrayHighlight() {
assertMacroExpansion(
"@NoStruct struct S { }",
expandedSource: "struct S { }",
diagnostics: [
.init(message: OnlyStruct().message, line: 1, column: 11, highlights: ["struct", "S"])
],
macros: ["NoStruct": NoStructMultiHighlight.self]
)
}
}