Skip to content

Commit 7909dff

Browse files
randleeclaude
andcommitted
feat: Complete Sprint 5 - CLI Polish & Class Diff
Sprint 5A: Class Matcher - ClassMatcher with multiple matching strategies - ExactName, Interface, Similarity, and Auto strategies - Support for partial classes, nested classes, generics - ClassMatchOptions and ClassMatchResult models - 39 comprehensive unit tests Sprint 5B: Class Command - New `class` subcommand for class-to-class comparison - ClassSpecParser for "file.cs:ClassName" syntax - --match-by, --interface, --similarity options - Integration with output formatters - 23 unit tests Sprint 5C: CLI Options - --ignore-whitespace (-w) option - --ignore-comments (-c) option - --context (-C) option for context lines - --mode (-m) option (auto/roslyn/line) - --output (-o) and --out-file options - --rich (-r) option for Spectre.Console output - Comprehensive help text with examples - 57 unit tests Test Results: 361 tests passing - Core: 147 tests (+39) - Output: 130 tests - CLI: 84 tests (+80) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 17df44d commit 7909dff

File tree

10 files changed

+3076
-34
lines changed

10 files changed

+3076
-34
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
namespace RoslynDiff.Cli;
2+
3+
/// <summary>
4+
/// Parses class specification in format "file.cs:ClassName" or "file.cs".
5+
/// </summary>
6+
public static class ClassSpecParser
7+
{
8+
/// <summary>
9+
/// Represents the result of parsing a class specification.
10+
/// </summary>
11+
/// <param name="FilePath">The file path from the specification.</param>
12+
/// <param name="ClassName">The class name from the specification, or null if not specified.</param>
13+
public record ClassSpec(string FilePath, string? ClassName);
14+
15+
/// <summary>
16+
/// Parses a class specification string.
17+
/// </summary>
18+
/// <param name="spec">The specification string in format "file.cs:ClassName" or "file.cs".</param>
19+
/// <returns>A tuple containing the file path and optional class name.</returns>
20+
/// <exception cref="ArgumentException">Thrown when the specification is null, empty, or invalid.</exception>
21+
public static (string FilePath, string? ClassName) Parse(string spec)
22+
{
23+
ArgumentException.ThrowIfNullOrWhiteSpace(spec);
24+
25+
var result = ParseToClassSpec(spec);
26+
return (result.FilePath, result.ClassName);
27+
}
28+
29+
/// <summary>
30+
/// Parses a class specification string into a <see cref="ClassSpec"/> record.
31+
/// </summary>
32+
/// <param name="spec">The specification string in format "file.cs:ClassName" or "file.cs".</param>
33+
/// <returns>A <see cref="ClassSpec"/> containing the parsed file path and optional class name.</returns>
34+
/// <exception cref="ArgumentException">Thrown when the specification is null, empty, or invalid.</exception>
35+
public static ClassSpec ParseToClassSpec(string spec)
36+
{
37+
ArgumentException.ThrowIfNullOrWhiteSpace(spec);
38+
39+
// Handle Windows-style paths with drive letters (e.g., C:\path\file.cs:ClassName)
40+
// We need to find a colon that's not part of a drive letter specification
41+
var colonIndex = FindClassNameSeparator(spec);
42+
43+
if (colonIndex == -1)
44+
{
45+
// No class name specified
46+
return new ClassSpec(spec.Trim(), null);
47+
}
48+
49+
var filePath = spec[..colonIndex].Trim();
50+
var className = spec[(colonIndex + 1)..].Trim();
51+
52+
if (string.IsNullOrWhiteSpace(filePath))
53+
{
54+
throw new ArgumentException("File path cannot be empty in class specification.", nameof(spec));
55+
}
56+
57+
if (string.IsNullOrWhiteSpace(className))
58+
{
59+
throw new ArgumentException("Class name cannot be empty when specified with colon separator.", nameof(spec));
60+
}
61+
62+
// Validate class name is a valid C# identifier
63+
if (!IsValidClassName(className))
64+
{
65+
throw new ArgumentException($"Invalid class name: '{className}'. Must be a valid C# identifier.", nameof(spec));
66+
}
67+
68+
return new ClassSpec(filePath, className);
69+
}
70+
71+
/// <summary>
72+
/// Finds the index of the colon that separates the file path from the class name.
73+
/// Handles Windows drive letter colons (e.g., C:) correctly.
74+
/// </summary>
75+
private static int FindClassNameSeparator(string spec)
76+
{
77+
// Start searching after potential drive letter (index 2 onwards for "C:\...")
78+
var startIndex = 0;
79+
80+
// If it looks like a Windows path with a drive letter, skip past it
81+
if (spec.Length >= 2 && char.IsLetter(spec[0]) && spec[1] == ':')
82+
{
83+
startIndex = 2;
84+
}
85+
86+
// Find the last colon (class name separator should be the last one)
87+
var lastColonIndex = spec.LastIndexOf(':');
88+
89+
if (lastColonIndex < startIndex)
90+
{
91+
return -1;
92+
}
93+
94+
return lastColonIndex;
95+
}
96+
97+
/// <summary>
98+
/// Validates that a string is a valid C# class name (identifier).
99+
/// </summary>
100+
private static bool IsValidClassName(string name)
101+
{
102+
if (string.IsNullOrEmpty(name))
103+
{
104+
return false;
105+
}
106+
107+
// First character must be a letter or underscore
108+
if (!char.IsLetter(name[0]) && name[0] != '_')
109+
{
110+
return false;
111+
}
112+
113+
// Remaining characters must be letters, digits, or underscores
114+
for (var i = 1; i < name.Length; i++)
115+
{
116+
var c = name[i];
117+
if (!char.IsLetterOrDigit(c) && c != '_')
118+
{
119+
return false;
120+
}
121+
}
122+
123+
return true;
124+
}
125+
}

0 commit comments

Comments
 (0)