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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
|
//===----------------------------------------------------------------------===//
//
// 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 SwiftParser
import SwiftParserDiagnostics
import SwiftSyntax
import XCTest
import _SwiftSyntaxTestSupport
/// A typealias representing a location marker.
///
/// This string serves to pinpoint the exact location of a particular token in the SwiftSyntax tree.
/// Once the token location is identified, it can be leveraged for various test-specific operations such as inserting diagnostics, notes, or fix-its,
/// or for closer examination of the syntax tree.
///
/// Markers are instrumental in writing unit tests that require precise location data. They are commonly represented using emojis like 1️⃣, 2️⃣, 3️⃣, etc., to improve readability.
///
/// ### Example
///
/// In the following test code snippet, the emojis 1️⃣ and 2️⃣ are used as location markers:
///
/// ```swift
/// func foo() -> Int {
/// if 1️⃣1 != 0 2️⃣{
/// return 0
/// }
/// return 1
/// }
/// ```
typealias LocationMarker = String
/// Represents a descriptor for constructing a diagnostic in testing.
struct DiagnosticDescriptor {
/// Represents errors that can occur while creating a `Diagnostic` instance.
private struct DiagnosticCreationError: Error, LocalizedError {
/// A human-readable message describing what went wrong.
let message: String
/// A localized message describing what went wrong. Required by `LocalizedError`.
var errorDescription: String? { message }
}
/// The marker pointing to location in source code.
let locationMarker: LocationMarker
/// The ID associated with the message, used for categorizing or referencing it.
let id: MessageID
/// The textual content of the message to be displayed.
let message: String
/// The severity level of the diagnostic message.
let severity: DiagnosticSeverity
/// The syntax elements to be highlighted for this diagnostic message.
let highlight: [Syntax] // TODO: How to create an abstract model for this?
/// Descriptors for any accompanying notes for this diagnostic message.
let noteDescriptors: [NoteDescriptor]
/// Descriptors for any Fix-Its that can be applied for this diagnostic message.
let fixIts: [FixIt] // TODO: How to create an abstract model for this?
/// Initializes a new `DiagnosticDescriptor`.
///
/// - Parameters:
/// - locationMarker: The marker pointing to location in source code.
/// - id: The message ID of the diagnostic.
/// - message: The textual message to display for the diagnostic.
/// - severity: The severity level of the diagnostic. Default is `.error`.
/// - highlight: The syntax elements to be highlighted. Default is an empty array.
/// - noteDescriptors: An array of note descriptors for additional context. Default is an empty array.
/// - fixIts: An array of Fix-It descriptors for quick fixes. Default is an empty array.
init(
locationMarker: LocationMarker,
id: MessageID = MessageID(domain: "test", id: "conjured"),
message: String,
severity: DiagnosticSeverity = .error,
highlight: [Syntax] = [],
noteDescriptors: [NoteDescriptor] = [],
fixIts: [FixIt] = []
) {
self.locationMarker = locationMarker
self.id = id
self.message = message
self.severity = severity
self.highlight = highlight
self.noteDescriptors = noteDescriptors
self.fixIts = fixIts
}
/// Creates a ``Diagnostic`` instance from a given ``DiagnosticDescriptor``, syntax tree, and location markers.
///
/// - Parameters:
/// - tree: The syntax tree where the diagnostic is rooted.
/// - markers: A dictionary mapping location markers to their respective offsets in the source code.
///
/// - Throws:
/// - Error if the location marker is not found in the source code.
/// - Error if a node corresponding to a given marker is not found in the syntax tree.
///
/// - Returns: A ``Diagnostic`` instance populated with details from the ``DiagnosticDescriptor``.
func createDiagnostic(
inSyntaxTree tree: some SyntaxProtocol,
usingLocationMarkers markers: [LocationMarker: Int]
) throws -> Diagnostic {
func node(at marker: LocationMarker) throws -> Syntax {
guard let markedOffset = markers[marker] else {
throw DiagnosticCreationError(message: "Marker \(marker) not found in the marked source")
}
let markedPosition = AbsolutePosition(utf8Offset: markedOffset)
guard let token = tree.token(at: markedPosition) else {
throw DiagnosticCreationError(message: "Node not found at marker \(marker)")
}
return Syntax(token)
}
let diagnosticNode = try node(at: self.locationMarker)
let notes = try self.noteDescriptors.map { noteDescriptor in
Note(
node: try node(at: noteDescriptor.locationMarker),
message: SimpleNoteMessage(message: noteDescriptor.message, noteID: noteDescriptor.id)
)
}
return Diagnostic(
node: diagnosticNode,
message: SimpleDiagnosticMessage(
message: self.message,
diagnosticID: self.id,
severity: self.severity
),
highlights: self.highlight,
notes: notes,
fixIts: self.fixIts
)
}
}
/// Represents a descriptor for constructing a note message in testing.
struct NoteDescriptor {
/// The marker pointing to location in source code.
let locationMarker: LocationMarker
/// The ID associated with the note message.
let id: MessageID
/// The textual content of the note to be displayed.
let message: String
}
/// A simple implementation of the `NoteMessage` protocol for testing.
/// This struct holds the message text and a fix-it ID for a note.
struct SimpleNoteMessage: NoteMessage {
/// The textual content of the note to be displayed.
let message: String
/// The unique identifier for this note message.
let noteID: MessageID
}
/// A simple implementation of the `DiagnosticMessage` protocol for testing.
/// This struct holds the message text, diagnostic ID, and severity for a diagnostic.
struct SimpleDiagnosticMessage: DiagnosticMessage {
/// The textual content of the diagnostic message to be displayed.
let message: String
/// The ID associated with the diagnostic message for categorization or referencing.
let diagnosticID: MessageID
/// The severity level of the diagnostic message.
let severity: DiagnosticSeverity
}
/// Asserts that the annotated source generated from diagnostics matches an expected annotated source.
///
/// - Parameters:
/// - markedSource: The source code with location markers `LocationMarker` for diagnostics.
/// - withDiagnostics: An array of diagnostic descriptors to generate diagnostics.
/// - matches: The expected annotated source after applying the diagnostics.
/// - file: The file in which failure occurred.
/// - line: The line number on which failure occurred.
func assertAnnotated(
markedSource: String,
withDiagnostics diagnosticDescriptors: [DiagnosticDescriptor],
matches expectedAnnotatedSource: String,
file: StaticString = #filePath,
line: UInt = #line
) {
let (markers, source) = extractMarkers(markedSource)
let tree = Parser.parse(source: source)
var diagnostics: [Diagnostic] = []
do {
diagnostics = try diagnosticDescriptors.map {
try $0.createDiagnostic(inSyntaxTree: tree, usingLocationMarkers: markers)
}
} catch {
XCTFail(error.localizedDescription, file: file, line: line)
}
let annotatedSource = DiagnosticsFormatter.annotatedSource(tree: tree, diags: diagnostics)
assertStringsEqualWithDiff(
annotatedSource,
expectedAnnotatedSource,
file: file,
line: line
)
}
|