NetDocsForLLM/Services/DocumentationGenerator.cs

667 lines
27 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
{
switch (settings.OutputFormat)
{
case OutputFormat.Json:
return JsonConvert.SerializeObject(documentation, Formatting.Indented,
new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
});
case OutputFormat.Xml:
return GenerateXmlOutput(documentation);
case OutputFormat.Yaml:
default:
return GenerateYamlOutput(documentation);
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Error al generar vista previa: {ex.Message}", ex);
}
}
private string GenerateXmlOutput(DocumentationModel documentation)
{
var doc = new XDocument(
new XDeclaration("1.0", "utf-8", null),
new XElement("doc",
new XElement("assembly",
new XElement("name", "DocumentationForLLM")
),
new XElement("members")
)
);
var membersElement = doc.Root.Element("members");
// Agregar tipos
foreach (var ns in documentation.Namespaces)
{
foreach (var type in ns.Types)
{
// Agregar elemento de tipo
var typeMember = new XElement("member",
new XAttribute("name", $"T:{type.FullName}"),
new XElement("summary", type.Description)
);
// Agregar información de base y interfaces si corresponde
if (type.BaseTypes.Count > 0 || type.Interfaces.Count > 0)
{
var remarks = new XElement("remarks");
if (type.BaseTypes.Count > 0)
{
remarks.Add(new XText($"Inherits from {type.BaseTypes[0]}"));
}
if (type.Interfaces.Count > 0)
{
if (type.BaseTypes.Count > 0)
remarks.Add(new XText("\n"));
remarks.Add(new XText($"Implements: {string.Join(", ", type.Interfaces)}"));
}
typeMember.Add(remarks);
}
membersElement.Add(typeMember);
// Agregar miembros
foreach (var member in type.Members)
{
XElement memberElement;
// Agregar con formato correcto según el tipo
switch (member.MemberType)
{
case "Method":
memberElement = CreateMethodElement(type, member);
break;
case "Property":
memberElement = CreatePropertyElement(type, member);
break;
case "Event":
memberElement = CreateEventElement(type, member);
break;
case "Field":
memberElement = CreateFieldElement(type, member);
break;
default:
continue; // Tipo no soportado
}
membersElement.Add(memberElement);
}
}
}
// Usar un StringWriter para convertir el XML a texto
using (var stringWriter = new StringWriter())
{
using (var writer = new XmlTextWriter(stringWriter))
{
writer.Formatting = (System.Xml.Formatting)Formatting.Indented;
writer.Indentation = 4;
doc.Save(writer);
}
return stringWriter.ToString();
}
}
private XElement CreateMethodElement(TypeDocumentation type, MemberDocumentation method)
{
// Crear ID XML para el método
string methodId;
if (method.Parameters.Count > 0)
{
var paramTypes = string.Join(",", method.Parameters.Select(p => p.Type));
methodId = $"M:{type.FullName}.{method.Name}({paramTypes})";
}
else
{
methodId = $"M:{type.FullName}.{method.Name}";
}
var element = new XElement("member",
new XAttribute("name", methodId),
new XElement("summary", method.Description)
);
// Agregar parámetros
foreach (var param in method.Parameters)
{
element.Add(new XElement("param",
new XAttribute("name", param.Name),
new XText(param.Description ?? $"A {param.Type} parameter.")
));
}
// Agregar información de retorno si no es void
if (!string.IsNullOrEmpty(method.ReturnType) && method.ReturnType != "void")
{
element.Add(new XElement("returns",
method.ReturnDescription ?? $"A {method.ReturnType} value."
));
}
// Agregar ejemplos si hay
foreach (var example in method.Examples)
{
element.Add(new XElement("example", example));
}
return element;
}
private XElement CreatePropertyElement(TypeDocumentation type, MemberDocumentation property)
{
var element = new XElement("member",
new XAttribute("name", $"P:{type.FullName}.{property.Name}"),
new XElement("summary", property.Description)
);
if (!string.IsNullOrEmpty(property.ReturnType))
{
element.Add(new XElement("value",
$"A {property.ReturnType} representing the property value."
));
}
return element;
}
private XElement CreateEventElement(TypeDocumentation type, MemberDocumentation eventMember)
{
var element = new XElement("member",
new XAttribute("name", $"E:{type.FullName}.{eventMember.Name}"),
new XElement("summary", eventMember.Description)
);
return element;
}
private XElement CreateFieldElement(TypeDocumentation type, MemberDocumentation field)
{
var element = new XElement("member",
new XAttribute("name", $"F:{type.FullName}.{field.Name}"),
new XElement("summary", field.Description)
);
return element;
}
private string GenerateYamlOutput(DocumentationModel documentation)
{
// Implementación simplificada de YAML (mantenemos la existente)
// Deserialize JSON primero, luego a YAML
var json = JsonConvert.SerializeObject(documentation, Formatting.None,
new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
});
// Convierte a YAML usando una implementación simple
var yaml = new System.Text.StringBuilder();
yaml.AppendLine("namespaces:");
var obj = JsonConvert.DeserializeObject<dynamic>(json);
foreach (var ns in obj.Namespaces)
{
yaml.AppendLine($" - name: {ns.Name}");
yaml.AppendLine($" description: {ns.Description}");
yaml.AppendLine(" types:");
foreach (var type in ns.Types)
{
yaml.AppendLine($" - name: {type.Name}");
yaml.AppendLine($" fullName: {type.FullName}");
yaml.AppendLine($" typeKind: {type.TypeKind}");
yaml.AppendLine($" description: {EscapeYamlString(type.Description)}");
yaml.AppendLine(" members:");
foreach (var member in type.Members)
{
yaml.AppendLine($" - name: {member.Name}");
yaml.AppendLine($" memberType: {member.MemberType}");
yaml.AppendLine($" signature: \"{EscapeYamlString(member.Signature)}\"");
yaml.AppendLine($" description: \"{EscapeYamlString(member.Description)}\"");
if (member.Parameters != null && member.Parameters.Count > 0)
{
yaml.AppendLine(" parameters:");
foreach (var param in member.Parameters)
{
yaml.AppendLine($" - name: {param.Name}");
yaml.AppendLine($" type: {param.Type}");
yaml.AppendLine($" description: \"{EscapeYamlString(param.Description)}\"");
yaml.AppendLine($" isOptional: {param.IsOptional.ToString().ToLower()}");
if (!string.IsNullOrEmpty(param.DefaultValue))
yaml.AppendLine($" defaultValue: \"{EscapeYamlString(param.DefaultValue)}\"");
}
}
if (!string.IsNullOrEmpty(member.ReturnType))
{
yaml.AppendLine($" returnType: {member.ReturnType}");
if (!string.IsNullOrEmpty(member.ReturnDescription))
yaml.AppendLine($" returnDescription: \"{EscapeYamlString(member.ReturnDescription)}\"");
}
if (member.Examples != null && member.Examples.Count > 0)
{
yaml.AppendLine(" examples:");
foreach (var example in member.Examples)
{
yaml.AppendLine($" - |\n {example.Replace("\n", "\n ")}");
}
}
}
}
}
return yaml.ToString();
}
private string EscapeYamlString(string input)
{
if (string.IsNullOrEmpty(input))
return "";
return input
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t");
}
}
}