using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using NodePipeline.Abstractions.Interfaces.Nodes; using NodePipeline.Engine.Abstractions; using NodePipeline.Engine.CodeGeneration.Abstractions.Models; // using NodePipeline.Engine.CodeGeneration.Models; namespace NodePipeline.Engine.CodeGeneration.Abstractions; public static class NodeModelBuilder { public static (NodesModel Model, List Diagnostics) Build(ImmutableArray nodes) { var items = new List(); var diagnostics = new List(); foreach (var node in nodes) { var fields = new List(); foreach (var prop in node.GetMembers().OfType()) { if (prop.Type is not INamedTypeSymbol nf || !nf.Name.StartsWith("NodeField")) continue; var nodeFieldAttr = prop.GetAttributes() .FirstOrDefault(a => a.AttributeClass?.Name == "NodeFieldAttribute"); if (nodeFieldAttr is null) continue; var descriptor = CreateFieldDescriptor(nodeFieldAttr, nf, prop, out var diagnostic); if (diagnostic != null) diagnostics.Add(diagnostic); fields.Add(descriptor); } var nodeValidators = GetNodeValidators(node); var assemblyQualifiedName = node.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var typeNameShort = GetTypeNameShort(node, assemblyQualifiedName); var nodeType = GetNodeType(node, typeNameShort, out var isNameDefinedInAttribute); var hasParameterlessConstructor = node.Constructors.Any(c => c.Parameters.Length == 0); items.Add(new NodeDescriptor(nodeType, isNameDefinedInAttribute, typeNameShort, assemblyQualifiedName, fields, hasParameterlessConstructor, nodeValidators)); } return (new NodesModel(items), diagnostics); } private static HashSet GetNodeFieldValidators(ImmutableArray propAttributes) { var result = new HashSet(); foreach (var attr in propAttributes) { if (attr.AttributeClass?.Name != "HasValidatorAttribute" || attr.ConstructorArguments.Length <= 0) continue; if (attr.ConstructorArguments[0].Value is not INamedTypeSymbol validatorType) continue; var validatorTypeName = validatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var hasParameterlessCtor = validatorType.Constructors.Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); result.Add(new ValidatorDescriptor(validatorTypeName, hasParameterlessCtor)); } return result; } private static object? GetFieldDefaultValue(ImmutableArray propAttributes, ITypeSymbol fieldType, IPropertySymbol prop, out Diagnostic? diagnostic) { object? result = null; var defaultValueAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name is "HasDefaultValueAttribute" or "DefaultValueAttribute"); if (defaultValueAttr is { ConstructorArguments.Length: > 0 }) { var value = defaultValueAttr.ConstructorArguments[0].Value; var typeArg = defaultValueAttr.ConstructorArguments.ElementAtOrDefault(1); diagnostic = ValidateDefaultValueCompatibility(fieldType, value, typeArg, prop); if (diagnostic != null) return null; result = value; } diagnostic = null; return result; } private static Diagnostic? ValidateDefaultValueCompatibility(ITypeSymbol fieldType, object? value, TypedConstant? attributeTypeArg, IPropertySymbol prop) { if (value is null) return null; if (attributeTypeArg is { Value: INamedTypeSymbol attrType }) { if (!SymbolEqualityComparer.Default.Equals(fieldType, attrType)) return ReportDiagnostic(prop, $"Default value type '{attrType.Name}' does not match field type '{fieldType.Name}'."); return null; } switch (value) { case int: { if (!IsIntegerType(fieldType)) return ReportDiagnostic(prop, $"Integer default value is not compatible with field type '{fieldType.Name}'."); return null; } case decimal: { if (!IsDecimalCompatible(fieldType)) return ReportDiagnostic(prop, $"Decimal default value is not compatible with field type '{fieldType.Name}'."); return null; } case string: { if (fieldType.SpecialType != SpecialType.System_String) return ReportDiagnostic(prop, $"String default value is not compatible with field type '{fieldType.Name}'."); return null; } } var valueType = value.GetType().FullName; if (!string.Equals(fieldType.ToDisplayString(), valueType, StringComparison.Ordinal)) return ReportDiagnostic(prop, $"Default value type '{valueType}' does not match field type '{fieldType.Name}'."); return null; } private static bool IsIntegerType(ITypeSymbol type) { return type.SpecialType is SpecialType.System_Int32 or SpecialType.System_Int64 or SpecialType.System_Int16 or SpecialType.System_Byte or SpecialType.System_UInt32 or SpecialType.System_UInt64 or SpecialType.System_UInt16 or SpecialType.System_SByte; } private static bool IsDecimalCompatible(ITypeSymbol type) { return type.SpecialType is SpecialType.System_Decimal or SpecialType.System_Double or SpecialType.System_Single; } private static Diagnostic ReportDiagnostic(IPropertySymbol prop, string message) { var descriptor = new DiagnosticDescriptor( "NP0001", "Invalid default value", message, "NodePipeline", DiagnosticSeverity.Error, true); var location = prop.Locations.FirstOrDefault(); return Diagnostic.Create(descriptor, location); } private static bool CanFieldDefaultValueBeInitialized(ITypeSymbol typeArg) { if (typeArg.IsValueType || typeArg.TypeKind == TypeKind.Enum) return true; if (typeArg.IsAbstract || typeArg.TypeKind == TypeKind.Interface) return false; var constructors = typeArg.GetMembers().OfType() .Where(m => m.MethodKind == MethodKind.Constructor); return constructors.Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); } private static (decimal? min, decimal? max) GetNumericBounds(ImmutableArray propAttributes) { decimal? numberMinBound = null; decimal? numberMaxBound = null; var hasValueBetweenAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name == "HasValueBetweenAttribute"); if (hasValueBetweenAttr is { ConstructorArguments.Length: >= 2 }) { var min = hasValueBetweenAttr.ConstructorArguments[0].Value as decimal? ?? Convert.ToDecimal(hasValueBetweenAttr.ConstructorArguments[0].Value); var max = hasValueBetweenAttr.ConstructorArguments[1].Value as decimal? ?? Convert.ToDecimal(hasValueBetweenAttr.ConstructorArguments[1].Value); numberMinBound = min; numberMaxBound = max; } var hasMinValueAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name == "HasMinValueAttribute"); if (hasMinValueAttr is { ConstructorArguments.Length: > 0 }) { var minVal = hasMinValueAttr.ConstructorArguments[0].Value as decimal? ?? Convert.ToDecimal(hasMinValueAttr.ConstructorArguments[0].Value); if (!numberMinBound.HasValue || minVal > numberMinBound.Value) numberMinBound = minVal; } var hasMaxValueAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name == "HasMaxValueAttribute"); if (hasMaxValueAttr is not { ConstructorArguments.Length: > 0 }) return (numberMinBound, numberMaxBound); var maxVal = hasMaxValueAttr.ConstructorArguments[0].Value as decimal? ?? Convert.ToDecimal(hasMaxValueAttr.ConstructorArguments[0].Value); if (!numberMaxBound.HasValue || maxVal < numberMaxBound.Value) numberMaxBound = maxVal; return (numberMinBound, numberMaxBound); } private static (int? min, int? max) GetStringLengthBounds(ImmutableArray propAttributes) { int? stringMinLength = null; int? stringMaxLength = null; var hasLengthBetweenAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name == "HasLengthBetweenAttribute"); if (hasLengthBetweenAttr is { ConstructorArguments.Length: >= 2 }) { var minLen = hasLengthBetweenAttr.ConstructorArguments[0].Value as int? ?? Convert.ToInt32(hasLengthBetweenAttr.ConstructorArguments[0].Value); var maxLen = hasLengthBetweenAttr.ConstructorArguments[1].Value as int? ?? Convert.ToInt32(hasLengthBetweenAttr.ConstructorArguments[1].Value); stringMinLength = minLen; stringMaxLength = maxLen; } var hasMinLengthAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name == "HasMinLengthAttribute"); if (hasMinLengthAttr is { ConstructorArguments.Length: > 0 }) { var minLenVal = hasMinLengthAttr.ConstructorArguments[0].Value as int? ?? Convert.ToInt32(hasMinLengthAttr.ConstructorArguments[0].Value); if (!stringMinLength.HasValue || minLenVal > stringMinLength.Value) stringMinLength = minLenVal; } var hasMaxLengthAttr = propAttributes.FirstOrDefault(a => a.AttributeClass?.Name == "HasMaxLengthAttribute"); if (hasMaxLengthAttr is not { ConstructorArguments.Length: > 0 }) return (stringMinLength, stringMaxLength); var maxLenVal = hasMaxLengthAttr.ConstructorArguments[0].Value as int? ?? Convert.ToInt32(hasMaxLengthAttr.ConstructorArguments[0].Value); if (!stringMaxLength.HasValue || maxLenVal < stringMaxLength.Value) stringMaxLength = maxLenVal; return (stringMinLength, stringMaxLength); } private static NodeFieldDescriptor.NodeFieldMetaData CreateNodeFieldMetaData(ITypeSymbol typeArg, IPropertySymbol prop, FieldDirection fieldDir, out Diagnostic? diagnostic) { var propAttributes = prop.GetAttributes(); var fieldValidators = GetNodeFieldValidators(propAttributes); var defaultValue = GetFieldDefaultValue(propAttributes, typeArg, prop, out diagnostic); var canDefaultValueBeInitialized = CanFieldDefaultValueBeInitialized(typeArg); var isRequired = propAttributes.Any(a => a.AttributeClass?.Name == "RequiredAttribute"); var disallowNullableOutput = fieldDir == FieldDirection.Input && propAttributes.Any(a => a.AttributeClass?.Name == "DisallowNullableOutputAttribute"); var (numberMinBound, numberMaxBound) = GetNumericBounds(propAttributes); var (stringMinLength, stringMaxLength) = GetStringLengthBounds(propAttributes); return new NodeFieldDescriptor.NodeFieldMetaData(isRequired, disallowNullableOutput, fieldValidators, defaultValue, canDefaultValueBeInitialized, numberMinBound, numberMaxBound, stringMinLength, stringMaxLength); } private static NodeFieldDescriptor CreateFieldDescriptor(AttributeData nodeFieldAttr, INamedTypeSymbol nf, IPropertySymbol prop, out Diagnostic? diagnostic) { var directionArg = nodeFieldAttr.ConstructorArguments[0].Value?.ToString(); var paramNameArg = nodeFieldAttr.ConstructorArguments.Length > 1 ? nodeFieldAttr.ConstructorArguments[1].Value?.ToString() : null; var fieldDir = Enum.TryParse(directionArg, out FieldDirection dir) ? dir : FieldDirection.Parameter; var typeArg = nf.TypeArguments[0]; var valueType = TrimSuffix(typeArg.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), "?"); var isNullableValueType = typeArg.NullableAnnotation == NullableAnnotation.Annotated; //TODO: consider NotAnnotated var isValueReferenceType = !typeArg.IsValueType && typeArg.TypeKind != TypeKind.Enum; var nodeFieldMetaData = CreateNodeFieldMetaData(typeArg, prop, fieldDir, out diagnostic); return new NodeFieldDescriptor(prop.Name, !string.IsNullOrWhiteSpace(paramNameArg) ? paramNameArg! : prop.Name, valueType, isNullableValueType, isValueReferenceType, typeArg.TypeKind == TypeKind.Enum, fieldDir, nodeFieldMetaData); } private static HashSet GetNodeValidators(INamedTypeSymbol node) { var result = new HashSet(); foreach (var attr in node.GetAttributes()) { if (attr.AttributeClass?.Name != "HasNodeValidatorAttribute" || attr.ConstructorArguments.Length <= 0) continue; if (attr.ConstructorArguments[0].Value is not INamedTypeSymbol validatorType) continue; var validatorTypeName = validatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var hasParameterlessCtor = validatorType.Constructors.Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); result.Add(new ValidatorDescriptor(validatorTypeName, hasParameterlessCtor)); } return result; } private static string GetTypeNameShort(INamedTypeSymbol node, string assemblyQualifiedName) { var assemblyName = node.ContainingAssembly.Identity.GetDisplayName(); var fullName = TrimPrefix(assemblyQualifiedName, assemblyName); return TrimPrefix(fullName, "global::"); } private static string GetNodeType(INamedTypeSymbol node, string typeNameShort, out bool isNameDefinedInAttribute) { var typeAttr = node.GetAttributes() .FirstOrDefault(a => a.AttributeClass?.Name == "NodeTypeAttribute"); return GetNodeType(typeAttr, typeNameShort, out isNameDefinedInAttribute); } private static string GetNodeType(AttributeData? typeAttr, string typeNameShort, out bool isNameDefinedInAttribute) { const string suffix = "Node"; var nameFromAttribute = typeAttr?.ConstructorArguments[0].Value?.ToString(); if (!string.IsNullOrWhiteSpace(nameFromAttribute)) { isNameDefinedInAttribute = true; return nameFromAttribute!; } isNameDefinedInAttribute = false; foreach (var prefix in NodeNamePrefixSettings.Prefixes) if (typeNameShort.StartsWith(prefix, StringComparison.Ordinal)) return TrimSuffix(typeNameShort.Substring(prefix.Length), suffix); return TrimSuffix(typeNameShort, suffix); } private static string TrimSuffix(string name, string suffix) { if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(suffix) || name.Length < suffix.Length) return name; return name.EndsWith(suffix, StringComparison.Ordinal) ? name.Substring(0, name.Length - suffix.Length) : name; } private static string TrimPrefix(string name, string prefix) { if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(prefix) || name.Length < prefix.Length) return name; return name.StartsWith(prefix, StringComparison.Ordinal) ? name.Substring(prefix.Length, name.Length - prefix.Length) : name; } public sealed class NodesModel(IReadOnlyList nodes) { public IReadOnlyList Nodes { get; } = nodes ?? throw new ArgumentNullException(nameof(nodes)); } }