Skip to content

Commit

Permalink
Make MEN018 support custom group and min sizes for each base
Browse files Browse the repository at this point in the history
  • Loading branch information
menees committed May 28, 2024
1 parent 22ed41f commit 3266dae
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 17 deletions.
22 changes: 5 additions & 17 deletions src/Menees.Analyzers/Men018UseDigitSeparators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override void Initialize(AnalysisContext context)

#region Private Methods

private static void HandleNumericLiteral(SyntaxNodeAnalysisContext context)
private void HandleNumericLiteral(SyntaxNodeAnalysisContext context)
{
// Only make a recommendation if the literal contains no separators already.
// If it's already separated in any way, we'll accept it.
Expand All @@ -62,24 +62,12 @@ private static void HandleNumericLiteral(SyntaxNodeAnalysisContext context)
&& NumericLiteral.TryParse(literalExpression.Token.Text, out NumericLiteral? literal)
&& literal.ScrubbedDigits == literal.OriginalDigits)
{
const byte PreferredHexGroupSize = 2; // Per-Byte
const byte PreferredBinaryGroupSize = 4; // Per-Nibble
const byte PreferredDecimalGroupSize = 3; // Per-Thousand
byte preferredGroupSize = literal.Base switch
{
NumericBase.Hexadecimal => PreferredHexGroupSize,
NumericBase.Binary => PreferredBinaryGroupSize,
_ => PreferredDecimalGroupSize,
};

// For integers, this length check is a quick short-circuit.
// For reals, it may be insufficient (e.g., 12.5 is 4 chars,
// but the integer part is only 2). However, comparing the ToString
// results below will be sufficient to avoid false positives.
if (literal.ScrubbedDigits.Length > preferredGroupSize)
byte literalSize = literal.GetSize();
(byte minSize, byte groupSize) = this.Settings.GetDigitSeparatorFormat(literal);
if (literalSize >= minSize)
{
string literalText = literal.ToString();
string preferredText = literal.ToString(preferredGroupSize);
string preferredText = literal.ToString(groupSize);
if (preferredText != literalText)
{
var builder = ImmutableDictionary.CreateBuilder<string, string?>();
Expand Down
29 changes: 29 additions & 0 deletions src/Menees.Analyzers/Menees.Analyzers.Settings.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
</xs:restriction>
</xs:simpleType>

<xs:complexType name="DigitSeparator">
<xs:attribute name="MinSize" use="required">
<xs:simpleType>
<xs:restriction base="xs:unsignedByte">
<xs:minInclusive value="2" />
<xs:maxInclusive value="28" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="GroupSize" use="required">
<xs:simpleType>
<xs:restriction base="xs:unsignedByte">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="10" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>

<xs:element name="Menees.Analyzers.Settings">
<xs:complexType>
<xs:all>
Expand Down Expand Up @@ -68,6 +87,16 @@
</xs:choice>
</xs:complexType>
</xs:element>

<xs:element name="DigitSeparators" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
<xs:element name="Decimal" type="DigitSeparator" minOccurs="0" maxOccurs="1"/>
<xs:element name="Hexadecimal" type="DigitSeparator" minOccurs="0" maxOccurs="1"/>
<xs:element name="Binary" type="DigitSeparator" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
Expand Down
47 changes: 47 additions & 0 deletions src/Menees.Analyzers/NumericLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,53 @@ public string ToString(byte groupSize)
return result;
}

/// <summary>
/// Gets the length of the longest part of the number (e.g., integer or fraction).
/// </summary>
public byte GetSize()
{
int size;

if (this.IsInteger)
{
size = this.ScrubbedDigits.Length;
}
else
{
// See comments in ToString(byte) for how we split up ScrubbedDigits.
int decimalIndex = this.ScrubbedDigits.IndexOf('.');
int exponentIndex = this.ScrubbedDigits.IndexOfAny(ExponentChar, decimalIndex + 1);

if (decimalIndex < 0 && exponentIndex < 0)
{
// Integer part only. Example: 123d
size = this.ScrubbedDigits.Length;
}
else if (exponentIndex < 0)
{
// Has fraction part and may have an integer part. Examples: .123 or 1.23
// We'll use max part length instead of total digit length so 1234.5 isn't
// formatted to 1_234.5 with minSize 5 and groupSize 3. Also, consider
// 5678.901234 and 567890.1234. We'd want to end up with 5_678.901_234
// and 567_890.123_4 for consistency.
size = Math.Max(decimalIndex, this.ScrubbedDigits.Length - (decimalIndex + 1));
}
else if (decimalIndex < 0)
{
// Has exponent part with a required integer part. Example: 12e3
size = exponentIndex;
}
else
{
// Has fraction part, exponent part, and maybe an integer part. Examples: .12e3 or 1.2e3
size = Math.Max(decimalIndex, exponentIndex - (decimalIndex + 1));
}
}

byte result = size > byte.MaxValue ? byte.MaxValue : (byte)size;
return result;
}

#endregion

#region Private Methods
Expand Down
55 changes: 55 additions & 0 deletions src/Menees.Analyzers/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ private Settings(XElement xml)
.Select(term => new KeyValuePair<string, string>(term.Attribute("Avoid").Value, term.Attribute("Prefer").Value))
.ToDictionary(pair => pair.Key, pair => pair.Value);
}

XElement digitSeparators = xml.Element("DigitSeparators");
if (digitSeparators != null)
{
this.DecimalSeparators = GetDigitSeparatorFormat(digitSeparators.Element("Decimal"), this.DecimalSeparators);
this.HexadecimalSeparators = GetDigitSeparatorFormat(digitSeparators.Element("Hexadecimal"), this.HexadecimalSeparators);
this.BinarySeparators = GetDigitSeparatorFormat(digitSeparators.Element("Binary"), this.BinarySeparators);
}
}

