-
-
Notifications
You must be signed in to change notification settings - Fork 32
Description
Description
The ParsedThemeRule class has a problematic implementation of GetHashCode() and Equals() methods are inconsistent, violating the fundamental hash code contract. This causes incorrect behavior when ParsedThemeRule instances are used testing equality or as keys in Dictionary<T> or stored in HashSet<T>.
I've noticed 3 distinct manifestations of this bug:
- Two
ParsedThemeRuleinstances which only differ bynameare considered equal - Two
ParsedThemeRuleinstances which are "equivalent" by definition do not produce the same hash code and thus break the GetHashCode API contract. - The
ParsedThemeRuletype cannot effectively be used as a key in a Dictionary
Important
Most of the GetHashCode implementations I have seen in TextMateSharp have a fundamental design issue: the fields are not immutable. Fields/properties that are used in GetHashCode should be immutable. This ensures that objects aren't lost in hash-based collectins.
The bug fix I propose here DOES NOT address the field immutability issue. That would be a breaking change since the fields are currently writable.
Root Causes
GetHashCode()method: does not usenameproperty as part of the hash code calculationEquals()method: UsesEnumerable.SequenceEqual()forparentScopescomparison (content-based)GetHashCode()method: UsesList<T>.GetHashCode()forparentScopes(identity-based/reference-based)
This means twoParsedThemeRuleobjects that are considered equal byEquals()can produce different hash codes, violating the contract:
If
obj1.Equals(obj2)returnstrue, thenobj1.GetHashCode()MUST equalobj2.GetHashCode()
Impact
This bug affects any code using ParsedThemeRule for Equality or in hash-based collections:
- ❌ Two
ParsedThemeRuleinstances which only differ bynameare considered equal - ❌ Objects which have the same content but different references are not considered equal
- ❌ Dictionary lookups may fail to find equal keys
- ❌ HashSet may store duplicate "equal" objects
- ❌ LINQ operations like
Distinct()may not work correctly - ❌ Caching strategies using these objects as keys will be unreliable
Steps to Reproduce
Several of the unit tests in the attached unit test class demonstrate the problem:
Equals_DifferentName_ReturnsFalseGetHashCode_CanBeUsedAsDictionaryKeyGetHashCode_EqualObjects_ProduceSameHashCodeToString_NullParentScopes_DoesNotThrow
Proposed Solution
The GetHashCode() method needs to:
- Include the
namefield in the hash computation - Compute a hash based on the contents of
parentScopes, not its reference
Here's the fix:
public override int GetHashCode()
{
int prime = 31;
int result = 1;
result = (prime * result) + ((background == null) ? 0 : background.GetHashCode());
result = (prime * result) + (int)fontStyle;
result = (prime * result) + ((foreground == null) ? 0 : foreground.GetHashCode());
result = (prime * result) + index;
result = (prime * result) + ((name == null) ? 0 : name.GetHashCode());
if (parentScopes != null)
{
foreach (string parentScope in parentScopes)
{
result = (prime * result) + ((parentScope == null) ? 0 : parentScope.GetHashCode());
}
}
result = (prime * result) + ((scope == null) ? 0 : scope.GetHashCode());
return result;
}
public override bool Equals(object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (GetType() != obj.GetType())
return false;
ParsedThemeRule other = (ParsedThemeRule)obj;
if (background == null)
{
if (other.background != null)
return false;
}
else if (!background.Equals(other.background))
return false;
if (fontStyle != other.fontStyle)
return false;
if (foreground == null)
{
if (other.foreground != null)
return false;
}
else if (!foreground.Equals(other.foreground))
return false;
if (index != other.index)
return false;
if (parentScopes == null)
{
if (other.parentScopes != null)
return false;
}
else if (!Enumerable.SequenceEqual(parentScopes, other.parentScopes))
return false;
if (scope == null)
{
if (other.scope != null)
return false;
}
else if (!scope.Equals(other.scope))
return false;
if (name == null)
{
if (other.name != null)
return false;
}
else if (!name.Equals(other.name))
return false;
return true;
}