clean up some lint documentation

This commit is contained in:
XeroOl 2025-07-12 16:02:21 -05:00
parent a54745c581
commit d219cfffcb
2 changed files with 65 additions and 50 deletions

View File

@ -1,46 +1,62 @@
# How to add a new lint # How to add a new lint
## Creating a new lint ## Creating a new lint
To start, you can set up all the plumbing: Go into `src/fennel-ls/lint.fnl` and create a new call to add-lint.
1. Go into `src/fennel-ls/lint.fnl` and create a new function.
2. At the bottom of `src/fennel-ls/lint.fnl`, add an if statement in the
`check` function.
* Choose which `each` loop to put your function in, so your lint can be
applied to right thing.
* add `(if checks.<your-check> (table.insert diagnostics (<your-check> self file <the rest of the args>)))`
3. Enable your lint! In `src/fennel-ls/config.fnl`, find the
`default-configuration` variable, and turn your check on by default.
## Writing your lint ## Writing your lint
Now, the fun part: writing your lint function. Now, the fun part: writing your lint function.
The goal is to check whether the given arguments should emit a warning, and The goal is to check whether the given arguments should emit a warning, and
what message to show. The current loops in `check` go over every: 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) * definition (Every time a new variable is bound)
* call (Every time the user calls a function or a special. Macros don't count.) * reference (Every time an identifier is referring to something in scope)
* lexical (Every source-tracked AST node in the whole file, before macro expansion.
Symbols, lists, tables, and varargs will be here; numbers, strings, booleans,
and nil will not.)
More loops might have been added since I wrote this document. More types might have been added since I wrote this document.
### Input arguments ### Input arguments
All lints give you `server` and `file`. They're mostly useful to pass to other All lints receive a `server` and `file`. These values are mostly useful to
functions. pass to other functions.
* `server` is the table that represents the language server. It carries metadata * `server` is the table that represents the language server. It carries
and stuff around. You probably don't need to use it directly. metadata and stuff around. You probably don't need to use it directly.
* `file` is an object that represents a .fnl file. It has some useful fields. * `file` is an object that represents a fennel source file. It has some
Check out what fields it has by looking at the end of `compiler.fnl`. useful fields. Check out what fields it has by looking at the end of
`compiler.fnl`.
`file.lexical` stores the value `true` for every single list, table, or symbol The next arguments depend on which type the lint is in:
that appears in the original file AST, but not things generated via
macroexpansion. Make sure that the AST you're checking is inside of
`file.lexical`; otherwise, your lint may not be actionable or relevant, because
the user won't be able to see or edit the code your lint is warning about.
The next arguments depend on which loop the lint is in: #### "Call" type lints. (aka combinations aka compound forms aka lists):
#### If your lint is linting definitions: 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
```fnl
(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
```fnl
(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. * `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` is a table full of information about what is being bound:
* `definition.binding` is the symbol again. * `definition.binding` is the symbol again.
@ -55,6 +71,9 @@ The next arguments depend on which loop the lint is in:
* `definition.var?` is a boolean, which tells if the `symbol` is introduced * `definition.var?` is a boolean, which tells if the `symbol` is introduced
as a variable. as a variable.
#### "Other" type lints
Don't write these. :)
For example, if I write the code `(var x 1000)`, the definition will be: For example, if I write the code `(var x 1000)`, the definition will be:
```fnl ```fnl
{:definition 1000 :binding `x :var? true} {:definition 1000 :binding `x :var? true}
@ -66,18 +85,11 @@ the definitions will be:
{:definition `(my-expression) {:definition `(my-expression)
:binding `x :binding `x
:multival 1 :multival 1
:referenced-by {:symbol `x.myfield :target @1 :ref-type "read"}} :referenced-by {:symbol `x.myfield :ref-type "read"}}
;; for y ;; for y
{:definition `(my-expression) :binding `y :multival 2 :keys [:foo :bar]} {:definition `(my-expression) :binding `y :multival 2 :keys [:foo :bar]}
``` ```
#### If your lint is linting calls (to functions or specials, not macros)
* `head` is the symbol that is being called. It is the same as `(. call 1)`.
* `call` is the list that represents the call.
#### If your lint is linting lexical nodes
* `ast` is the AST node
### Output: ### Output:
Your lint function should return `nil` if there's nothing to report, or Your lint function should return `nil` if there's nothing to report, or
return a diagnostic object representing your lint message. return a diagnostic object representing your lint message.
@ -87,18 +99,18 @@ The return value should have these fields:
* `range`: make these with `message.ast->range` to get the range for a list or * `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 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 specific segment of a multisym. Try to report specifically on which piece of
AST is wrong. If its the entire call, give the range of the call. If its a AST is wrong. If its an entire list, give the range of the list. If a
specific argument, give the range of that argument if possible, and the call specific argument is problematic, give the range of that argument if possible,
if not. Remember that we can't get the range of things like numbers and and the call if not. `message.ast->range` will not fail on lists, symbols, or
strings, because they don't have tracking info. 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 * `message`: this is the message your lint will produce. Try to make it
specific and helpful as possible. 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 * `severity`: hardcode this to `message.severity.WARN`. ERROR is for compiler
errors, and WARN is for lints. errors, and WARN is for lints.
* `code`: Please use a new number counting up from 301 for each lint. The codes * `fix`: Optional. If there's a way to address this programmatically, you can
are nice to have so that the message of the lint can change without breaking add a "fix" field with the code to generate a quickfix. See the other lints
tests that only check for the presence or absence of a lint. for examples.
* `codeDescription`: Use the name of your function.
### Testing: ### Testing:
I will think about this later. :) For now see examples in `test/lint.fnl`. I will think about this later. :) For now see examples in `test/lint.fnl`.

View File

@ -5,11 +5,14 @@ There are only two functions exposed here:
* `read` receives and parses a message from the client. * `read` receives and parses a message from the client.
* `write` serializes and sends a message to the client. * `write` serializes and sends a message to the client.
It's probably not compliant yet, because serialization of [] and {} is the same. dkjson defaults to emitting [] when given an empty table, so we need to be
Luckily, I'm testing with Neovim, so I can pretend these problems don't exist for now." sure our object-like tables have at least one key, or we apply metatable magic
on the empty table to tell dkjson to serialize as {}."
(local {: encode : decode} (require :dkjson)) (local {: encode : decode} (require :dkjson))
(local http-separator (local header-separator
;; Something in windows replaces \n with \r\n,
;; so we have to leave the \r's out
(if (string.match package.config "^\\") (if (string.match package.config "^\\")
"\n\n" "\n\n"
"\r\n\r\n")) "\r\n\r\n"))
@ -55,7 +58,7 @@ Returns a table with the message if it succeeded, or a string with the parse err
(λ write [out msg] (λ write [out msg]
"Serializes and writes a JSON-RPC message to the given output stream" "Serializes and writes a JSON-RPC message to the given output stream"
(let [content (encode msg) (let [content (encode msg)
msg-stringified (.. "Content-Length: " (length content) http-separator content)] msg-stringified (.. "Content-Length: " (length content) header-separator content)]
(out:write msg-stringified) (out:write msg-stringified)
(when out.flush (when out.flush
(out:flush)))) (out:flush))))