using System.Text;
using CodeGraph.Graph;
namespace CodeGraph.Export;
///
/// Serializes and deserializes CodeGraph to/from a human-readable text format.
/// Format (no headers, auto-detection):
/// ElementType Id [ name=Name] [ full=FullName] [ parent=ParentId] [ external] [ attr=Attr1,Attr2]
/// [loc=File:Line,Col]*
/// SourceId RelType TargetId [ Attr1,Attr2]
/// [loc=File:Line,Col]*
///
public static class CodeGraphSerializer
{
private const string Separator = " ";
public static string Serialize(Graph.CodeGraph graph)
{
var sb = new StringBuilder();
// Serialize elements first (no header)
var sortedElements = GetSortedElements(graph);
foreach (var element in sortedElements)
{
SerializeElement(sb, element);
}
// Serialize relationships (no header, no blank line)
var relationships = graph.GetAllRelationships().OrderBy(r => GetRelationshipSortKey(graph, r));
foreach (var rel in relationships)
{
SerializeRelationship(sb, rel);
}
return sb.ToString();
}
private static string GetRelationshipSortKey(Graph.CodeGraph graph, Relationship r)
{
return $"{graph.Nodes[r.SourceId].FullName}{r.Type.ToString()}{graph.Nodes[r.TargetId].FullName}";
}
public static void SerializeToFile(Graph.CodeGraph graph, string filePath)
{
var content = Serialize(graph);
File.WriteAllText(filePath, content, Encoding.UTF8);
}
private static List GetSortedElements(Graph.CodeGraph graph)
{
return graph.Nodes.Values.OrderBy(r => r.FullName).ToList();
}
private static void SerializeElement(StringBuilder sb, CodeElement element)
{
sb.Append($"{element.ElementType} {element.Id}");
if (element.Name != element.Id)
{
sb.Append($" name={element.Name}");
}
if (element.FullName != element.Name)
{
sb.Append($" full={element.FullName}");
}
if (element.Parent != null)
{
sb.Append($"{Separator}parent={element.Parent.Id}");
}
if (element.IsExternal)
{
sb.Append($"{Separator}external");
}
if (element.Attributes.Count > 0)
{
var attrs = string.Join(",", element.Attributes.OrderBy(a => a));
sb.Append($"{Separator}attr={attrs}");
}
if (element.SourceLocations.Count > 0)
{
SerializeSourceLocations(sb, element.SourceLocations);
}
sb.AppendLine();
}
private static void SerializeSourceLocations(StringBuilder sb, List locations)
{
foreach (var loc in locations)
{
sb.AppendLine();
sb.Append($"loc={loc.ToString()}");
}
}
private static void SerializeRelationship(StringBuilder sb, Relationship rel)
{
sb.Append($"{rel.SourceId} {rel.Type} {rel.TargetId}");
if (rel.Attributes != RelationshipAttribute.None)
{
var attrs = GetAttributeFlags(rel.Attributes);
sb.Append($" {string.Join(",", attrs)}");
}
if (rel.SourceLocations.Count > 0)
{
SerializeSourceLocations(sb, rel.SourceLocations);
}
sb.AppendLine();
}
private static List GetAttributeFlags(RelationshipAttribute attr)
{
var flags = new List();
foreach (RelationshipAttribute flag in Enum.GetValues(typeof(RelationshipAttribute)))
{
if (flag != RelationshipAttribute.None && attr.HasFlag(flag))
{
flags.Add(flag.ToString());
}
}
return flags;
}
public static Graph.CodeGraph Deserialize(string content)
{
var graph = new Graph.CodeGraph();
var lines = content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Select(l => l.TrimEnd()).ToArray();
var currentLine = 0;
var parentIds = new Dictionary(); // childId -> parentId
// Parse line by line with auto-detection
while (currentLine < lines.Length)
{
var line = lines[currentLine].Trim();
// Skip empty lines or comments
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
{
currentLine++;
continue;
}
var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
throw new InvalidOperationException($"Invalid line format at {currentLine}: {line}");
}
// Try to detect: Element or Relationship?
var isElement = IsElementLine(parts, graph, currentLine);
if (isElement)
{
// Parse as Element
var (element, parentId, linesConsumed) = ParseElement(lines, currentLine);
currentLine += linesConsumed;
graph.Nodes[element.Id] = element;
if (parentId != null)
{
parentIds[element.Id] = parentId;
}
}
else
{
// Parse as Relationship
var (relationship, linesConsumed) = ParseRelationship(lines, currentLine);
currentLine += linesConsumed;
graph.Nodes[relationship.SourceId].Relationships.Add(relationship);
}
}
// Link parent-child relationships
foreach (var (childId, parentId) in parentIds)
{
if (graph.Nodes.TryGetValue(childId, out var child) &&
graph.Nodes.TryGetValue(parentId, out var parent))
{
child.Parent = parent;
parent.Children.Add(child);
}
}
return graph;
}
///
/// Determines if a line represents an Element or Relationship.
/// Strategy:
/// 1. Check if it's a relationship: existing sourceId + valid RelationshipType + ANY targetId → Relationship
/// 2. Otherwise, try to parse first token as ElementType (case-insensitive) → Element
/// 3. Otherwise, → Error
///
/// This order is critical: Relationships have priority to avoid ambiguity when an element ID
/// matches an ElementType name (e.g., element with ID="Class").
///
private static bool IsElementLine(string[] parts, Graph.CodeGraph graph, int currentLine)
{
// Check if it's a relationship: existing ID + RelationshipType + existing ID
if (parts.Length >= 3 &&
graph.Nodes.ContainsKey(parts[0]) &&
graph.Nodes.ContainsKey(parts[2]) &&
Enum.TryParse(parts[1], true, out _))
{
return false; // It's a relationship
}
// Try to parse as ElementType (case-insensitive)
if (Enum.TryParse(parts[0], true, out _))
{
return true;
}
throw new InvalidOperationException(
$"Malformed line. Cannot determine if it is CodeElement or Relationship at line {currentLine}");
}
public static Graph.CodeGraph DeserializeFromFile(string filePath)
{
var content = File.ReadAllText(filePath, Encoding.UTF8);
return Deserialize(content);
}
private static (CodeElement element, string? parentId, int linesConsumed) ParseElement(string[] lines, int startLine)
{
var mainLine = lines[startLine];
var parts = mainLine.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
throw new InvalidOperationException($"Invalid element format at line {startLine}: {mainLine}");
}
// Parse with ignoreCase
if (!Enum.TryParse(parts[0], true, out var elementType))
{
throw new InvalidOperationException($"Unknown element type '{parts[0]}' at line {startLine}");
}
var id = parts[1];
string? name = null;
string? fullName = null;
string? parentId = null;
var isExternal = false;
var attributes = new HashSet();
// Parse optional fields
for (var i = 2; i < parts.Length; i++)
{
var part = parts[i];
if (part.StartsWith("name="))
{
name = part.Substring("name=".Length);
}
else if (part.StartsWith("full="))
{
fullName = part.Substring("full=".Length);
}
else if (part.StartsWith("parent="))
{
parentId = part.Substring("parent=".Length);
}
else if (part == "external")
{
isExternal = true;
}
else if (part.StartsWith("attr="))
{
var attrList = part.Substring("attr=".Length).Split(',');
foreach (var attr in attrList)
{
attributes.Add(attr);
}
}
}
// Fallbacks
name ??= id;
fullName ??= name;
// Create element without parent - will be linked later
var element = new CodeElement(id, elementType, name, fullName, null)
{
IsExternal = isExternal
};
foreach (var attr in attributes)
{
element.Attributes.Add(attr);
}
// Parse source locations
var linesConsumed = 1;
var (sourceLocations, locLinesConsumed) = ParseSourceLocations(lines, startLine + 1);
element.SourceLocations.AddRange(sourceLocations);
linesConsumed += locLinesConsumed;
return (element, parentId, linesConsumed);
}
private static (Relationship relationship, int linesConsumed) ParseRelationship(string[] lines, int startLine)
{
var mainLine = lines[startLine];
var parts = mainLine.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 3)
{
throw new InvalidOperationException($"Invalid relationship format at line {startLine}: {mainLine}");
}
var sourceId = parts[0];
// Parse with ignoreCase
if (!Enum.TryParse(parts[1], true, out var relType))
{
throw new InvalidOperationException($"Unknown relationship type '{parts[1]}' at line {startLine}");
}
var targetId = parts[2];
var attributes = RelationshipAttribute.None;
// Parse optional attributes
if (parts.Length > 3)
{
var attrList = parts[3].Split(',');
foreach (var attr in attrList)
{
if (Enum.TryParse(attr.Trim(), true, out var flag))
{
attributes |= flag;
}
}
}
var relationship = new Relationship(sourceId, targetId, relType, attributes);
// Parse source locations
var linesConsumed = 1;
var (sourceLocations, locLinesConsumed) = ParseSourceLocations(lines, startLine + 1);
relationship.SourceLocations.AddRange(sourceLocations);
linesConsumed += locLinesConsumed;
return (relationship, linesConsumed);
}
private static SourceLocation ParseSourceLocation(string locString)
{
// Format: File:Line,Column
var colonIndex = locString.LastIndexOf(':');
if (colonIndex == -1)
{
throw new InvalidOperationException($"Invalid source location format: {locString}");
}
var file = locString.Substring(0, colonIndex);
var lineColPart = locString.Substring(colonIndex + 1);
var commaIndex = lineColPart.IndexOf(',');
if (commaIndex == -1)
{
throw new InvalidOperationException($"Invalid source location format: {locString}");
}
var line = int.Parse(lineColPart.Substring(0, commaIndex));
var column = int.Parse(lineColPart.Substring(commaIndex + 1));
return new SourceLocation(file, line, column);
}
private static (List locations, int linesConsumed) ParseSourceLocations(string[] lines, int startLine)
{
var locations = new List();
var linesConsumed = 0;
var currentLine = startLine;
while (currentLine < lines.Length)
{
var line = lines[currentLine].Trim();
if (line.StartsWith("loc="))
{
var location = ParseSourceLocation(line.Substring("loc=".Length));
locations.Add(location);
linesConsumed++;
currentLine++;
}
else
{
break;
}
}
return (locations, linesConsumed);
}
}