using Microsoft.TypeSpec.Generator.ClientModel; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; using System; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; namespace OpenAILibraryPlugin; public class OpenAILibraryVisitor : ScmLibraryVisitor { private const string RawDataPropertyName = "SerializedAdditionalRawData"; private const string AdditionalPropertiesFieldName = "_additionalBinaryDataProperties"; private const string SentinelValueFieldName = "_sentinelValue"; private const string ModelSerializationExtensionsTypeName = "ModelSerializationExtensions"; private const string IsSentinelValueMethodName = "IsSentinelValue"; private const string JsonModelWriteCoreMethodName = "JsonModelWriteCore"; // This dictionary defines properties within types that should have their plain serialization calls wrapped with // a conditional that includes an appropriate "Optional" check, e.g.: // - Optional.IsCollectionDefined(Messages) ... writer.WritePropertyName("messages"u8) // - Optional.IsDefined(Model) ... writer.WritePropertyName("model"u8) private static WritePropertyNameAdditionalReplacementInfo _readonlyStatusReplacementInfo = new("Status", "status", isCollection: false); private static readonly Dictionary> TypeNameToWritePropertyNameAdditionalConditionMap = new() { ["ChatCompletionOptions"] = [ new("Messages", "messages", isCollection: true), new("Model", "model", isCollection: false) ], ["ResponseItem"] = [ new("Id", "id", isCollection: false), ], ["ApplyPatchCallItem"] = [_readonlyStatusReplacementInfo], ["CodeInterpreterCallResponseItem"] = [_readonlyStatusReplacementInfo], ["ComputerCallResponseItem"] = [_readonlyStatusReplacementInfo], ["ComputerCallOutputResponseItem"] = [_readonlyStatusReplacementInfo], ["FileSearchCallResponseItem"] = [_readonlyStatusReplacementInfo], ["FunctionCallResponseItem"] = [_readonlyStatusReplacementInfo], ["FunctionCallOutputResponseItem"] = [_readonlyStatusReplacementInfo], ["ImageGenerationCallResponseItem"] = [_readonlyStatusReplacementInfo], ["MessageResponseItem"] = [_readonlyStatusReplacementInfo], ["ReasoningResponseItem"] = [_readonlyStatusReplacementInfo], ["WebSearchCallResponseItem"] = [_readonlyStatusReplacementInfo], }; private static readonly SingleLineCommentStatement OptionalDefinedCheckComment = new("Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup"); protected override TypeProvider VisitType(TypeProvider type) { var additionalPropertiesField = type.Fields.FirstOrDefault(f => f.Name == AdditionalPropertiesFieldName); if (type is ModelProvider { BaseModelProvider: null } && additionalPropertiesField != null) { // Add an internal AdditionalProperties property to all base models var properties = new List(type.Properties) { new PropertyProvider($"", MethodSignatureModifiers.Internal, typeof(IDictionary), RawDataPropertyName, new ExpressionPropertyBody( additionalPropertiesField, type.DeclarationModifiers.HasFlag(TypeSignatureModifiers.ReadOnly) ? null : additionalPropertiesField.Assign(Value)), type) }; type.Update(properties: properties); } else if (type.Name == ModelSerializationExtensionsTypeName) { // Add a static BinaryData field representing the sentinel value var sentinelValueField = new FieldProvider( FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(BinaryData), SentinelValueFieldName, type, $"", BinaryDataSnippets.FromBytes(LiteralU8("\"__EMPTY__\"").Invoke("ToArray"))); var fields = new List(type.Fields) { sentinelValueField }; // Add the IsSentinelValue method var valueParameter = new ParameterProvider("value", $"", typeof(BinaryData)); var methods = new List(type.Methods) { new MethodProvider( new MethodSignature( IsSentinelValueMethodName, $"", MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static, typeof(bool), $"", [valueParameter]), new[] { Declare("sentinelSpan", typeof(ReadOnlySpan), sentinelValueField.As().ToMemory().Property("Span"), out var sentinelVariable), Declare("valueSpan", typeof(ReadOnlySpan), valueParameter.As().ToMemory().Property("Span"), out var valueVariable), Return(sentinelVariable.Invoke("SequenceEqual", valueVariable)) }, type) }; type.Update(fields: fields, methods: methods); } return type; } protected override FieldProvider VisitField(FieldProvider field) { // Make the backing additional properties field not be read only as long as the type is not readonly. if (field.Name == AdditionalPropertiesFieldName && !field.EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.ReadOnly)) { field.Modifiers &= ~FieldModifiers.ReadOnly; } return field; } protected override MethodProvider VisitMethod(MethodProvider method) { // If there are no body statements, or the body statements are not MethodBodyStatements, // return the method as is return the method as is if (method.Signature.Name != JsonModelWriteCoreMethodName || method.BodyStatements is not MethodBodyStatements statements) { return method; } var updatedStatements = new List(); var flattenedStatements = new List(); foreach (var stmt in statements) { if (stmt is SuppressionStatement { Inner: not null } suppressionStatement) { // TO-DO: remove once enumerable logic is updated to handle nested suppression statements flattenedStatements.Add(suppressionStatement.DisableStatement); flattenedStatements.AddRange(suppressionStatement.Inner); flattenedStatements.Add(suppressionStatement.RestoreStatement); } else { flattenedStatements.Add(stmt); } } List additionalConditionsForWritingType = TypeNameToWritePropertyNameAdditionalConditionMap.GetValueOrDefault(method.EnclosingType.Name) ?? []; for (int line = 0; line < flattenedStatements.Count; line++) { var statement = flattenedStatements[line]; // Much of the customization centers around treatment of WritePropertyName string? writePropertyNameTarget = GetWritePropertyNameTargetFromStatement(statement); switch (statement) { // If we already have an if statement that contains property writing, we need to add the condition to the existing if statement. // For dynamic models, we can skip adding the SARD condition. case IfStatement ifStatement: ProcessIfStatement(ifStatement, writePropertyNameTarget, additionalConditionsForWritingType, updatedStatements); break; case IfElseStatement ifElseStatement when GetPatchContainsExpression(ifElseStatement.If.Condition) != null: ProcessIfElseStatement(ifElseStatement, writePropertyNameTarget, additionalConditionsForWritingType, updatedStatements); break; case var _ when writePropertyNameTarget is not null: line = ProcessWritePropertyNameStatement(statement, writePropertyNameTarget, additionalConditionsForWritingType, flattenedStatements, line, updatedStatements); break; default: updatedStatements.Add(statement); break; } } method.Update(bodyStatements: updatedStatements); return method; } private static void ProcessIfStatement( IfStatement ifStatement, string? writePropertyNameTarget, List additionalConditionsForWritingType, List updatedStatements) { if (writePropertyNameTarget is not null) { ValueExpression? patchContainsCondition = GetPatchContainsExpression(ifStatement.Condition); if (patchContainsCondition is null) { ifStatement.Update(condition: ifStatement.Condition.As().And(GetContainsKeyCondition(writePropertyNameTarget))); } else if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null) { updatedStatements.Add(OptionalDefinedCheckComment); ifStatement.Update(condition: GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo).And(ifStatement.Condition)); } } // Handle writing AdditionalProperties else if (ifStatement.Body.First() is ForEachStatement foreachStatement) { foreachStatement.Body.Insert( 0, new IfStatement( Static(new ModelSerializationExtensionsDefinition().Type).Invoke( IsSentinelValueMethodName, foreachStatement.ItemVariable.Property("Value"))) { Continue }); } updatedStatements.Add(ifStatement); } private static void ProcessIfElseStatement( IfElseStatement ifElseStatement, string? writePropertyNameTarget, List additionalConditionsForWritingType, List updatedStatements) { if (ifElseStatement.Else is null) { updatedStatements.Add(ifElseStatement); return; } if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null) { var enclosingCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo); var updatedCondition = new IfStatement(enclosingCondition) { ifElseStatement.Else }; ifElseStatement.Update(elseStatement: new MethodBodyStatements([OptionalDefinedCheckComment, updatedCondition])); } updatedStatements.Add(ifElseStatement); } private static int ProcessWritePropertyNameStatement( MethodBodyStatement statement, string writePropertyNameTarget, List additionalConditionsForWritingType, List flattenedStatements, int currentLine, List updatedStatements) { var line = currentLine; ScopedApi enclosingIfCondition = GetContainsKeyCondition(writePropertyNameTarget); if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null) { updatedStatements.Add(OptionalDefinedCheckComment); enclosingIfCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo).And(enclosingIfCondition); } var ifSt = new IfStatement(enclosingIfCondition) { statement }; // If this is a plain expression statement, we need to add the next statement as well which // will either write the property value or start writing an array if (statement is ExpressionStatement) { ifSt.Add(flattenedStatements[++line]); // Include array writing in the if statement if (flattenedStatements[line + 1] is ForEachStatement) { // Foreach ifSt.Add(flattenedStatements[++line]); // End array ifSt.Add(flattenedStatements[++line]); } } updatedStatements.Add(ifSt); return line; } private static ScopedApi GetContainsKeyCondition(string propertyName) { return This.Property(AdditionalPropertiesFieldName) .NullConditional() .Invoke("ContainsKey", Literal(propertyName)).NotEqual(True); } private static string? GetWritePropertyNameTargetFromStatement(MethodBodyStatement? statement) { if (statement is ExpressionStatement expressionStatement && expressionStatement.Expression is InvokeMethodExpression expressionMethodInvocation && expressionMethodInvocation.MethodName == "WritePropertyName" && expressionMethodInvocation.Arguments.Count == 1 && expressionMethodInvocation.Arguments[0] is ScopedApi scopedStringApi && scopedStringApi.Original is UnaryOperatorExpression stringUnaryTargetExpression && stringUnaryTargetExpression.Operator == "u8" && stringUnaryTargetExpression.Operand is LiteralExpression stringLiteralExpression) { return stringLiteralExpression.Literal?.ToString(); } if (statement is SuppressionStatement suppressionStatement) { return GetWritePropertyNameTargetFromStatement(suppressionStatement.Inner); } else if (statement is MethodBodyStatements compoundStatements) { foreach (MethodBodyStatement innerStatement in compoundStatements.Statements) { if (GetWritePropertyNameTargetFromStatement(innerStatement) is string innerTarget) { return innerTarget; } } } else if (statement is IfStatement ifStatement) { return GetWritePropertyNameTargetFromStatement(ifStatement.Body); } else if (statement is IfElseStatement ifElseStatement) { return GetWritePropertyNameTargetFromStatement(ifElseStatement.If); } return null; } private static ScopedApi GetOptionalIsCollectionDefinedCondition(WritePropertyNameAdditionalReplacementInfo replacementInfo) { string methodName = replacementInfo.IsCollection ? "IsCollectionDefined" : "IsDefined"; return new MemberExpression(null, "Optional") .Invoke(methodName, new MemberExpression(null, replacementInfo.PropertyName)) .As(); } public class WritePropertyNameAdditionalReplacementInfo(string propertyName, string jsonName, bool isCollection) { public string PropertyName { get; set; } = propertyName; public string JsonName { get; set; } = jsonName; public bool IsCollection { get; set; } = isCollection; } /// /// Recursively checks if the given expression or any of its sub-expressions is a call to Patch.Contains(). /// Handles various wrapping scenarios including unary operators, binary operators, and nested expressions. /// private static ValueExpression? GetPatchContainsExpression(ValueExpression? expression) { if (expression is null) { return null; } #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. return expression switch { // Case 1: Direct Patch.Contains() call ScopedApi { Original: InvokeMethodExpression { InstanceReference: ScopedApi } } => expression, // Case 2: !Patch.Contains() call ScopedApi { Original: UnaryOperatorExpression { Operator: "!", Operand: ScopedApi { Original: InvokeMethodExpression { InstanceReference: ScopedApi } } } } => expression, // Case 3 & 4: Binary operator expression (wrapped or unwrapped) ScopedApi { Original: BinaryOperatorExpression binaryExpr } => GetPatchContainsExpression(binaryExpr.Left) ?? GetPatchContainsExpression(binaryExpr.Right), BinaryOperatorExpression binaryExpr => GetPatchContainsExpression(binaryExpr.Left) ?? GetPatchContainsExpression(binaryExpr.Right), // Case 5: Direct UnaryOperatorExpression (not wrapped in ScopedApi) UnaryOperatorExpression { Operator: "!" } unaryExpr => GetPatchContainsExpression(unaryExpr.Operand) != null ? expression : null, // Case 6: Direct InvokeMethodExpression (not wrapped in ScopedApi) InvokeMethodExpression { InstanceReference: ScopedApi } => expression, _ => null }; #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. } }