Skip to content

Commit 4cdb3c1

Browse files
authored
Merge pull request github#3658 from RasmusWL/python-3.8-dict-ismapping
Approved by tausbn
2 parents f7c6b13 + 48b2d2c commit 4cdb3c1

File tree

9 files changed

+192
-8
lines changed

9 files changed

+192
-8
lines changed

python/ql/src/semmle/python/objects/ObjectAPI.qll

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,14 @@ class ClassValue extends Value {
517517
/** Holds if this class is a container(). That is, does it have a __getitem__ method. */
518518
predicate isContainer() { exists(this.lookup("__getitem__")) }
519519

520-
/** Holds if this class is probably a sequence. */
520+
/**
521+
* Holds if this class is a sequence. Mutually exclusive with `isMapping()`.
522+
*
523+
* Following the definition from
524+
* https://docs.python.org/3/glossary.html#term-sequence.
525+
* We don't look at the keys accepted by `__getitem__, but default to treating a class
526+
* as a sequence (so might treat some mappings as sequences).
527+
*/
521528
predicate isSequence() {
522529
/*
523530
* To determine whether something is a sequence or a mapping is not entirely clear,
@@ -538,16 +545,26 @@ class ClassValue extends Value {
538545
or
539546
major_version() = 3 and this.getASuperType() = Value::named("collections.abc.Sequence")
540547
or
541-
/* Does it have an index or __reversed__ method? */
542-
this.isContainer() and
543-
(
544-
this.hasAttribute("index") or
545-
this.hasAttribute("__reversed__")
546-
)
548+
this.hasAttribute("__getitem__") and
549+
this.hasAttribute("__len__") and
550+
not this.getASuperType() = ClassValue::dict() and
551+
not this.getASuperType() = Value::named("collections.Mapping") and
552+
not this.getASuperType() = Value::named("collections.abc.Mapping")
547553
}
548554

549-
/** Holds if this class is a mapping. */
555+
/**
556+
* Holds if this class is a mapping. Mutually exclusive with `isSequence()`.
557+
*
558+
* Although a class will satisfy the requirement by the definition in
559+
* https://docs.python.org/3.8/glossary.html#term-mapping, we don't look at the keys
560+
* accepted by `__getitem__, but default to treating a class as a sequence (so might
561+
* treat some mappings as sequences).
562+
*/
550563
predicate isMapping() {
564+
major_version() = 2 and this.getASuperType() = Value::named("collections.Mapping")
565+
or
566+
major_version() = 3 and this.getASuperType() = Value::named("collections.abc.Mapping")
567+
or
551568
this.hasAttribute("__getitem__") and
552569
not this.isSequence()
553570
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
| mapping | builtin-class collections.defaultdict |
2+
| mapping | builtin-class dict |
3+
| mapping | class MyDictSubclass |
4+
| mapping | class MyMappingABC |
5+
| mapping | class OrderedDict |
6+
| neither sequence nor mapping | builtin-class set |
7+
| sequence | builtin-class list |
8+
| sequence | builtin-class str |
9+
| sequence | builtin-class tuple |
10+
| sequence | builtin-class unicode |
11+
| sequence | class MySequenceABC |
12+
| sequence | class MySequenceImpl |
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import python
2+
3+
from ClassValue cls, string res
4+
where
5+
exists(CallNode call |
6+
call.getFunction().(NameNode).getId() = "test" and
7+
call.getAnArg().pointsTo(cls)
8+
) and
9+
(
10+
cls.isSequence() and
11+
cls.isMapping() and
12+
res = "IS BOTH. SHOULD NOT HAPPEN. THEY ARE MUTUALLY EXCLUSIVE."
13+
or
14+
cls.isSequence() and not cls.isMapping() and res = "sequence"
15+
or
16+
not cls.isSequence() and cls.isMapping() and res = "mapping"
17+
or
18+
not cls.isSequence() and not cls.isMapping() and res = "neither sequence nor mapping"
19+
)
20+
select res, cls.toString()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: --lang=2 --max-import-depth=2
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from collections import OrderedDict, defaultdict
2+
3+
# Python 2 specific
4+
from collections import Sequence, Mapping
5+
6+
def test(*args):
7+
pass
8+
9+
class MySequenceABC(Sequence):
10+
pass
11+
12+
class MyMappingABC(Mapping):
13+
pass
14+
15+
class MySequenceImpl(object):
16+
def __getitem__(self, key):
17+
pass
18+
19+
def __len__(self):
20+
pass
21+
22+
class MyDictSubclass(dict):
23+
pass
24+
25+
test(
26+
list,
27+
tuple,
28+
str,
29+
unicode,
30+
bytes,
31+
MySequenceABC,
32+
MySequenceImpl,
33+
set,
34+
dict,
35+
OrderedDict,
36+
defaultdict,
37+
MyMappingABC,
38+
MyDictSubclass,
39+
)
40+
41+
for seq_cls in (list, tuple, str, bytes):
42+
assert issubclass(seq_cls, collections.abc.Sequence)
43+
assert not issubclass(seq_cls, collections.abc.Mapping)
44+
45+
for map_cls in (dict, OrderedDict, defaultdict):
46+
assert not issubclass(map_cls, collections.abc.Sequence)
47+
assert issubclass(map_cls, collections.abc.Mapping)
48+
49+
assert not issubclass(set, collections.abc.Sequence)
50+
assert not issubclass(set, collections.abc.Mapping)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
| mapping | builtin-class collections.OrderedDict |
2+
| mapping | builtin-class collections.defaultdict |
3+
| mapping | builtin-class dict |
4+
| mapping | class MyDictSubclass |
5+
| mapping | class MyMappingABC |
6+
| mapping | class OrderedDict |
7+
| neither sequence nor mapping | builtin-class set |
8+
| sequence | builtin-class bytes |
9+
| sequence | builtin-class list |
10+
| sequence | builtin-class str |
11+
| sequence | builtin-class tuple |
12+
| sequence | class MySequenceABC |
13+
| sequence | class MySequenceImpl |
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import python
2+
3+
from ClassValue cls, string res
4+
where
5+
exists(CallNode call |
6+
call.getFunction().(NameNode).getId() = "test" and
7+
call.getAnArg().pointsTo(cls)
8+
) and
9+
(
10+
cls.isSequence() and
11+
cls.isMapping() and
12+
res = "IS BOTH. SHOULD NOT HAPPEN. THEY ARE MUTUALLY EXCLUSIVE."
13+
or
14+
cls.isSequence() and not cls.isMapping() and res = "sequence"
15+
or
16+
not cls.isSequence() and cls.isMapping() and res = "mapping"
17+
or
18+
not cls.isSequence() and not cls.isMapping() and res = "neither sequence nor mapping"
19+
)
20+
select res, cls.toString()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: --max-import-depth=2
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from collections import OrderedDict, defaultdict
2+
3+
# Python 3 specific
4+
from collections.abc import Sequence, Mapping
5+
6+
def test(*args):
7+
pass
8+
9+
class MySequenceABC(Sequence):
10+
pass
11+
12+
class MyMappingABC(Mapping):
13+
pass
14+
15+
class MySequenceImpl(object):
16+
def __getitem__(self, key):
17+
pass
18+
19+
def __len__(self):
20+
pass
21+
22+
class MyDictSubclass(dict):
23+
pass
24+
25+
test(
26+
list,
27+
tuple,
28+
str,
29+
unicode,
30+
bytes,
31+
MySequenceABC,
32+
MySequenceImpl,
33+
set,
34+
dict,
35+
OrderedDict,
36+
defaultdict,
37+
MyMappingABC,
38+
MyDictSubclass,
39+
)
40+
41+
for seq_cls in (list, tuple, str, bytes):
42+
assert issubclass(seq_cls, collections.abc.Sequence)
43+
assert not issubclass(seq_cls, collections.abc.Mapping)
44+
45+
for map_cls in (dict, OrderedDict, defaultdict):
46+
assert not issubclass(map_cls, collections.abc.Sequence)
47+
assert issubclass(map_cls, collections.abc.Mapping)
48+
49+
assert not issubclass(set, collections.abc.Sequence)
50+
assert not issubclass(set, collections.abc.Mapping)

0 commit comments

Comments
 (0)