Skip to content

Commit 5e72b8b

Browse files
authored
Merge pull request #19497 from michaelnebel/csharp/gethashcode
C#: Improve the query `cs/gethashcode-is-not-defined`.
2 parents f6a8909 + 4d79015 commit 5e72b8b

File tree

6 files changed

+87
-14
lines changed

6 files changed

+87
-14
lines changed

csharp/ql/lib/semmle/code/csharp/frameworks/system/collections/Generic.qll

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ class SystemCollectionsGenericIDictionaryInterface extends SystemCollectionsGene
159159
}
160160
}
161161

162+
/** The ``System.Collections.Generic.IReadOnlyDictionary`2`` interface. */
163+
class SystemCollectionsGenericIReadOnlyDictionaryInterface extends SystemCollectionsGenericUnboundGenericInterface
164+
{
165+
SystemCollectionsGenericIReadOnlyDictionaryInterface() {
166+
this.hasName("IReadOnlyDictionary`2") and
167+
this.getNumberOfTypeParameters() = 2
168+
}
169+
}
170+
162171
/** The ``System.Collections.Generic.IReadOnlyCollection`1`` interface. */
163172
class SystemCollectionsGenericIReadOnlyCollectionTInterface extends SystemCollectionsGenericUnboundGenericInterface
164173
{
@@ -176,3 +185,11 @@ class SystemCollectionsGenericIReadOnlyListTInterface extends SystemCollectionsG
176185
this.getNumberOfTypeParameters() = 1
177186
}
178187
}
188+
189+
/** The ``System.Collections.Generic.HashSet`1`` class. */
190+
class SystemCollectionsGenericHashSetClass extends SystemCollectionsGenericUnboundGenericClass {
191+
SystemCollectionsGenericHashSetClass() {
192+
this.hasName("HashSet`1") and
193+
this.getNumberOfTypeParameters() = 1
194+
}
195+
}

csharp/ql/src/Likely Bugs/HashedButNoHash.ql

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,44 @@
1111

1212
import csharp
1313
import semmle.code.csharp.frameworks.System
14+
import semmle.code.csharp.frameworks.system.Collections
15+
import semmle.code.csharp.frameworks.system.collections.Generic
1416

15-
predicate dictionary(ConstructedType constructed) {
16-
exists(UnboundGenericType dict |
17-
dict.hasFullyQualifiedName("System.Collections.Generic", "Dictionary`2") and
18-
constructed = dict.getAConstructedGeneric()
17+
/**
18+
* Holds if `t` is a dictionary type.
19+
*/
20+
predicate dictionary(ValueOrRefType t) {
21+
exists(Type base | base = t.getABaseType*().getUnboundDeclaration() |
22+
base instanceof SystemCollectionsGenericIDictionaryInterface or
23+
base instanceof SystemCollectionsGenericIReadOnlyDictionaryInterface or
24+
base instanceof SystemCollectionsIDictionaryInterface
1925
)
2026
}
2127

22-
predicate hashtable(Class c) { c.hasFullyQualifiedName("System.Collections", "Hashtable") }
28+
/**
29+
* Holds if `c` is a hashset type.
30+
*/
31+
predicate hashSet(ValueOrRefType t) {
32+
t.getABaseType*().getUnboundDeclaration() instanceof SystemCollectionsGenericHashSetClass
33+
}
2334

24-
predicate hashstructure(Type t) { hashtable(t) or dictionary(t) }
35+
predicate hashStructure(Type t) { dictionary(t) or hashSet(t) }
2536

26-
predicate hashAdd(Expr e) {
37+
/**
38+
* Holds if the expression `e` relies on `GetHashCode()` implementation.
39+
* That is, if the call assumes that `e1.Equals(e2)` implies `e1.GetHashCode() == e2.GetHashCode()`.
40+
*/
41+
predicate usesHashing(Expr e) {
2742
exists(MethodCall mc, string name |
28-
(name = "Add" or name = "ContainsKey") and
43+
name = ["Add", "Contains", "ContainsKey", "Remove", "TryAdd", "TryGetValue"] and
2944
mc.getArgument(0) = e and
3045
mc.getTarget().hasName(name) and
31-
hashstructure(mc.getTarget().getDeclaringType())
46+
hashStructure(mc.getTarget().getDeclaringType())
47+
)
48+
or
49+
exists(IndexerCall ic |
50+
ic.getArgument(0) = e and
51+
dictionary(ic.getTarget().getDeclaringType())
3252
)
3353
}
3454

@@ -46,7 +66,7 @@ predicate hashCall(Expr e) {
4666

4767
from Expr e, Type t
4868
where
49-
(hashAdd(e) or hashCall(e)) and
69+
(usesHashing(e) or hashCall(e)) and
5070
e.getType() = t and
5171
eqWithoutHash(t)
5272
select e,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The precision of the query `cs/gethashcode-is-not-defined` has been improved (false negative reduction). Calls to more methods (and indexers) that rely on the invariant `e1.Equals(e2)` implies `e1.GetHashCode() == e2.GetHashCode()` are taken into account.

csharp/ql/test/query-tests/Likely Bugs/HashedButNoHash/HashedButNoHash.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,27 @@ public class Test
66
public void M()
77
{
88
var h = new Hashtable();
9-
h.Add(this, null); // BAD
9+
h.Add(this, null); // $ Alert
10+
h.Contains(this); // $ Alert
11+
h.ContainsKey(this); // $ Alert
12+
h[this] = null; // $ Alert
13+
h.Remove(this); // $ Alert
14+
15+
var l = new List<Test>();
16+
l.Add(this); // Good
17+
1018
var d = new Dictionary<Test, bool>();
11-
d.Add(this, false); // BAD
19+
d.Add(this, false); // $ Alert
20+
d.ContainsKey(this); // $ Alert
21+
d[this] = false; // $ Alert
22+
d.Remove(this); // $ Alert
23+
d.TryAdd(this, false); // $ Alert
24+
d.TryGetValue(this, out bool _); // $ Alert
25+
26+
var hs = new HashSet<Test>();
27+
hs.Add(this); // $ Alert
28+
hs.Contains(this); // $ Alert
29+
hs.Remove(this); // $ Alert
1230
}
1331

1432
public override bool Equals(object other)
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
11
| HashedButNoHash.cs:9:15:9:18 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
2-
| HashedButNoHash.cs:11:15:11:18 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
2+
| HashedButNoHash.cs:10:20:10:23 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
3+
| HashedButNoHash.cs:11:23:11:26 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
4+
| HashedButNoHash.cs:12:11:12:14 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
5+
| HashedButNoHash.cs:13:18:13:21 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
6+
| HashedButNoHash.cs:19:15:19:18 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
7+
| HashedButNoHash.cs:20:23:20:26 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
8+
| HashedButNoHash.cs:21:11:21:14 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
9+
| HashedButNoHash.cs:22:18:22:21 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
10+
| HashedButNoHash.cs:23:18:23:21 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
11+
| HashedButNoHash.cs:24:23:24:26 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
12+
| HashedButNoHash.cs:27:16:27:19 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
13+
| HashedButNoHash.cs:28:21:28:24 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
14+
| HashedButNoHash.cs:29:19:29:22 | this access | This expression is hashed, but type 'Test' only defines Equals(...) not GetHashCode(). |
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
Likely Bugs/HashedButNoHash.ql
1+
query: Likely Bugs/HashedButNoHash.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql
3+

0 commit comments

Comments
 (0)