using NetDocsForLLM.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; namespace NetDocsForLLM.Services { public class XmlDocGenerator : IDocFxService { private readonly string _workingDirectory; public XmlDocGenerator() { // Crear un directorio temporal para trabajar _workingDirectory = Path.Combine(Path.GetTempPath(), "NetDocsForLLM_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_workingDirectory); AppLogger.LogInfo($"Directorio de trabajo creado: {_workingDirectory}"); } public async Task GenerateMetadataAsync(IEnumerable assemblies) { // Crear directorio para metadatos var metadataPath = Path.Combine(_workingDirectory, "metadata"); Directory.CreateDirectory(metadataPath); AppLogger.LogInfo($"Directorio de metadatos creado: {metadataPath}"); AppLogger.LogInfo($"Iniciando análisis en {DateTime.Now}"); AppLogger.LogInfo($"Ensamblados a procesar: {assemblies.Count()}"); // Para cada ensamblado, generar documentación XML foreach (var assembly in assemblies) { await Task.Run(() => GenerateXmlDocumentation(assembly, metadataPath)); } return metadataPath; } private void GenerateXmlDocumentation(AssemblyModel assemblyModel, string outputPath) { try { Assembly assembly = assemblyModel.LoadedAssembly; if (assembly == null) { AppLogger.LogError($"ERROR: El ensamblado {assemblyModel.Name} no está cargado"); return; } AppLogger.LogInfo($"\n=== Procesando ensamblado: {assemblyModel.Name} ==="); AppLogger.LogInfo($"Ruta: {assemblyModel.FilePath}"); AppLogger.LogInfo($"Versión: {assemblyModel.Version}"); // Crear el documento XML XDocument doc = new XDocument( new XDeclaration("1.0", "utf-8", null), new XElement("doc", new XElement("assembly", new XElement("name", assembly.GetName().Name) ), new XElement("members") ) ); // Obtener el elemento members para agregar miembros var membersElement = doc.Root.Element("members"); // Cargar documentación XML existente si hay XDocument existingXmlDoc = null; if (assemblyModel.HasXmlDocumentation && File.Exists(assemblyModel.XmlDocPath)) { try { existingXmlDoc = XDocument.Load(assemblyModel.XmlDocPath); AppLogger.LogInfo($"Documentación XML cargada: {assemblyModel.XmlDocPath}"); // Mostrar estadísticas del XML existente var memberNodes = existingXmlDoc.Descendants("member").ToList(); AppLogger.LogInfo($"Entradas en XML existente: {memberNodes.Count}"); // Log de los primeros miembros para depuración foreach (var node in memberNodes.Take(5)) { string id = node.Attribute("name")?.Value; AppLogger.LogDebug($" Miembro XML: {id}"); } } catch (Exception ex) { AppLogger.LogWarning($"Error al cargar XML: {ex.Message}"); existingXmlDoc = null; } } else { AppLogger.LogWarning("No se encontró documentación XML para este ensamblado"); } // Procesar todos los tipos exportados var exportedTypes = assembly.GetExportedTypes().ToList(); AppLogger.LogInfo($"Tipos exportados encontrados: {exportedTypes.Count}"); // Procesar todos los tipos exportados int typesWithMembers = 0; int typesWithoutMembers = 0; int totalMethods = 0; int totalProperties = 0; int totalEvents = 0; int totalFields = 0; foreach (var type in exportedTypes) { try { AppLogger.IncrementTypeCount(); AppLogger.LogDebug($"Procesando tipo: {type.FullName}"); // Generar documentación para el tipo bool hasMembers = GenerateTypeDocumentation(type, membersElement, existingXmlDoc); if (hasMembers) { typesWithMembers++; } else { typesWithoutMembers++; AppLogger.LogWarning($"⚠️ No se encontraron miembros para el tipo: {type.FullName}"); } // Contar miembros por tipo para estadísticas var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) .Where(m => !m.IsSpecialName).ToList(); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).ToList(); var events = type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).ToList(); var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).ToList(); totalMethods += methods.Count; totalProperties += properties.Count; totalEvents += events.Count; totalFields += fields.Count; AppLogger.LogDebug($" Miembros del tipo {type.Name}: Métodos={methods.Count}, Propiedades={properties.Count}, Eventos={events.Count}, Campos={fields.Count}"); // Generar entradas XML para los miembros if (methods.Count > 0) { foreach (var method in methods) { AppLogger.IncrementMemberCount(); GenerateMethodDocumentation(method, membersElement, existingXmlDoc); } } if (properties.Count > 0) { foreach (var property in properties) { AppLogger.IncrementMemberCount(); GeneratePropertyDocumentation(property, membersElement, existingXmlDoc); } } if (events.Count > 0) { foreach (var eventInfo in events) { AppLogger.IncrementMemberCount(); GenerateEventDocumentation(eventInfo, membersElement, existingXmlDoc); } } if (fields.Count > 0) { foreach (var field in fields) { AppLogger.IncrementMemberCount(); GenerateFieldDocumentation(field, membersElement, existingXmlDoc); } } } catch (Exception ex) { AppLogger.LogError($"Error procesando tipo {type.FullName}: {ex.Message}"); } } // Estadísticas finales AppLogger.LogInfo($"Estadísticas de procesamiento para {assemblyModel.Name}:"); AppLogger.LogInfo($" Tipos totales: {exportedTypes.Count}"); AppLogger.LogInfo($" Tipos con miembros: {typesWithMembers}"); AppLogger.LogInfo($" Tipos sin miembros: {typesWithoutMembers}"); AppLogger.LogInfo($" Total de métodos: {totalMethods}"); AppLogger.LogInfo($" Total de propiedades: {totalProperties}"); AppLogger.LogInfo($" Total de eventos: {totalEvents}"); AppLogger.LogInfo($" Total de campos: {totalFields}"); // Guardar la documentación XML generada string xmlFileName = $"{assembly.GetName().Name}_generated.xml"; string xmlFilePath = Path.Combine(outputPath, xmlFileName); // Guardar con formato using (var writer = new XmlTextWriter(xmlFilePath, Encoding.UTF8)) { writer.Formatting = System.Xml.Formatting.Indented; writer.Indentation = 4; doc.Save(writer); } AppLogger.LogInfo($"Documentación XML guardada en: {xmlFilePath}"); } catch (Exception ex) { AppLogger.LogError($"ERROR general procesando ensamblado {assemblyModel.Name}: {ex.Message}"); AppLogger.LogError(ex.StackTrace); } } private bool GenerateTypeDocumentation(Type type, XElement membersElement, XDocument existingXmlDoc) { string typeId = $"T:{type.FullName}"; // Intentar encontrar documentación existente string summary = null; string remarks = null; if (existingXmlDoc != null) { var existingMember = existingXmlDoc.Descendants("member") .FirstOrDefault(m => m.Attribute("name")?.Value == typeId); if (existingMember != null) { AppLogger.LogDebug($" Encontrada documentación XML existente para {typeId}"); summary = existingMember.Element("summary")?.Value?.Trim(); remarks = existingMember.Element("remarks")?.Value?.Trim(); } } // Si no hay documentación existente, generar una descripción genérica if (string.IsNullOrEmpty(summary)) { summary = $"Represents a {GetTypeKindDescription(type)}: {type.Name}"; } var memberElement = new XElement("member", new XAttribute("name", typeId)); memberElement.Add(new XElement("summary", summary)); // Agregar observaciones si están disponibles o generarlas if (!string.IsNullOrEmpty(remarks)) { memberElement.Add(new XElement("remarks", remarks)); } else { var remarksBuilder = new StringBuilder(); // Si es una clase que hereda de otra (excepto Object), agregar esa información if (type.IsClass && type.BaseType != null && type.BaseType != typeof(object)) { remarksBuilder.AppendLine($"Inherits from {type.BaseType.Name}"); } // Si implementa interfaces, mencionarlas var interfaces = type.GetInterfaces(); if (interfaces.Length > 0) { if (remarksBuilder.Length > 0) remarksBuilder.AppendLine(); remarksBuilder.Append($"Implements: {string.Join(", ", interfaces.Select(i => i.Name))}"); } if (remarksBuilder.Length > 0) { memberElement.Add(new XElement("remarks", remarksBuilder.ToString())); } } membersElement.Add(memberElement); // Verificar si el tipo tiene miembros bool hasMembers = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).Any(m => !m.IsSpecialName) || type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).Any() || type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).Any() || type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly).Any(); return hasMembers; } private void GenerateMethodDocumentation(MethodInfo method, XElement membersElement, XDocument existingXmlDoc) { try { // Generar ID XML para el método string methodId = GetMethodXmlId(method); AppLogger.LogDebug($" Procesando método: {methodId}"); // Intentar encontrar documentación existente string summary = null; string returns = null; Dictionary paramDescriptions = new Dictionary(); if (existingXmlDoc != null) { var existingMember = existingXmlDoc.Descendants("member") .FirstOrDefault(m => m.Attribute("name")?.Value == methodId); if (existingMember != null) { summary = existingMember.Element("summary")?.Value?.Trim(); returns = existingMember.Element("returns")?.Value?.Trim(); foreach (var paramElement in existingMember.Elements("param")) { string paramName = paramElement.Attribute("name")?.Value; string paramDesc = paramElement.Value?.Trim(); if (!string.IsNullOrEmpty(paramName) && !string.IsNullOrEmpty(paramDesc)) { paramDescriptions[paramName] = paramDesc; } } } } // Si no hay documentación existente, generar una descripción genérica if (string.IsNullOrEmpty(summary)) { summary = $"{method.Name} {GetMethodDescription(method)}"; } var element = new XElement("member", new XAttribute("name", methodId), new XElement("summary", summary) ); // Agregar parámetros var parameters = method.GetParameters(); foreach (var param in parameters) { string paramDesc; if (!paramDescriptions.TryGetValue(param.Name, out paramDesc)) { paramDesc = GetParameterDescription(param); } element.Add(new XElement("param", new XAttribute("name", param.Name), new XText(paramDesc) )); } // Agregar información de retorno si no es void if (method.ReturnType != typeof(void)) { if (string.IsNullOrEmpty(returns)) { returns = $"A {GetSimpleTypeName(method.ReturnType)} value."; } element.Add(new XElement("returns", returns)); } membersElement.Add(element); } catch (Exception ex) { AppLogger.LogError($"Error generando documentación para método {method.Name}: {ex.Message}"); } } private void GeneratePropertyDocumentation(PropertyInfo property, XElement membersElement, XDocument existingXmlDoc) { try { string propertyId = $"P:{property.DeclaringType.FullName}.{property.Name}"; AppLogger.LogDebug($" Procesando propiedad: {propertyId}"); // Intentar encontrar documentación existente string summary = null; string value = null; if (existingXmlDoc != null) { var existingMember = existingXmlDoc.Descendants("member") .FirstOrDefault(m => m.Attribute("name")?.Value == propertyId); if (existingMember != null) { summary = existingMember.Element("summary")?.Value?.Trim(); value = existingMember.Element("value")?.Value?.Trim(); } } // Si no hay documentación existente, generar una descripción genérica if (string.IsNullOrEmpty(summary)) { summary = $"Gets{(property.CanWrite ? " or sets" : "")} the {property.Name} property."; } var element = new XElement("member", new XAttribute("name", propertyId), new XElement("summary", summary) ); // Agregar descripción de valor si está disponible o generarla if (string.IsNullOrEmpty(value)) { value = $"A {GetSimpleTypeName(property.PropertyType)} representing the {property.Name}."; } element.Add(new XElement("value", value)); membersElement.Add(element); } catch (Exception ex) { AppLogger.LogError($"Error generando documentación para propiedad {property.Name}: {ex.Message}"); } } private void GenerateEventDocumentation(EventInfo eventInfo, XElement membersElement, XDocument existingXmlDoc) { try { string eventId = $"E:{eventInfo.DeclaringType.FullName}.{eventInfo.Name}"; AppLogger.LogDebug($" Procesando evento: {eventId}"); // Intentar encontrar documentación existente string summary = null; if (existingXmlDoc != null) { var existingMember = existingXmlDoc.Descendants("member") .FirstOrDefault(m => m.Attribute("name")?.Value == eventId); if (existingMember != null) { summary = existingMember.Element("summary")?.Value?.Trim(); } } // Si no hay documentación existente, generar una descripción genérica if (string.IsNullOrEmpty(summary)) { summary = $"Occurs when {eventInfo.Name} is raised."; } var element = new XElement("member", new XAttribute("name", eventId), new XElement("summary", summary) ); // Agregar remarks con el tipo del handler element.Add(new XElement("remarks", $"Event handler type: {GetSimpleTypeName(eventInfo.EventHandlerType)}" )); membersElement.Add(element); } catch (Exception ex) { AppLogger.LogError($"Error generando documentación para evento {eventInfo.Name}: {ex.Message}"); } } private void GenerateFieldDocumentation(FieldInfo field, XElement membersElement, XDocument existingXmlDoc) { try { string fieldId = $"F:{field.DeclaringType.FullName}.{field.Name}"; AppLogger.LogDebug($" Procesando campo: {fieldId}"); // Intentar encontrar documentación existente string summary = null; if (existingXmlDoc != null) { var existingMember = existingXmlDoc.Descendants("member") .FirstOrDefault(m => m.Attribute("name")?.Value == fieldId); if (existingMember != null) { summary = existingMember.Element("summary")?.Value?.Trim(); } } // Modificadores string modifiers = ""; if (field.IsStatic) modifiers += "static "; if (field.IsInitOnly) modifiers += "readonly "; if (field.IsLiteral) modifiers += "constant "; // Si no hay documentación existente, generar una descripción genérica if (string.IsNullOrEmpty(summary)) { summary = $"Represents the {modifiers}{field.Name} field."; } var element = new XElement("member", new XAttribute("name", fieldId), new XElement("summary", summary) ); membersElement.Add(element); } catch (Exception ex) { AppLogger.LogError($"Error generando documentación para campo {field.Name}: {ex.Message}"); } } // Métodos auxiliares private string GetMethodXmlId(MethodInfo method) { try { var parameters = method.GetParameters(); if (parameters.Length == 0) { return $"M:{method.DeclaringType.FullName}.{method.Name}"; } var paramTypes = string.Join(",", parameters.Select(p => GetParameterTypeName(p.ParameterType))); return $"M:{method.DeclaringType.FullName}.{method.Name}({paramTypes})"; } catch (Exception ex) { AppLogger.LogError($"Error generando XML ID para método {method.Name}: {ex.Message}"); return $"M:{method.DeclaringType.FullName}.{method.Name}"; } } private string GetParameterTypeName(Type type) { try { // El formato de los parámetros en los IDs XML de documentación es específico // Arrays if (type.IsArray) { return $"{GetParameterTypeName(type.GetElementType())}[]"; } // Genéricos if (type.IsGenericType) { var genericTypeDef = type.GetGenericTypeDefinition(); var genericArgs = string.Join(",", type.GetGenericArguments().Select(GetParameterTypeName)); // Para tipos como List, devolver algo como System.Collections.Generic.List{T} string baseTypeName; if (genericTypeDef.FullName != null && genericTypeDef.FullName.Contains('`')) { baseTypeName = genericTypeDef.FullName.Split('`')[0]; } else { baseTypeName = genericTypeDef.Namespace + "." + genericTypeDef.Name.Split('`')[0]; } return $"{baseTypeName}{{{genericArgs}}}"; } // Para tipos normales, usar el nombre completo return type.FullName; } catch (Exception) { // En caso de error, devolver un nombre genérico return "System.Object"; } } private string GetTypeKindDescription(Type type) { if (type.IsEnum) return "enumeration"; if (type.IsInterface) return "interface"; if (type.IsValueType && !type.IsPrimitive) return "structure"; if (type.IsClass) { if (type.IsAbstract && type.IsSealed) return "static class"; if (type.IsAbstract) return "abstract class"; return "class"; } return "type"; } private string GetMethodDescription(MethodInfo method) { // Crear una descripción básica basada en el nombre y parámetros var parameters = method.GetParameters(); if (parameters.Length == 0) { if (method.ReturnType == typeof(void)) { return "performs an operation."; } return $"returns a {GetSimpleTypeName(method.ReturnType)} value."; } return $"with {parameters.Length} parameter{(parameters.Length > 1 ? "s" : "")}."; } private string GetParameterDescription(ParameterInfo param) { string desc = $"A {GetSimpleTypeName(param.ParameterType)}"; if (param.IsOptional) { object defaultValue = param.DefaultValue; string defaultValueStr = defaultValue?.ToString() ?? "null"; if (param.ParameterType == typeof(string) && defaultValue != null) { defaultValueStr = $"\"{defaultValueStr}\""; } desc += $" (Optional, defaults to {defaultValueStr})"; } if (param.IsOut) { desc += " (Output)"; } else if (param.ParameterType.IsByRef) { desc += " (Reference)"; } return desc; } private string GetSimpleTypeName(Type type) { try { if (type == null) return "void"; // Si es un tipo por referencia (como out o ref parámetros) if (type.IsByRef) { return GetSimpleTypeName(type.GetElementType()); } // Tipos primitivos if (type == typeof(void)) return "void"; if (type == typeof(int)) return "int"; if (type == typeof(string)) return "string"; if (type == typeof(bool)) return "bool"; if (type == typeof(double)) return "double"; if (type == typeof(float)) return "float"; if (type == typeof(decimal)) return "decimal"; if (type == typeof(object)) return "object"; // Arrays if (type.IsArray) { return $"{GetSimpleTypeName(type.GetElementType())}[]"; } // Genéricos if (type.IsGenericType) { string baseName = type.Name; int tickIndex = baseName.IndexOf('`'); if (tickIndex > 0) { baseName = baseName.Substring(0, tickIndex); } var argTypes = type.GetGenericArguments() .Select(GetSimpleTypeName) .ToArray(); return $"{baseName}<{string.Join(", ", argTypes)}>"; } // Para tipos normales, usar solo el nombre simple return type.Name; } catch (Exception) { // En caso de error, devolver un nombre genérico return "object"; } } } }