NetDocsForLLM/Services/DocumentationGenerator.cs

537 lines
22 KiB
C#

using NetDocsForLLM.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Formatting = Newtonsoft.Json.Formatting;
namespace NetDocsForLLM.Services
{
public class DocumentationGenerator : IDocumentationGenerator
{
private readonly IDocFxService _metadataService;
public DocumentationGenerator(IDocFxService metadataService)
{
_metadataService = metadataService ?? throw new ArgumentNullException(nameof(metadataService));
}
public async Task<DocumentationModel> GenerateDocumentation(IEnumerable<AssemblyModel> assemblies, ExportSettings settings)
{
try
{
// Generate metadata using XmlDocGenerator
var metadataPath = await _metadataService.GenerateMetadataAsync(assemblies);
// Create a new documentation model
var documentation = new DocumentationModel();
var namespaces = new Dictionary<string, NamespaceDocumentation>();
// Process XML files produced by our XmlDocGenerator
var xmlFiles = Directory.GetFiles(metadataPath, "*.xml");
foreach (var xmlFile in xmlFiles)
{
ProcessXmlDocumentation(xmlFile, documentation, namespaces, settings);
}
return documentation;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error al generar documentación: {ex.Message}", ex);
}
}
private void ProcessXmlDocumentation(string xmlFile, DocumentationModel documentation,
Dictionary<string, NamespaceDocumentation> namespaces,
ExportSettings settings)
{
try
{
// Cargar el archivo XML
var xmlDoc = XDocument.Load(xmlFile);
// Obtener el nombre del ensamblado
string assemblyName = xmlDoc.Root.Element("assembly")?.Element("name")?.Value;
if (string.IsNullOrEmpty(assemblyName))
return;
// Leer todos los miembros
var memberElements = xmlDoc.Root.Element("members")?.Elements("member");
if (memberElements == null)
return;
// Procesar los tipos primero (para la estructura jerárquica)
var typeMembers = memberElements.Where(m =>
m.Attribute("name")?.Value?.StartsWith("T:") == true);
foreach (var typeMember in typeMembers)
{
ProcessTypeMember(typeMember, documentation, namespaces);
}
// Luego procesar miembros de tipos
var nonTypeMembers = memberElements.Where(m =>
m.Attribute("name")?.Value?.StartsWith("T:") == false);
foreach (var member in nonTypeMembers)
{
ProcessMember(member, documentation, namespaces, settings);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error procesando archivo XML {xmlFile}: {ex.Message}");
}
}
private void ProcessTypeMember(XElement typeMember, DocumentationModel documentation,
Dictionary<string, NamespaceDocumentation> namespaces)
{
try
{
string typeId = typeMember.Attribute("name")?.Value;
if (string.IsNullOrEmpty(typeId) || !typeId.StartsWith("T:"))
return;
// Extraer el nombre completo del tipo
string fullTypeName = typeId.Substring(2); // Quitar "T:"
// Obtener namespace y nombre del tipo
string namespaceName = "Global";
string typeName = fullTypeName;
int lastDotPos = fullTypeName.LastIndexOf('.');
if (lastDotPos > 0)
{
namespaceName = fullTypeName.Substring(0, lastDotPos);
typeName = fullTypeName.Substring(lastDotPos + 1);
}
// Buscar o crear el namespace
if (!namespaces.TryGetValue(namespaceName, out var namespaceDoc))
{
namespaceDoc = new NamespaceDocumentation
{
Name = namespaceName,
Description = $"Contains types from the assembly"
};
namespaces[namespaceName] = namespaceDoc;
documentation.Namespaces.Add(namespaceDoc);
}
// Obtener información del tipo desde XML
string description = typeMember.Element("summary")?.Value?.Trim() ?? "No description available";
string remarks = typeMember.Element("remarks")?.Value?.Trim();
// Determinar el tipo de tipo
string typeKind = "Class";
if (description.Contains("interface:"))
typeKind = "Interface";
else if (description.Contains("structure:"))
typeKind = "Struct";
else if (description.Contains("enumeration:"))
typeKind = "Enum";
else if (description.Contains("abstract class:"))
typeKind = "Abstract Class";
else if (description.Contains("static class:"))
typeKind = "Static Class";
// Crear documentación de tipo
var typeDoc = new TypeDocumentation
{
Name = typeName,
FullName = fullTypeName,
Description = description,
TypeKind = typeKind
};
// Procesar herencia e interfaces si están disponibles en remarks
if (!string.IsNullOrEmpty(remarks))
{
if (remarks.Contains("Inherits from"))
{
int start = remarks.IndexOf("Inherits from") + "Inherits from".Length;
int end = remarks.IndexOf("\n", start);
if (end < 0) end = remarks.Length;
string baseTypeName = remarks.Substring(start, end - start).Trim();
typeDoc.BaseTypes.Add(baseTypeName);
}
if (remarks.Contains("Implements:"))
{
int start = remarks.IndexOf("Implements:") + "Implements:".Length;
int end = remarks.IndexOf("\n", start);
if (end < 0) end = remarks.Length;
string interfacesStr = remarks.Substring(start, end - start).Trim();
string[] interfaces = interfacesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var interfaceName in interfaces)
{
typeDoc.Interfaces.Add(interfaceName.Trim());
}
}
}
namespaceDoc.Types.Add(typeDoc);
}
catch (Exception ex)
{
Console.WriteLine($"Error procesando tipo: {ex.Message}");
}
}
private void ProcessMember(XElement memberElement, DocumentationModel documentation,
Dictionary<string, NamespaceDocumentation> namespaces,
ExportSettings settings)
{
try
{
string memberId = memberElement.Attribute("name")?.Value;
if (string.IsNullOrEmpty(memberId))
return;
// Determinar el tipo de miembro
char memberTypeChar = memberId[0];
if (memberId.Length < 3 || memberId[1] != ':')
return;
string memberType;
switch (memberTypeChar)
{
case 'M': memberType = "Method"; break;
case 'P': memberType = "Property"; break;
case 'F': memberType = "Field"; break;
case 'E': memberType = "Event"; break;
default: return; // No es un miembro que procesamos
}
// Extraer información del tipo al que pertenece el miembro
string fullMemberId = memberId.Substring(2); // Quitar "X:"
int lastDotPos = fullMemberId.LastIndexOf('.');
if (lastDotPos <= 0)
return;
string fullTypeName = fullMemberId.Substring(0, lastDotPos);
string memberName = fullMemberId.Substring(lastDotPos + 1);
// Para métodos con parámetros, extraer el nombre real
if (memberType == "Method" && memberName.Contains("("))
{
memberName = memberName.Substring(0, memberName.IndexOf('('));
}
// Encontrar el namespace y tipo
string namespaceName = "Global";
string typeName = fullTypeName;
int lastTypeNameDotPos = fullTypeName.LastIndexOf('.');
if (lastTypeNameDotPos > 0)
{
namespaceName = fullTypeName.Substring(0, lastTypeNameDotPos);
typeName = fullTypeName.Substring(lastTypeNameDotPos + 1);
}
// Buscar el namespace
if (!namespaces.TryGetValue(namespaceName, out var namespaceDoc))
{
return; // No encontramos el namespace, lo cual es extraño
}
// Buscar el tipo
var typeDoc = namespaceDoc.Types.FirstOrDefault(t => t.FullName == fullTypeName || t.Name == typeName);
if (typeDoc == null)
{
return; // No encontramos el tipo, lo cual es extraño
}
// Extraer descripción y otra información
string description = memberElement.Element("summary")?.Value?.Trim() ?? "No description available";
string returns = memberElement.Element("returns")?.Value?.Trim();
string value = memberElement.Element("value")?.Value?.Trim();
// Crear documentación del miembro
var memberDoc = new MemberDocumentation
{
Name = memberName,
MemberType = memberType,
Description = description,
ReturnType = DeriveReturnType(memberType, returns, value)
};
// Para métodos, procesar parámetros
if (memberType == "Method")
{
foreach (var paramElement in memberElement.Elements("param"))
{
string paramName = paramElement.Attribute("name")?.Value;
string paramDesc = paramElement.Value?.Trim();
if (!string.IsNullOrEmpty(paramName))
{
// Extraer tipo del parámetro de la descripción
string paramType = "object";
if (!string.IsNullOrEmpty(paramDesc) && paramDesc.StartsWith("A "))
{
int spacePos = paramDesc.IndexOf(' ', 2);
if (spacePos > 0)
{
paramType = paramDesc.Substring(2, spacePos - 2);
}
}
var paramDoc = new ParameterDocumentation
{
Name = paramName,
Type = paramType,
Description = paramDesc,
IsOptional = paramDesc?.Contains("Optional") == true
};
memberDoc.Parameters.Add(paramDoc);
}
}
// Generar firma basada en parámetros
string returnType = memberDoc.ReturnType ?? "void";
string parameters = string.Join(", ", memberDoc.Parameters.Select(p => $"{p.Type} {p.Name}"));
memberDoc.Signature = $"{returnType} {memberName}({parameters})";
}
else if (memberType == "Property")
{
// Generar firma para propiedades
string propType = memberDoc.ReturnType ?? "object";
string accessors = "";
if (description.Contains("Gets"))
{
accessors += "get; ";
}
if (description.Contains("sets"))
{
accessors += "set; ";
}
memberDoc.Signature = $"{propType} {memberName} {{ {accessors}}}";
}
else
{
// Firma simple para otros miembros
memberDoc.Signature = $"{memberDoc.ReturnType ?? "void"} {memberName}";
}
// Agregar el miembro al tipo
typeDoc.Members.Add(memberDoc);
}
catch (Exception ex)
{
Console.WriteLine($"Error procesando miembro: {ex.Message}");
}
}
private string DeriveReturnType(string memberType, string returns, string value)
{
// Intentar extraer el tipo de retorno de las etiquetas returns o value
if (!string.IsNullOrEmpty(returns))
{
// Intentar extraer de algo como "A string value."
if (returns.StartsWith("A ") || returns.StartsWith("An "))
{
int spacePos = returns.IndexOf(' ', 2);
if (spacePos > 0)
{
return returns.Substring(2, spacePos - 2);
}
}
}
if (!string.IsNullOrEmpty(value))
{
// Intentar extraer de algo como "A string representing..."
if (value.StartsWith("A ") || value.StartsWith("An "))
{
int spacePos = value.IndexOf(' ', 2);
if (spacePos > 0)
{
return value.Substring(2, spacePos - 2);
}
}
}
// Valores por defecto para diferentes tipos de miembros
switch (memberType)
{
case "Method": return "void";
case "Property": return "object";
case "Field": return "object";
case "Event": return "EventHandler";
default: return null;
}
}
public string GenerateDocumentationPreview(DocumentationModel documentation, ExportSettings settings)
{
try
{
if (settings.OutputFormat == OutputFormat.Json)
{
return JsonConvert.SerializeObject(documentation, Formatting.Indented,
new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
});
}
else // YAML
{
// En un escenario real, usaríamos YamlDotNet
// Para esta implementación, usaremos una conversión manual simplificada a XML
var doc = new XDocument(
new XDeclaration("1.0", "utf-8", null),
new XElement("documentation",
new XElement("namespaces")
)
);
var namespacesElement = doc.Root.Element("namespaces");
foreach (var ns in documentation.Namespaces)
{
var nsElement = new XElement("namespace",
new XAttribute("name", ns.Name),
new XElement("description", ns.Description),
new XElement("types")
);
var typesElement = nsElement.Element("types");
foreach (var type in ns.Types)
{
var typeElement = new XElement("type",
new XAttribute("name", type.Name),
new XAttribute("fullName", type.FullName),
new XAttribute("kind", type.TypeKind),
new XElement("description", type.Description),
new XElement("members")
);
if (type.BaseTypes.Count > 0)
{
var baseTypesElement = new XElement("baseTypes");
foreach (var baseType in type.BaseTypes)
{
baseTypesElement.Add(new XElement("baseType", baseType));
}
typeElement.Add(baseTypesElement);
}
if (type.Interfaces.Count > 0)
{
var interfacesElement = new XElement("interfaces");
foreach (var iface in type.Interfaces)
{
interfacesElement.Add(new XElement("interface", iface));
}
typeElement.Add(interfacesElement);
}
var membersElement = typeElement.Element("members");
foreach (var member in type.Members)
{
var memberElement = new XElement("member",
new XAttribute("name", member.Name),
new XAttribute("type", member.MemberType),
new XElement("description", member.Description),
new XElement("signature", member.Signature)
);
if (!string.IsNullOrEmpty(member.ReturnType))
{
memberElement.Add(new XElement("returnType", member.ReturnType));
if (!string.IsNullOrEmpty(member.ReturnDescription))
{
memberElement.Add(new XElement("returnDescription", member.ReturnDescription));
}
}
if (member.Parameters.Count > 0)
{
var paramsElement = new XElement("parameters");
foreach (var param in member.Parameters)
{
var paramElement = new XElement("parameter",
new XAttribute("name", param.Name),
new XAttribute("type", param.Type),
new XElement("description", param.Description ?? "")
);
if (param.IsOptional)
{
paramElement.Add(new XAttribute("optional", "true"));
if (!string.IsNullOrEmpty(param.DefaultValue))
{
paramElement.Add(new XAttribute("defaultValue", param.DefaultValue));
}
}
paramsElement.Add(paramElement);
}
memberElement.Add(paramsElement);
}
if (member.Examples.Count > 0)
{
var examplesElement = new XElement("examples");
foreach (var example in member.Examples)
{
examplesElement.Add(new XElement("example", example));
}
memberElement.Add(examplesElement);
}
membersElement.Add(memberElement);
}
typesElement.Add(typeElement);
}
namespacesElement.Add(nsElement);
}
// Convertir a string con formato
using (var stringWriter = new StringWriter())
{
using (var xmlWriter = new XmlTextWriter(stringWriter)
{
Formatting = (System.Xml.Formatting)Formatting.Indented,
Indentation = 2
})
{
doc.Save(xmlWriter);
}
return stringWriter.ToString();
}
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error al generar vista previa: {ex.Message}", ex);
}
}
}
}