How to add a new lint
Creating a new lint
Go into src/fennel-ls/lint.fnl and create a new call to add-lint.
Writing your lint
Now, the fun part: writing your lint function.
A lint checks whether the given arguments should emit a warning, and what message to show. You can request that your lint is called for every
- function-call (Every time the user calls a function)
- special-call (Every time the user calls a special)
- macro-call (Every time the user calls a macro)
- definition (Every time a new variable is bound)
- reference (Every time an identifier is referring to something in scope)
More types might have been added since I wrote this document.
Input arguments
All lints receive a server and file. These values are mostly useful to
pass to other functions.
serveris the table that represents the language server. It carries
metadata and stuff around. You probably don't need to use it directly.
fileis an object that represents a fennel source file. It has some
useful fields. Check out what fields it has by looking at the end of
compiler.fnl.
The next arguments depend on which type the lint is in:
"Call" type lints. (aka combinations aka compound forms aka lists):
There are three call types: function-call, special-call, and macro-call.
astis the AST of the call. it will be a list.macroexpandedwill be the AST generated from the expansion of the macro,
if the call was invoking a macro.
For example, if I had the code
(let [(x y) (values 1 2)]
(print (+ 1 x y)))and I created a function-call lint, My lint would would be called once
with ast as (print (+ 1 x y)). If I created a special-call lint,
my lint would be called with ast as (let [(x y) (values 1 2)] (print (+ 1 x y))),
with (values 1 2), and with (+ 1 x y).
"Reference" type lints
References are any time a symbol is referring to a local or global variable.
symbolis the symbol that's referring to something.
For example, in the code
(let [x 10]
(print x))let and x on line 1 are not references. let is a special, and x is
introducing a new binding, not referring to existing ones.
print and x on line 2 are references, and so a reference type
lint would be called for print and for x.
"Definition" type lints
symbolis the symbol being bound. It is just a regular fennel sym.definitionis a table full of information about what is being bound:definition.bindingis the symbol again.definition.definition, if present, is the expression that we're
evaluating.
definition.referenced-byis a list of "reference" object things.definition.keys, if present, tells you what part of the definition is
getting bound to symbol. It might be nil.
definition.multivaltells you which value of the definition is getting
bound to symbol, assuming definition.definition produces multiple
values.
definition.var?is a boolean, which tells if thesymbolis introduced
as a variable.
"Other" type lints
Don't write these. :)
For example, if I write the code (var x 1000), the definition will be:
{:definition 1000 :binding `x :var? true}If I write the code (let [(x {:foo {:bar y}}) (my-expression)] x.myfield),
the definitions will be:
;; for x
{:definition `(my-expression)
:binding `x
:multival 1
:referenced-by {:symbol `x.myfield :ref-type "read"}}
;; for y
{:definition `(my-expression) :binding `y :multival 2 :keys [:foo :bar]}Output:
Your lint function should return nil if there's nothing to report, or
return a diagnostic object representing your lint message.
The return value should have these fields:
range: make these withmessage.ast->rangeto get the range for a list or
symbol or table, or with message.multisym->range to get the range of a
specific segment of a multisym. Try to report specifically on which piece of
AST is wrong. If its an entire list, give the range of the list. If a
specific argument is problematic, give the range of that argument if possible,
and the call if not. message.ast->range will not fail on lists, symbols, or
tables, but it may fail on other AST items. (by returning nil)
message: this is the message your lint will produce. Try to make it
specific and helpful as possible; it doesn't have to be the same every time the lint is triggered.
severity: hardcode this tomessage.severity.WARN. ERROR is for compiler
errors, and WARN is for lints.
fix: Optional. If there's a way to address this programmatically, you can
add a "fix" field with the code to generate a quickfix. See the other lints for examples.
Other places:
At some point I want to add doc-testing of the :example documentation, but for
now, you have to add your tests manually to test/lint.fnl.
If you add a lint, also add it to changelog.md.