From 5c19cb3a830f9963ee99019970dfa8b47aae43bf Mon Sep 17 00:00:00 2001 From: XeroOl Date: Sun, 18 Feb 2024 16:08:23 -0600 Subject: [PATCH] Instructions for creating lint rules --- Adding-a-Lint-Rule.md | 93 +++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++ 2 files changed, 97 insertions(+) create mode 100644 Adding-a-Lint-Rule.md diff --git a/Adding-a-Lint-Rule.md b/Adding-a-Lint-Rule.md new file mode 100644 index 0000000..7759163 --- /dev/null +++ b/Adding-a-Lint-Rule.md @@ -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. (table.insert diagnostics ( self file )))` +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. :) diff --git a/README.md b/README.md index bbf3dda..9e3652a 100644 --- a/README.md +++ b/README.md @@ -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: