Is your feature request related to a problem? Please describe.
At analysis_level=4, a function parameter has two disjoint def-sites in the DDG — the @formal_in:<i> port and the @entry node — with no edge between them, and only @entry reaches the argument ports (@<line>:<col>/actual_in:<i>) of the calls in that body. A consumer that starts a traversal at the formal_in port (the only vertex addressable by parameter name) therefore cannot reach the first callee the parameter is passed to.
The consequence is not a missing path — it is a proved absence of flow. A taint query that finds no path and no diagnostic is entitled to report the pair as refuted, so scrub(user_input) reads as "user_input does not reach scrub".
formal_in:<i> is the vertex a consumer can name: it is the only one carrying var, so it is what a parameter selector resolves to. Starting there:
| query |
result |
correct |
user_input (in handle) → raw (in scrub) |
0 paths |
1 |
raw (in scrub) → cleaned (in run_query) |
2 paths |
✓ |
The second row shows the interprocedural machinery is otherwise intact — PY_PARAM_IN / PY_PARAM_OUT are present and carry var since 1.5.1. The break is specifically the first crossing out of the callable whose parameter you started from.
Direction of the error matters: a missing edge in a dataflow graph yields a false negative, and a consumer cannot distinguish "no path exists" from "the path is not in the graph". Any caller-parameter-to-callee-parameter taint question is answered wrongly and confidently.
Describe the solution you'd like
Not stated in the original issue.
Describe alternatives you've considered
Not stated in the original issue.
Additional context
Reproduction
app.py:
def scrub(raw):
return "".join(ch for ch in raw if ch.isalnum())
class Handler:
def handle(self, user_input):
answer = scrub(user_input)
return answer
Emitted with AnalysisOptions(..., emit=EmitTarget.NEO4J, analysis_level=4) on codeanalyzer-python 1.5.1.
user_input has two def-sites:
MATCH (a{id:'...handle(self,user_input)@formal_in:1'})-[r:PY_DDG]->(b) RETURN b.id, r.var
--['user_input']-> ...handle(self,user_input)@35:8 # the call *statement*
--['user_input']-> ...handle(self,user_input)@36:8
MATCH (a{id:'...handle(self,user_input)@entry'})-[r:PY_DDG]->(b) RETURN b.id, r.var
--['user_input']-> ...handle(self,user_input)@35:8
--['user_input']-> ...handle(self,user_input)@35:8/actual_in:0 # the argument *port*
--['user_input']-> ...handle(self,user_input)@36:8
--['user_input']-> ...handle(self,user_input)@36:8/actual_in:0
and there is no edge of any type between @formal_in:1 and @entry, in either direction.
The argument port's only DDG predecessors are @entry and earlier statements — never the statement that makes the call, and never the enclosing formal_in:
...handle@entry --PY_DDG['user_input']-> ...handle@35:8/actual_in:0
...handle@entry --PY_DDG['user_input']-> ...handle@36:8/actual_in:0
...handle@35:8 --PY_DDG['user_input']-> ...handle@36:8/actual_in:0
So @35:8/actual_in:0 — the argument of the first call — is reachable only from @entry.
Secondary observation (same emit, lower severity)
Reaching-definition edges appear to be labelled with the use's variable rather than the def's. In the four-statement body below, statements that do not define answer still carry an answer-labelled edge into its use:
...handle@35:8 --PY_DDG['answer']-> ...handle@38:8/actual_in:0 # 35 defines answer -- correct
...handle@36:8 --PY_DDG['answer']-> ...handle@38:8/actual_in:0 # 36 defines hop
...handle@37:8 --PY_DDG['answer']-> ...handle@38:8/actual_in:0 # 37 defines note
This is over-approximation, which is the safe direction, but it inflates path counts and makes var unreliable as a def-site label. Filing it here rather than separately because it is likely the same reaching-definition pass.
CAVEATS
- Measured on one small fixture at level 4 with the Neo4j emit. Not checked against the JSON emit, and not checked on a large project — a body with no calls before the first one may be the special case, or it may be every body.
- Whether
@entry or @formal_in:<i> is meant to be the canonical parameter def-site is the maintainer's call. Either fixes this, as long as the two are connected or the surviving one reaches both statements and argument ports.
- The sibling analyzers were not checked. If the reaching-definition pass is shared in design, the same shape is worth measuring in codeanalyzer-java and codeanalyzer-typescript.
DEFINITION OF DONE
- From a parameter's
@formal_in:<i> port there is a DDG path to the actual_in port of every call in that body that receives it — including the first, with no preceding statement.
- The fixture above yields a non-empty path set for
user_input (in handle) → raw (in scrub).
- A regression test asserts that reachability, in the emitter's own test suite, so a consumer's refutation cannot silently become wrong again.
Is your feature request related to a problem? Please describe.
At
analysis_level=4, a function parameter has two disjoint def-sites in the DDG — the@formal_in:<i>port and the@entrynode — with no edge between them, and only@entryreaches the argument ports (@<line>:<col>/actual_in:<i>) of the calls in that body. A consumer that starts a traversal at theformal_inport (the only vertex addressable by parameter name) therefore cannot reach the first callee the parameter is passed to.The consequence is not a missing path — it is a proved absence of flow. A taint query that finds no path and no diagnostic is entitled to report the pair as refuted, so
scrub(user_input)reads as "user_inputdoes not reachscrub".formal_in:<i>is the vertex a consumer can name: it is the only one carryingvar, so it is what a parameter selector resolves to. Starting there:user_input(inhandle) →raw(inscrub)raw(inscrub) →cleaned(inrun_query)The second row shows the interprocedural machinery is otherwise intact —
PY_PARAM_IN/PY_PARAM_OUTare present and carryvarsince 1.5.1. The break is specifically the first crossing out of the callable whose parameter you started from.Direction of the error matters: a missing edge in a dataflow graph yields a false negative, and a consumer cannot distinguish "no path exists" from "the path is not in the graph". Any caller-parameter-to-callee-parameter taint question is answered wrongly and confidently.
Describe the solution you'd like
Not stated in the original issue.
Describe alternatives you've considered
Not stated in the original issue.
Additional context
Reproduction
app.py:Emitted with
AnalysisOptions(..., emit=EmitTarget.NEO4J, analysis_level=4)on codeanalyzer-python 1.5.1.user_inputhas two def-sites:and there is no edge of any type between
@formal_in:1and@entry, in either direction.The argument port's only DDG predecessors are
@entryand earlier statements — never the statement that makes the call, and never the enclosingformal_in:So
@35:8/actual_in:0— the argument of the first call — is reachable only from@entry.Secondary observation (same emit, lower severity)
Reaching-definition edges appear to be labelled with the use's variable rather than the def's. In the four-statement body below, statements that do not define
answerstill carry ananswer-labelled edge into its use:This is over-approximation, which is the safe direction, but it inflates path counts and makes
varunreliable as a def-site label. Filing it here rather than separately because it is likely the same reaching-definition pass.CAVEATS
@entryor@formal_in:<i>is meant to be the canonical parameter def-site is the maintainer's call. Either fixes this, as long as the two are connected or the surviving one reaches both statements and argument ports.DEFINITION OF DONE
@formal_in:<i>port there is a DDG path to theactual_inport of every call in that body that receives it — including the first, with no preceding statement.user_input(inhandle) →raw(inscrub).