Skip to content

Commit bf93149

Browse files
Add extraction for review discussions
- Add extraction logic - Add Discussion struct and property to Proposal - Add validation issues for missing and malformed discussions - Update tests with new expected results - Add proposals to Malformed snapshot to test missing or malformed field values
1 parent f3887b0 commit bf93149

File tree

11 files changed

+5075
-6
lines changed

11 files changed

+5075
-6
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
9+
10+
import Markdown
11+
import EvolutionMetadataModel
12+
13+
struct DiscussionExtractor: MarkupWalker, ValueExtractor {
14+
15+
private var warnings: [Proposal.Issue] = []
16+
private var errors: [Proposal.Issue] = []
17+
18+
private var discussions: [Proposal.Discussion] = []
19+
20+
mutating func extractValue(from sourceValues: (headerFieldsByLabel: [String : ListItem], proposalID: String)) -> ExtractionResult<[Proposal.Discussion]> {
21+
22+
// VALIDATION ENHANCEMENT: Normalize naming to 'Review' in the source proposals.
23+
if let (_, headerField) = sourceValues.headerFieldsByLabel[["Review", "Reviews", "Decision Notes", "Decision notes"]] {
24+
visit(headerField)
25+
26+
// VALIDATION ENHANCEMENT: Correct proposals with known issues and remove special case logic.
27+
// Currently a fair number of older proposals are missing links to discussions or do not
28+
// format discussions correctly. Those issues should be corrected in the proposals themselves.
29+
// Once all of those issues are resolved, the legacy check can be removed.
30+
if discussions.isEmpty && !Legacy.discussionExtractionFailures.contains(sourceValues.proposalID) {
31+
errors.append(ValidationIssue.discussionExtractionFailure)
32+
}
33+
} else {
34+
// VALIDATION ENHANCEMENT: Add field to proposals with missing field and remove special case logic.
35+
// 'Review' is a required field, but currently a fair number of older proposals are missing the
36+
// field for a variety of reasons. Those issues should be corrected in the proposals themselves.
37+
// Once all of those issues are resolved, the legacy check can be removed.
38+
// Note that some very early proposals may not have valid discussions be extracted.
39+
if !Legacy.missingReviewFields.contains(sourceValues.proposalID) {
40+
errors.append(ValidationIssue.missingReviewField)
41+
}
42+
}
43+
44+
return ExtractionResult(value: discussions, warnings: warnings, errors: errors)
45+
}
46+
47+
// Existing proposals with known validation errors.
48+
// The listed exceptions will not generate warnings and errors for the known issues.
49+
private enum Legacy {
50+
static let missingReviewFields: Set<String> = ["SE-0001", "SE-0002", "SE-0004", "SE-0020", "SE-0051", "SE-0079", "SE-0100", "SE-0176", "SE-0177", "SE-0188", "SE-0193", "SE-0194", "SE-0196", "SE-0198", "SE-0201", "SE-0203", "SE-0205", "SE-0208", "SE-0209", "SE-0210", "SE-0212", "SE-0213", "SE-0219", "SE-0243", "SE-0245", "SE-0247", "SE-0248", "SE-0249", "SE-0250", "SE-0252", "SE-0259", "SE-0263", "SE-0268", "SE-0269", "SE-0273", "SE-0278", "SE-0284", "SE-0289", "SE-0295", "SE-0300", "SE-0303", "SE-0304", "SE-0312", "SE-0313", "SE-0317", "SE-0318", "SE-0337", "SE-0338", "SE-0341", "SE-0343", "SE-0344", "SE-0348", "SE-0350", "SE-0356", "SE-0365", "SE-0385"]
51+
52+
static let discussionExtractionFailures: Set<String> = ["SE-0099", "SE-0363", "SE-0378", "SE-0391", "SE-0392"]
53+
}
54+
55+
mutating func visitLink(_ link: Link) -> () {
56+
guard let linkInfo = LinkInfo(link: link) else {
57+
return
58+
}
59+
60+
if let discussionURL = linkInfo.swiftForumsDestination {
61+
discussions.append(Proposal.Discussion(name: linkInfo.text, link: discussionURL))
62+
} else {
63+
warnings.append(ValidationIssue.invalidDiscussionLink)
64+
}
65+
}
66+
}

Sources/EvolutionMetadataExtraction/Extractors/ProposalMetadataExtractor.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ struct ProposalMetadataExtractor {
7676
proposal.trackingBugs = extractValue(from: headerFieldsByLabel, with: TrackingBugExtractor.self)
7777
proposal.implementation = extractValue(from: headerFieldsByLabel, with: ImplementationExtractor.self)
7878

79+
if let discussions = extractValue(from: (headerFieldsByLabel, proposalSpec.id), with: DiscussionExtractor.self) {
80+
proposal.discussions = discussions
81+
} else {
82+
errors.append(ValidationIssue.missingReviewField)
83+
}
84+
7985
if let status = extractValue(from: (headerFieldsByLabel, extractionDate), with: StatusExtractor.self) {
8086
if case .implemented(let version) = status, version == "none" {
8187
// VALIDATION ENHANCEMENT: Figure out a better way to special case the missing version strings for these proposals
@@ -167,6 +173,14 @@ struct LinkInfo {
167173
return nil
168174
}
169175
}
176+
177+
var swiftForumsDestination: String? {
178+
if destination.starts(with: "https://forums.swift.org") {
179+
return destination
180+
} else {
181+
return nil
182+
}
183+
}
170184
}
171185

172186

Sources/EvolutionMetadataExtraction/Utilities/ValidationIssue.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ enum ValidationIssue {
7676
message: "Failed to extract previous proposal IDs."
7777
)
7878

79+
static let missingReviewField = Proposal.Issue(
80+
kind: .error,
81+
stage: .parse,
82+
message: "Missing Review field."
83+
)
84+
85+
static let discussionExtractionFailure = Proposal.Issue(
86+
kind: .error,
87+
stage: .parse,
88+
message: "Failed to extract discussions from Review field."
89+
)
90+
7991
// MARK: - Parse Warnings
8092

8193
// VALIDATION ENHANCEMENT: Why is this only a warning?
@@ -166,6 +178,12 @@ enum ValidationIssue {
166178
stage: .validate,
167179
message: "Implementation links to a non-Swift repository."
168180
)
181+
182+
static let invalidDiscussionLink = Proposal.Issue(
183+
kind: .warning,
184+
stage: .validate,
185+
message: "Discussion link doesn't refer to a Swift forum thread. Discussion removed."
186+
)
169187

170188
// VALIDATION ENHANCEMENTS: Consider not including the date in the review message, or not including time components.
171189
// Comparing results currently breaks if recorded test values and actual values are generated in different time zones.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
9+
10+
extension Proposal {
11+
12+
/// Type that represents a discussion abou a proposal.
13+
public struct Discussion: Sendable, Codable, Equatable {
14+
15+
/// Name of the discussion
16+
public let name: String
17+
18+
/// URL string to the discussion
19+
public let link: String
20+
21+
public init(name: String = "", link: String = "") {
22+
self.name = name
23+
self.link = link
24+
}
25+
}
26+
}

Sources/EvolutionMetadataModel/Proposal.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ public struct Proposal: Equatable, Sendable, Codable, Identifiable {
114114
/// Optional array of implementation links
115115
public var implementation: [Implementation]?
116116

117+
/// Array of discussions about the proposal. May be an empty array.
118+
public var discussions: [Discussion]
119+
117120
/// Optional array of warnings
118121
///
119122
/// Present only if validation warnings were found when extracting metadata from this proposal.
@@ -124,7 +127,7 @@ public struct Proposal: Equatable, Sendable, Codable, Identifiable {
124127
/// Present only if validation errors were found when extracting metadata from this proposal.
125128
public var errors: [Issue]?
126129

127-
public init(id: String = "", title: String = "", summary: String = "", link: String = "", sha: String = "", authors: [Person] = [], reviewManagers: [Person] = [], status: Status = .statusExtractionNotAttempted, trackingBugs: [TrackingBug]? = nil, implementation: [Implementation]? = nil, warnings: [Issue]? = nil, errors: [Issue]? = nil)
130+
public init(id: String = "", title: String = "", summary: String = "", link: String = "", sha: String = "", authors: [Person] = [], reviewManagers: [Person] = [], status: Status = .statusExtractionNotAttempted, trackingBugs: [TrackingBug]? = nil, implementation: [Implementation]? = nil, discussions: [Discussion] = [], warnings: [Issue]? = nil, errors: [Issue]? = nil)
128131
{
129132
self.id = id
130133
self.title = title
@@ -137,6 +140,7 @@ public struct Proposal: Equatable, Sendable, Codable, Identifiable {
137140
self.status = status
138141
self.trackingBugs = trackingBugs
139142
self.implementation = implementation
143+
self.discussions = discussions
140144
self.warnings = warnings
141145
self.errors = errors
142146
}

0 commit comments

Comments
 (0)