Features¶
What can it find?¶
- simple deadcode detection (e.g if true/false)
- simple premature-return deadcode detection
- Bitwise
&
,|
being used in a Bool context. Suggest&&
and||
- Declared but unused variables or arguments. Overruled by adding the no-op statement
@lintpragma("Ignore unused [variable name]")
just before the warning line. - Using an undefined variable
- Duplicate key as in
[:a=>1, :b=>2, :a=>3]
- Mixed types in uniform dictionary
[:a=>1, :b=>""]
(ERROR) - Uniform types in mixed dictionary
{:a=>1, :b=>2}
(performance INFO) - Exporting non-existing symbols (However, it cannot catch symbols generated by macros)
- Exporting the same symbol more than once
- Name overlap between a variable and a lambda argument
- Implicitly-local (local keyword not provided, but neither is global) variable assignment that looks like it should be a global variable binding, if the name is long enough
- Assignment in an if-predicate, as a potential confusion with
==
- warn
length()
being used as Bool, suggest!isempty()
- Out-of-scope local variable name being reused again inside the same code block. (legal but frowned upon)
- Function arguments being Container on non-leaf type e.g.
f(x::Array{Number,1})
. Suggestf{T<:Number}(x::Array{T,1})
- Concatenation of strings using
+
. It also catches common string functions, e.g.string(...) + replace(...)
- Iteration over an apparent dictionary using only one variable instead of (k,v) tuple
- Incorrect ADT usage in function definition, e.g.
f{Int}(x::Int)
,f{T<:Int64}(x::T)
,f{Int<:Real}(x::Int)
- Suspicious range literals e.g.
10:1
when it probably should be10:-1:1
- Understandable but actually non-existent constructors e.g. String(), Symbol()
- Redefining mathematical constants, such as
e = 2.718
- Illegal
using
statement inside a function definition - Repeated function arguments e.g.
f(x,y,x)
- Wrong usage of ellipsis in function arguments e.g.
f(x, y... , z...)
- Wrong usage of default value in function arguments e.g.
f(x=1, y)
- Named arguments without default value e.g.
f(x; y, q=1)
- Code extending deprecated functions, as defined in deprecated.jl (Base)
- Mispelled constructor function name (when it calls
new(...)
inside) - Constructor forgetting to return the constructed object
- calling new() with the number of arguments different from the number of fields.
- Rudimentary type instability warnings e.g.
a = 1
then followed bya = 2.0
. Overruled using a no-op statement@lintpragma("Ignore unstable type variable [variable name]")
just before the warning. - Incompatible type assertion and assignment e.g.
a::Int = 1.0
- Incompatible tuple assignment sizes e.g.
(a,b) = (1,2,3)
- Loop over a single number. e.g.
for i=1 end
- More indices than dimensions in an array lookup
- Look up a dictionary with the wrong key type, if the key's type can be inferred.
- assign to a variable that happens to have the same name as the host function.
- Inform upcoming 0.3->0.4 changes in Dictionary and Array construction
- Warn [1 :end], or [end -1]
- If the first statement in a true branch of an if-statement is proven to be a boolean expression,
warn for potentially missing
||
or&&
- A type field with undeclared type or array type without dimension (performance INFO)
- Constant global without assigning a value to it.
- Staged function: catch one forgetting that the arguments are all types. Inform unused arguments in both the staged function's logic as well as the quoted code.
- Catch silly curly bracket misuse/abuse e.g. Dict{:Symbol, Any}
- Understands and checks basic
@doc
- Catch assigning
a.b = something
whena
is proven to be of an immutable type, e.g.Complex
- Follow VERSION if-statements to properly lint based on version-appropriate standard
@lintpragma: steering Lint-time behavior¶
You can insert @lintpragma to suppress or generate messages. At runtime, @lintpragma is a no-op, so it gives no performance penalty. However, at lint-time, these pragmas steer the lint behavior. Module designers do not even have to import the macro from Lint.jl in their module, as long as they just create an empty macro like this, early in their module scripts:
macro lintpragma(s)
end
Lint message suppression (do not include the square brackets)
@lintpragma("Ignore unused [variable name]")
. Works for unused arguments also.@lintpragma("Ignore unstable type variable [variable name]")
. Ignore type instability warnings.@lintpragma("Ignore deprecated [function name]")
@lintpragma("Ignore undefined module [module name]")
. Useful to support Julia packages across different Julia releases.@lintpragma("Ignore untyped field [field name]")
.@lintpragma("Ignore dimensionless array field [field name]")
. Useful if we really want to store arrays with uncertain/runtime-calculated dimension@lintpragma("Ignore use of undeclared variable [variable name]")
. Useful when using macros to generate symbols on the fly.@lintpragma("Ignore incompatible type comparison")
. Useful to silence deliberately different-type comparison
Lint message generation (do not include the square brackets)
@lintpragma("Info type [expression]")
. Generate the best guess type of the expression during lint-time.@lintpragma("Info me [any text]")
. An alternative to-do.@lintpragma("Warn me [any text]")
. Remind yourself this code isn't done yet.@lintpragma("Error me [any text]")
. Remind yourself this code is wrong.
The macro also supports lint-time terminal output that generates no Lint message:
@lintpragma("Print type [expression]")
. Just print out the type@lintpragma("Print me [any text]")
. Lint-time printing
Useful version tracing tool
@lintpragma("Info version [version]")
. lint-time version reachability test
VERSION branch¶
As julia evolves, some coding style that is an error becomes valid (and vice versa). It is common to use VERSION if-statements to implement cross-version packages. As long as the if statement is simple, Lint can pick them up and suppress version-dependent errors that are not reachable in the current version.
Examples:
# lint won't complaint about missing `Base.Dates` in 0.3 or missing `Dates` in 0.4
if VERSION < v"0.4-"
using Dates
else
using Base.Dates
end
# this passes lint in 0.3 but it generates an INFO in 0.4
s = symbol("end")
# this is an error 0.3 but it passes in 0.4
s = Symbol("end")
# this will lint clean cross versions
if VERSION < v"0.4-"
s = symbol("end")
else
s = Symbol("end")
end
You can directly test for version reachability by inserting lint-pragmas like so
if VERSION >= v"0.4-"
@lintpragma("Info version 0.3")
@lintpragma("Info version 0.4.0-dev+1833")
else
@lintpragma("Info version 0.3")
@lintpragma("Info version 0.4.0-dev+1833")
end
You will see line-by-line reachability in your output. See test/versions.jl for further examples.
Current false positives¶
- Because macros can generate new symbols on the fly. Lint will have a hard time dealing
with that. To help Lint and to reduce noise, module designers can add a
lint_helper
function to their module.
Module specific lint helper(WIP)¶
Key info about adding a lint_helper
function in your module
- You don't need to export this function. Lint will find it.
- It must return true if an expression is part of your module's enabling expression (most likely a macrocall). Return false otherwise so that Lint can give other modules a go at it. Note that if you always returning true in your code you will break Lint.
lint_helper
takes two argument, anExpr
instance and a context.- if you find an issue in your expression, call
Lint.msg(ctx, code, variable, "explanation")
- typical structure looks like this
function lint_helper(ex::Expr, ctx)
if ex.head == :macrocall
if ex.args[1] == symbol("@fancy_macro1")
# your own checking code
return true
elseif ex.args[1]== symbol("@fancy_macro2")
# more checking code
return true
end
end
return false
end
See advanced lint helper interface for details on how to use ctx
.
Ignoring messages¶
julia> s = """
a = :bar
@eval \$a = 5
bar
"""
julia> lintstr(s)
1-element Array{Lint.LintMessage,1}:
none:3 E321 bar: use of undeclared symbol
Using the keyword argument ignore
you can create a LintContext that will ignore any messages you specified.
ignore
takes and array of LintIgnore
. LintIgnore
is a combinition of a messages error code and the messages variable.
julia> ctx = LintContext("none", ignore=[Lint.LintIgnore(:E321, "bar")])
julia> @test isempty(lintstr(s, ctx))
Lintserver¶
Make Julia start listening on a given port and return lint messages to requests on that connection. This feature is useful when you want to lint julia code in a non julia environment.
Existing plugins:
- Sublime Text: SublimeLinter-contrib-julialintserver
- linter-julia for Atom: linter-julia
The new protocol for the server is JSON in both input and output:
{
"file":"path_to_the_file",
"code_str":"full_text_of_the_file",
"ignore_codes":["E381","W361","I171"],
"ignore_info":false,
"ignore_warnings":false,
"show_code":true
}
Only the two first "file"
and "code_str"
are mandatory fields. For the output
there are four different protocols from which the "lint-message"
is the direct
match of LintMessage
and this way will be always up to date, but can also break.
Other three types are for convenience, they give you the opportunity to
directly pass the messages forward for example Atom linter. Here is one full example,
to see more examples, see the tests.
julia> using Lint
julia> using JSON
julia> if is_windows()
pipe_lm = "\\\\.\\pipe\\testsocket"
else # linux, osx
pipe_lm = tempname()
end
"/tmp/julial73DPo"
julia> server_lm = @async lintserver(pipe_lm,"lint-message")
Server running on port/pipe /tmp/julial73DPo ...
Task (queued) @0x00007f1b20a38280
julia> input = Dict("file" => "none", "code_str" => "something")
Dict{String,String} with 2 entries:
"file" => "none"
"code_str" => "something"
julia> conn = connect(pipe_lm)
Base.PipeEndpoint(open, 0 bytes waiting)
julia> JSON.print(conn, input)
julia> out = JSON.parse(conn)
1-element Array{Any,1}:
Dict{String,Any}(Pair{String,Any}("line",1),Pair{String,Any}("scope",""),Pair{String,Any}("message","use of undeclared symbol"),Pair{String,Any}("file","none"),Pair{String,Any}("code","E321"),Pair{String,Any}("variable","something"))
julia> out[1]
Dict{String,Any} with 6 entries:
"line" => 1
"scope" => ""
"message" => "use of undeclared symbol"
"file" => "none"
"code" => "E321"
"variable" => "something"
julia>
lintserver(port,style)
style will accept four values:
- "lint-message", which is the preferred and shown in above example
- "standard-linter-v1"
- "vscode"
- "standard-linter-v2"
If any of the above four JSON formats is not answering your needs, please make a
new pull request. The file you want
to edit is Lint.jl
and the function is called convertmsgtojson
. It is enough to add one elseif
block, here is one of them as an example:
elseif style == "standard-linter-v2"
push!(output, Dict("severity" => etype,
"location" => Dict("file" => file,
"position" => errorrange),
"excerpt" => code,
"description" => "$evar: $txt"))
end
The old protocol for the server is:
- The file path followed by a new line
- The number of bytes of code being sent followed by a new line
- The actual code
The server will respond with the messages produced when linting the code followed by a empty line (i.e. if it linted cleanly it will respond with a single newline).
Launch the server:
using Lint
lintserver(2222)
Connect and send requests:
socket = connect(2222)
println(socket, "none") # filename
str = """
test = "Hello" + "World"
"""
println(socket, sizeof(str)) # bytes of code
println(socket, str) # code
response = ""
line = ""
while isopen(socket)
response *= line
line = readline(socket)
end
@assert response == "none:1 E422 : string uses * to concatenate\n\n"
Note that the first request might take some time because the linting functions are being imported and compiled.