fennel-ls/docs/linting.md
2025-07-17 14:43:53 -05:00

4.8 KiB

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.

  • server is the table that represents the language server. It carries metadata and stuff around. You probably don't need to use it directly.
  • file is 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.

  • ast is the AST of the call. it will be a list.
  • macroexpanded will 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.

  • symbol is 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

  • symbol is the symbol being bound. It is just a regular fennel sym.
  • definition is a table full of information about what is being bound:
    • definition.binding is the symbol again.
    • definition.definition, if present, is the expression that we're evaluating.
    • definition.referenced-by is 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.multival tells 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 the symbol is 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 with message.ast->range to 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 to message.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.