66using Microsoft . TypeSpec . Generator . Snippets ;
77using Microsoft . TypeSpec . Generator . Statements ;
88using System ;
9+ using System . ClientModel . Primitives ;
910using System . Collections . Generic ;
1011using System . Linq ;
1112using static Microsoft . TypeSpec . Generator . Snippets . Snippet ;
@@ -46,6 +47,8 @@ public class OpenAILibraryVisitor : ScmLibraryVisitor
4647 [ "ReasoningResponseItem" ] = [ _readonlyStatusReplacementInfo ] ,
4748 [ "WebSearchCallResponseItem" ] = [ _readonlyStatusReplacementInfo ] ,
4849 } ;
50+ private static readonly SingleLineCommentStatement OptionalDefinedCheckComment =
51+ new ( "Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup" ) ;
4952
5053 protected override TypeProvider VisitType ( TypeProvider type )
5154 {
@@ -118,101 +121,161 @@ protected override FieldProvider VisitField(FieldProvider field)
118121
119122 protected override MethodProvider VisitMethod ( MethodProvider method )
120123 {
121- if ( method . Signature . Name != JsonModelWriteCoreMethodName )
124+ // If there are no body statements, or the body statements are not MethodBodyStatements,
125+ // return the method as is return the method as is
126+ if ( method . Signature . Name != JsonModelWriteCoreMethodName ||
127+ method . BodyStatements is not MethodBodyStatements statements )
122128 {
123129 return method ;
124130 }
125131
126- // If there are no body statements, return the method as is
127- if ( method . BodyStatements == null )
128- {
129- return method ;
130- }
132+ var updatedStatements = new List < MethodBodyStatement > ( ) ;
133+ var flattenedStatements = new List < MethodBodyStatement > ( ) ;
131134
132- // If the body statements are not MethodBodyStatements, return the method as is
133- if ( method . BodyStatements is not MethodBodyStatements statements )
135+ foreach ( var stmt in statements )
134136 {
135- return method ;
137+ if ( stmt is SuppressionStatement { Inner : not null } suppressionStatement )
138+ {
139+ // TO-DO: remove once enumerable logic is updated to handle nested suppression statements
140+ flattenedStatements . Add ( suppressionStatement . DisableStatement ) ;
141+ flattenedStatements . AddRange ( suppressionStatement . Inner ) ;
142+ flattenedStatements . Add ( suppressionStatement . RestoreStatement ) ;
143+ }
144+ else
145+ {
146+ flattenedStatements . Add ( stmt ) ;
147+ }
136148 }
137149
138- var updatedStatements = new List < MethodBodyStatement > ( ) ;
139- var flattenedStatements = statements . ToArray ( ) ;
140-
141150 List < WritePropertyNameAdditionalReplacementInfo > additionalConditionsForWritingType
142151 = TypeNameToWritePropertyNameAdditionalConditionMap . GetValueOrDefault ( method . EnclosingType . Name ) ?? [ ] ;
143152
144- for ( int line = 0 ; line < flattenedStatements . Length ; line ++ )
153+ for ( int line = 0 ; line < flattenedStatements . Count ; line ++ )
145154 {
146155 var statement = flattenedStatements [ line ] ;
147156
148157 // Much of the customization centers around treatment of WritePropertyName
149158 string ? writePropertyNameTarget = GetWritePropertyNameTargetFromStatement ( statement ) ;
150159
151- if ( statement is IfStatement ifStatement )
160+ switch ( statement )
152161 {
153- // If we already have an if statement that contains property writing, we need to add the condition to the existing if statement
154- if ( writePropertyNameTarget is not null )
155- {
156- ifStatement . Update ( condition : ifStatement . Condition . As < bool > ( ) . And ( GetContainsKeyCondition ( writePropertyNameTarget ) ) ) ;
157- }
162+ // If we already have an if statement that contains property writing, we need to add the condition to the existing if statement.
163+ // For dynamic models, we can skip adding the SARD condition.
164+ case IfStatement ifStatement :
165+ ProcessIfStatement ( ifStatement , writePropertyNameTarget , additionalConditionsForWritingType , updatedStatements ) ;
166+ break ;
167+ case IfElseStatement ifElseStatement when GetPatchContainsExpression ( ifElseStatement . If . Condition ) != null :
168+ ProcessIfElseStatement ( ifElseStatement , writePropertyNameTarget , additionalConditionsForWritingType , updatedStatements ) ;
169+ break ;
170+ case var _ when writePropertyNameTarget is not null :
171+ line = ProcessWritePropertyNameStatement ( statement , writePropertyNameTarget , additionalConditionsForWritingType , flattenedStatements , line , updatedStatements ) ;
172+ break ;
173+ default :
174+ updatedStatements . Add ( statement ) ;
175+ break ;
176+ }
177+ }
158178
159- // Handle writing AdditionalProperties
160- else if ( ifStatement . Body . First ( ) is ForEachStatement foreachStatement )
161- {
162- foreachStatement . Body . Insert (
163- 0 ,
164- new IfStatement (
165- Static ( new ModelSerializationExtensionsDefinition ( ) . Type ) . Invoke (
166- IsSentinelValueMethodName ,
167- foreachStatement . ItemVariable . Property ( "Value" ) ) )
168- {
169- Continue
170- } ) ;
171- }
179+ method . Update ( bodyStatements : updatedStatements ) ;
180+ return method ;
181+ }
182+
183+ private static void ProcessIfStatement (
184+ IfStatement ifStatement ,
185+ string ? writePropertyNameTarget ,
186+ List < WritePropertyNameAdditionalReplacementInfo > additionalConditionsForWritingType ,
187+ List < MethodBodyStatement > updatedStatements )
188+ {
189+ if ( writePropertyNameTarget is not null )
190+ {
191+ ValueExpression ? patchContainsCondition = GetPatchContainsExpression ( ifStatement . Condition ) ;
172192
173- updatedStatements . Add ( ifStatement ) ;
193+ if ( patchContainsCondition is null )
194+ {
195+ ifStatement . Update ( condition : ifStatement . Condition . As < bool > ( ) . And ( GetContainsKeyCondition ( writePropertyNameTarget ) ) ) ;
174196 }
175- else if ( writePropertyNameTarget is not null )
197+ else if ( additionalConditionsForWritingType . FirstOrDefault ( additionalCondition => additionalCondition . JsonName == writePropertyNameTarget ) is var matchingReplacementInfo && matchingReplacementInfo != null )
176198 {
177- ScopedApi < bool > enclosingIfCondition = GetContainsKeyCondition ( writePropertyNameTarget ) ;
178-
179- if ( additionalConditionsForWritingType
180- . FirstOrDefault ( additionalCondition => additionalCondition . JsonName == writePropertyNameTarget )
181- is WritePropertyNameAdditionalReplacementInfo matchingReplacementInfo )
199+ updatedStatements . Add ( OptionalDefinedCheckComment ) ;
200+ ifStatement . Update ( condition : GetOptionalIsCollectionDefinedCondition ( matchingReplacementInfo ) . And ( ifStatement . Condition ) ) ;
201+ }
202+ }
203+ // Handle writing AdditionalProperties
204+ else if ( ifStatement . Body . First ( ) is ForEachStatement foreachStatement )
205+ {
206+ foreachStatement . Body . Insert (
207+ 0 ,
208+ new IfStatement (
209+ Static ( new ModelSerializationExtensionsDefinition ( ) . Type ) . Invoke (
210+ IsSentinelValueMethodName ,
211+ foreachStatement . ItemVariable . Property ( "Value" ) ) )
182212 {
183- MethodBodyStatement commentStatement
184- = new SingleLineCommentStatement ( "Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup" ) ;
185- updatedStatements . Add ( commentStatement ) ;
186- enclosingIfCondition = GetOptionalIsCollectionDefinedCondition ( matchingReplacementInfo )
187- . And ( enclosingIfCondition ) ;
188- }
213+ Continue
214+ } ) ;
215+ }
189216
190- var ifSt = new IfStatement ( enclosingIfCondition ) { statement } ;
217+ updatedStatements . Add ( ifStatement ) ;
218+ }
191219
192- // If this is a plain expression statement, we need to add the next statement as well which
193- // will either write the property value or start writing an array
194- if ( statement is ExpressionStatement )
195- {
196- ifSt . Add ( flattenedStatements [ ++ line ] ) ;
197- // Include array writing in the if statement
198- if ( flattenedStatements [ line + 1 ] is ForEachStatement )
199- {
200- // Foreach
201- ifSt . Add ( flattenedStatements [ ++ line ] ) ;
202- // End array
203- ifSt . Add ( flattenedStatements [ ++ line ] ) ;
204- }
205- }
206- updatedStatements . Add ( ifSt ) ;
207- }
208- else
220+ private static void ProcessIfElseStatement (
221+ IfElseStatement ifElseStatement ,
222+ string ? writePropertyNameTarget ,
223+ List < WritePropertyNameAdditionalReplacementInfo > additionalConditionsForWritingType ,
224+ List < MethodBodyStatement > updatedStatements )
225+ {
226+ if ( ifElseStatement . Else is null )
227+ {
228+ updatedStatements . Add ( ifElseStatement ) ;
229+ return ;
230+ }
231+
232+ if ( additionalConditionsForWritingType . FirstOrDefault ( additionalCondition => additionalCondition . JsonName == writePropertyNameTarget ) is var matchingReplacementInfo && matchingReplacementInfo != null )
233+ {
234+ var enclosingCondition = GetOptionalIsCollectionDefinedCondition ( matchingReplacementInfo ) ;
235+ var updatedCondition = new IfStatement ( enclosingCondition ) { ifElseStatement . Else } ;
236+
237+ ifElseStatement . Update ( elseStatement : new MethodBodyStatements ( [ OptionalDefinedCheckComment , updatedCondition ] ) ) ;
238+ }
239+
240+ updatedStatements . Add ( ifElseStatement ) ;
241+ }
242+
243+ private static int ProcessWritePropertyNameStatement (
244+ MethodBodyStatement statement ,
245+ string writePropertyNameTarget ,
246+ List < WritePropertyNameAdditionalReplacementInfo > additionalConditionsForWritingType ,
247+ List < MethodBodyStatement > flattenedStatements ,
248+ int currentLine ,
249+ List < MethodBodyStatement > updatedStatements )
250+ {
251+ var line = currentLine ;
252+ ScopedApi < bool > enclosingIfCondition = GetContainsKeyCondition ( writePropertyNameTarget ) ;
253+
254+ if ( additionalConditionsForWritingType . FirstOrDefault ( additionalCondition => additionalCondition . JsonName == writePropertyNameTarget ) is var matchingReplacementInfo && matchingReplacementInfo != null )
255+ {
256+ updatedStatements . Add ( OptionalDefinedCheckComment ) ;
257+ enclosingIfCondition = GetOptionalIsCollectionDefinedCondition ( matchingReplacementInfo ) . And ( enclosingIfCondition ) ;
258+ }
259+
260+ var ifSt = new IfStatement ( enclosingIfCondition ) { statement } ;
261+
262+ // If this is a plain expression statement, we need to add the next statement as well which
263+ // will either write the property value or start writing an array
264+ if ( statement is ExpressionStatement )
265+ {
266+ ifSt . Add ( flattenedStatements [ ++ line ] ) ;
267+ // Include array writing in the if statement
268+ if ( flattenedStatements [ line + 1 ] is ForEachStatement )
209269 {
210- updatedStatements . Add ( statement ) ;
270+ // Foreach
271+ ifSt . Add ( flattenedStatements [ ++ line ] ) ;
272+ // End array
273+ ifSt . Add ( flattenedStatements [ ++ line ] ) ;
211274 }
212275 }
213-
214- method . Update ( bodyStatements : updatedStatements ) ;
215- return method ;
276+
277+ updatedStatements . Add ( ifSt ) ;
278+ return line ;
216279 }
217280
218281 private static ScopedApi < bool > GetContainsKeyCondition ( string propertyName )
@@ -235,6 +298,10 @@ private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
235298 {
236299 return stringLiteralExpression . Literal ? . ToString ( ) ;
237300 }
301+ if ( statement is SuppressionStatement suppressionStatement )
302+ {
303+ return GetWritePropertyNameTargetFromStatement ( suppressionStatement . Inner ) ;
304+ }
238305 else if ( statement is MethodBodyStatements compoundStatements )
239306 {
240307 foreach ( MethodBodyStatement innerStatement in compoundStatements . Statements )
@@ -270,4 +337,45 @@ public class WritePropertyNameAdditionalReplacementInfo(string propertyName, str
270337 public string JsonName { get ; set ; } = jsonName ;
271338 public bool IsCollection { get ; set ; } = isCollection ;
272339 }
340+
341+
342+ /// <summary>
343+ /// Recursively checks if the given expression or any of its sub-expressions is a call to Patch.Contains().
344+ /// Handles various wrapping scenarios including unary operators, binary operators, and nested expressions.
345+ /// </summary>
346+ private static ValueExpression ? GetPatchContainsExpression ( ValueExpression ? expression )
347+ {
348+ if ( expression is null )
349+ {
350+ return null ;
351+ }
352+
353+ #pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
354+ return expression switch
355+ {
356+ // Case 1: Direct Patch.Contains() call
357+ ScopedApi < bool > { Original : InvokeMethodExpression { InstanceReference : ScopedApi < JsonPatch > } } => expression ,
358+
359+ // Case 2: !Patch.Contains() call
360+ ScopedApi < bool > { Original : UnaryOperatorExpression { Operator : "!" , Operand : ScopedApi < bool > { Original : InvokeMethodExpression { InstanceReference : ScopedApi < JsonPatch > } } } } => expression ,
361+
362+ // Case 3 & 4: Binary operator expression (wrapped or unwrapped)
363+ ScopedApi < bool > { Original : BinaryOperatorExpression binaryExpr } =>
364+ GetPatchContainsExpression ( binaryExpr . Left ) ?? GetPatchContainsExpression ( binaryExpr . Right ) ,
365+
366+ BinaryOperatorExpression binaryExpr =>
367+ GetPatchContainsExpression ( binaryExpr . Left ) ?? GetPatchContainsExpression ( binaryExpr . Right ) ,
368+
369+ // Case 5: Direct UnaryOperatorExpression (not wrapped in ScopedApi)
370+ UnaryOperatorExpression { Operator : "!" } unaryExpr =>
371+ GetPatchContainsExpression ( unaryExpr . Operand ) != null ? expression : null ,
372+
373+ // Case 6: Direct InvokeMethodExpression (not wrapped in ScopedApi)
374+ InvokeMethodExpression { InstanceReference : ScopedApi < JsonPatch > } => expression ,
375+
376+ _ => null
377+ } ;
378+
379+ #pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
380+ }
273381}
0 commit comments