-
Notifications
You must be signed in to change notification settings - Fork 231
Expand file tree
/
Copy pathiterator_leak.star
More file actions
92 lines (78 loc) · 3.38 KB
/
iterator_leak.star
File metadata and controls
92 lines (78 loc) · 3.38 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
# This file defines the iterator_leak analyzer, which checks for
# calls to the starlark.Iterator function (or Iterator.Iterate method)
# without a corresponding call to Done, or where the Done call exists
# but is preceded by a return statement.
#
# It is written in Starlark using Stargo,
# as a proof-of-concept of dynamically loaded checkers.
load(
"go",
analysis = "golang.org/x/tools/go/analysis",
ast = "go/ast",
inspect = "golang.org/x/tools/go/analysis/passes/inspect",
)
def run(pass_):
# Short cut: inspect only packages that directly import starlark.Value.
if "go.starlark.net/starlark" not in [p.Path() for p in pass_.Pkg.Imports()]:
return None, None
inspector = pass_.ResultOf[inspect.Analyzer]
# hot types
assign_stmt = *ast.AssignStmt
selector_expr = *ast.SelectorExpr
call_expr = *ast.CallExpr
return_stmt = *ast.ReturnStmt
iterators = {} # maps iterator *types.Var to Iterate *ast.CallExpr
def visit(n, push, stack):
t = go.typeof(n)
if t == assign_stmt:
if (len(n.Lhs) == 1 and
len(n.Rhs) == 1 and
go.typeof(n.Rhs[0]) == call_expr and
go.typeof(n.Rhs[0].Fun) == selector_expr and
n.Rhs[0].Fun.Sel.Name == "Iterate" and
go.typeof(n.Lhs[0]) == *ast.Ident):
# n is one of:
# iter = value.Iterate()
# iter = starlark.Iterate(...)
# TODO: check that it's our Iterate method/func and not some other.
var = pass_.TypesInfo.ObjectOf(n.Lhs[0])
iterators[var] = n.Rhs[0]
elif t == call_expr:
if (go.typeof(n.Fun) == selector_expr and
n.Fun.Sel.Name == "Done" and
go.typeof(n.Fun.X) == *ast.Ident):
# n is iter.Done().
var = pass_.TypesInfo.ObjectOf(n.Fun.X)
iterators.pop(var, None) # delete
elif t == return_stmt:
# Report iterators leaked by an early return
if len(iterators) > 0:
if (len(stack) > 3 and
go.typeof(stack[-3]) == *ast.IfStmt and
go.typeof(stack[-3].Cond) == *ast.BinaryExpr and
go.typeof(stack[-3].Cond.X) == *ast.Ident and
pass_.TypesInfo.ObjectOf(stack[-3].Cond.X) in iterators):
pass # Allow: if iter == ... { return ... }
else:
for var, call in iterators.items():
pass_.Reportf(n.Return, "iterator leak (missing defer %s.Done())", var.Name())
iterators.pop(var, None) # delete
return True
node_types = (
assign_stmt(),
call_expr(),
return_stmt(),
)
inspector.WithStack(node_types, visit)
# Report iterators for which there is no Done call.
for var, call in iterators.items():
pass_.Reportf(call.Lparen, "iterator leak (missing %s.Done())", var.Name())
return None, None
# TODO: go/analysis doesn't know that our run may panic a starlark.EvalError.
# How can we make it display the stack? Should we wrap run ourselves?
# iterator_leak analyzer
iterator_leak = go.new(analysis.Analyzer)
iterator_leak.Name = "iterator_leak"
iterator_leak.Doc = "report calls to starlark.Iterator without a matching Done"
iterator_leak.Run = run
iterator_leak.Requires = [inspect.Analyzer]