Skip to content

Commit 17d1768

Browse files
committed
Python: Allow absolute imports in directories with scripts
Fixes the import logic to account for absolute imports. We do this by classifying which files and folders may serve as the entry point for execution, based on a few simple heuristics. If the file `module.py` is in the same folder as a file `main.py` that may be executed directly, then we allow `module` to be a valid name for `module.py` so that `import module` will work as expected.
1 parent 4289e35 commit 17d1768

File tree

3 files changed

+38
-1
lines changed

3 files changed

+38
-1
lines changed

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

Lines changed: 30 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 maybeExecutedDirectly() {
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() = "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) {
@@ -121,6 +148,9 @@ class Folder extends Container {
121148
this.getBaseName().regexpMatch("[^\\d\\W]\\w*") and
122149
result = this.getParent().getImportRoot(n)
123150
}
151+
152+
/** Holds if execution may start in a file in this directory. */
153+
predicate mayContainEntryPoint() { any(File f | f.getParent() = this).maybeExecutedDirectly() }
124154
}
125155

126156
/**

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,13 @@ private string moduleNameFromBase(Container file) {
204204
string moduleNameFromFile(Container file) {
205205
exists(string basename |
206206
basename = moduleNameFromBase(file) and
207-
legalShortName(basename) and
207+
legalShortName(basename)
208+
|
208209
result = moduleNameFromFile(file.getParent()) + "." + basename
210+
or
211+
// If execution can start in the folder containing this module, then we will assume `file` can
212+
// be imported as an absolute import, and hence return `basename` as a possible name.
213+
file.getParent().(Folder).mayContainEntryPoint() and result = basename
209214
)
210215
or
211216
isPotentialSourcePackage(file) and

python/ql/test/3/library-tests/modules/entry_point/modules.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
| main | code/main.py:0:0:0:0 | Script main |
2+
| module | code/module.py:0:0:0:0 | Module module |
13
| package | code/package:0:0:0:0 | Package package |
24
| package.__init__ | code/package/__init__.py:0:0:0:0 | Module package.__init__ |
35
| package.package_main | code/package/package_main.py:0:0:0:0 | Module package.package_main |

0 commit comments

Comments
 (0)