Skip to content

Commit 123dcc7

Browse files
authored
Merge pull request #16971 from RasmusWL/mad-dict-source
Python: Add MaD support for DictionaryElement/DictionaryElementAny for sources
2 parents 8901b1f + efcd4e2 commit 123dcc7

File tree

5 files changed

+50
-4
lines changed

5 files changed

+50
-4
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added support for `DictionaryElement[<key>]` and `DictionaryElementAny` when Customizing Library Models for `sourceModel` (see https://codeql.github.com/docs/codeql-language-guides/customizing-library-models-for-python/)

python/ql/lib/semmle/python/frameworks/data/internal/ApiGraphModelsSpecific.qll

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,25 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathTokenBase token) {
134134
token.getAnArgument() = "any-named" and
135135
result = node.getKeywordParameter(_)
136136
)
137+
or
138+
// content based steps
139+
//
140+
// note: if we want to migrate to use `FlowSummaryImpl::Input::encodeContent` like
141+
// they do in Ruby, be aware that we currently don't make
142+
// `DataFlow::DictionaryElementContent` just from seeing a subscript read, so we would
143+
// need to add that. (also need to handle things like `DictionaryElementAny` which
144+
// doesn't have any value for .getAnArgument())
145+
(
146+
token.getName() = "DictionaryElement" and
147+
result = node.getSubscript(token.getAnArgument())
148+
or
149+
token.getName() = "DictionaryElementAny" and
150+
result = node.getASubscript() and
151+
not exists(token.getAnArgument())
152+
// TODO: ListElement/SetElement/TupleElement
153+
)
137154
// Some features don't have MaD tokens yet, they would need to be added to API-graphs first.
138155
// - decorators ("DecoratedClass", "DecoratedMember", "DecoratedParameter")
139-
// - Array/Map elements ("ArrayElement", "Element", "MapKey", "MapValue")
140156
}
141157

142158
/**
@@ -242,15 +258,19 @@ InvokeNode getAnInvocationOf(API::Node node) { result = node.getACall() }
242258
*/
243259
bindingset[name]
244260
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
245-
name = ["Member", "Instance", "Awaited", "Call", "Method", "Subclass"]
261+
name =
262+
[
263+
"Member", "Instance", "Awaited", "Call", "Method", "Subclass", "DictionaryElement",
264+
"DictionaryElementAny"
265+
]
246266
}
247267

248268
/**
249269
* Holds if `name` is a valid name for an access path token with no arguments, occurring
250270
* in an identifying access path.
251271
*/
252272
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
253-
name = ["Instance", "Awaited", "Call", "Subclass"]
273+
name = ["Instance", "Awaited", "Call", "Subclass", "DictionaryElementAny"]
254274
}
255275

256276
/**
@@ -259,7 +279,7 @@ predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
259279
*/
260280
bindingset[name, argument]
261281
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
262-
name = ["Member", "Method"] and
282+
name = ["Member", "Method", "DictionaryElement"] and
263283
exists(argument)
264284
or
265285
name = ["Argument", "Parameter"] and

python/ql/test/library-tests/frameworks/data/test.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ isSource
106106
| test.py:117:31:117:41 | ControlFlowNode for getSource() | test-source |
107107
| test.py:118:35:118:45 | ControlFlowNode for getSource() | test-source |
108108
| test.py:119:20:119:30 | ControlFlowNode for getSource() | test-source |
109+
| test.py:124:1:124:33 | ControlFlowNode for Attribute() | test-source |
110+
| test.py:126:11:126:43 | ControlFlowNode for Attribute() | test-source |
109111
syntaxErrors
110112
| Member[foo |
111113
| Member[foo] .Member[bar] |

python/ql/test/library-tests/frameworks/data/test.ext.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ extensions:
2323
- ["testlib", "Member[ArgPos].Member[MyClass].Subclass.Member[otherSelfTest].Parameter[0]", "test-source"]
2424
- ["testlib", "Member[ArgPos].Member[MyClass].Subclass.Member[anyParam].Parameter[any]", "test-source"]
2525
- ["testlib", "Member[ArgPos].Member[MyClass].Subclass.Member[anyNamed].Parameter[any-named]", "test-source"]
26+
# test steps through content
27+
- ["testlib", "Member[source_dict].DictionaryElement[key].Member[func].ReturnValue", "test-source"]
28+
- ["testlib", "Member[source_dict_any].DictionaryElementAny.Member[func].ReturnValue", "test-source"]
29+
# TODO: Add support for list/tuples
30+
# - ["testlib", "Member[source_list].ListElement.Member[func].ReturnValue", "test-source"]
31+
# - ["testlib", "Member[source_tuple].TupleElement[0].Member[func].ReturnValue", "test-source"]
2632

2733
- addsTo:
2834
pack: codeql/python-all

python/ql/test/library-tests/frameworks/data/test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,17 @@ def anyNamed(self, name1, name2=2): # Parameter[any-named] matches all non-self
117117
testlib.foo().bar().fuzzyCall(getSource()) # NOT OK
118118
testlib.foo(lambda x: x.fuzzyCall(getSource())) # NOT OK
119119
otherlib.fuzzyCall(getSource()) # OK
120+
121+
# defining sources through content steps
122+
123+
# dictionaries
124+
testlib.source_dict["key"].func() # source
125+
testlib.source_dict["safe"].func() # not a source
126+
lambda k: testlib.source_dict_any[k].func() # source
127+
128+
# TODO: implement support for lists
129+
lambda i: testlib.source_list[i].func()
130+
131+
# TODO: implement support for tuples
132+
testlib.source_tuple[0].func() # a source
133+
testlib.source_tuple[1].func() # not a source

0 commit comments

Comments
 (0)