Skip to content

Commit a5db396

Browse files
authored
Merge pull request #156 from link-foundation/claude/review-test-coverage-011UTL6DkCfp7LuwpUZj9HyC
Review test coverage summary documentation
2 parents d60dc30 + e0411c5 commit a5db396

File tree

16 files changed

+1921
-12
lines changed

16 files changed

+1921
-12
lines changed

FORMATTER_AND_ROUNDTRIP_TEST_ANALYSIS.md

Lines changed: 498 additions & 0 deletions
Large diffs are not rendered by default.

IMPLEMENTATION_SUMMARY.md

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.

csharp/Link.Foundation.Links.Notation.Tests/ApiTests.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,44 @@ public static void QuotedReferencesTest()
9191
var input = @"(""quoted id"": ""value with spaces"")";
9292
var parser = new Parser();
9393
var parsed = parser.Parse(input);
94-
94+
9595
var output = parsed.Format();
9696
Assert.Contains("quoted id", output);
9797
Assert.Contains("value with spaces", output);
9898
}
99+
100+
[Fact]
101+
public static void IndentedIdSyntaxRoundtripTest()
102+
{
103+
var input = "id:\n value1\n value2";
104+
var parser = new Parser();
105+
var parsed = parser.Parse(input);
106+
107+
// Validate that we can format with indented syntax using FormatOptions
108+
var options = new FormatOptions
109+
{
110+
MaxInlineRefs = 1, // Force indentation with more than 1 ref
111+
PreferInline = false
112+
};
113+
var output = parsed.Format(options);
114+
Assert.Equal(input, output);
115+
}
116+
117+
[Fact]
118+
public static void MultipleIndentedIdSyntaxRoundtripTest()
119+
{
120+
var input = "id1:\n a\n b\nid2:\n c\n d";
121+
var parser = new Parser();
122+
var parsed = parser.Parse(input);
123+
124+
// Validate that we can format with indented syntax using FormatOptions
125+
var options = new FormatOptions
126+
{
127+
MaxInlineRefs = 1, // Force indentation with more than 1 ref
128+
PreferInline = false
129+
};
130+
var output = parsed.Format(options);
131+
Assert.Equal(input, output);
132+
}
99133
}
100134
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
3+
namespace Link.Foundation.Links.Notation
4+
{
5+
/// <summary>
6+
/// FormatOptions for Lino notation formatting.
7+
/// Provides configuration options for controlling how Link objects are formatted.
8+
/// </summary>
9+
public class FormatOptions
10+
{
11+
/// <summary>
12+
/// If true, omit parentheses where safe (default: false)
13+
/// </summary>
14+
public bool LessParentheses { get; set; } = false;
15+
16+
/// <summary>
17+
/// Maximum line length before auto-indenting (default: 80)
18+
/// </summary>
19+
public int MaxLineLength { get; set; } = 80;
20+
21+
/// <summary>
22+
/// If true, indent lines exceeding MaxLineLength (default: false)
23+
/// </summary>
24+
public bool IndentLongLines { get; set; } = false;
25+
26+
/// <summary>
27+
/// Maximum number of references before auto-indenting (default: null = unlimited)
28+
/// </summary>
29+
public int? MaxInlineRefs { get; set; } = null;
30+
31+
/// <summary>
32+
/// If true, group consecutive links with same ID (default: false)
33+
/// </summary>
34+
public bool GroupConsecutive { get; set; } = false;
35+
36+
/// <summary>
37+
/// String to use for indentation (default: " " = two spaces)
38+
/// </summary>
39+
public string IndentString { get; set; } = " ";
40+
41+
/// <summary>
42+
/// If true, prefer inline format when under thresholds (default: true)
43+
/// </summary>
44+
public bool PreferInline { get; set; } = true;
45+
46+
/// <summary>
47+
/// Check if line should be indented based on length
48+
/// </summary>
49+
/// <param name="line">The line to check</param>
50+
/// <returns>True if line should be indented based on length threshold</returns>
51+
public bool ShouldIndentByLength(string line)
52+
{
53+
if (!IndentLongLines)
54+
{
55+
return false;
56+
}
57+
// Count printable unicode characters
58+
return line.Length > MaxLineLength;
59+
}
60+
61+
/// <summary>
62+
/// Check if link should be indented based on reference count
63+
/// </summary>
64+
/// <param name="refCount">Number of references in the link</param>
65+
/// <returns>True if link should be indented based on reference count threshold</returns>
66+
public bool ShouldIndentByRefCount(int refCount)
67+
{
68+
if (MaxInlineRefs == null)
69+
{
70+
return false;
71+
}
72+
return refCount > MaxInlineRefs.Value;
73+
}
74+
}
75+
}

