2025-07-23 –, Main Room 1 (Main stage)
StaticLint.jl is a popular linter for Julia. Despite its wide acceptance, StaticLint suffers from many shortcomings related to extensibility and poor interoperability.
ReLint is an MIT open-source Lint checker used at RelationalAI. ReLint is fast, extensible, offers tools to match abstract syntax trees, and can be used by a GitHub workflow or a pre-commit hook. ReLint has been successfully employed to enforce programming conventions on a large code base (over 300K LOC).
Despite its wide acceptance, StaticLint.jl has some shortcomings that got in the way of our use:
- Adding new rules is cumbersome,
- StaticLint cannot be easily run by a GitHub action at each commit or used to gate a commit if changes violate some lint rules.
This talk presents and demonstrates ReLint, an open-source Lint checker used at RelationalAI, released under the MIT License. ReLint is fast, extensible, offers tools to match abstract syntax trees, and can be used by a GitHub workflow or a pre-commit hook.
Example 1: Consider the scenario: we would like to forbid the use of @async
in a codebase to favor @spawn
. We can define an empty struct as follows:
struct AsyncRule <: LintRule end
And the function check
can then be overloaded:
check(::AsyncRule, x::EXPR) =
generic_check(x, "@async hole_variable", "Use `@spawn` instead of `@async`.")
The pattern to be matched is provided as a Julia string code "@async hole_variable"
. The pseudo-variable hole_variable
matches any expression. Each instance of this pattern in a source code produces the error "Use @spawn instead of @async."
accompanied by the exact location in the source code.
ReLint can be run on a file or a folder using ReLint.run_lint("path/to/folder_or_julia_file/")
. A Julia code containing the instruction @async foo() + bar()
will be reported by ReLint. Such a report can be automatically posted as a comment in a Pull Request or used to prevent the code from being committed.
Example 2: A less trivial rule is forbidding the use of Threads.nthreads()
in a const variable. This is important if an application is built on a particular machine but runs on a different machine with a different spec.
function check(::InitializingWithFunctionRule, x::EXPR, markers::Dict{Symbol,Symbol})
# Threads.nthreads() must not be used in a const field, but it is allowed elsewhere
haskey(markers, :const) || return
generic_check(x, "Threads.nthreads()", "`Threads.nthreads()` should not be used in a constant variable.")
end
A line of code such as const MAX_QUEUE = max(2*Threads.nthreads(), 16)
will be reported by RAILint.
Example 3: As a last example, consider the case of branches that cannot be reached:
if foo()
bar()
elseif foo()
zork()
end
Obviously, the zork()
function will never be executed because the two sequential if
have the same condition. This error may not be obvious in the presence of dozens of lines of code in each branch. An instance of this pattern can be easily caught with:
function check(t::UnreachableBranchRule, x::EXPR)
generic_check(
t,
x,
"if hole_variableA \
hole_variable \
elseif hole_variableA \
hole_variable \
end",
"Unreachable branch.")
end
Multiple uses of the pseudo-variable hole_variableA
impose the matched code to be identical, AST-wise.
This presentation: The main features of ReLint, accompanied by several scenarios, will be demonstrated. We will also cover extensibility, embedding ReLint in a GitHub action (useful to have a Lint report for every commit), and pre-commit (useful to gate a commit if it contains some rule violations). ReLint has been used in an industrial environment to enforce programming conventions and security rules for over 1 year in a large, critical application.