NetDocsForLLM/Services/XmlDocGenerator.cs

701 lines
28 KiB
C#

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<string> GenerateMetadataAsync(IEnumerable<AssemblyModel> 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<string, string> paramDescriptions = new Dictionary<string, string>();
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<T>, 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";
}
}
}
}