Skip to content

Commit f5a8084

Browse files
authored
Merge pull request github#2827 from BekaValentine/objectapi-to-valueapi-expectedmappingforformatstring
Python: ObjectAPI to ValueAPI: ExpectedMappingForFormatString
2 parents 47cd9c8 + 909e064 commit f5a8084

File tree

3 files changed

+51
-5
lines changed

3 files changed

+51
-5
lines changed

python/ql/src/Expressions/ExpectedMappingForFormatString.ql

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
import python
1414
import semmle.python.strings
1515

16-
from Expr e, ClassObject t
17-
where exists(BinaryExpr b | b.getOp() instanceof Mod and format_string(b.getLeft()) and e = b.getRight() and
18-
mapping_format(b.getLeft()) and e.refersTo(_, t, _) and not t.isMapping())
16+
from Expr e, ClassValue t
17+
where
18+
exists(BinaryExpr b |
19+
b.getOp() instanceof Mod and
20+
format_string(b.getLeft()) and
21+
e = b.getRight() and
22+
mapping_format(b.getLeft()) and
23+
e.pointsTo().getClass() = t and
24+
not t.isMapping()
25+
)
1926
select e, "Right hand side of a % operator must be a mapping, not class $@.", t, t.getName()

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,45 @@ class ClassValue extends Value {
456456
or
457457
this.hasAttribute("__getitem__")
458458
}
459+
460+
/** Holds if this class is a container(). That is, does it have a __getitem__ method.*/
461+
predicate isContainer() {
462+
exists(this.lookup("__getitem__"))
463+
}
464+
465+
/** Holds if this class is probably a sequence. */
466+
predicate isSequence() {
467+
/* To determine whether something is a sequence or a mapping is not entirely clear,
468+
* so we need to guess a bit.
469+
*/
470+
this.getASuperType() = ClassValue::tuple()
471+
or
472+
this.getASuperType() = ClassValue::list()
473+
or
474+
this.getASuperType() = ClassValue::range()
475+
or
476+
this.getASuperType() = ClassValue::bytes()
477+
or
478+
this.getASuperType() = ClassValue::unicode()
479+
or
480+
major_version() = 2 and this.getASuperType() = Value::named("collections.Sequence")
481+
or
482+
major_version() = 3 and this.getASuperType() = Value::named("collections.abc.Sequence")
483+
or
484+
/* Does it have an index or __reversed__ method? */
485+
this.isContainer() and
486+
(
487+
this.hasAttribute("index") or
488+
this.hasAttribute("__reversed__")
489+
)
490+
}
491+
492+
/** Holds if this class is a mapping. */
493+
predicate isMapping() {
494+
this.hasAttribute("__getitem__")
495+
and
496+
not this.isSequence()
497+
}
459498

460499
/** Holds if this class is a descriptor. */
461500
predicate isDescriptorType() {
@@ -859,7 +898,7 @@ module ClassValue {
859898
ClassValue complex() {
860899
result = TBuiltinClassObject(Builtin::special("complex"))
861900
}
862-
901+
863902
/** Get the `ClassValue` for the `bytes` class (also called `str` in Python 2). */
864903
ClassValue bytes() {
865904
result = TBuiltinClassObject(Builtin::special("bytes"))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
| str_fmt_test.py:5:26:5:26 | x | Right hand side of a % operator must be a mapping, not class $@. | file://:Compiled Code:0:0:0:0 | builtin-class list | list |
1+
| str_fmt_test.py:5:26:5:26 | x | Right hand side of a % operator must be a mapping, not class $@. | file://:0:0:0:0 | builtin-class list | list |

0 commit comments

Comments
 (0)