|
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