diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index cc014440291..9797f727e47 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -512,6 +512,54 @@ module Ast implements AstSig { } } + /** + * An `import` statement (`import a, b` or `from m import a, b`). + * + * Each alias contributes two children in evaluation order: first the + * value expression (which performs the import side-effect), then the + * bound `asname` Name (the in-scope binding). This makes both reachable + * from the CFG and allows `Name.defines(v)` for `asname` Names to have + * corresponding CFG nodes — which is essential for SSA to see import + * bindings. + */ + additional class ImportStmt extends Stmt { + private Py::Import imp; + + ImportStmt() { this = TPyStmt(imp) } + + /** Gets the value (module/member expression) of the `n`th alias. */ + Expr getValue(int n) { result.asExpr() = imp.getName(n).getValue() } + + /** Gets the bound `asname` of the `n`th alias. */ + Expr getAsname(int n) { result.asExpr() = imp.getName(n).getAsname() } + + /** Gets the number of aliases in this import statement. */ + int getNumberOfAliases() { result = count(int i | exists(imp.getName(i))) } + + override AstNode getChild(int index) { + exists(int i | + index = 2 * i and result = this.getValue(i) + or + index = 2 * i + 1 and result = this.getAsname(i) + ) + } + } + + /** + * A `from m import *` statement. Evaluates the module expression but + * binds no name (the bindings happen by side-effect at runtime, which + * is not modelled at the CFG level). + */ + additional class ImportStarStmt extends Stmt { + private Py::ImportStar imp; + + ImportStarStmt() { this = TPyStmt(imp) } + + Expr getModule() { result.asExpr() = imp.getModule() } + + override AstNode getChild(int index) { index = 0 and result = this.getModule() } + } + /** A `with` statement. */ additional class WithStmt extends Stmt { private Py::With withStmt; diff --git a/python/ql/test/library-tests/ControlFlow/bindings/imports.py b/python/ql/test/library-tests/ControlFlow/bindings/imports.py index 1b657c7db6c..c8834b5332a 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/imports.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/imports.py @@ -1,12 +1,14 @@ -# Import aliases. All bound names below currently lack a CFG node. +# Import aliases — all bound names below are now reachable via the new +# CFG's `ImportStmt` wrapper. -import os # $ MISSING: cfgdefines=os -import os.path # $ MISSING: cfgdefines=os -import os as o # $ MISSING: cfgdefines=o -from os import path # $ MISSING: cfgdefines=path -from os import path as p # $ MISSING: cfgdefines=p -from os import sep, linesep # $ MISSING: cfgdefines=sep MISSING: cfgdefines=linesep +import os # $ cfgdefines=os +import os.path # $ cfgdefines=os +import os as o # $ cfgdefines=o +from os import path # $ cfgdefines=path +from os import path as p # $ cfgdefines=p +from os import sep, linesep # $ cfgdefines=sep cfgdefines=linesep from os import ( - getcwd, # $ MISSING: cfgdefines=getcwd - getcwdb, # $ MISSING: cfgdefines=getcwdb + getcwd, # $ cfgdefines=getcwd + getcwdb, # $ cfgdefines=getcwdb ) +