@@ -132,6 +132,77 @@ impl Display for ResolutionKind {
132132 }
133133}
134134
135+ /// Identifies the kind of a `Problem` without carrying borrowed data.
136+ ///
137+ /// Each variant corresponds 1:1 to a `Problem` variant. The exhaustive
138+ /// match in `Problem::kind` ensures that adding a new `Problem` variant
139+ /// without updating this enum causes a compile error.
140+ #[ derive( Clone , Copy , Debug , Eq , PartialEq , Hash ) ]
141+ #[ expect( missing_docs) ]
142+ pub enum ProblemKind {
143+ LocalSpecFileOrphaned ,
144+ UnparseableLocalFile ,
145+ BlessedVersionMissingLocal ,
146+ BlessedVersionExtraLocalSpec ,
147+ BlessedVersionCompareError ,
148+ BlessedVersionBroken ,
149+ BlessedLatestVersionBytewiseMismatch ,
150+ LockstepMissingLocal ,
151+ LockstepStale ,
152+ LocalVersionMissingLocal ,
153+ LocalVersionExtra ,
154+ LocalVersionStale ,
155+ GeneratedSourceMissing ,
156+ GeneratedValidationError ,
157+ ExtraFileStale ,
158+ LatestLinkMissing ,
159+ LatestLinkStale ,
160+ BlessedVersionShouldBeGitStub ,
161+ GitStubShouldBeJson ,
162+ BlessedVersionCorruptedLocal ,
163+ DuplicateLocalFile ,
164+ GitStubCommitStale ,
165+ GitStubFirstCommitUnknown ,
166+ }
167+
168+ /// Owned summary of a `Problem` for test assertions.
169+ ///
170+ /// Contains just enough information to identify a problem: which API it
171+ /// belongs to, which version (if any), and its [`ProblemKind`]. Because all
172+ /// fields are owned and implement `PartialEq`, summaries can be compared
173+ /// with `assert_eq!`.
174+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
175+ pub struct ProblemSummary {
176+ /// The API this problem is associated with.
177+ pub api_ident : ApiIdent ,
178+ /// The version this problem is associated with, or `None` for
179+ /// non-version-specific problems (e.g. orphaned files, symlinks).
180+ pub version : Option < semver:: Version > ,
181+ /// The kind of problem.
182+ pub kind : ProblemKind ,
183+ }
184+
185+ impl ProblemSummary {
186+ /// Creates a new problem summary for a version-specific problem.
187+ pub fn new ( api_ident : & str , version : & str , kind : ProblemKind ) -> Self {
188+ ProblemSummary {
189+ api_ident : ApiIdent :: from ( api_ident) ,
190+ version : Some ( version. parse ( ) . expect ( "valid semver" ) ) ,
191+ kind,
192+ }
193+ }
194+
195+ /// Creates a new problem summary for a non-version-specific problem
196+ /// (e.g. symlink issues, orphaned files).
197+ pub fn for_api ( api_ident : & str , kind : ProblemKind ) -> Self {
198+ ProblemSummary {
199+ api_ident : ApiIdent :: from ( api_ident) ,
200+ version : None ,
201+ kind,
202+ }
203+ }
204+ }
205+
135206/// Describes a problem resolving the blessed spec(s), generated spec(s), and
136207/// local spec files for a particular API.
137208#[ derive( Debug , Error ) ]
@@ -363,6 +434,72 @@ pub enum Problem<'a> {
363434}
364435
365436impl < ' a > Problem < ' a > {
437+ /// Returns the discriminant of this problem as a [`ProblemKind`].
438+ ///
439+ /// The match is exhaustive (no wildcard), so adding a new `Problem`
440+ /// variant without updating this method causes a compile error.
441+ pub fn kind ( & self ) -> ProblemKind {
442+ match self {
443+ Problem :: LocalSpecFileOrphaned { .. } => {
444+ ProblemKind :: LocalSpecFileOrphaned
445+ }
446+ Problem :: UnparseableLocalFile { .. } => {
447+ ProblemKind :: UnparseableLocalFile
448+ }
449+ Problem :: BlessedVersionMissingLocal { .. } => {
450+ ProblemKind :: BlessedVersionMissingLocal
451+ }
452+ Problem :: BlessedVersionExtraLocalSpec { .. } => {
453+ ProblemKind :: BlessedVersionExtraLocalSpec
454+ }
455+ Problem :: BlessedVersionCompareError { .. } => {
456+ ProblemKind :: BlessedVersionCompareError
457+ }
458+ Problem :: BlessedVersionBroken { .. } => {
459+ ProblemKind :: BlessedVersionBroken
460+ }
461+ Problem :: BlessedLatestVersionBytewiseMismatch { .. } => {
462+ ProblemKind :: BlessedLatestVersionBytewiseMismatch
463+ }
464+ Problem :: LockstepMissingLocal { .. } => {
465+ ProblemKind :: LockstepMissingLocal
466+ }
467+ Problem :: LockstepStale { .. } => ProblemKind :: LockstepStale ,
468+ Problem :: LocalVersionMissingLocal { .. } => {
469+ ProblemKind :: LocalVersionMissingLocal
470+ }
471+ Problem :: LocalVersionExtra { .. } => ProblemKind :: LocalVersionExtra ,
472+ Problem :: LocalVersionStale { .. } => ProblemKind :: LocalVersionStale ,
473+ Problem :: GeneratedSourceMissing { .. } => {
474+ ProblemKind :: GeneratedSourceMissing
475+ }
476+ Problem :: GeneratedValidationError { .. } => {
477+ ProblemKind :: GeneratedValidationError
478+ }
479+ Problem :: ExtraFileStale { .. } => ProblemKind :: ExtraFileStale ,
480+ Problem :: LatestLinkMissing { .. } => ProblemKind :: LatestLinkMissing ,
481+ Problem :: LatestLinkStale { .. } => ProblemKind :: LatestLinkStale ,
482+ Problem :: BlessedVersionShouldBeGitStub { .. } => {
483+ ProblemKind :: BlessedVersionShouldBeGitStub
484+ }
485+ Problem :: GitStubShouldBeJson { .. } => {
486+ ProblemKind :: GitStubShouldBeJson
487+ }
488+ Problem :: BlessedVersionCorruptedLocal { .. } => {
489+ ProblemKind :: BlessedVersionCorruptedLocal
490+ }
491+ Problem :: DuplicateLocalFile { .. } => {
492+ ProblemKind :: DuplicateLocalFile
493+ }
494+ Problem :: GitStubCommitStale { .. } => {
495+ ProblemKind :: GitStubCommitStale
496+ }
497+ Problem :: GitStubFirstCommitUnknown { .. } => {
498+ ProblemKind :: GitStubFirstCommitUnknown
499+ }
500+ }
501+ }
502+
366503 pub fn is_fixable ( & self ) -> bool {
367504 self . fix ( ) . is_some ( )
368505 }
@@ -914,7 +1051,7 @@ fn symlink_file(target: &str, path: &Utf8Path) -> std::io::Result<()> {
9141051/// local spec files for a given API
9151052pub struct Resolved < ' a > {
9161053 notes : Vec < Note > ,
917- non_version_problems : Vec < Problem < ' a > > ,
1054+ non_version_problems : Vec < ( ApiIdent , Option < semver :: Version > , Problem < ' a > ) > ,
9181055 api_results : BTreeMap < ApiIdent , ApiResolved < ' a > > ,
9191056 nexpected_documents : usize ,
9201057}
@@ -961,12 +1098,23 @@ impl<'a> Resolved<'a> {
9611098 // Get the other easy case out of the way: if there are any local spec
9621099 // files for APIs or API versions that aren't supported any more, that's
9631100 // a (fixable) problem.
964- let mut non_version_problems: Vec < Problem < ' _ > > =
965- resolve_orphaned_local_specs ( & supported_versions_by_api, local)
966- . map ( |spec_file_name| Problem :: LocalSpecFileOrphaned {
967- spec_file_name : spec_file_name. clone ( ) ,
968- } )
969- . collect ( ) ;
1101+ let mut non_version_problems: Vec < (
1102+ ApiIdent ,
1103+ Option < semver:: Version > ,
1104+ Problem < ' _ > ,
1105+ ) > = resolve_orphaned_local_specs ( & supported_versions_by_api, local)
1106+ . map ( |spec_file_name| {
1107+ let ident = spec_file_name. ident ( ) . clone ( ) ;
1108+ let version = Some ( spec_file_name. version ( ) . clone ( ) ) ;
1109+ (
1110+ ident,
1111+ version,
1112+ Problem :: LocalSpecFileOrphaned {
1113+ spec_file_name : spec_file_name. clone ( ) ,
1114+ } ,
1115+ )
1116+ } )
1117+ . collect ( ) ;
9701118
9711119 // Resolve each of the supported API versions first, so we know what
9721120 // paths will be written. (Do this in parallel across each API version.)
@@ -1039,13 +1187,17 @@ impl<'a> Resolved<'a> {
10391187 }
10401188 }
10411189
1042- for ( _ident , api_files) in local. iter ( ) {
1190+ for ( ident , api_files) in local. iter ( ) {
10431191 for unparseable in api_files. unparseable_files ( ) {
10441192 // Only report if no fix will overwrite this path.
10451193 if !paths_written. contains ( & unparseable. path ) {
1046- non_version_problems. push ( Problem :: UnparseableLocalFile {
1047- unparseable_file : unparseable. clone ( ) ,
1048- } ) ;
1194+ non_version_problems. push ( (
1195+ ident. clone ( ) ,
1196+ None ,
1197+ Problem :: UnparseableLocalFile {
1198+ unparseable_file : unparseable. clone ( ) ,
1199+ } ,
1200+ ) ) ;
10491201 }
10501202 }
10511203 }
@@ -1067,7 +1219,7 @@ impl<'a> Resolved<'a> {
10671219 }
10681220
10691221 pub fn general_problems ( & self ) -> impl Iterator < Item = & Problem < ' a > > + ' _ {
1070- self . non_version_problems . iter ( )
1222+ self . non_version_problems . iter ( ) . map ( | ( _ , _ , problem ) | problem )
10711223 }
10721224
10731225 pub fn resolution_for_api_version (
@@ -1086,6 +1238,46 @@ impl<'a> Resolved<'a> {
10861238 self . general_problems ( ) . any ( |p| !p. is_fixable ( ) )
10871239 || self . api_results . values ( ) . any ( |a| a. has_unfixable_problems ( ) )
10881240 }
1241+
1242+ /// Returns an owned, ordered list of all problems as summaries.
1243+ ///
1244+ /// Order: general (non-version-specific) problems first, then per-API
1245+ /// (sorted by ident), per-version (sorted by semver), then symlink
1246+ /// problems.
1247+ pub fn problem_summaries ( & self ) -> Vec < ProblemSummary > {
1248+ let mut summaries = Vec :: new ( ) ;
1249+
1250+ // General problems.
1251+ for ( ident, version, problem) in & self . non_version_problems {
1252+ summaries. push ( ProblemSummary {
1253+ api_ident : ident. clone ( ) ,
1254+ version : version. clone ( ) ,
1255+ kind : problem. kind ( ) ,
1256+ } ) ;
1257+ }
1258+
1259+ // Per-API problems.
1260+ for ( ident, api_resolved) in & self . api_results {
1261+ for ( version, resolution) in & api_resolved. by_version {
1262+ for problem in resolution. problems ( ) {
1263+ summaries. push ( ProblemSummary {
1264+ api_ident : ident. clone ( ) ,
1265+ version : Some ( version. clone ( ) ) ,
1266+ kind : problem. kind ( ) ,
1267+ } ) ;
1268+ }
1269+ }
1270+ if let Some ( symlink) = & api_resolved. symlink {
1271+ summaries. push ( ProblemSummary {
1272+ api_ident : ident. clone ( ) ,
1273+ version : None ,
1274+ kind : symlink. kind ( ) ,
1275+ } ) ;
1276+ }
1277+ }
1278+
1279+ summaries
1280+ }
10891281}
10901282
10911283struct ApiResolved < ' a > {
0 commit comments