using Microsoft.TypeSpec.Generator.ClientModel;
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.Collections.Generic;
using System.Linq;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
namespace OpenAILibraryPlugin.Visitors;
///
/// This visitor performs several in-place modifications of model-based type constructors:
/// 1. All generated constructors ensure that collections are initialized (via null coalescence) in their bodies
/// 2. All generated default constructors chain initialization to the generated serialization constructor
///
public class ConstructorFixupVisitor : ScmLibraryVisitor
{
private static readonly MethodBodyStatement CommentStatement
= new SingleLineCommentStatement("Plugin customization: ensure initialization of collections");
protected override TypeProvider? PostVisitType(TypeProvider type)
{
if (type is not ModelProvider modelProvider
|| modelProvider.Type.IsValueType
|| modelProvider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Static))
{
return type;
}
List allGeneratedConstructors =
[
.. modelProvider.Constructors,
.. modelProvider.SerializationProviders.SelectMany(mrwProvider => mrwProvider.Constructors),
];
List allConstructorsIncludingCustom =
[
.. allGeneratedConstructors,
.. (modelProvider?.CustomCodeView?.Constructors ?? [])
];
ConstructorProvider? serializationConstructor = allConstructorsIncludingCustom.MaxBy(ctr => ctr.Signature.Parameters.Count);
ConstructorProvider? generatedDefaultConstructor = allGeneratedConstructors.FirstOrDefault(ctr => ctr.Signature.Parameters.Count == 0);
bool adjustmentPerformed = false;
foreach (ConstructorProvider constructor in allGeneratedConstructors)
{
adjustmentPerformed |= TryUpdateConstructorForCollectionInitialization(constructor);
}
// If any generated constructors were updated to perform additional initialization, OR if the type uses a discriminator, adjust the
// default constructor to chain to the serialization constructor that will produce an independently valid instance state.
if (generatedDefaultConstructor is not null
&& (adjustmentPerformed || modelProvider?.DiscriminatorValueExpression is not null))
{
CSharpType? discriminatorType = modelProvider?.DiscriminatorValueExpression is MemberExpression enclosingDiscriminatorValueExpression
&& enclosingDiscriminatorValueExpression.Inner is TypeReferenceExpression enclosingDiscriminatorTypeReferenceExpression
? enclosingDiscriminatorTypeReferenceExpression.Type
: null;
List initializationExpressions = [];
foreach (ParameterProvider parameter in serializationConstructor?.Signature.Parameters ?? [])
{
initializationExpressions.Add(
parameter.Type == discriminatorType && modelProvider?.DiscriminatorValueExpression is not null
? modelProvider.DiscriminatorValueExpression!
: parameter.Type.IsValueType
? Default
: Null);
}
ConstructorSignature updatedSignature = new(
generatedDefaultConstructor.Signature.Type,
generatedDefaultConstructor.Signature.Description,
generatedDefaultConstructor.Signature.Modifiers,
generatedDefaultConstructor.Signature.Parameters,
generatedDefaultConstructor.Signature.Attributes,
new(false, initializationExpressions));
generatedDefaultConstructor.Update(signature: updatedSignature, bodyStatements: new MethodBodyStatements([]));
}
return type;
}
private static bool TryUpdateConstructorForCollectionInitialization(ConstructorProvider constructor)
{
IEnumerable eligibleParameters
= constructor?.Signature.Parameters
.Where(parameter => parameter?.Type?.IsValueType == false
&& parameter?.Name != "additionalBinaryDataProperties"
&& (parameter?.Type?.IsList == true
|| parameter?.Type?.IsCollection == true
|| parameter?.Type?.IsDictionary == true
|| parameter?.Type?.BaseType?.Name?.Contains("Collection") == true)) ?? [];
if (eligibleParameters.Any() != true)
{
return false;
}
List bodyStatements
= constructor?.BodyStatements?.ToList() ?? [];
if (TryUpdateStatementsForParameters(bodyStatements, eligibleParameters))
{
bodyStatements.Insert(0, CommentStatement);
constructor?.Update(bodyStatements: bodyStatements);
return true;
}
return false;
}
private static bool TryUpdateStatementsForParameters(
List statements,
IEnumerable parameters)
{
bool changed = false;
for (int i = 0; i < statements.Count; i++)
{
changed |= TryUpdateStatementForParameters(
statements[i],
parameters,
out MethodBodyStatement handledStatement);
statements[i] = handledStatement;
}
return changed;
}
private static bool TryUpdateStatementForParameters(
MethodBodyStatement originalStatement,
IEnumerable parameters,
out MethodBodyStatement handledStatement)
{
if (originalStatement is ExpressionStatement expressionStatement
&& expressionStatement.Expression is AssignmentExpression assignmentExpression)
{
foreach (ParameterProvider parameter in parameters)
{
if (TryUpdateStatementForParameter(
assignmentExpression,
parameter,
out MethodBodyStatement? updatedStatement)
&& updatedStatement is not null)
{
handledStatement = updatedStatement;
return true;
}
}
}
handledStatement = originalStatement;
return false;
}
private static bool TryUpdateStatementForParameter(
AssignmentExpression assignmentExpression,
ParameterProvider parameter,
out MethodBodyStatement? handledStatement)
{
if (assignmentExpression.Value.ToDisplayString() == parameter.Name)
{
ValueExpression nullFallbackExpression = parameter.Type.IsList || parameter.Type.IsDictionary
? New.Instance(parameter.Type.PropertyInitializationType)
: New.Instance(parameter.Type);
ValueExpression coalescedValueExpression = assignmentExpression.Value
.NullCoalesce(nullFallbackExpression);
handledStatement = assignmentExpression.Variable
.Assign(coalescedValueExpression)
.Terminate();
return true;
}
handledStatement = null;
return false;
}
}