#endregion
Expand Down Expand Up @@ -181,6 +189,16 @@ private Settings(XElement xml)

#endregion

#region Private Properties

private (byte MinSize, byte GroupSize) DecimalSeparators { get; } = (5, 3); // Group Per-Thousand

private (byte MinSize, byte GroupSize) HexadecimalSeparators { get; } = (5, 2); // Group Per-Byte

private (byte MinSize, byte GroupSize) BinarySeparators { get; } = (6, 4); // Group Per-Nibble

#endregion

#region Public Methods

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

public (byte MinSize, byte GroupSize) GetDigitSeparatorFormat(NumericLiteral literal)
{
(byte MinSize, byte GroupSize) result = literal.Base switch
{
NumericBase.Hexadecimal => this.HexadecimalSeparators,
NumericBase.Binary => this.BinarySeparators,
_ => this.DecimalSeparators,
};

return result;
}

#endregion

#region Private Methods
Expand Down Expand Up @@ -409,6 +439,31 @@ private static (string Scrubbed, NumericBase Base) SplitNumericLiteral(string te
return (text, numericBase);
}

private (byte MinSize, byte GroupSize) GetDigitSeparatorFormat(XElement? baseElement, (byte MinSize, byte GroupSize) defaultSeparators)
{
(byte MinSize, byte GroupSize) result = defaultSeparators;

if (baseElement != null)
{
byte minSize = GetByte(baseElement, "MinSize", defaultSeparators.MinSize);
byte groupSize = GetByte(baseElement, "GroupSize", defaultSeparators.GroupSize);
result = (minSize, groupSize);
}

static byte GetByte(XElement element, string attributeName, byte defaultValue)
{
string? value = element.Attribute(attributeName)?.Value;
if (!byte.TryParse(value, out byte result))
{
result = defaultValue;
}

return result;
}

return result;
}

#endregion

#region Private Delegates
Expand Down
6 changes: 6 additions & 0 deletions tests/Menees.Analyzers.Test/Menees.Analyzers.Settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@
<Term Avoid="Indices" Prefer="Indexes" />
<Term Avoid="Kustom" Prefer="Custom"/>
</PreferredTerms>

<DigitSeparators>
<Decimal MinSize="4" GroupSize="3" />
<Hexadecimal MinSize="3" GroupSize="2" />
<Binary MinSize="5" GroupSize="4" />
</DigitSeparators>
</Menees.Analyzers.Settings>
34 changes: 34 additions & 0 deletions tests/Menees.Analyzers.Test/NumericLiteralTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,38 @@ static void Test(string text, string expected, byte? groupSize = null)
literal.ToString(groupSize.Value).ShouldBe(expected, text);
}
}

[TestMethod]
public void GetSizeTest()
{
Test("123", 3);
Test("10543765Lu", 8);
Test("1_2__3___4____5", 5);

Test("0xFf", 2);
Test("0X1ba044fEL", 8);

Test("0B1001_1010u", 8);
Test("0b1111_1111_0000UL", 12);
Test("0B__111", 3);

Test("1.234_567", 6);
Test("1_234.567", 4);
Test("123_456.7", 6);
Test(".123456", 6);
Test(".12345e67", 5);
Test("1234567d", 7);
Test("1234.567e89", 4);

Test(".3e5f", 1);
Test("2345E-2_0", 4);
Test("15D", 2);
Test("19.73M", 2);

static void Test(string text, byte expected)
{
NumericLiteral.TryParse(text, out NumericLiteral? literal).ShouldBeTrue(text);
literal.GetSize().ShouldBe(expected, text);
}
}
}

0 comments on commit 3266dae

Please sign in to comment.