|
8 | 8 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors
|
9 | 9 | //
|
10 | 10 |
|
11 |
| -/// Combine an instance of ``KnownIssueMatcher`` with any previously-set one. |
12 |
| -/// |
13 |
| -/// - Parameters: |
14 |
| -/// - issueMatcher: A function to invoke when an issue occurs that is used to |
15 |
| -/// determine if the issue is known to occur. |
16 |
| -/// - matchCounter: The counter responsible for tracking the number of matches |
17 |
| -/// found with `issueMatcher`. |
18 |
| -/// |
19 |
| -/// - Returns: A new instance of ``Configuration`` or `nil` if there was no |
20 |
| -/// current configuration set. |
21 |
| -private func _combineIssueMatcher(_ issueMatcher: @escaping KnownIssueMatcher, matchesCountedBy matchCounter: Locked<Int>) -> KnownIssueMatcher { |
22 |
| - let oldIssueMatcher = Issue.currentKnownIssueMatcher |
23 |
| - return { issue in |
24 |
| - if issueMatcher(issue) || true == oldIssueMatcher?(issue) { |
25 |
| - matchCounter.increment() |
26 |
| - return true |
| 11 | +/// A type that represents an active |
| 12 | +/// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` |
| 13 | +/// call and any parent calls. |
| 14 | +/// |
| 15 | +/// A stack of these is stored in `KnownIssueScope.current`. |
| 16 | +struct KnownIssueScope: Sendable { |
| 17 | + /// A function which determines if an issue matches a known issue scope or |
| 18 | + /// any of its ancestor scopes. |
| 19 | + /// |
| 20 | + /// - Parameters: |
| 21 | + /// - issue: The issue being matched. |
| 22 | + /// |
| 23 | + /// - Returns: A known issue context containing information about the known |
| 24 | + /// issue, if the issue is considered "known" by this known issue scope or any |
| 25 | + /// ancestor scope, or `nil` otherwise. |
| 26 | + typealias Matcher = @Sendable (_ issue: Issue) -> Issue.KnownIssueContext? |
| 27 | + |
| 28 | + /// The matcher function for this known issue scope. |
| 29 | + var matcher: Matcher |
| 30 | + |
| 31 | + /// The number of issues this scope and its ancestors have matched. |
| 32 | + let matchCounter: Locked<Int> |
| 33 | + |
| 34 | + /// Create a new ``KnownIssueScope`` by combining a new issue matcher with |
| 35 | + /// any already-active scope. |
| 36 | + /// |
| 37 | + /// - Parameters: |
| 38 | + /// - parent: The context that should be checked next if `issueMatcher` |
| 39 | + /// fails to match an issue. Defaults to ``KnownIssueScope.current``. |
| 40 | + /// - issueMatcher: A function to invoke when an issue occurs that is used |
| 41 | + /// to determine if the issue is known to occur. |
| 42 | + /// - context: The context to be associated with issues matched by |
| 43 | + /// `issueMatcher`. |
| 44 | + init(parent: KnownIssueScope? = .current, issueMatcher: @escaping KnownIssueMatcher, context: Issue.KnownIssueContext) { |
| 45 | + let matchCounter = Locked(rawValue: 0) |
| 46 | + self.matchCounter = matchCounter |
| 47 | + matcher = { issue in |
| 48 | + let matchedContext = if issueMatcher(issue) { |
| 49 | + context |
| 50 | + } else { |
| 51 | + parent?.matcher(issue) |
| 52 | + } |
| 53 | + if matchedContext != nil { |
| 54 | + matchCounter.increment() |
| 55 | + } |
| 56 | + return matchedContext |
27 | 57 | }
|
28 |
| - return false |
29 | 58 | }
|
| 59 | + |
| 60 | + /// The active known issue scope for the current task, if any. |
| 61 | + /// |
| 62 | + /// If there is no call to |
| 63 | + /// ``withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)`` |
| 64 | + /// executing on the current task, the value of this property is `nil`. |
| 65 | + @TaskLocal |
| 66 | + static var current: KnownIssueScope? |
30 | 67 | }
|
31 | 68 |
|
32 | 69 | /// Check if an error matches using an issue-matching function, and throw it if
|
33 | 70 | /// it does not.
|
34 | 71 | ///
|
35 | 72 | /// - Parameters:
|
36 | 73 | /// - error: The error to test.
|
37 |
| -/// - issueMatcher: A function to which `error` is passed (after boxing it in |
38 |
| -/// an instance of ``Issue``) to determine if it is known to occur. |
| 74 | +/// - scope: The known issue scope that is processing the error. |
39 | 75 | /// - comment: An optional comment to apply to any issues generated by this
|
40 | 76 | /// function.
|
41 | 77 | /// - sourceLocation: The source location to which the issue should be
|
42 | 78 | /// attributed.
|
43 |
| -private func _matchError(_ error: any Error, using issueMatcher: KnownIssueMatcher, comment: Comment?, sourceLocation: SourceLocation) throws { |
| 79 | +private func _matchError(_ error: any Error, in scope: KnownIssueScope, comment: Comment?, sourceLocation: SourceLocation) throws { |
44 | 80 | let sourceContext = SourceContext(backtrace: Backtrace(forFirstThrowOf: error), sourceLocation: sourceLocation)
|
45 |
| - var issue = Issue(kind: .errorCaught(error), comments: Array(comment), sourceContext: sourceContext) |
46 |
| - if issueMatcher(issue) { |
| 81 | + var issue = Issue(kind: .errorCaught(error), comments: [], sourceContext: sourceContext) |
| 82 | + if let context = scope.matcher(issue) { |
47 | 83 | // It's a known issue, so mark it as such before recording it.
|
48 |
| - issue.isKnown = true |
| 84 | + issue.knownIssueContext = context |
49 | 85 | issue.record()
|
50 | 86 | } else {
|
51 | 87 | // Rethrow the error, allowing the caller to catch it or for it to propagate
|
@@ -184,18 +220,17 @@ public func withKnownIssue(
|
184 | 220 | guard precondition() else {
|
185 | 221 | return try body()
|
186 | 222 | }
|
187 |
| - let matchCounter = Locked(rawValue: 0) |
188 |
| - let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: matchCounter) |
| 223 | + let scope = KnownIssueScope(issueMatcher: issueMatcher, context: Issue.KnownIssueContext(comment: comment)) |
189 | 224 | defer {
|
190 | 225 | if !isIntermittent {
|
191 |
| - _handleMiscount(by: matchCounter, comment: comment, sourceLocation: sourceLocation) |
| 226 | + _handleMiscount(by: scope.matchCounter, comment: comment, sourceLocation: sourceLocation) |
192 | 227 | }
|
193 | 228 | }
|
194 |
| - try Issue.$currentKnownIssueMatcher.withValue(issueMatcher) { |
| 229 | + try KnownIssueScope.$current.withValue(scope) { |
195 | 230 | do {
|
196 | 231 | try body()
|
197 | 232 | } catch {
|
198 |
| - try _matchError(error, using: issueMatcher, comment: comment, sourceLocation: sourceLocation) |
| 233 | + try _matchError(error, in: scope, comment: comment, sourceLocation: sourceLocation) |
199 | 234 | }
|
200 | 235 | }
|
201 | 236 | }
|
@@ -304,18 +339,17 @@ public func withKnownIssue(
|
304 | 339 | guard await precondition() else {
|
305 | 340 | return try await body()
|
306 | 341 | }
|
307 |
| - let matchCounter = Locked(rawValue: 0) |
308 |
| - let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: matchCounter) |
| 342 | + let scope = KnownIssueScope(issueMatcher: issueMatcher, context: Issue.KnownIssueContext(comment: comment)) |
309 | 343 | defer {
|
310 | 344 | if !isIntermittent {
|
311 |
| - _handleMiscount(by: matchCounter, comment: comment, sourceLocation: sourceLocation) |
| 345 | + _handleMiscount(by: scope.matchCounter, comment: comment, sourceLocation: sourceLocation) |
312 | 346 | }
|
313 | 347 | }
|
314 |
| - try await Issue.$currentKnownIssueMatcher.withValue(issueMatcher) { |
| 348 | + try await KnownIssueScope.$current.withValue(scope) { |
315 | 349 | do {
|
316 | 350 | try await body()
|
317 | 351 | } catch {
|
318 |
| - try _matchError(error, using: issueMatcher, comment: comment, sourceLocation: sourceLocation) |
| 352 | + try _matchError(error, in: scope, comment: comment, sourceLocation: sourceLocation) |
319 | 353 | }
|
320 | 354 | }
|
321 | 355 | }
|
0 commit comments