Skip to content

Commit ca22ec6

Browse files
authored
Merge pull request #2042 from tausbn/python-fix-unused-import-fps
Python: Fix false positives in `py/unused-import`.
2 parents fa5388b + 9878e4f commit ca22ec6

File tree

2 files changed

+59
-48
lines changed

2 files changed

+59
-48
lines changed

python/ql/src/Imports/UnusedImport.ql

Lines changed: 45 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import python
1414
import Variables.Definition
1515

1616
predicate global_name_used(Module m, Variable name) {
17-
exists (Name u, GlobalVariable v |
17+
exists(Name u, GlobalVariable v |
1818
u.uses(v) and
1919
v.getId() = name.getId() and
2020
u.getEnclosingModule() = m
2121
)
2222
or
23-
/* A use of an undefined class local variable, will use the global variable */
23+
// A use of an undefined class local variable, will use the global variable
2424
exists(Name u, LocalVariable v |
2525
u.uses(v) and
2626
v.getId() = name.getId() and
@@ -31,24 +31,21 @@ predicate global_name_used(Module m, Variable name) {
3131

3232
/** Holds if a module has `__all__` but we don't understand it */
3333
predicate all_not_understood(Module m) {
34-
exists(GlobalVariable a |
35-
a.getId() = "__all__" and a.getScope() = m |
36-
/* __all__ is not defined as a simple list */
34+
exists(GlobalVariable a | a.getId() = "__all__" and a.getScope() = m |
35+
// `__all__` is not defined as a simple list
3736
not m.declaredInAll(_)
3837
or
39-
/* __all__ is modified */
38+
// `__all__` is modified
4039
exists(Call c | c.getFunc().(Attribute).getObject() = a.getALoad())
4140
)
4241
}
4342

4443
predicate imported_module_used_in_doctest(Import imp) {
4544
exists(string modname |
46-
imp.getAName().getAsname().(Name).getId() = modname
47-
and
48-
/* Look for doctests containing the patterns:
49-
* >>> …name…
50-
* ... …name…
51-
*/
45+
imp.getAName().getAsname().(Name).getId() = modname and
46+
// Look for doctests containing the patterns:
47+
// >>> …name…
48+
// ... …name…
5249
exists(StrConst doc |
5350
doc.getEnclosingModule() = imp.getScope() and
5451
doc.isDocString() and
@@ -58,52 +55,52 @@ predicate imported_module_used_in_doctest(Import imp) {
5855
}
5956

6057
predicate imported_module_used_in_typehint(Import imp) {
61-
exists(string modname |
62-
imp.getAName().getAsname().(Name).getId() = modname
63-
and
64-
/* Look for typehints containing the patterns:
65-
* # type: …name…
66-
*/
58+
exists(string modname, Location loc |
59+
imp.getAName().getAsname().(Name).getId() = modname and
60+
loc.getFile() = imp.getScope().(Module).getFile()
61+
|
62+
// Look for type hints containing the patterns:
63+
// # type: …name…
6764
exists(Comment typehint |
68-
typehint.getLocation().getFile() = imp.getScope().(Module).getFile() and
65+
loc = typehint.getLocation() and
6966
typehint.getText().regexpMatch("# type:.*" + modname + ".*")
7067
)
68+
or
69+
// Type hint is inside a string annotation, as needed for forward references
70+
exists(string typehint, Expr annotation |
71+
annotation = any(Arguments a).getAnAnnotation()
72+
or
73+
annotation = any(AnnAssign a).getAnnotation()
74+
|
75+
annotation.pointsTo(Value::forString(typehint)) and
76+
loc = annotation.getLocation() and
77+
typehint.regexpMatch(".*\\b" + modname + "\\b.*")
78+
)
7179
)
7280
}
7381

74-
7582
predicate unused_import(Import imp, Variable name) {
76-
((Name)imp.getAName().getAsname()).getVariable() = name
77-
and
78-
not imp.getAnImportedModuleName() = "__future__"
79-
and
80-
not imp.getEnclosingModule().declaredInAll(name.getId())
81-
and
82-
imp.getScope() = imp.getEnclosingModule()
83-
and
84-
not global_name_used(imp.getScope(), name)
85-
and
86-
/* Imports in __init__.py are used to force module loading */
87-
not imp.getEnclosingModule().isPackageInit()
88-
and
89-
/* Name may be imported for use in epytext documentation */
90-
not exists(Comment cmt |
91-
cmt.getText().matches("%L{" + name.getId() + "}%") |
83+
imp.getAName().getAsname().(Name).getVariable() = name and
84+
not imp.getAnImportedModuleName() = "__future__" and
85+
not imp.getEnclosingModule().declaredInAll(name.getId()) and
86+
imp.getScope() = imp.getEnclosingModule() and
87+
not global_name_used(imp.getScope(), name) and
88+
// Imports in `__init__.py` are used to force module loading
89+
not imp.getEnclosingModule().isPackageInit() and
90+
// Name may be imported for use in epytext documentation
91+
not exists(Comment cmt | cmt.getText().matches("%L{" + name.getId() + "}%") |
9292
cmt.getLocation().getFile() = imp.getLocation().getFile()
93-
)
94-
and
95-
not name_acceptable_for_unused_variable(name)
96-
and
97-
/* Assume that opaque `__all__` includes imported module */
98-
not all_not_understood(imp.getEnclosingModule())
99-
and
100-
not imported_module_used_in_doctest(imp)
101-
and
102-
not imported_module_used_in_typehint(imp)
93+
) and
94+
not name_acceptable_for_unused_variable(name) and
95+
// Assume that opaque `__all__` includes imported module
96+
not all_not_understood(imp.getEnclosingModule()) and
97+
not imported_module_used_in_doctest(imp) and
98+
not imported_module_used_in_typehint(imp) and
99+
// Only consider import statements that actually point-to something (possibly an unknown module).
100+
// If this is not the case, it's likely that the import statement never gets executed.
101+
imp.getAName().getValue().pointsTo(_)
103102
}
104103

105-
106104
from Stmt s, Variable name
107105
where unused_import(s, name)
108106
select s, "Import of '" + name.getId() + "' is not used."
109-

python/ql/test/query-tests/Imports/unused/imports_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,17 @@ def f():
7676

7777
foo = None # type: typing.Optional[int]
7878

79+
80+
# OK since the import statement is never executed
81+
if False:
82+
import never_imported
83+
84+
# OK since the imported module is used in a forward-referenced type annotation.
85+
import only_used_in_parameter_annotation
86+
87+
def func(x: 'Optional[only_used_in_parameter_annotation]'):
88+
pass
89+
90+
import only_used_in_annotated_assignment
91+
92+
var : 'Optional[only_used_in_annotated_assignment]' = 5

0 commit comments

Comments
 (0)