@@ -59,10 +59,16 @@ public sealed record ExpectedError(string Code, ExpectedErrorSource? Source);
5959 /// Result of parsing expected-errors.json.
6060 /// <see cref="Legacy"/> is true when the file used the pre-FR5a
6161 /// <c>[{code}]</c> array shape (no envelope assertions possible).
62+ /// <para>
63+ /// FR5c-finalize — <see cref="Warnings"/> holds per-entry expected warnings
64+ /// (same shape as <see cref="Errors"/>). When each entry has a non-null
65+ /// <c>Source</c>, the runner asserts per-warning envelope alignment;
66+ /// otherwise only the count is asserted.
67+ /// </para>
6268 /// </summary>
6369 public sealed record ExpectedErrorsEnvelope (
6470 IReadOnlyList < ExpectedError > Errors ,
65- int WarningsCount ,
71+ IReadOnlyList < ExpectedError > Warnings ,
6672 bool Legacy ) ;
6773
6874 /// <summary>
@@ -92,7 +98,7 @@ public static ExpectedErrorsEnvelope ParseExpectedErrorsEnvelope(JsonElement roo
9298 legacyErrors . Add ( new ExpectedError ( codeProp . GetString ( ) ! , null ) ) ;
9399 i ++ ;
94100 }
95- return new ExpectedErrorsEnvelope ( legacyErrors , 0 , Legacy : true ) ;
101+ return new ExpectedErrorsEnvelope ( legacyErrors , Array . Empty < ExpectedError > ( ) , Legacy : true ) ;
96102 }
97103
98104 // FR5a envelope shape.
@@ -104,34 +110,59 @@ public static ExpectedErrorsEnvelope ParseExpectedErrorsEnvelope(JsonElement roo
104110 throw new InvalidOperationException (
105111 "expected-errors.json: 'errors' must be an array" ) ;
106112
107- var errors = new List < ExpectedError > ( ) ;
113+ var errors = ParseEnvelopeEntryArray ( errorsEl , "errors" ) ;
114+
115+ // FR5c-finalize — warnings entries are parsed with the same envelope
116+ // shape as errors. When a fixture omits `source` on a warning entry,
117+ // the runner asserts only the count for that entry.
118+ IReadOnlyList < ExpectedError > warnings = Array . Empty < ExpectedError > ( ) ;
119+ if ( root . TryGetProperty ( "warnings" , out var warnEl ) )
120+ {
121+ if ( warnEl . ValueKind != JsonValueKind . Array )
122+ throw new InvalidOperationException (
123+ "expected-errors.json: 'warnings' must be an array" ) ;
124+ warnings = ParseEnvelopeEntryArray ( warnEl , "warnings" ) ;
125+ }
126+ return new ExpectedErrorsEnvelope ( errors , warnings , Legacy : false ) ;
127+ }
128+
129+ /// <summary>
130+ /// Parse an array of envelope entries (<c>[{code, source?}, ...]</c>).
131+ /// Used by both <c>errors[]</c> and <c>warnings[]</c> in expected-errors.json.
132+ /// <paramref name="fieldName"/> is the parent property name, used purely for
133+ /// error messages.
134+ /// </summary>
135+ private static IReadOnlyList < ExpectedError > ParseEnvelopeEntryArray (
136+ JsonElement arrEl , string fieldName )
137+ {
138+ var entries = new List < ExpectedError > ( ) ;
108139 int idx = 0 ;
109- foreach ( var item in errorsEl . EnumerateArray ( ) )
140+ foreach ( var item in arrEl . EnumerateArray ( ) )
110141 {
111142 if ( item . ValueKind != JsonValueKind . Object )
112143 throw new InvalidOperationException (
113- $ "expected-errors.json entry { idx } is not an object") ;
144+ $ "expected-errors.json { fieldName } [ { idx } ] is not an object") ;
114145 if ( ! item . TryGetProperty ( "code" , out var codeProp ) ||
115146 codeProp . ValueKind != JsonValueKind . String )
116147 throw new InvalidOperationException (
117- $ "expected-errors.json entry { idx } missing string 'code' field") ;
148+ $ "expected-errors.json { fieldName } [ { idx } ] missing string 'code' field") ;
118149 ExpectedErrorSource ? source = null ;
119150 if ( item . TryGetProperty ( "source" , out var srcEl ) &&
120151 srcEl . ValueKind == JsonValueKind . Object )
121152 {
122153 if ( ! srcEl . TryGetProperty ( "format" , out var fmtEl ) ||
123154 fmtEl . ValueKind != JsonValueKind . String )
124155 throw new InvalidOperationException (
125- $ "expected-errors.json entry { idx } : 'source.format' must be a string") ;
156+ $ "expected-errors.json { fieldName } [ { idx } ] : 'source.format' must be a string") ;
126157 if ( ! srcEl . TryGetProperty ( "files" , out var filesEl ) ||
127158 filesEl . ValueKind != JsonValueKind . Array )
128159 throw new InvalidOperationException (
129- $ "expected-errors.json entry { idx } : 'source.files' must be a string[]") ;
160+ $ "expected-errors.json { fieldName } [ { idx } ] : 'source.files' must be a string[]") ;
130161 var files = filesEl . EnumerateArray ( )
131162 . Select ( f => f . ValueKind == JsonValueKind . String
132163 ? f . GetString ( ) !
133164 : throw new InvalidOperationException (
134- $ "expected-errors.json entry { idx } : 'source.files' contains non-string") )
165+ $ "expected-errors.json { fieldName } [ { idx } ] : 'source.files' contains non-string") )
135166 . ToList ( ) ;
136167 string ? jsonPath = null ;
137168 if ( srcEl . TryGetProperty ( "jsonPath" , out var jpEl ) &&
@@ -157,24 +188,17 @@ public static ExpectedErrorsEnvelope ParseExpectedErrorsEnvelope(JsonElement roo
157188 {
158189 if ( referrer is null )
159190 throw new InvalidOperationException (
160- $ "expected-errors.json entry { idx } : 'source.referrer' is required when format='resolved'") ;
191+ $ "expected-errors.json { fieldName } [ { idx } ] : 'source.referrer' is required when format='resolved'") ;
161192 if ( target is null )
162193 throw new InvalidOperationException (
163- $ "expected-errors.json entry { idx } : 'source.target' is required when format='resolved'") ;
194+ $ "expected-errors.json { fieldName } [ { idx } ] : 'source.target' is required when format='resolved'") ;
164195 }
165196 source = new ExpectedErrorSource ( fmt , files , jsonPath , referrer , target ) ;
166197 }
167- errors . Add ( new ExpectedError ( codeProp . GetString ( ) ! , source ) ) ;
198+ entries . Add ( new ExpectedError ( codeProp . GetString ( ) ! , source ) ) ;
168199 idx ++ ;
169200 }
170-
171- int warningsCount = 0 ;
172- if ( root . TryGetProperty ( "warnings" , out var warnEl ) &&
173- warnEl . ValueKind == JsonValueKind . Array )
174- {
175- warningsCount = warnEl . GetArrayLength ( ) ;
176- }
177- return new ExpectedErrorsEnvelope ( errors , warningsCount , Legacy : false ) ;
201+ return entries ;
178202 }
179203
180204 /// <summary>
0 commit comments