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

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.

metadata and stuff around. You probably don't need to use it directly.

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.

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.

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

evaluating.

getting bound to symbol. It might be nil.

bound to symbol, assuming definition.definition produces multiple values.

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:

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)

specific and helpful as possible; it doesn't have to be the same every time the lint is triggered.

errors, and WARN is for lints.

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.