Skip to content

Commit 1deb7ab

Browse files
fix: update visitors to account for jsonpatch changes
1 parent a6a4dfe commit 1deb7ab

32 files changed

+230
-122
lines changed

codegen/generator/src/OpenAILibraryVisitor.cs

Lines changed: 175 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.TypeSpec.Generator.Snippets;
77
using Microsoft.TypeSpec.Generator.Statements;
88
using System;
9+
using System.ClientModel.Primitives;
910
using System.Collections.Generic;
1011
using System.Linq;
1112
using 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
}

codegen/generator/src/Visitors/VisitorHelpers.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ List<MethodBodyStatement> foreachBodyStatements
2727
foreachStatement.Body.Clear();
2828
foreachStatement.Body.Add(new MethodBodyStatements(foreachBodyStatements));
2929
}
30+
else if (statements[i] is SuppressionStatement suppressionStatement
31+
&& suppressionStatement.Inner != null)
32+
{
33+
List<MethodBodyStatement> suppressionInnerStatement = [.. suppressionStatement.Inner.SelectMany(bodyStatement => bodyStatement)];
34+
VisitExplodedMethodBodyStatements(suppressionInnerStatement!, visitorFunc);
35+
var updatedSuppressionStatement = new SuppressionStatement(
36+
suppressionInnerStatement,
37+
suppressionStatement.Code,
38+
suppressionStatement.Justification);
39+
statements[i] = updatedSuppressionStatement;
40+
}
3041
else if (statements[i] is IfStatement ifStatement)
3142
{
3243
List<MethodBodyStatement> ifBodyStatements

src/Generated/Models/Chat/ChatCompletionOptions.Serialization.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,15 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
102102
}
103103
else
104104
{
105-
writer.WritePropertyName("messages"u8);
106-
SerializeMessagesValue(writer, options);
105+
// Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup
106+
if (Optional.IsCollectionDefined(Messages))
107+
{
108+
writer.WritePropertyName("messages"u8);
109+
SerializeMessagesValue(writer, options);
110+
}
107111
}
108-
if (!Patch.Contains("$.model"u8))
112+
// Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup
113+
if (Optional.IsDefined(Model) && !Patch.Contains("$.model"u8))
109114
{
110115
writer.WritePropertyName("model"u8);
111116
writer.WriteStringValue(Model);

src/Generated/Models/Chat/ChatMessage.Serialization.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,12 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
3131
writer.WritePropertyName("role"u8);
3232
writer.WriteStringValue(Role.ToSerialString());
3333
}
34-
if (Optional.IsDefined(Content) && !Patch.Contains("$.content"u8))
34+
// Plugin customization: add Content.IsInnerCollectionDefined() check
35+
if (Optional.IsDefined(Content) && Content.IsInnerCollectionDefined() && !Patch.Contains("$.content"u8))
3536
{
3637
writer.WritePropertyName("content"u8);
3738
SerializeContentValue(writer, options);
3839
}
39-
40-
Patch.WriteTo(writer);
4140
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
4241
}
4342

src/Generated/Models/Chat/ChatMessageContentPart.Serialization.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
2121
{
2222
throw new FormatException($"The model {nameof(ChatMessageContentPart)} does not support writing '{format}' format.");
2323
}
24-
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
25-
26-
Patch.WriteTo(writer);
27-
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
2824
}
2925

3026
ChatMessageContentPart IJsonModel<ChatMessageContentPart>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => JsonModelCreateCore(ref reader, options);

src/Generated/Models/Chat/ChatOutputPrediction.Serialization.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
4444
writer.WritePropertyName("type"u8);
4545
writer.WriteStringValue(Kind.ToString());
4646
}
47-
48-
Patch.WriteTo(writer);
4947
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
5048
}
5149

src/Generated/Models/Chat/ChatResponseFormat.Serialization.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
3131
writer.WritePropertyName("type"u8);
3232
writer.WriteStringValue(Kind.ToString());
3333
}
34-
35-
Patch.WriteTo(writer);
3634
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
3735
}
3836

src/Generated/Models/Chat/InternalChatCompletionStreamResponseDelta.Serialization.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit
4141
writer.WritePropertyName("audio"u8);
4242
writer.WriteObjectValue(Audio, options);
4343
}
44-
if (Optional.IsDefined(Content) && !Patch.Contains("$.content"u8))
44+
// Plugin customization: add Content.IsInnerCollectionDefined() check
45+
if (Optional.IsDefined(Content) && Content.IsInnerCollectionDefined() && !Patch.Contains("$.content"u8))
4546
{
4647
writer.WritePropertyName("content"u8);
4748
SerializeContentValue(writer, options);

0 commit comments

Comments
 (0)