-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathUnusedImport.ql
More file actions
153 lines (142 loc) · 5.15 KB
/
UnusedImport.ql
File metadata and controls
153 lines (142 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* @name Unused import
* @description Import is not required as it is not used
* @kind problem
* @tags quality
* maintainability
* useless-code
* @problem.severity recommendation
* @sub-severity high
* @precision very-high
* @id py/unused-import
*/
import python
private import LegacyPointsTo
import Variables.Definition
import semmle.python.ApiGraphs
private predicate is_pytest_fixture(Import imp, Variable name) {
exists(Alias a, API::Node pytest_fixture, API::Node decorator |
pytest_fixture = API::moduleImport("pytest").getMember("fixture") and
// The additional `.getReturn()` is to account for the difference between
// ```
// @pytest.fixture
// def foo():
// ...
// ```
// and
// ```
// @pytest.fixture(some, args, here)
// def foo():
// ...
// ```
decorator in [pytest_fixture, pytest_fixture.getReturn()] and
a = imp.getAName() and
a.getAsname().(Name).getVariable() = name and
a.getValue() = decorator.getReturn().getAValueReachableFromSource().asExpr()
)
}
predicate global_name_used(Module m, string name) {
exists(Name u, GlobalVariable v |
u.uses(v) and
v.getId() = name and
u.getEnclosingModule() = m
)
or
// A use of an undefined class local variable, will use the global variable
exists(Name u, LocalVariable v |
u.uses(v) and
v.getId() = name and
u.getEnclosingModule() = m and
not v.getScope().getEnclosingScope*() instanceof Function
)
}
/** Holds if a module has `__all__` but we don't understand it */
predicate all_not_understood(Module m) {
exists(GlobalVariable a | a.getId() = "__all__" and a.getScope() = m |
// `__all__` is not defined as a simple list
not m.declaredInAll(_)
or
// `__all__` is modified
exists(Call c | c.getFunc().(Attribute).getObject() = a.getALoad())
)
}
predicate imported_module_used_in_doctest(Import imp) {
exists(string modname, string docstring |
imp.getAName().getAsname().(Name).getId() = modname and
// Look for doctests containing the patterns:
// >>> …name…
// ... …name…
docstring = doctest_in_scope(imp.getScope()) and
docstring.regexpMatch("[\\s\\S]*(>>>|\\.\\.\\.).*" + modname + "[\\s\\S]*")
)
}
pragma[noinline]
private string doctest_in_scope(Scope scope) {
exists(StringLiteral doc |
doc.getEnclosingModule() = scope and
doc.isDocString() and
result = doc.getText() and
result.regexpMatch("[\\s\\S]*(>>>|\\.\\.\\.)[\\s\\S]*")
)
}
pragma[noinline]
private string typehint_annotation_in_module(Module module_scope) {
exists(StringLiteral annotation |
annotation = any(Arguments a).getAnAnnotation().getASubExpression*()
or
annotation = any(AnnAssign a).getAnnotation().getASubExpression*()
or
annotation = any(FunctionExpr f).getReturns().getASubExpression*()
|
annotation.(ExprWithPointsTo).pointsTo(Value::forString(result)) and
annotation.getEnclosingModule() = module_scope
)
}
pragma[noinline]
private string typehint_comment_in_file(File file) {
exists(Comment typehint |
file = typehint.getLocation().getFile() and
result = typehint.getText() and
result.matches("# type:%")
)
}
/** Holds if the imported alias `name` from `imp` is used in a typehint (in the same file as `imp`) */
predicate imported_alias_used_in_typehint(Import imp, Variable name) {
imp.getAName().getAsname().(Name).getVariable() = name and
exists(File file, Module module_scope |
module_scope = imp.getEnclosingModule() and
file = module_scope.getFile()
|
// Look for type hints containing the patterns:
// # type: …name…
typehint_comment_in_file(file).regexpMatch("# type:.*" + name.getId() + ".*")
or
// Type hint is inside a string annotation, as needed for forward references
typehint_annotation_in_module(module_scope).regexpMatch(".*\\b" + name.getId() + "\\b.*")
)
}
predicate unused_import(Import imp, Variable name) {
imp.getAName().getAsname().(Name).getVariable() = name and
not imp.getAnImportedModuleName() = "__future__" and
not imp.getEnclosingModule().declaredInAll(name.getId()) and
imp.getScope() = imp.getEnclosingModule() and
not global_name_used(imp.getScope(), name.getId()) and
// Imports in `__init__.py` are used to force module loading
not imp.getEnclosingModule().isPackageInit() and
// Name may be imported for use in epytext documentation
not exists(Comment cmt | cmt.getText().matches("%L{" + name.getId() + "}%") |
cmt.getLocation().getFile() = imp.getLocation().getFile()
) and
not name_acceptable_for_unused_variable(name) and
// Assume that opaque `__all__` includes imported module
not all_not_understood(imp.getEnclosingModule()) and
not imported_module_used_in_doctest(imp) and
not imported_alias_used_in_typehint(imp, name) and
not is_pytest_fixture(imp, name) and
// Only consider import statements that actually point-to something (possibly an unknown module).
// If this is not the case, it's likely that the import statement never gets executed.
imp.getAName().getValue().(ExprWithPointsTo).pointsTo(_)
}
from Stmt s, Variable name
where unused_import(s, name)
select s, "Import of '" + name.getId() + "' is not used."