This document explains how to use Skylint, the Skylark linter.
To build the linter from source, use the following commands:
$ git clone https://github.com/bazelbuild/bazel.git $ cd bazel $ bazel build //src/tools/skylark/java/com/google/devtools/skylark/skylint:Skylint
After that, the linter is available at
You can copy it to somewhere else if you prefer.
Assuming you have the linter binary available at
/path/to/Skylint from the
previous step, you can run it like this:
$ /path/to/Skylint /path/to/bzl/file.bzl
/path/to/Skylint is the path of the binary from the previous section.
This section explains which checks the linter performs and how to deal with false positives.
def foo(): """An example function. Deprecated: <reason and alternative> """ # … def bar(): foo() # warning: "usage of 'foo' is deprecated: <reason and alternative>"
Note that the explanation starts on the next line after
Deprecated: and may
occupy multiple lines, with all lines indented by two spaces.
You can import a helper function from Skylib and use it like this:
load("@bazel_skylib//lib:dicts.bzl", "dicts") dicts.add(d1, d2, d3) # instead of d1 + d2 + d3
See documentation on depsets for background and examples of use.
d1 = depset(items1) d2 = depset(items2) combined = depset(transitive=[d1, d2])
The Skylark conventions for docstrings are similar to the the Python conventions. Docstrings are triple-quoted string literals and the closing quotes have to appear on their own line unless the docstring is only one line long.
A file-level docstring is the first statement of a file, even before the
load() statements. A function docstring is the first statement in the function
Sections in the docstring (such as
Returns:) are separated by a
blank line. Their contents are indented by two spaces.
"""This module contains some docstrings examples.""" def example_function1(): """Illustrates the usage of a one-line function docstring.""" def example_function2(foo, bar): """Illustrates the format of a longer function docstring. After the one-line summary comes the description body. It contains additional information about the function. The description can span more than one paragraph. Args: foo: documentation of the first parameter. Subsequent lines have to be indented by two spaces. bar: documentation of the second parameter Returns: documentation of the return value Deprecated: This function is deprecated for <reason>. Use <alternative> instead. """ return "baz"
If the linter gives you confusing error messages, check the indentation of the
docstring. (The indentation rules are the same as for
The analyzer tries to warn you about bad indentation but it may not catch all
edge cases. If the indentation is wrong, the analyzer may not recognize some
sections, such as
Args:, which may then lead to further errors about missing
documentation for the parameters.
If a function has a multi-line docstring, you also have to document (in order):
return fooinstead of just
TL;DR: Most Skylark identifiers should be
In detail, the rules are the following:
FooBarInfo = provider(...). In addition provider names have to end in the suffix
True. Rebinding these is too confusing.
I(easy to confuse with
a, _ = tuple.
If a statement is just an expression that is not a function call, the analyzer
expression result not used. Most likely, you forgot to do something with
that value. Examples:
1 + foo(),
List comprehensions inside a function should be transformed to a for-loop. This transformation is not possible at the top level because for-loops are only allowed inside a function to encourage declarative code at the top level. Therefore list comprehension at the top level are allowed but you should consider whether it would be more readable to move them to a function. Example:
# This is allowed at the top level (but consider moving it to a function): [do_something_with(foo) for foo in bar] def baz(): # This is BAD inside a function: [do_something_with(foo) for foo in bar] # Instead, use a for loop: for foo in bar: do_something_with(foo)
If a function returns with a value (
return foo) in some execution paths and
without one (just
return or reaching the end of a function) in other execution
paths, the analyzer will warn about this. The reason for this is that you
probably forgot to return the right value in some execution paths. If this is
not the case, you should make your intent clear by writing
def foo(): if ...: return False elif ...: # do stuff # forgot return statement here, which generates the warning else: return True
Suppose you have code like this:
def foo(): if cond1: return foo elif cond2: return bar # I know the else-branch can't happen but the analyzer complains
In such a case, just add
fail("unreachable") or something along these lines
to the end of the function in order to silence the warning.
def foo(): if cond1: foo = bar elif cond2: foo = baz print(foo) # warning: 'foo' may not have been initialized
Most likely, the author forgot to initialize
foo in the
However, if this is a false positive because you know that the
else branch can
never happen, add an
else-branch with the line
something similar. Then the analyzer won't complain.
If your code is more complex and it is impossible to determine statically that a
variable is initialized before usage, just initialize the variable with
before using it.
_PRIVATE_GLOBAL_UNUSED = 0 # warns because never used PUBLIC_GLOBAL_UNUSED = 1 # doesn't warn because it might be exported def public_function_unused(): # doesn't warn because it might be exported pass # warns because unused function and parameter: def _private_function_unused(param_unused): # warns because unused local variable → rename to '_' or '_foo': for foo in range(0, 100): pass
The analyzer warns about unused identifiers except in the following cases:
load()ed from somewhere else.
_, there is no warning because the underscore signals that the a value is ignored.
If you want to silence the warning, you can do one of the following:
In case of a parameter whose name you cannot change (because you have to conform to some API), you can use the following pattern:
def foo(param1, param2): _ignore = (param1, param2) # or: _ = (param1, param2) ...
This way, the parameters are used for the assignment of the ignored variable
_ignore. Hence the analyzer will not warn.
If you want to
load() a name just to re-export it (and not use it in the
current file), use the following pattern.
# If you just want to re-export 'foo', DON'T do this: load("bar.bzl", "foo") # DO this instead: load("bar.bzl", _foo="foo") foo = _foo
This way, the name is still re-exported but doesn't generate a warning.
See documentation for more information.