docs/lints.md is now a generated file. maybe I shouldn't keep it tracked anymore, lol
This commit is contained in:
parent
dbc596d10f
commit
ff601e4c17
12
Makefile
12
Makefile
@ -24,7 +24,7 @@ endif
|
|||||||
.PHONY: all clean test repl install docs docs-love2d ci selflint \
|
.PHONY: all clean test repl install docs docs-love2d ci selflint \
|
||||||
deps count testall check-deps check-docs
|
deps count testall check-deps check-docs
|
||||||
|
|
||||||
all: $(EXE)
|
all: $(EXE) docs/lints.md
|
||||||
|
|
||||||
$(EXE): $(SRC)
|
$(EXE): $(SRC)
|
||||||
echo "#!/usr/bin/env $(LUA)" > $@
|
echo "#!/usr/bin/env $(LUA)" > $@
|
||||||
@ -48,6 +48,9 @@ src/fennel-ls/docs/generated/%.fnl:
|
|||||||
mkdir -p src/fennel-ls/docs/generated/
|
mkdir -p src/fennel-ls/docs/generated/
|
||||||
$(FENNEL) $(FENNELFLAGS) tools/generate-lua-docs.fnl ${*} > $@
|
$(FENNEL) $(FENNELFLAGS) tools/generate-lua-docs.fnl ${*} > $@
|
||||||
|
|
||||||
|
docs/lints.md: src/fennel-ls/lint.fnl
|
||||||
|
$(FENNEL) $(FENNELFLAGS) tools/extract-lint-docs.fnl > $@
|
||||||
|
|
||||||
docs-love2d:
|
docs-love2d:
|
||||||
@echo "This has moved to a separate source. Please see the wiki:"
|
@echo "This has moved to a separate source. Please see the wiki:"
|
||||||
@echo "https://wiki.fennel-lang.org/LanguageServer"
|
@echo "https://wiki.fennel-lang.org/LanguageServer"
|
||||||
@ -108,9 +111,10 @@ clean:
|
|||||||
# Steps to release a new fennel-ls version
|
# Steps to release a new fennel-ls version
|
||||||
|
|
||||||
# 0. run `make test` and `make selflint`, and/or check builds.sr.ht to ensure things are working.
|
# 0. run `make test` and `make selflint`, and/or check builds.sr.ht to ensure things are working.
|
||||||
# 1. Ensure fennel is up to date
|
# 1. Ensure fennel and dkjson are up to date with `make check-deps`
|
||||||
# 2. Ensure dkjson is up to date
|
# 2. Ensure lua documentation is up to date with `make check-docs`.
|
||||||
# 3. Remove "-dev" suffix in version src/fennel-ls/utils.fnl
|
# (occasionally, lua devs fix typos or change wording of their reference)
|
||||||
|
# 3. Remove "-dev" suffix in version src/fennel-ls/utils.fnl, and in :since fields in src/fennel-ls/lint.fnl
|
||||||
# 4. Add the version to changelog.md
|
# 4. Add the version to changelog.md
|
||||||
# 5. Create a commit titled "Release 0.2.2"
|
# 5. Create a commit titled "Release 0.2.2"
|
||||||
# 6. git tag --sign --annotate 0.2.2
|
# 6. git tag --sign --annotate 0.2.2
|
||||||
|
|||||||
114
docs/lints.md
114
docs/lints.md
@ -13,7 +13,6 @@ remove the binding, or add an `_` to the variable name.
|
|||||||
(var value 100)
|
(var value 100)
|
||||||
(set value 10)
|
(set value 10)
|
||||||
```
|
```
|
||||||
|
|
||||||
Instead, use the value, remove it, or add `_` to the variable name.
|
Instead, use the value, remove it, or add `_` to the variable name.
|
||||||
```fnl
|
```fnl
|
||||||
(var value 100)
|
(var value 100)
|
||||||
@ -23,30 +22,32 @@ Instead, use the value, remove it, or add `_` to the variable name.
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
Fennel's pattern matching macros also check for leading `_` for symbol names.
|
Fennel's pattern matching macros also check for leading `_` in symbols.
|
||||||
This means that adding an `_` can change the semantics of the code. In this
|
This means that adding `_` can change the semantics of the code. In this
|
||||||
situation, add the `_` to the end of the symbol to disable the lint without
|
situation, the user needs to add the `_` to the **end** of the symbol
|
||||||
changing the pattern's meaning.
|
to disable only the lint, without changing the pattern's meaning.
|
||||||
|
Only use a trailing underscore when it's required to prevent code from
|
||||||
|
changing meaning.
|
||||||
```fnl
|
```fnl
|
||||||
|
;; Original. Works, but `b` is flagged by the lint
|
||||||
(match [10 nil]
|
(match [10 nil]
|
||||||
;; pattern works as intended, but triggers the lint
|
[a b] (print a "unintended")
|
||||||
[a b] (print a "unintended")
|
_ (print "we want this one")) ;; Prints this one!
|
||||||
_ (print "unintended"))
|
|
||||||
|
|
||||||
|
;; Suppressing lint normally causes problems
|
||||||
(match [10 nil]
|
(match [10 nil]
|
||||||
;; pattern matches when we don't want it to
|
[a _b] (print a "unintended") ;; Uh oh, we're printing "unintended" now!
|
||||||
[a _b] (print a "unintended")
|
_ (print "we want this one"))
|
||||||
_ (print "unintended"))
|
|
||||||
|
|
||||||
|
;; Solution! Underscore at the end
|
||||||
(match [10 nil]
|
(match [10 nil]
|
||||||
;; works as intended and doesn't trigger lint
|
[a b_] (print a "unintended")
|
||||||
[a b_] (print a "unintended")
|
_ (print "we want this one")) ;; Prints the right one
|
||||||
_ (print "unintended"))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Think of it this way:
|
Think of the trailing underscore as the fourth possible sigil:
|
||||||
`identifier` - must be used, and should be non-`nil`
|
|
||||||
`?identifier` - must be used, and can be `nil`
|
`?identifier` - must be used, and can be `nil`
|
||||||
|
`identifier` - must be used, and should be non-`nil`
|
||||||
`_identifier` - may be unused, and can be `nil`
|
`_identifier` - may be unused, and can be `nil`
|
||||||
`identifer_` - may be unused, but should be non-`nil`
|
`identifer_` - may be unused, but should be non-`nil`
|
||||||
|
|
||||||
@ -228,7 +229,7 @@ Instead, use:
|
|||||||
""
|
""
|
||||||
```
|
```
|
||||||
|
|
||||||
## Note
|
## Known limitations
|
||||||
This lint isn't actually very useful.
|
This lint isn't actually very useful.
|
||||||
|
|
||||||
# no-decreasing-comparison (off by default)
|
# no-decreasing-comparison (off by default)
|
||||||
@ -277,7 +278,7 @@ Instead, use:
|
|||||||
_ "other")
|
_ "other")
|
||||||
```
|
```
|
||||||
|
|
||||||
# inline-values
|
# inline-unpack
|
||||||
## What it does
|
## What it does
|
||||||
Warns when multiple values from `values` or `unpack` are used in a non-final
|
Warns when multiple values from `values` or `unpack` are used in a non-final
|
||||||
position of a function call, where only the first value will be used.
|
position of a function call, where only the first value will be used.
|
||||||
@ -304,7 +305,7 @@ Instead, use:
|
|||||||
(print a b c 4)
|
(print a b c 4)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations
|
## Known limitations
|
||||||
It doesn't make sense to flag *all* places where a multival is discarded, because
|
It doesn't make sense to flag *all* places where a multival is discarded, because
|
||||||
discarding extra values is common in Lua. For example, in the standard library
|
discarding extra values is common in Lua. For example, in the standard library
|
||||||
of Lua, `string.gsub` and `require` actually return two results, even though
|
of Lua, `string.gsub` and `require` actually return two results, even though
|
||||||
@ -314,6 +315,79 @@ This lint specifically flags discarding multivals from `values` and `unpack`,
|
|||||||
instead of flagging all discards, because these forms indicate that the user
|
instead of flagging all discards, because these forms indicate that the user
|
||||||
*intends* for something to happen with multivals.
|
*intends* for something to happen with multivals.
|
||||||
|
|
||||||
## Note
|
|
||||||
You find more information about Lua's multivals in [Benaiah's excellent post explaining Lua's multivals](https://benaiah.me/posts/everything-you-didnt-want-to-know-about-lua-multivals),
|
You find more information about Lua's multivals in [Benaiah's excellent post explaining Lua's multivals](https://benaiah.me/posts/everything-you-didnt-want-to-know-about-lua-multivals),
|
||||||
or by searching the word "adjust" in the [Lua Manual](https://www.lua.org/manual/5.4/manual.html#3.4.12).
|
or by searching the word "adjust" in the [Lua Manual](https://www.lua.org/manual/5.4/manual.html#3.4.12).
|
||||||
|
|
||||||
|
# empty-let
|
||||||
|
## What it does
|
||||||
|
Warns about `(let [] ...)` that should be `(do ...)`.
|
||||||
|
|
||||||
|
## Why is this bad?
|
||||||
|
Using `let` with no bindings is unnecessarily verbose when `do` serves the same purpose more clearly.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```fnl
|
||||||
|
(let []
|
||||||
|
(print "hello")
|
||||||
|
(print "world"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(do
|
||||||
|
(print "hello")
|
||||||
|
(print "world"))
|
||||||
|
```
|
||||||
|
|
||||||
|
# mismatched-argument-count (off by default)
|
||||||
|
## What it does
|
||||||
|
Checks if function calls have the correct number of arguments based on the function's signature.
|
||||||
|
|
||||||
|
## Why is this bad?
|
||||||
|
Calling functions with the wrong number of arguments can lead to runtime errors
|
||||||
|
or unexpected behavior. This lint helps catch these issues early.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```fnl
|
||||||
|
(string.sub "hello") ; missing required arguments
|
||||||
|
(string.sub "hello" 1 2 3) ; too many arguments
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(string.sub "hello" 1) ; provide all required arguments
|
||||||
|
(string.sub "hello" 1 2) ; remove extra arguments
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
This lint is disabled by default because it can produce false positives.
|
||||||
|
It assumes that all optional arguments are reliably annotated with a ? sigil,
|
||||||
|
and any other arguments can be assumed to be required. This is reasonably
|
||||||
|
accurate if the code follows Fennel conventions. Also this lint is very new and
|
||||||
|
may have issues, so I'd like to let people try it on their own terms before
|
||||||
|
enabling it by default.
|
||||||
|
|
||||||
|
In the future I may split it into "too-many-arguments" (which is accurate regardless of code style)
|
||||||
|
and "not-enough-arguments" (which needs the arglist to be annotated properly)
|
||||||
|
|
||||||
|
# duplicate-table-keys
|
||||||
|
## What it does
|
||||||
|
Detects when the same key appears multiple times in a table literal.
|
||||||
|
|
||||||
|
## Why is this bad?
|
||||||
|
Duplicate keys in a table are usually a mistake and the later value will
|
||||||
|
overwrite the earlier one, which can lead to bugs.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```fnl
|
||||||
|
{:name "Alice"
|
||||||
|
:age 25
|
||||||
|
:name "Bob"} ; "Alice" gets overwritten by "Bob"
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
{:name "Bob"
|
||||||
|
:age 25}
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
Provides the function (check server file), which goes through a file and mutates
|
Provides the function (check server file), which goes through a file and mutates
|
||||||
the `file.diagnostics` field, filling it with diagnostics."
|
the `file.diagnostics` field, filling it with diagnostics."
|
||||||
|
|
||||||
(local {: sym? : list? : table? : view
|
(local {: sym? : list? : table? : varg? : view
|
||||||
: sym : list &as fennel} (require :fennel))
|
: sym : list &as fennel} (require :fennel))
|
||||||
(local {: special? : op?} (require :fennel-ls.compiler))
|
(local {: special? : op?} (require :fennel-ls.compiler))
|
||||||
(local analyzer (require :fennel-ls.analyzer))
|
(local analyzer (require :fennel-ls.analyzer))
|
||||||
@ -37,7 +37,56 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
|
|
||||||
|
|
||||||
(add-lint :unused-definition
|
(add-lint :unused-definition
|
||||||
{:type :definition
|
{:what-it-does
|
||||||
|
"Marks bindings that aren't read. Completely overwriting a value doesn't count
|
||||||
|
as reading it. A variable that starts or ends with an `_` will not trigger this
|
||||||
|
lint. Use this to suppress the lint."
|
||||||
|
:why-care?
|
||||||
|
"Unused definitions can lead to bugs and make code harder to understand. Either
|
||||||
|
remove the binding, or add an `_` to the variable name."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(var value 100)
|
||||||
|
(set value 10)
|
||||||
|
```
|
||||||
|
Instead, use the value, remove it, or add `_` to the variable name.
|
||||||
|
```fnl
|
||||||
|
(var value 100)
|
||||||
|
(set value 10)
|
||||||
|
;; use the value
|
||||||
|
(print value)
|
||||||
|
```"
|
||||||
|
:limitations
|
||||||
|
"Fennel's pattern matching macros also check for leading `_` in symbols.
|
||||||
|
This means that adding `_` can change the semantics of the code. In this
|
||||||
|
situation, the user needs to add the `_` to the **end** of the symbol
|
||||||
|
to disable only the lint, without changing the pattern's meaning.
|
||||||
|
Only use a trailing underscore when it's required to prevent code from
|
||||||
|
changing meaning.
|
||||||
|
```fnl
|
||||||
|
;; Original. Works, but `b` is flagged by the lint
|
||||||
|
(match [10 nil]
|
||||||
|
[a b] (print a \"unintended\")
|
||||||
|
_ (print \"we want this one\")) ;; Prints this one!
|
||||||
|
|
||||||
|
;; Suppressing lint normally causes problems
|
||||||
|
(match [10 nil]
|
||||||
|
[a _b] (print a \"unintended\") ;; Uh oh, we're printing \"unintended\" now!
|
||||||
|
_ (print \"we want this one\"))
|
||||||
|
|
||||||
|
;; Solution! Underscore at the end
|
||||||
|
(match [10 nil]
|
||||||
|
[a b_] (print a \"unintended\")
|
||||||
|
_ (print \"we want this one\")) ;; Prints the right one
|
||||||
|
```
|
||||||
|
|
||||||
|
Think of the trailing underscore as the fourth possible sigil:
|
||||||
|
`?identifier` - must be used, and can be `nil`
|
||||||
|
`identifier` - must be used, and should be non-`nil`
|
||||||
|
`_identifier` - may be unused, and can be `nil`
|
||||||
|
`identifer_` - may be unused, but should be non-`nil`"
|
||||||
|
:since "0.1.0"
|
||||||
|
:type :definition
|
||||||
:impl (fn [server file symbol definition]
|
:impl (fn [server file symbol definition]
|
||||||
"local variable that is defined but not used"
|
"local variable that is defined but not used"
|
||||||
(if (not (or (= "_" (: (tostring symbol) :sub 1 1))
|
(if (not (or (= "_" (: (tostring symbol) :sub 1 1))
|
||||||
@ -60,7 +109,7 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
(accumulate [in? false call (pairs calls) &until in?]
|
(accumulate [in? false call (pairs calls) &until in?]
|
||||||
(and (sym? (. call 1) :or) (utils.find call symbol))))
|
(and (sym? (. call 1) :or) (utils.find call symbol))))
|
||||||
|
|
||||||
(fn module-field-helper [server file symbol ?ast ?stack]
|
(fn unknown-module-field [server file symbol ?ast ?stack]
|
||||||
"if ?ast is a module field that isn't known, return a diagnostic"
|
"if ?ast is a module field that isn't known, return a diagnostic"
|
||||||
(let [opts {}
|
(let [opts {}
|
||||||
item (analyzer.search server file ?ast opts {:stack ?stack})]
|
item (analyzer.search server file ?ast opts {:stack ?stack})]
|
||||||
@ -74,19 +123,64 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
:severity message.severity.WARN})))
|
:severity message.severity.WARN})))
|
||||||
|
|
||||||
(add-lint :unknown-module-field
|
(add-lint :unknown-module-field
|
||||||
{:type :reference
|
{:what-it-does
|
||||||
|
"Looks for module fields that can't be statically determined to exist. This only
|
||||||
|
triggers if the module is found, but there's no definition of the field inside
|
||||||
|
of the module."
|
||||||
|
:why-care?
|
||||||
|
"This is probably a typo, or a missing function in the module."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
;;; in `a.fnl`
|
||||||
|
{: print}
|
||||||
|
|
||||||
|
;;; in `b.fnl`
|
||||||
|
(local a (require :a))
|
||||||
|
(a.printtt 100)
|
||||||
|
```
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
;;; in `b.fnl`
|
||||||
|
(local a (require :a))
|
||||||
|
(a.print 100) ; typo fixed
|
||||||
|
```"
|
||||||
|
:limitations
|
||||||
|
"Fennel-ls doesn't have a full type system, so we're not able to check every
|
||||||
|
multisym statically, but as a heuristic, usually modules are able to be
|
||||||
|
evaluated statically. If you have a module that can't be figured out, please
|
||||||
|
let us know on the bug tracker."
|
||||||
|
:since "0.1.0"
|
||||||
|
:type :reference
|
||||||
:impl (fn [server file symbol]
|
:impl (fn [server file symbol]
|
||||||
|
;; only multisyms, like `my-module.field`
|
||||||
(if (. (utils.multi-sym-split symbol) 2)
|
(if (. (utils.multi-sym-split symbol) 2)
|
||||||
(module-field-helper server file symbol symbol)))}
|
(unknown-module-field server file symbol symbol)))}
|
||||||
{:type :definition
|
{:type :definition
|
||||||
:impl (fn [server file symbol definition]
|
:impl (fn [server file symbol definition]
|
||||||
|
;; definitions, like `(local {: field} (require :my-module))`
|
||||||
(if definition.keys
|
(if definition.keys
|
||||||
(module-field-helper server file symbol definition.definition
|
(unknown-module-field server file symbol definition.definition
|
||||||
(fcollect [i (length definition.keys) 1 -1]
|
(fcollect [i (length definition.keys) 1 -1]
|
||||||
(. definition.keys i)))))})
|
(. definition.keys i)))))})
|
||||||
|
|
||||||
(add-lint :unnecessary-method
|
(add-lint :unnecessary-method
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Checks for unnecessary uses of the `:` method call syntax when a simple multisym
|
||||||
|
would work."
|
||||||
|
:why-care?
|
||||||
|
"Using the method call syntax unnecessarily adds complexity and can make code
|
||||||
|
harder to understand."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(: alien :shoot-laser {:x 10 :y 20})
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(alien:shoot-laser {:x 10 :y 20})
|
||||||
|
```"
|
||||||
|
:since "0.1.0"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
"a call to the : builtin that could just be a multisym"
|
"a call to the : builtin that could just be a multisym"
|
||||||
(let [object (. ast 2)
|
(let [object (. ast 2)
|
||||||
@ -99,7 +193,22 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
:severity message.severity.WARN})))})
|
:severity message.severity.WARN})))})
|
||||||
|
|
||||||
(add-lint :unnecessary-tset
|
(add-lint :unnecessary-tset
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Identifies unnecessary uses of `tset` when a `set` with a multisym would be clearer."
|
||||||
|
:why-care?
|
||||||
|
"Using `tset` makes the code more verbose and harder to read when a simpler
|
||||||
|
alternative exists."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(tset alien :health 1337)
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(set alien.health 1337)
|
||||||
|
```"
|
||||||
|
:since "0.2.0"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
(let [all-rewritable? (faccumulate [syms true
|
(let [all-rewritable? (faccumulate [syms true
|
||||||
i 3 (- (length ast) 1)
|
i 3 (- (length ast) 1)
|
||||||
@ -122,7 +231,25 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
{:do true :values true :+ true :* true :and true :or true :band true :bor true ".." true})
|
{:do true :values true :+ true :* true :and true :or true :band true :bor true ".." true})
|
||||||
|
|
||||||
(add-lint :unnecessary-unary
|
(add-lint :unnecessary-unary
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Warns about unnecessary `do` or `values` forms that only contain a single expression."
|
||||||
|
:why-care?
|
||||||
|
"Extra forms that don't do anything add syntactic noise."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(do (print \"hello\"))
|
||||||
|
|
||||||
|
(values (+ 1 2))
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(print \"hello\")
|
||||||
|
|
||||||
|
(+ 1 2)
|
||||||
|
```"
|
||||||
|
:since "0.2.0"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
(if (and (sym? (. ast 1))
|
(if (and (sym? (. ast 1))
|
||||||
(. redundant-wrappers (tostring (. ast 1)))
|
(. redundant-wrappers (tostring (. ast 1)))
|
||||||
@ -138,7 +265,25 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
(values form body-form?)))
|
(values form body-form?)))
|
||||||
|
|
||||||
(add-lint :redundant-do
|
(add-lint :redundant-do
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Identifies redundant `do` blocks within implicit do forms like `fn`, `let`, etc."
|
||||||
|
:why-care?
|
||||||
|
"Redundant `do` blocks add unnecessary nesting and make code harder to read."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(fn [] (do
|
||||||
|
(print \"first\")
|
||||||
|
(print \"second\")))
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(fn []
|
||||||
|
(print \"first\")
|
||||||
|
(print \"second\"))
|
||||||
|
```"
|
||||||
|
:since "0.2.0"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
(let [last-body (. ast (length ast))]
|
(let [last-body (. ast (length ast))]
|
||||||
(if (and (. implicit-do-forms (tostring (. ast 1)))
|
(if (and (. implicit-do-forms (tostring (. ast 1)))
|
||||||
@ -155,7 +300,29 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
" ")}]}})))})
|
" ")}]}})))})
|
||||||
|
|
||||||
(add-lint :bad-unpack
|
(add-lint :bad-unpack
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Warns when `unpack` or `table.unpack` is used with operators that aren't
|
||||||
|
variadic at runtime."
|
||||||
|
:why-care?
|
||||||
|
"Fennel operators like `+`, `*`, etc. look like they should work with `unpack`,
|
||||||
|
but they don't actually accept a variable number of arguments at runtime."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(+ 1 (unpack [2 3 4])) ; Only adds 1 and 2
|
||||||
|
(.. (unpack [\"a\" \"b\" \"c\"])) ; Only concatenates \"a\"
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
;; For concatenation:
|
||||||
|
(table.concat [\"a\" \"b\" \"c\"])
|
||||||
|
|
||||||
|
;; For other operators, use a loop:
|
||||||
|
(accumulate [sum 0 _ n (ipairs [1 2 3 4])]
|
||||||
|
(+ sum n))
|
||||||
|
```"
|
||||||
|
:since "0.1.0"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file call]
|
:impl (fn [server file call]
|
||||||
"an unpack call leading into an operator"
|
"an unpack call leading into an operator"
|
||||||
(let [op (. call 1)
|
(let [op (. call 1)
|
||||||
@ -183,7 +350,24 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
:newText (.. "(table.concat " (view (. last 2)) ")")}]})})))})
|
:newText (.. "(table.concat " (view (. last 2)) ")")}]})})))})
|
||||||
|
|
||||||
(add-lint :var-never-set
|
(add-lint :var-never-set
|
||||||
{:type :definition
|
{:what-it-does
|
||||||
|
"Identifies variables declared with `var` that are never modified with `set`."
|
||||||
|
:why-care?
|
||||||
|
"If a `var` is never modified, it should be declared with `local` or `let` instead
|
||||||
|
for clarity."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(var x 10)
|
||||||
|
(print x)
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(let [x 10]
|
||||||
|
(print x))
|
||||||
|
```"
|
||||||
|
:since "0.1.0"
|
||||||
|
:type :definition
|
||||||
:impl (fn [server file symbol definition]
|
:impl (fn [server file symbol definition]
|
||||||
(if (and definition.var? (not definition.var-set))
|
(if (and definition.var? (not definition.var-set))
|
||||||
;; we can't provide a quickfix for this because the hooks don't give us
|
;; we can't provide a quickfix for this because the hooks don't give us
|
||||||
@ -196,7 +380,29 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
(local op-identity-value {:+ 0 :* 1 :and true :or false :band -1 :bor 0 :.. ""})
|
(local op-identity-value {:+ 0 :* 1 :and true :or false :band -1 :bor 0 :.. ""})
|
||||||
|
|
||||||
(add-lint :op-with-no-arguments
|
(add-lint :op-with-no-arguments
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Warns when an operator is called with no arguments, which can be replaced with
|
||||||
|
an identity value."
|
||||||
|
:why-care?
|
||||||
|
"Calling operators with no arguments is less clear than using the identity value
|
||||||
|
directly."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(+) ; Returns 0
|
||||||
|
(*) ; Returns 1
|
||||||
|
(..) ; Returns \"\"
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
0
|
||||||
|
1
|
||||||
|
\"\"
|
||||||
|
```"
|
||||||
|
:limitations
|
||||||
|
"This lint isn't actually very useful."
|
||||||
|
:since "0.1.1"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
"A call like (+) that could be replaced with a literal"
|
"A call like (+) that could be replaced with a literal"
|
||||||
(let [op (. ast 1)
|
(let [op (. ast 1)
|
||||||
@ -204,7 +410,7 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
(if (and (op? op)
|
(if (and (op? op)
|
||||||
(= 1 (length ast))
|
(= 1 (length ast))
|
||||||
(not= nil identity))
|
(not= nil identity))
|
||||||
{:range (message.ast->range server file ast)
|
{:range (message.ast->range server file ast)
|
||||||
:message (.. "write " (view identity) " instead of (" (tostring op) ")")
|
:message (.. "write " (view identity) " instead of (" (tostring op) ")")
|
||||||
:severity message.severity.WARN
|
:severity message.severity.WARN
|
||||||
:fix #{:title (.. "Replace (" (tostring op) ") with " (view identity))
|
:fix #{:title (.. "Replace (" (tostring op) ") with " (view identity))
|
||||||
@ -212,19 +418,37 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
:newText (view identity)}]}})))})
|
:newText (view identity)}]}})))})
|
||||||
|
|
||||||
(add-lint :no-decreasing-comparison
|
(add-lint :no-decreasing-comparison
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Suggests using increasing comparison operators (`<`, `<=`) instead of decreasing ones (`>`, `>=`)."
|
||||||
|
:why-care?
|
||||||
|
"Consistency in comparison direction makes code more readable and maintainable,
|
||||||
|
especially in languages with lisp syntax. You can think of `<` as a function that
|
||||||
|
tests if the arguments are in sorted order."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(> a b)
|
||||||
|
(>= x y z)
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(< b a)
|
||||||
|
(<= z y x)
|
||||||
|
```"
|
||||||
|
:since "0.2.0"
|
||||||
|
:type :special-call
|
||||||
:disabled true
|
:disabled true
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
(let [op (. ast 1)]
|
(let [op (. ast 1)]
|
||||||
(if (or (sym? op :>) (sym? op :>=))
|
(if (or (sym? op :>) (sym? op :>=))
|
||||||
{:range (message.ast->range server file ast)
|
{:range (message.ast->range server file ast)
|
||||||
:message "Use increasing operator instead of decreasing"
|
:message "Use increasing operator instead of decreasing"
|
||||||
:severity message.severity.WARN
|
:severity message.severity.WARN
|
||||||
:fix #{:title "Reverse the comparison"
|
:fix #{:title "Reverse the comparison"
|
||||||
:changes [{:range (message.ast->range server file ast)
|
:changes [{:range (message.ast->range server file ast)
|
||||||
:newText (let [new (if (sym? op :>=) (fennel.sym :<=) (fennel.sym :<))
|
:newText (let [new (if (sym? op :>=) (sym :<=) (sym :<))
|
||||||
reversed (fcollect [i (length ast) 2 -1
|
reversed (fcollect [i (length ast) 2 -1
|
||||||
&into (list (sym new))]
|
&into (list new)]
|
||||||
(. ast i))]
|
(. ast i))]
|
||||||
(view reversed))}]}})))})
|
(view reversed))}]}})))})
|
||||||
|
|
||||||
@ -235,7 +459,29 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
(match-reference? subast references))))
|
(match-reference? subast references))))
|
||||||
|
|
||||||
(add-lint :match-should-case
|
(add-lint :match-should-case
|
||||||
{:type :macro-call
|
{:what-it-does
|
||||||
|
"Suggests using `case` instead of `match` when the meaning would not be altered."
|
||||||
|
:why-care?
|
||||||
|
"The `match` macro's meaning depends on the local variables in scope. When a
|
||||||
|
`match` call doesn't use the local variables, it can be replaced with the
|
||||||
|
`case` form."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(match value
|
||||||
|
10 \"ten\"
|
||||||
|
20 \"twenty\"
|
||||||
|
_ \"other\")
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(case value
|
||||||
|
10 \"ten\"
|
||||||
|
20 \"twenty\"
|
||||||
|
_ \"other\")
|
||||||
|
```"
|
||||||
|
:since "0.2.0"
|
||||||
|
:type :macro-call
|
||||||
:impl (fn [server {: references &as file} ast]
|
:impl (fn [server {: references &as file} ast]
|
||||||
(when (and (list? ast)
|
(when (and (list? ast)
|
||||||
(sym? (. ast 1) :match)
|
(sym? (. ast 1) :match)
|
||||||
@ -249,7 +495,43 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
:newText "case"}]}}))})
|
:newText "case"}]}}))})
|
||||||
|
|
||||||
(add-lint :inline-unpack
|
(add-lint :inline-unpack
|
||||||
{:type [:function-call :special-call]
|
{:what-it-does
|
||||||
|
"Warns when multiple values from `values` or `unpack` are used in a non-final
|
||||||
|
position of a function call, where only the first value will be used."
|
||||||
|
:why-care?
|
||||||
|
"In Fennel (and Lua), multiple values are only preserved when they appear in the
|
||||||
|
final position of a function call. Using them elsewhere results in only the
|
||||||
|
first value being used. This is likely not what was intended, since the use of
|
||||||
|
`values` or `unpack` seems to imply that the code is interested in handling
|
||||||
|
multivals instead of discarding them."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(print (values 1 2 3) 4) ; confusingly prints \"1 4\"
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
;; Try putting the multival at the end:
|
||||||
|
(print 4 (values 1 2 3))
|
||||||
|
|
||||||
|
;; Try writing the logic out manually instead of using multival
|
||||||
|
(let [(a b c) (values 1 2 3)]
|
||||||
|
(print a b c 4)
|
||||||
|
```"
|
||||||
|
:limitations
|
||||||
|
"It doesn't make sense to flag *all* places where a multival is discarded, because
|
||||||
|
discarding extra values is common in Lua. For example, in the standard library
|
||||||
|
of Lua, `string.gsub` and `require` actually return two results, even though
|
||||||
|
most of the time, only the first one is what's wanted.
|
||||||
|
|
||||||
|
This lint specifically flags discarding multivals from `values` and `unpack`,
|
||||||
|
instead of flagging all discards, because these forms indicate that the user
|
||||||
|
*intends* for something to happen with multivals.
|
||||||
|
|
||||||
|
You find more information about Lua's multivals in [Benaiah's excellent post explaining Lua's multivals](https://benaiah.me/posts/everything-you-didnt-want-to-know-about-lua-multivals),
|
||||||
|
or by searching the word \"adjust\" in the [Lua Manual](https://www.lua.org/manual/5.4/manual.html#3.4.12)."
|
||||||
|
:since "0.1.2"
|
||||||
|
:type [:function-call :special-call]
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
"generally, values and unpack are signs that the user is trying to do
|
"generally, values and unpack are signs that the user is trying to do
|
||||||
something with multiple values. However, multiple values will get
|
something with multiple values. However, multiple values will get
|
||||||
@ -269,7 +551,25 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
:severity message.severity.WARN}))))})
|
:severity message.severity.WARN}))))})
|
||||||
|
|
||||||
(add-lint :empty-let
|
(add-lint :empty-let
|
||||||
{:type :special-call
|
{:what-it-does
|
||||||
|
"Warns about `(let [] ...)` that should be `(do ...)`."
|
||||||
|
:why-care?
|
||||||
|
"Using `let` with no bindings is unnecessarily verbose when `do` serves the same purpose more clearly."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(let []
|
||||||
|
(print \"hello\")
|
||||||
|
(print \"world\"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(do
|
||||||
|
(print \"hello\")
|
||||||
|
(print \"world\"))
|
||||||
|
```"
|
||||||
|
:since "0.2.2-dev"
|
||||||
|
:type :special-call
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
(case ast
|
(case ast
|
||||||
(where [let* binding]
|
(where [let* binding]
|
||||||
@ -287,20 +587,51 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
|
|
||||||
(fn possibly-multival? [ast]
|
(fn possibly-multival? [ast]
|
||||||
;; operators all adjust to 1 value
|
;; operators all adjust to 1 value
|
||||||
(or (and (fennel.list? ast) (not (op? (. ast 1))) (not (sym? (. ast 1) :not)))
|
(or (and (list? ast) (not (op? (. ast 1))) (not (sym? (. ast 1) :not)))
|
||||||
(fennel.varg? ast)))
|
(varg? ast)))
|
||||||
|
|
||||||
(add-lint :mismatched-argument-count
|
(add-lint :mismatched-argument-count
|
||||||
{:type [:function-call :special-call :macro-call]
|
{:what-it-does
|
||||||
|
"Checks if function calls have the correct number of arguments based on the function's signature."
|
||||||
|
:why-care?
|
||||||
|
"Calling functions with the wrong number of arguments can lead to runtime errors
|
||||||
|
or unexpected behavior. This lint helps catch these issues early."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
(string.sub \"hello\") ; missing required arguments
|
||||||
|
(string.sub \"hello\" 1 2 3) ; too many arguments
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
(string.sub \"hello\" 1) ; provide all required arguments
|
||||||
|
(string.sub \"hello\" 1 2) ; remove extra arguments
|
||||||
|
```"
|
||||||
|
:limitations
|
||||||
|
"This lint is disabled by default because it can produce false positives.
|
||||||
|
It assumes that all optional arguments are reliably annotated with a ? sigil,
|
||||||
|
and any other arguments can be assumed to be required. This is reasonably
|
||||||
|
accurate if the code follows Fennel conventions. Also this lint is very new and
|
||||||
|
may have issues, so I'd like to let people try it on their own terms before
|
||||||
|
enabling it by default.
|
||||||
|
|
||||||
|
In the future I may split it into \"too-many-arguments\" (which is accurate regardless of code style)
|
||||||
|
and \"not-enough-arguments\" (which needs the arglist to be annotated properly)"
|
||||||
|
:since "0.2.2-dev"
|
||||||
|
:type [:function-call :special-call :macro-call]
|
||||||
:disabled true
|
:disabled true
|
||||||
:impl (fn [server file ast]
|
:impl (fn [server file ast]
|
||||||
(case (analyzer.search server file (. ast 1) {} {})
|
(case (analyzer.search server file (. ast 1) {} {})
|
||||||
{:indeterminate nil &as result}
|
{:indeterminate nil &as result}
|
||||||
(case (?. (navigate.getmetadata server result) :fnl/arglist)
|
(case (?. (navigate.getmetadata server result) :fnl/arglist)
|
||||||
signature
|
signature
|
||||||
(let [number-of-args (- (length ast) (if (and (sym? (. ast 1)) (string.find (tostring (. ast 1)) ".:"))
|
(let [number-of-args (- (length ast)
|
||||||
0 1))
|
(if (and (sym? (. ast 1))
|
||||||
passes-extra-args (and (not= 1 (length ast)) (possibly-multival? (. ast (length ast))))
|
(string.find (tostring (. ast 1)) ".:"))
|
||||||
|
0
|
||||||
|
1))
|
||||||
|
passes-extra-args (and (not= 1 (length ast))
|
||||||
|
(possibly-multival? (. ast (length ast))))
|
||||||
(min-params infinite-params?) (faccumulate [(last-required-argument vararg) nil
|
(min-params infinite-params?) (faccumulate [(last-required-argument vararg) nil
|
||||||
i (length signature) 1 -1]
|
i (length signature) 1 -1]
|
||||||
(let [s (tostring (. signature i))
|
(let [s (tostring (. signature i))
|
||||||
@ -318,39 +649,64 @@ the `file.diagnostics` field, filling it with diagnostics."
|
|||||||
;; exception: (/ a b) only needs 1 argument
|
;; exception: (/ a b) only needs 1 argument
|
||||||
min-params (if (= result (docs.get-builtin server :-)) 1
|
min-params (if (= result (docs.get-builtin server :-)) 1
|
||||||
(= result (docs.get-builtin server :/)) 1
|
(= result (docs.get-builtin server :/)) 1
|
||||||
|
;; TODO Fennel 1.5.4+ has `fn`'s arglist fixed
|
||||||
;; exception: fn only needs one argument
|
;; exception: fn only needs one argument
|
||||||
;; TODO Fennel 1.5.4+ has `fn`'s arglist fixed.
|
|
||||||
(= result (docs.get-builtin server :fn)) 1
|
(= result (docs.get-builtin server :fn)) 1
|
||||||
(or min-params 0))
|
(or min-params 0))
|
||||||
;; exception: (table.insert table item) can take a third argument
|
;; exception: (table.insert table item) can take a third argument
|
||||||
max-params (if (= result (. (docs.get-global server :table) :fields :insert)) 3 (length signature))]
|
max-params (if (= result (. (docs.get-global server :table) :fields :insert))
|
||||||
(if (and (< number-of-args min-params) (not passes-extra-args))
|
3
|
||||||
|
(length signature))]
|
||||||
|
(if (and (< number-of-args min-params)
|
||||||
|
(not passes-extra-args))
|
||||||
{:range (message.ast->range server file ast)
|
{:range (message.ast->range server file ast)
|
||||||
:message (.. "not enough args. my analysis of the signature says you need at least " min-params " arguments but I only see " number-of-args)
|
:message (.. "not enough args. my analysis of the signature says you need at least " min-params " arguments but I only see " number-of-args)
|
||||||
:severity message.severity.WARN}
|
:severity message.severity.WARN}
|
||||||
(and (< max-params number-of-args) (not infinite-params?))
|
(and (< max-params number-of-args)
|
||||||
|
(not infinite-params?))
|
||||||
{:range (message.ast->range server file ast)
|
{:range (message.ast->range server file ast)
|
||||||
:message (.. "too many args. my analysis of the signature says we ignore any arguments past " min-params " arguments but you've provided " number-of-args)
|
:message (.. "too many args. my analysis of the signature says we ignore any arguments past " max-params " arguments but you've provided " number-of-args)
|
||||||
:severity message.severity.WARN})))))})
|
:severity message.severity.WARN})))))})
|
||||||
|
|
||||||
(add-lint :duplicate-table-keys
|
(add-lint :duplicate-table-keys
|
||||||
{:type :other
|
{:what-it-does
|
||||||
|
"Detects when the same key appears multiple times in a table literal."
|
||||||
|
:why-care?
|
||||||
|
"Duplicate keys in a table are usually a mistake and the later value will
|
||||||
|
overwrite the earlier one, which can lead to bugs."
|
||||||
|
:example
|
||||||
|
"```fnl
|
||||||
|
{:name \"Alice\"
|
||||||
|
:age 25
|
||||||
|
:name \"Bob\"} ; \"Alice\" gets overwritten by \"Bob\"
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use:
|
||||||
|
```fnl
|
||||||
|
{:name \"Bob\"
|
||||||
|
:age 25}
|
||||||
|
```"
|
||||||
|
:since "0.2.0"
|
||||||
|
:type :other
|
||||||
:impl (fn [server file]
|
:impl (fn [server file]
|
||||||
(let [seen []]
|
(let [seen []]
|
||||||
(each [ast (pairs file.lexical)]
|
(each [ast (pairs file.lexical)]
|
||||||
(when (table? ast)
|
(when (table? ast)
|
||||||
(case (getmetatable ast)
|
(case (getmetatable ast)
|
||||||
{: keys} (let [dkey (accumulate [_ 1 i v (ipairs keys) &until (. seen v)]
|
{: keys}
|
||||||
(do (set (. seen v) i)
|
(let [dkey (accumulate [_ 1
|
||||||
(+ i 1)))]
|
i v (ipairs keys)
|
||||||
(when (. keys dkey)
|
&until (. seen v)]
|
||||||
(coroutine.yield
|
(do (set (. seen v) i)
|
||||||
{:code :duplicate-table-keys
|
(+ i 1)))]
|
||||||
:range (message.ast->range server file ast)
|
(when (. keys dkey)
|
||||||
:message (.. "key " (tostring (. keys dkey)) " appears more than once")
|
(coroutine.yield
|
||||||
:severity message.severity.WARN}))
|
{:code :duplicate-table-keys
|
||||||
(each [k (pairs seen)]
|
:range (message.ast->range server file ast)
|
||||||
(set (. seen k) nil))))))))})
|
:message (.. "key " (tostring (. keys dkey)) " appears more than once")
|
||||||
|
:severity message.severity.WARN}))
|
||||||
|
(each [k (pairs seen)]
|
||||||
|
(set (. seen k) nil))))))))})
|
||||||
|
|
||||||
(local lint-mt {:__tojson (fn [{: self} state] (dkjson.encode self state))
|
(local lint-mt {:__tojson (fn [{: self} state] (dkjson.encode self state))
|
||||||
:__index #(. $1 :self $2)})
|
:__index #(. $1 :self $2)})
|
||||||
|
|||||||
46
tools/extract-lint-docs.fnl
Normal file
46
tools/extract-lint-docs.fnl
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"Script to generate docs/lints.md from the embedded documentation in lint.fnl"
|
||||||
|
|
||||||
|
(local lint (require :fennel-ls.lint))
|
||||||
|
|
||||||
|
(fn strip-indentation [text]
|
||||||
|
"Remove common leading whitespace from multiline strings"
|
||||||
|
(let [lines (icollect [line (text:gmatch "[^\n]*")]
|
||||||
|
line)
|
||||||
|
;; Find minimum indentation from lines 2+ (ignoring empty lines)
|
||||||
|
min-indent (accumulate [min-spaces math.huge i line (ipairs lines)]
|
||||||
|
(if (or (= i 1) (line:find "^%s*$"))
|
||||||
|
min-spaces ; skip first line and empty lines
|
||||||
|
(math.min min-spaces (- (line:find "[^%s]") 1))))]
|
||||||
|
;; Only strip if we found indentation
|
||||||
|
(if (and (not= min-indent math.huge) (< 0 min-indent))
|
||||||
|
(table.concat
|
||||||
|
(icollect [i line (ipairs lines)]
|
||||||
|
(if (or (= i 1) (line:find "^%s*$"))
|
||||||
|
line
|
||||||
|
(line:sub (+ min-indent 1))))
|
||||||
|
"\n")
|
||||||
|
text)))
|
||||||
|
|
||||||
|
(fn main []
|
||||||
|
"Convert lint info to markdown format"
|
||||||
|
(each [_ lint-info (ipairs lint.list)]
|
||||||
|
(print (.. "# " lint-info.name
|
||||||
|
(if lint-info.disabled " (off by default)" "")))
|
||||||
|
(when lint-info.what-it-does
|
||||||
|
(print "## What it does")
|
||||||
|
(print (strip-indentation lint-info.what-it-does))
|
||||||
|
(print ""))
|
||||||
|
(when lint-info.why-care?
|
||||||
|
(print "## Why is this bad?")
|
||||||
|
(print (strip-indentation lint-info.why-care?))
|
||||||
|
(print ""))
|
||||||
|
(when lint-info.example
|
||||||
|
(print "## Example")
|
||||||
|
(print (strip-indentation lint-info.example))
|
||||||
|
(print))
|
||||||
|
(when lint-info.limitations
|
||||||
|
(print "## Known limitations")
|
||||||
|
(print (strip-indentation lint-info.limitations))
|
||||||
|
(print))))
|
||||||
|
|
||||||
|
(main)
|
||||||
Loading…
Reference in New Issue
Block a user