/* Copyright 2020 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package warn import "testing" func TestMissingReturnValueWarning(t *testing.T) { // empty return checkFindings(t, "return-value", ` def foo(): if x: return x else: return `, []string{ `:5: Some but not all execution paths of "foo" return a value.`, }, scopeEverywhere) // empty return; implicit return in the end checkFindings(t, "return-value", ` def bar(): if x: pass elif y: return y else: for z in t: return `, []string{ `:1: Some but not all execution paths of "bar" return a value. The function may terminate by an implicit return in the end.`, `:8: Some but not all execution paths of "bar" return a value.`, }, scopeEverywhere) // implicit return in the end checkFindings(t, "return-value", ` def foo(): if x: return x else: bar() `, []string{ `:1: Some but not all execution paths of "foo" return a value. The function may terminate by an implicit return in the end.`, }, scopeEverywhere) // implicit return in the end checkFindings(t, "return-value", ` def bar(): if x: return x elif y: return y else: foo if z: return z if foo: return not foo `, []string{ `:1: Some but not all execution paths of "bar" return a value. The function may terminate by an implicit return in the end.`, }, scopeEverywhere) // only returned values and fail() statements, ok checkFindings(t, "return-value", ` def bar(): if x: return x elif y: return y else: foo if z: return z if foo: return not foo else: fail("unreachable") `, []string{}, scopeEverywhere) // implicit return in the end because fail() is not a statement checkFindings(t, "return-value", ` def bar(): if x: return x elif y: return y else: foo if z: return z if foo: return not foo else: foo() or fail("unreachable") `, []string{ `:1: Some but not all execution paths of "bar" return a value. The function may terminate by an implicit return in the end.`, }, scopeEverywhere) // only empty returns, ok checkFindings(t, "return-value", ` def bar(): if x: x() elif y: return else: foo if z: fail() if foo: return `, []string{}, scopeEverywhere) // no returns, ok checkFindings(t, "return-value", ` def foobar(): pass `, []string{}, scopeEverywhere) // only fails, ok checkFindings(t, "return-value", ` def foobar(): if foo: fail() `, []string{}, scopeEverywhere) // nested functions, no errors checkFindings(t, "return-value", ` def foo(): def bar(): pass return bar `, []string{}, scopeEverywhere) // nested functions, missing return value in the outer function checkFindings(t, "return-value", ` def foo(): def bar(): pass if bar(): return 42 `, []string{ `:1: Some but not all execution paths of "foo" return a value. The function may terminate by an implicit return in the end.`, }, scopeEverywhere) // nested functions, missing return value in the inner function checkFindings(t, "return-value", ` def foo(): def bar(): if something: return something if bar(): return 42 return 43 `, []string{ `:2: Some but not all execution paths of "bar" return a value. The function may terminate by an implicit return in the end.`, }, scopeEverywhere) // nested functions, missing return value in both checkFindings(t, "return-value", ` def foo(): def bar(): if something: return something if bar(): return 42 `, []string{ `:1: Some but not all execution paths of "foo" return a value. The function may terminate by an implicit return in the end.`, `:2: Some but not all execution paths of "bar" return a value. The function may terminate by an implicit return in the end.`, }, scopeEverywhere) } func TestUnreachableStatementWarning(t *testing.T) { // after return checkFindings(t, "unreachable", ` def foo(): return bar() baz() `, []string{ `:3: The statement is unreachable.`, }, scopeEverywhere) // two returns checkFindings(t, "unreachable", ` def foo(): return 1 return 2 `, []string{ `:3: The statement is unreachable.`, }, scopeEverywhere) // after fail() checkFindings(t, "unreachable", ` def foo(): fail("die") bar() baz() `, []string{ `:3: The statement is unreachable.`, }, scopeEverywhere) // after break and continue checkFindings(t, "unreachable", ` def foo(): for x in y: if x: break bar() # unreachable if y: continue bar() # unreachable def bar(): for x in y: if x: break elif y: continue else: return x foo() # unreachable foobar() # potentially reachable `, []string{ `:5: The statement is unreachable.`, `:8: The statement is unreachable.`, `:19: The statement is unreachable.`, }, scopeEverywhere) // ok checkFindings(t, "unreachable", ` def foo(): if x: return bar() `, []string{}, scopeEverywhere) // ok checkFindings(t, "unreachable", ` def foo(): x() or fail("maybe") bar() `, []string{}, scopeEverywhere) // unreacheable statement inside a nested function checkFindings(t, "unreachable", ` def foo(): def bar(): fail("die") baz() `, []string{ `:4: The statement is unreachable.`, }, scopeEverywhere) } func TestNoEffect(t *testing.T) { checkFindings(t, "no-effect", ` """Docstring.""" def bar(): """Other Docstring""" fct() pass return 2 [f() for i in rang(3)] # top-level comprehension is okay `, []string{}, scopeEverywhere) checkFindings(t, "no-effect", ` def foo(): [fct() for i in range(3)] `, []string{":2: Expression result is not used. Use a for-loop instead"}, scopeEverywhere) checkFindings(t, "no-effect", `None`, []string{":1: Expression result is not used."}, scopeEverywhere) checkFindings(t, "no-effect", ` foo # 1 foo() def bar(): [1, 2] # 5 if True: "string" # 7 `, []string{":1:", ":5:", ":7:"}, scopeEverywhere) checkFindings(t, "no-effect", ` # A comment """A docstring""" # Another comment """Not a docstring""" def bar(): """A docstring""" foo """ Not a docstring""" return foo `, []string{ ":7: Expression result is not used. Docstrings should be the first statements of a file or a function (they may follow comment lines).", ":11: Expression result is not used.", ":12: Expression result is not used. Docstrings should be the first statements of a file or a function (they may follow comment lines).", }, scopeEverywhere) checkFindings(t, "no-effect", ` foo == bar foo = bar a + b c // d -e foo != bar foo += bar bar -= bar `, []string{":1:", ":3:", ":4:", ":5:", ":6:"}, scopeEverywhere) checkFindings(t, "no-effect", ` def foo(): """Doc.""" def bar(): """Doc.""" foo == bar `, []string{":5:"}, scopeEverywhere) } func TestWarnUnusedVariable(t *testing.T) { checkFindings(t, "unused-variable", ` load(":f.bzl", "x") x = "unused" y = "also unused" z = "name" t = "unused by design" # @unused _foo, _bar = pair #@unused cc_library(name = z) def f(): pass def g(): pass g() + 3 `, []string{":2: Variable \"x\" is unused.", ":3: Variable \"y\" is unused.", ":9: Function \"f\" is unused."}, scopeDeclarative) checkFindings(t, "unused-variable", ` a = 1 b = 2 c = 3 d = (a if b else c) # only d is unused `, []string{":4: Variable \"d\" is unused."}, scopeDeclarative) checkFindings(t, "unused-variable", ` _a = 1 _a += 2 _b = 3 print(_b) def _f(): pass def _g(): pass _g() `, []string{ ":1: Variable \"_a\" is unused.", ":6: Function \"_f\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` a = 1 def foo( x, y = 0, z = 1): b = 2 c = 3 d = (a if b else c) # only d is unused e = 7 f = 8 # @unused # @unused g = 9 return e + z foo() `, []string{ ":4: Variable \"x\" is unused.", ":5: Variable \"y\" is unused.", ":9: Variable \"d\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` a = 1 def foo(a): b = 2 return a def foo(): pass def bar(c, cc): d = 3 print(c) def baz(): foo() d = 4 return a bar() `, []string{ ":4: Variable \"b\" is unused.", ":10: Variable \"cc\" is unused.", ":11: Variable \"d\" is unused.", ":14: Function \"baz\" is unused.", ":16: Variable \"d\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(): a = 1 b = 2 c = 3 def bar( x = a + baz(c = 4), y = b): pass foo() `, []string{ ":4: Variable \"c\" is unused.", ":6: Function \"bar\" is unused.", ":7: Variable \"x\" is unused.", ":8: Variable \"y\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(): a = 1 b = 2 c = [x for a in aa if a % b for x in a] return c foo() `, []string{ ":2: Variable \"a\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(): a = 1 b = 2 c = [ a + b for a in [b for b in bb if b] if a ] return c foo() `, []string{ ":2: Variable \"a\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(): a = 1 b = 2 def bar(*args): def baz(**kwargs): def foobar(*a, **kw): return b return foobar(**kwargs) return baz(*args) return bar() foo() `, []string{ ":2: Variable \"a\" is unused.", ":7: Variable \"a\" is unused.", ":8: Variable \"kw\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(): a = 1 b = 2 c = 3 d = 4 e, f = 5, 6 for x, yy in xx: for (y, z, _, _t) in yy: print(a + y) if bar: print(c) elif baz: print(d) else: print(e) foo() `, []string{ ":3: Variable \"b\" is unused.", ":6: Variable \"f\" is unused.", ":8: Variable \"x\" is unused.", ":9: Variable \"z\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(): # @unused def bar(): pass def baz(): pass foo() `, []string{ ":7: Function \"baz\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo(my_iterable, arg, _some_unused_argument, _also_unused = None, *_args, **_kwargs): a, b, _c = 1, 2, 3 # ok to not use _c print(a) _d, _e = 4, 5 # all are underscored print(_d) for f, g, _h, _ in my_iterable: # ok to not use any underscored print(f) for _i, (_j, _k) in another_iterable: # ok to not use any of them pass [1 for (_y, _z) in bar] foo() `, []string{ ":1: Variable \"arg\" is unused.", ":3: Variable \"b\" is unused.", ":6: Variable \"_e\" is unused.", ":9: Variable \"g\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo( x, _y, z, # @unused t = 42, #@unused *args, # @unused **kwargs, ### also @unused ): pass foo() `, []string{ ":2: Variable \"x\" is unused.", }, scopeEverywhere) checkFindings(t, "unused-variable", ` def foo( name, x): pass def bar( name = "", y = 3): pass foo() bar() `, []string{ ":3: Variable \"x\" is unused.", ":9: Variable \"y\" is unused.", }, scopeEverywhere) } func TestRedefinedVariable(t *testing.T) { checkFindings(t, "redefined-variable", ` x = "old_value" x = "new_value" x[1] = "new" cc_library(name = x)`, []string{":2: Variable \"x\" has already been defined."}, scopeEverywhere) checkFindings(t, "redefined-variable", ` x = "a" def foo(): x = "b" y = "c" y = "d" def bar(): x = "e" y = "f" y = "g"`, []string{}, scopeEverywhere) checkFindings(t, "redefined-variable", ` x = [1, 2, 3] y = [a for a in b] z = list() n = 43 x += something() y += something() z += something() n += something() x -= something()`, []string{ ":9: Variable \"n\" has already been defined.", ":10: Variable \"x\" has already been defined.", }, scopeEverywhere) checkFindings(t, "redefined-variable", ` x = [1, 2, 3] y = [a for a in b] z = list() a = something() b = something() c = something() d = something() e = something() a += x b += y c += z d += [42] e += foo`, []string{ ":15: Variable \"e\" has already been defined.", }, scopeEverywhere) } func TestWarnUnusedLoad(t *testing.T) { checkFindingsAndFix(t, "load", ` load(":f.bzl", "s1", "s2") load(":bar.bzl", "s1") foo(name = s1)`, ` load(":f.bzl", "s1") load(":bar.bzl", "s1") foo(name = s1)`, []string{ ":1: Loaded symbol \"s2\" is unused.", ":2: A different symbol \"s1\" has already been loaded on line 1.", }, scopeEverywhere) checkFindingsAndFix(t, "load", ` load("foo", "b", "a", "c") load("foo", "a", "d", "e") z = a + b + d`, ` load("foo", "a", "b") load("foo", "d") z = a + b + d`, []string{ ":1: Loaded symbol \"c\" is unused.", ":2: Symbol \"a\" has already been loaded on line 1.", ":2: Loaded symbol \"e\" is unused.", }, scopeEverywhere) checkFindingsAndFix(t, "load", ` load("foo", "a") a(1) load("bar", "a") a(2) load("bar", a = "a") a(3) load("bar", a = "b") a(4) load("foo", "a") a(5) load("foo", "a") a(6) load("foo", a = "a") a(7)`, ` load("foo", "a") a(1) load("bar", "a") a(2) a(3) load("bar", a = "b") a(4) load("foo", "a") a(5) a(6) a(7)`, []string{ ":3: A different symbol \"a\" has already been loaded on line 1.", ":5: Symbol \"a\" has already been loaded on line 3.", ":7: A different symbol \"a\" has already been loaded on line 5.", ":9: A different symbol \"a\" has already been loaded on line 7.", ":11: Symbol \"a\" has already been loaded on line 9.", ":13: Symbol \"a\" has already been loaded on line 11.", }, scopeEverywhere) checkFindingsAndFix(t, "load", ` load( ":f.bzl", "s1", "s2", # @unused (s2) ) # @unused - both s3 and s4 load( ":f.bzl", "s3", "s4", )`, ` load( ":f.bzl", "s2", # @unused (s2) ) # @unused - both s3 and s4 load( ":f.bzl", "s3", "s4", )`, []string{":3: Loaded symbol \"s1\" is unused."}, scopeEverywhere) checkFindingsAndFix(t, "load", ` load(":f.bzl", "x") x = "unused"`, ` x = "unused"`, []string{":1: Loaded symbol \"x\" is unused."}, scopeEverywhere) checkFindings(t, "load", ` load( ":f.bzl", "s1", ) def test(x: s1): pass `, []string{}, scopeEverywhere) checkFindings(t, "load", ` load( ":f.bzl", "s1", "s2", ) def test(x: s1) -> List[s2]: pass `, []string{}, scopeEverywhere) checkFindingsAndFix(t, "load", ` load( ":f.bzl", "s1", "s2", ) load( ":s.bzl", "s3", ) def test(x: s1) -> List[s2]: pass `, ` load( ":f.bzl", "s1", "s2", ) def test(x: s1) -> List[s2]: pass `, []string{ ":9: Loaded symbol \"s3\" is unused.", }, scopeEverywhere) } func TestUninitializedVariable(t *testing.T) { checkFindings(t, "uninitialized", ` def foo(x): if bar: x = 1 y = 2 bar = True print(x + y) `, []string{ ":2: Variable \"bar\" may not have been initialized.", ":7: Variable \"y\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): for t in s: x = 1 y = 2 print(x + y) `, []string{ ":6: Variable \"y\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): if bar: x = 1 y = 2 else: if foobar: x = 3 y = 4 else: x = 5 print(x + y) `, []string{ ":12: Variable \"y\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): if bar: t = 1 else: for t in maybe_empty: pass print(t) `, []string{ ":8: Variable \"t\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): if bar: t = 1 else: for y in maybe_empty: return print(t) `, []string{ ":8: Variable \"t\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): if bar: for t in [2, 3]: pass print(t) `, []string{ ":6: Variable \"t\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): print(t) # maybe global or loaded `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): if bar: y = 1 print(y) x, y = y, x print(y) `, []string{ ":5: Variable \"y\" may not have been initialized.", ":6: Variable \"y\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): if a: x = 1 y = 1 z = 1 elif b: x = 2 z = 2 t = 2 else: x = 3 y = 3 t = 3 print(x + y + z + t) `, []string{ ":15: Variable \"y\" may not have been initialized.", ":15: Variable \"z\" may not have been initialized.", ":15: Variable \"t\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(y): if y < 0: x = -1 elif y > 0: x = 1 else: fail() print(x) `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(y): if y < 0: x = -1 elif y > 0: x = 1 else: if z: fail("z") else: fail("not z") print(x) `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(y): if y < 0: return elif y > 0: x = 1 else: return x # not initialized print(x) `, []string{ ":7: Variable \"x\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(y): if y < 0: return elif y > 0: x = 1 else: pass print(x) # not initialized `, []string{ ":9: Variable \"x\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): for x in y: print(x) print(x.attr) print(x) print(x.attr) `, []string{ ":6: Variable \"x\" may not have been initialized.", ":7: Variable \"x\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): for x in y: a = x print(a) print(a) `, []string{ ":6: Variable \"a\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def f(): if foo: x = foo f(x = y) `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def f(): if foo: x = foo f(x + y) `, []string{ ":5: Variable \"x\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def f(): if foo: x = foo x, y = 1, x `, []string{ ":5: Variable \"x\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def f(): if foo: x = foo x, y = 1, 2 `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def f(y): return [x for x in y if x] x = 1 `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def f(): if foo: f(x = foo) else: x = 3 print(x) `, []string{ ":7: Variable \"x\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x): for y in x: if foo: break elif bar: continue elif baz: return else: z = 3 print(z) `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x: int, y: int = 2): if bar: x = 1 y = 2 z = 3 print(x + y + z) `, []string{ ":7: Variable \"z\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x: int, y: int = 2): def bar(y=x): if baz: x = 1 y = 2 z = 3 print(x + y + z) if something: x = bar() return x `, []string{ ":8: Variable \"x\" may not have been initialized.", ":8: Variable \"z\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(x: int, y: int = 2): if bar: y, z, t = 2, 3, 4 w, s = 5, 6 r = 7 [t for t in range(5)] [a for a in range(z + y)] {b: c + s for b, c in [ d * 2 for d in range(t) if d != baz(r=w) ]} `, []string{ ":8: Variable \"z\" may not have been initialized.", ":9: Variable \"s\" may not have been initialized.", ":10: Variable \"t\" may not have been initialized.", ":11: Variable \"w\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): x = 1 for y, z in t: print(y, z) def bar(x, y, s = z): pass `, []string{ ":6: Variable \"z\" may not have been initialized.", }, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): for bar in baz: pass y = [baz.get(bar) for bar in bars] x = lambda bar: baz.get(bar) `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): def bar(x): print(x) for x, y in z: bar(x) `, []string{}, scopeEverywhere) checkFindings(t, "uninitialized", ` def foo(): [x, y] = [1, 2] x = 3 print(x) `, []string{}, scopeEverywhere) }