Skip to content

Commit 6d4ddc0

Browse files
authored
Merge pull request github#5614 from tausbn/python-allow-absolute-imports-from-source-directory
Python: Allow absolute imports from source directory
2 parents bc56d16 + 6c69c1a commit 6d4ddc0

File tree

26 files changed

+118
-1
lines changed

26 files changed

+118
-1
lines changed

python/ql/src/semmle/python/Files.qll

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,33 @@ class File extends Container {
7272
* are specified to be extracted.
7373
*/
7474
string getContents() { file_contents(this, result) }
75+
76+
/** Holds if this file is likely to get executed directly, and thus act as an entry point for execution. */
77+
predicate isPossibleEntryPoint() {
78+
// Only consider files in the source code, and not things like the standard library
79+
exists(this.getRelativePath()) and
80+
(
81+
// The file doesn't have the extension `.py` but still contains Python statements
82+
not this.getExtension().matches("py%") and
83+
exists(Stmt s | s.getLocation().getFile() = this)
84+
or
85+
// The file contains the usual `if __name__ == '__main__':` construction
86+
exists(If i, Name name, StrConst main, Cmpop op |
87+
i.getScope().(Module).getFile() = this and
88+
op instanceof Eq and
89+
i.getTest().(Compare).compares(name, op, main) and
90+
name.getId() = "__name__" and
91+
main.getText() = "__main__"
92+
)
93+
or
94+
// The file contains a `#!` line referencing the python interpreter
95+
exists(Comment c |
96+
c.getLocation().getFile() = this and
97+
c.getLocation().getStartLine() = 1 and
98+
c.getText().regexpMatch("^#! */.*python(2|3)?[ \\\\t]*$")
99+
)
100+
)
101+
}
75102
}
76103

77104
private predicate occupied_line(File f, int n) {

python/ql/src/semmle/python/Module.qll

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,31 @@ private string moduleNameFromBase(Container file) {
205205
file instanceof File and result = file.getStem()
206206
}
207207

208+
/**
209+
* Holds if `file` may be transitively imported from a file that may serve as the entry point of
210+
* the execution.
211+
*/
212+
private predicate transitively_imported_from_entry_point(File file) {
213+
file.getExtension().matches("%py%") and
214+
exists(File importer |
215+
importer.getParent() = file.getParent() and
216+
exists(ImportExpr i | i.getLocation().getFile() = importer and i.getName() = file.getStem())
217+
|
218+
importer.isPossibleEntryPoint() or transitively_imported_from_entry_point(importer)
219+
)
220+
}
221+
208222
string moduleNameFromFile(Container file) {
209223
exists(string basename |
210224
basename = moduleNameFromBase(file) and
211-
legalShortName(basename) and
225+
legalShortName(basename)
226+
|
212227
result = moduleNameFromFile(file.getParent()) + "." + basename
228+
or
229+
// If `file` is a transitive import of a file that's executed directly, we allow references
230+
// to it by its `basename`.
231+
transitively_imported_from_entry_point(file) and
232+
result = basename
213233
)
214234
or
215235
isPotentialSourcePackage(file) and
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#! /usr/bin/python3
2+
print(__file__)
3+
import module
4+
import package
5+
import namespace_package
6+
import namespace_package.namespace_package_main
7+
print(module.message)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
print(__file__.split("entry_point")[1])
2+
message = "Hello world!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
print(__file__.split("entry_point")[1])
2+
import namespace_package.namespace_package_module
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print(__file__.split("entry_point")[1])
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
print(__file__.split("entry_point")[1])
2+
from . import package_main
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
print(__file__.split("entry_point")[1])
2+
from . import package_module
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print(__file__.split("entry_point")[1])
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
| module | hash_bang/module.py:0:0:0:0 | Module module |
2+
| module | name_main/module.py:0:0:0:0 | Module module |
3+
| package | hash_bang/package:0:0:0:0 | Package package |
4+
| package | name_main/package:0:0:0:0 | Package package |
5+
| package | no_py_extension/package:0:0:0:0 | Package package |
6+
| package.__init__ | hash_bang/package/__init__.py:0:0:0:0 | Module package.__init__ |
7+
| package.__init__ | name_main/package/__init__.py:0:0:0:0 | Module package.__init__ |
8+
| package.__init__ | no_py_extension/package/__init__.py:0:0:0:0 | Module package.__init__ |
9+
| package.package_main | hash_bang/package/package_main.py:0:0:0:0 | Module package.package_main |
10+
| package.package_main | name_main/package/package_main.py:0:0:0:0 | Module package.package_main |
11+
| package.package_main | no_py_extension/package/package_main.py:0:0:0:0 | Module package.package_main |
12+
| package.package_module | hash_bang/package/package_module.py:0:0:0:0 | Module package.package_module |
13+
| package.package_module | name_main/package/package_module.py:0:0:0:0 | Module package.package_module |
14+
| package.package_module | no_py_extension/package/package_module.py:0:0:0:0 | Module package.package_module |

0 commit comments

Comments
 (0)