csharp/Link.Foundation.Links.Notation/IListExtensions.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,84 @@ public static string Format<TLinkAddress>(this IList<Link<TLinkAddress>> links,
3939
return string.Join(Environment.NewLine, links.Select(l => l.ToString().TrimSingle('(').TrimSingle(')')));
4040
}
4141
}
42+
43+
/// <summary>
44+
/// Formats a collection of links using FormatOptions configuration.
45+
/// </summary>
46+
/// <typeparam name="TLinkAddress">The type used for link addresses/identifiers.</typeparam>
47+
/// <param name="links">The collection of links to format.</param>
48+
/// <param name="options">The FormatOptions to use for formatting.</param>
49+
/// <returns>A multi-line string representation of the links.</returns>
50+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
51+
public static string Format<TLinkAddress>(this IList<Link<TLinkAddress>> links, FormatOptions options)
52+
{
53+
if (links == null || links.Count == 0)
54+
{
55+
return string.Empty;
56+
}
57+
58+
// Apply consecutive link grouping if enabled
59+
var linksToFormat = options.GroupConsecutive ? GroupConsecutiveLinks(links) : links;
60+
61+
// Format each link with options
62+
var formattedLinks = linksToFormat.Select(l => l.FormatWithOptions(options));
63+
return string.Join(Environment.NewLine, formattedLinks);
64+
}
65+
66+
/// <summary>
67+
/// Group consecutive links with the same ID.
68+
/// </summary>
69+
private static List<Link<TLinkAddress>> GroupConsecutiveLinks<TLinkAddress>(IList<Link<TLinkAddress>> links)
70+
{
71+
if (links == null || links.Count == 0)
72+
{
73+
return new List<Link<TLinkAddress>>();
74+
}
75+
76+
var grouped = new List<Link<TLinkAddress>>();
77+
int i = 0;
78+
79+
while (i < links.Count)
80+
{
81+
var current = links[i];
82+
83+
// Look ahead for consecutive links with same ID
84+
if (current.Id != null && current.Values != null && current.Values.Count > 0)
85+
{
86+
// Collect all values with same ID
87+
var sameIdValues = new List<Link<TLinkAddress>>(current.Values);
88+
int j = i + 1;
89+
90+
while (j < links.Count)
91+
{
92+
var nextLink = links[j];
93+
if (EqualityComparer<TLinkAddress>.Default.Equals(nextLink.Id, current.Id) &&
94+
nextLink.Values != null && nextLink.Values.Count > 0)
95+
{
96+
sameIdValues.AddRange(nextLink.Values);
97+
j++;
98+
}
99+
else
100+
{
101+
break;
102+
}
103+
}
104+
105+
// If we found consecutive links, create grouped link
106+
if (j > i + 1)
107+
{
108+
var groupedLink = new Link<TLinkAddress>(current.Id, sameIdValues);
109+
grouped.Add(groupedLink);
110+
i = j;
111+
continue;
112+
}
113+
}
114+
115+
grouped.Add(current);
116+
i++;
117+
}
118+
119+
return grouped;
120+
}
42121
}
43122
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Text;
6+
7+
namespace Link.Foundation.Links.Notation
8+
{
9+
/// <summary>
10+
/// Provides extension methods for formatting <see cref="Link{TLinkAddress}"/> instances with FormatOptions.
11+
/// </summary>
12+
public static class LinkFormatExtensions
13+
{
14+
/// <summary>
15+
/// Format a link using FormatOptions configuration.
16+
/// </summary>
17+
/// <typeparam name="TLinkAddress">The type used for link addresses/identifiers.</typeparam>
18+
/// <param name="link">The link to format.</param>
19+
/// <param name="options">The FormatOptions to use.</param>
20+
/// <returns>Formatted string representation.</returns>
21+
public static string FormatWithOptions<TLinkAddress>(this Link<TLinkAddress> link, FormatOptions options)
22+
{
23+
// Empty link
24+
if (link.Id == null && (link.Values == null || link.Values.Count == 0))
25+
{
26+
return options.LessParentheses ? "" : "()";
27+
}
28+
29+
// Link with only ID, no values
30+
if (link.Values == null || link.Values.Count == 0)
31+
{
32+
var escapedId = Link<TLinkAddress>.EscapeReference(link.Id?.ToString());
33+
return options.LessParentheses && !NeedsParentheses(link.Id?.ToString()) ?
34+
escapedId :
35+
$"({escapedId})";
36+
}
37+
38+
// Check if we should use indented format
39+
bool shouldIndent = false;
40+
if (options.ShouldIndentByRefCount(link.Values.Count))
41+
{
42+
shouldIndent = true;
43+
}
44+
else
45+
{
46+
// Try inline format first
47+
var valuesStr = string.Join(" ", link.Values.Select(v => GetValueString(v)));
48+
string testLine;
49+
if (link.Id != null)
50+
{
51+
var idStr = Link<TLinkAddress>.EscapeReference(link.Id.ToString());
52+
testLine = options.LessParentheses ? $"{idStr}: {valuesStr}" : $"({idStr}: {valuesStr})";
53+
}
54+
else
55+
{
56+
testLine = options.LessParentheses ? valuesStr : $"({valuesStr})";
57+
}
58+
59+
if (options.ShouldIndentByLength(testLine))
60+
{
61+
shouldIndent = true;
62+
}
63+
}
64+
65+
// Format with indentation if needed
66+
if (shouldIndent && options.PreferInline == false)
67+
{
68+
return FormatIndented(link, options);
69+
}
70+
71+
// Standard inline formatting
72+
var values = string.Join(" ", link.Values.Select(v => GetValueString(v)));
73+
74+
// Link with values only (null id)
75+
if (link.Id == null)
76+
{
77+
if (options.LessParentheses)
78+
{
79+
var allSimple = link.Values.All(v => v.Values == null || v.Values.Count == 0);
80+
if (allSimple)
81+
{
82+
return string.Join(" ", link.Values.Select(v => Link<TLinkAddress>.EscapeReference(v.Id?.ToString())));
83+
}
84+
return values;
85+
}
86+
return $"({values})";
87+
}
88+
89+
// Link with ID and values
90+
var id = Link<TLinkAddress>.EscapeReference(link.Id.ToString());
91+
var withColon = $"{id}: {values}";
92+
return options.LessParentheses && !NeedsParentheses(link.Id?.ToString()) ?
93+
withColon :
94+
$"({withColon})";
95+
}
96+
97+
/// <summary>
98+
/// Format a link with indentation.
99+
/// </summary>
100+
private static string FormatIndented<TLinkAddress>(Link<TLinkAddress> link, FormatOptions options)
101+
{
102+
if (link.Id == null)
103+
{
104+
// Values only - format each on separate line
105+
var lines = link.Values.Select(v => options.IndentString + GetValueString(v));
106+
return string.Join(Environment.NewLine, lines);
107+
}
108+
109+
// Link with ID - format as id:\n value1\n value2
110+
var idStr = Link<TLinkAddress>.EscapeReference(link.Id.ToString());
111+
var sb = new StringBuilder();
112+
sb.Append($"{idStr}:");
113+
114+
foreach (var v in link.Values)
115+
{
116+
sb.Append(Environment.NewLine);
117+
sb.Append(options.IndentString);
118+
sb.Append(GetValueString(v));
119+
}
120+
121+
return sb.ToString();
122+
}
123+
124+
/// <summary>
125+
/// Get the string representation of a value.
126+
/// </summary>
127+
private static string GetValueString<TLinkAddress>(Link<TLinkAddress> value)
128+
{
129+
return value.ToLinkOrIdString();
130+
}
131+
132+
/// <summary>
133+
/// Check if a string needs to be wrapped in parentheses.
134+
/// </summary>
135+
private static bool NeedsParentheses(string s)
136+
{
137+
return s != null && (s.Contains(" ") || s.Contains(":") || s.Contains("(") || s.Contains(")"));
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)