Skip to content

Commit 3266dae

Browse files
committed
Make MEN018 support custom group and min sizes for each base
1 parent 22ed41f commit 3266dae

File tree

6 files changed

+176
-17
lines changed

6 files changed

+176
-17
lines changed

src/Menees.Analyzers/Men018UseDigitSeparators.cs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public override void Initialize(AnalysisContext context)
5353

5454
#region Private Methods
5555

56-
private static void HandleNumericLiteral(SyntaxNodeAnalysisContext context)
56+
private void HandleNumericLiteral(SyntaxNodeAnalysisContext context)
5757
{
5858
// Only make a recommendation if the literal contains no separators already.
5959
// If it's already separated in any way, we'll accept it.
@@ -62,24 +62,12 @@ private static void HandleNumericLiteral(SyntaxNodeAnalysisContext context)
6262
&& NumericLiteral.TryParse(literalExpression.Token.Text, out NumericLiteral? literal)
6363
&& literal.ScrubbedDigits == literal.OriginalDigits)
6464
{
65-
const byte PreferredHexGroupSize = 2; // Per-Byte
66-
const byte PreferredBinaryGroupSize = 4; // Per-Nibble
67-
const byte PreferredDecimalGroupSize = 3; // Per-Thousand
68-
byte preferredGroupSize = literal.Base switch
69-
{
70-
NumericBase.Hexadecimal => PreferredHexGroupSize,
71-
NumericBase.Binary => PreferredBinaryGroupSize,
72-
_ => PreferredDecimalGroupSize,
73-
};
74-
75-
// For integers, this length check is a quick short-circuit.
76-
// For reals, it may be insufficient (e.g., 12.5 is 4 chars,
77-
// but the integer part is only 2). However, comparing the ToString
78-
// results below will be sufficient to avoid false positives.
79-
if (literal.ScrubbedDigits.Length > preferredGroupSize)
65+
byte literalSize = literal.GetSize();
66+
(byte minSize, byte groupSize) = this.Settings.GetDigitSeparatorFormat(literal);
67+
if (literalSize >= minSize)
8068
{
8169
string literalText = literal.ToString();
82-
string preferredText = literal.ToString(preferredGroupSize);
70+
string preferredText = literal.ToString(groupSize);
8371
if (preferredText != literalText)
8472
{
8573
var builder = ImmutableDictionary.CreateBuilder<string, string?>();

src/Menees.Analyzers/Menees.Analyzers.Settings.xsd

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@
66
</xs:restriction>
77
</xs:simpleType>
88

9+
<xs:complexType name="DigitSeparator">
10+
<xs:attribute name="MinSize" use="required">
11+
<xs:simpleType>
12+
<xs:restriction base="xs:unsignedByte">
13+
<xs:minInclusive value="2" />
14+
<xs:maxInclusive value="28" />
15+
</xs:restriction>
16+
</xs:simpleType>
17+
</xs:attribute>
18+
<xs:attribute name="GroupSize" use="required">
19+
<xs:simpleType>
20+
<xs:restriction base="xs:unsignedByte">
21+
<xs:minInclusive value="1"/>
22+
<xs:maxInclusive value="10" />
23+
</xs:restriction>
24+
</xs:simpleType>
25+
</xs:attribute>
26+
</xs:complexType>
27+
928
<xs:element name="Menees.Analyzers.Settings">
1029
<xs:complexType>
1130
<xs:all>
@@ -68,6 +87,16 @@
6887
</xs:choice>
6988
</xs:complexType>
7089
</xs:element>
90+
91+
<xs:element name="DigitSeparators" minOccurs="0" maxOccurs="1">
92+
<xs:complexType>
93+
<xs:sequence>
94+
<xs:element name="Decimal" type="DigitSeparator" minOccurs="0" maxOccurs="1"/>
95+
<xs:element name="Hexadecimal" type="DigitSeparator" minOccurs="0" maxOccurs="1"/>
96+
<xs:element name="Binary" type="DigitSeparator" minOccurs="0" maxOccurs="1"/>
97+
</xs:sequence>
98+
</xs:complexType>
99+
</xs:element>
71100
</xs:all>
72101
</xs:complexType>
73102
</xs:element>

src/Menees.Analyzers/NumericLiteral.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,53 @@ public string ToString(byte groupSize)
177177
return result;
178178
}
179179

180+
/// <summary>
181+
/// Gets the length of the longest part of the number (e.g., integer or fraction).
182+
/// </summary>
183+
public byte GetSize()
184+
{
185+
int size;
186+
187+
if (this.IsInteger)
188+
{
189+
size = this.ScrubbedDigits.Length;
190+
}
191+
else
192+
{
193+
// See comments in ToString(byte) for how we split up ScrubbedDigits.
194+
int decimalIndex = this.ScrubbedDigits.IndexOf('.');
195+
int exponentIndex = this.ScrubbedDigits.IndexOfAny(ExponentChar, decimalIndex + 1);
196+
197+
if (decimalIndex < 0 && exponentIndex < 0)
198+
{
199+
// Integer part only. Example: 123d
200+
size = this.ScrubbedDigits.Length;
201+
}
202+
else if (exponentIndex < 0)
203+
{
204+
// Has fraction part and may have an integer part. Examples: .123 or 1.23
205+
// We'll use max part length instead of total digit length so 1234.5 isn't
206+
// formatted to 1_234.5 with minSize 5 and groupSize 3. Also, consider
207+
// 5678.901234 and 567890.1234. We'd want to end up with 5_678.901_234
208+
// and 567_890.123_4 for consistency.
209+
size = Math.Max(decimalIndex, this.ScrubbedDigits.Length - (decimalIndex + 1));
210+
}
211+
else if (decimalIndex < 0)
212+
{
213+
// Has exponent part with a required integer part. Example: 12e3
214+
size = exponentIndex;
215+
}
216+
else
217+
{
218+
// Has fraction part, exponent part, and maybe an integer part. Examples: .12e3 or 1.2e3
219+
size = Math.Max(decimalIndex, exponentIndex - (decimalIndex + 1));
220+
}
221+
}
222+
223+
byte result = size > byte.MaxValue ? byte.MaxValue : (byte)size;
224+
return result;
225+
}
226+
180227
#endregion
181228

182229
#region Private Methods

src/Menees.Analyzers/Settings.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ private Settings(XElement xml)
133133
.Select(term => new KeyValuePair<string, string>(term.Attribute("Avoid").Value, term.Attribute("Prefer").Value))
134134
.ToDictionary(pair => pair.Key, pair => pair.Value);
135135
}
136+
137+
XElement digitSeparators = xml.Element("DigitSeparators");
138+
if (digitSeparators != null)
139+
{
140+
this.DecimalSeparators = GetDigitSeparatorFormat(digitSeparators.Element("Decimal"), this.DecimalSeparators);
141+
this.HexadecimalSeparators = GetDigitSeparatorFormat(digitSeparators.Element("Hexadecimal"), this.HexadecimalSeparators);
142+
this.BinarySeparators = GetDigitSeparatorFormat(digitSeparators.Element("Binary"), this.BinarySeparators);
143+
}
136144
}
137145

138146
#endregion
@@ -181,6 +189,16 @@ private Settings(XElement xml)
181189

182190
#endregion
183191

192+
#region Private Properties
193+
194+
private (byte MinSize, byte GroupSize) DecimalSeparators { get; } = (5, 3); // Group Per-Thousand
195+
196+
private (byte MinSize, byte GroupSize) HexadecimalSeparators { get; } = (5, 2); // Group Per-Byte
197+
198+
private (byte MinSize, byte GroupSize) BinarySeparators { get; } = (6, 4); // Group Per-Nibble
199+
200+
#endregion
201+
184202
#region Public Methods
185203

186204
public static Settings Cache(AnalysisContext context, AnalyzerOptions options, CancellationToken cancellationToken)
@@ -302,6 +320,18 @@ public bool UsePreferredTerm(string term, out string preferredTerm)
302320
return result;
303321
}
304322

323+
public (byte MinSize, byte GroupSize) GetDigitSeparatorFormat(NumericLiteral literal)
324+
{
325+
(byte MinSize, byte GroupSize) result = literal.Base switch
326+
{
327+
NumericBase.Hexadecimal => this.HexadecimalSeparators,
328+
NumericBase.Binary => this.BinarySeparators,
329+
_ => this.DecimalSeparators,
330+
};
331+
332+
return result;
333+
}
334+
305335
#endregion
306336

307337
#region Private Methods
@@ -409,6 +439,31 @@ private static (string Scrubbed, NumericBase Base) SplitNumericLiteral(string te
409439
return (text, numericBase);
410440
}
411441

442+
private (byte MinSize, byte GroupSize) GetDigitSeparatorFormat(XElement? baseElement, (byte MinSize, byte GroupSize) defaultSeparators)
443+
{
444+
(byte MinSize, byte GroupSize) result = defaultSeparators;
445+
446+
if (baseElement != null)
447+
{
448+
byte minSize = GetByte(baseElement, "MinSize", defaultSeparators.MinSize);
449+
byte groupSize = GetByte(baseElement, "GroupSize", defaultSeparators.GroupSize);
450+
result = (minSize, groupSize);
451+
}
452+
453+
static byte GetByte(XElement element, string attributeName, byte defaultValue)
454+
{
455+
string? value = element.Attribute(attributeName)?.Value;
456+
if (!byte.TryParse(value, out byte result))
457+
{
458+
result = defaultValue;
459+
}
460+
461+
return result;
462+
}
463+
464+
return result;
465+
}
466+
412467
#endregion
413468

414469
#region Private Delegates

tests/Menees.Analyzers.Test/Menees.Analyzers.Settings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,10 @@
5151
<Term Avoid="Indices" Prefer="Indexes" />
5252
<Term Avoid="Kustom" Prefer="Custom"/>
5353
</PreferredTerms>
54+
55+
<DigitSeparators>
56+
<Decimal MinSize="4" GroupSize="3" />
57+
<Hexadecimal MinSize="3" GroupSize="2" />
58+
<Binary MinSize="5" GroupSize="4" />
59+
</DigitSeparators>
5460
</Menees.Analyzers.Settings>

tests/Menees.Analyzers.Test/NumericLiteralTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,38 @@ static void Test(string text, string expected, byte? groupSize = null)
133133
literal.ToString(groupSize.Value).ShouldBe(expected, text);
134134
}
135135
}
136+
137+
[TestMethod]
138+
public void GetSizeTest()
139+
{
140+
Test("123", 3);
141+
Test("10543765Lu", 8);
142+
Test("1_2__3___4____5", 5);
143+
144+
Test("0xFf", 2);
145+
Test("0X1ba044fEL", 8);
146+
147+
Test("0B1001_1010u", 8);
148+
Test("0b1111_1111_0000UL", 12);
149+
Test("0B__111", 3);
150+
151+
Test("1.234_567", 6);
152+
Test("1_234.567", 4);
153+
Test("123_456.7", 6);
154+
Test(".123456", 6);
155+
Test(".12345e67", 5);
156+
Test("1234567d", 7);
157+
Test("1234.567e89", 4);
158+
159+
Test(".3e5f", 1);
160+
Test("2345E-2_0", 4);
161+
Test("15D", 2);
162+
Test("19.73M", 2);
163+
164+
static void Test(string text, byte expected)
165+
{
166+
NumericLiteral.TryParse(text, out NumericLiteral? literal).ShouldBeTrue(text);
167+
literal.GetSize().ShouldBe(expected, text);
168+
}
169+
}
136170
}

0 commit comments

Comments
 (0)