Instructions for creating lint rules

This commit is contained in:
XeroOl 2024-02-18 16:08:23 -06:00
parent 647421f21b
commit 5c19cb3a83
2 changed files with 97 additions and 0 deletions

93
Adding-a-Lint-Rule.md Normal file
View File

@ -0,0 +1,93 @@
# Creating a new lint
To start, you can set up all the plumbing:
1. Go into `src/fennel-ls/diagnostics.fnl` and create a new function.
2. At the bottom of `src/fennel-ls/diagnostics.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/state.fnl`, find the
`default-configuration` variable, and turn your check on by default.
# Writing your lint
Now, the fun part: writing your lint function.
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:
* definition (Every time a new variable is bound)
* call (Every time the user calls a function or a special. Macros don't count.)
More loops might have been added since I wrote this document.
## Input arguments
All lints give you `self` and `file`. They're mostly useful to pass to other
functions.
* `self` 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 .fnl file. It has some useful fields.
Check out what fields it has by looking at the end of `compiler.fnl`. The
most important one for lints is probably `lexical`. `file.lexical` stores the
value `true` for every single list, table, or symbol that appears in the
original file AST. If your lint is supposed to apply to hand written code,
but doesn't really apply to generated code, you should make sure that the AST
you're checking is inside of file.lexical.
The next arguments depend on which loop the lint is in:
### If your lint is linting definitions:
* `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.
For example, if I write the code `(var x 1000)`, the definition will be:
```fnl
{:definition 1000 :binding `x :var? true}
```
If I write the code `(let [(x {:foo {:bar y}}) (my-expression)] x.myfield)`,
the definitions will be:
```fnl
;; for x
{:definition `(my-expression)
:binding `x
:multival 1
:referenced-by {:symbol `x.field :target @1 :ref-type "read"}}
;; for y
{: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.
## Output:
Return `nil` if there's nothing to report, return a diagnostic object with your
lint. 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 the entire call, give the range of the call. If its a
specific argument, give the range of that argument if possible, and the call
if not. Remember that we can't get the range of things like numbers and
strings, because they don't have tracking info.
* `message`: this is the message your lint will produce. Try to make it
specific and helpful as possible.
* `severity`: hardcode this to `message.severity.WARN`. ERROR is for compiler
errors, and WARN is for lints.
* `code`: Please use a new number counting up from 301 for each lint. The codes
are nice to have so that the message of the lint can change without breaking
tests that only check for the presence or absence of a lint.
* `codeDescription`: Use the name of your function.
## Testing:
I will think about this later. :)

View File

@ -73,6 +73,10 @@ Your editor can send these settings using one of these two methods:
* The client sends an `initialize` request with the structure `{initializationOptions: {"fennel-ls": {...}}, ...}`
* The client sends a `workspace/didChangeConfiguration` notfication containing the field `{settings: {"fennel-ls": {YOUR_SETTINGS}}}`
## Adding a lint
You can't load external lint rules with fennel-ls, but I would love to receive patches that add new lint rules!
[Instructions to add a lint.](Adding-a-Lint-Rule.md)
## License
fennel-ls is licensed under the MIT license. See LICENSE for more info.
This project also contains files from other projects: