File: DiagnosticTestingUtils.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (222 lines) | stat: -rw-r--r-- 8,163 bytes parent folder | download
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
  )
}