1;; An example of some possible linters using Fennel's --plugin option.
2
3;; The first two linters here can only function on static module
4;; use. For instance, this code can be checked because they use static
5;; field access on a local directly bound to a require call:
6
7;; (local m (require :mymodule))
8;; (print m.field) ; fails if mymodule lacks a :field field
9;; (print (m.function 1 2 3)) ; fails unless mymodule.function takes 3 args
10
11;; However, these cannot:
12
13;; (local m (do (require :mymodule)) ; m is not directly bound
14;; (print (. m field)) ; not a static field reference
15;; (let [f m.function]
16;;   (print (f 1 2 3)) ; intermediate local, not a static field call on m
17
18;; Still, pretty neat, huh?
19
20;; This file is provided as an example and is not part of Fennel's public API.
21
22(fn save-require-meta [from to scope]
23  "When destructuring, save module name if local is bound to a `require' call.
24Doesn't do any linting on its own; just saves the data for other linters."
25  (when (and (sym? to) (not (multi-sym? to)) (list? from)
26             (sym? (. from 1)) (= :require (tostring (. from 1)))
27             (= :string (type (. from 2))))
28    (let [meta (. scope.symmeta (tostring to))]
29      (set meta.required (tostring (. from 2))))))
30
31(fn check-module-fields [symbol scope]
32  "When referring to a field in a local that's a module, make sure it exists."
33  (let [[module-local field] (or (multi-sym? symbol) [])
34        module-name (-?> scope.symmeta (. (tostring module-local)) (. :required))
35        module (and module-name (require module-name))]
36    (assert-compile (or (= module nil) (not= (. module field) nil))
37                    (string.format "Missing field %s in module %s"
38                                   (or field :?) (or module-name :?)) symbol)))
39
40(fn arity-check? [module module-name]
41  (or (-?> module getmetatable (. :arity-check?))
42      (pcall debug.getlocal #nil 1) ; PUC 5.1 can't use debug.getlocal for this
43      ;; I don't love this method of configuration but it gets the job done.
44      (match (and module-name os os.getenv (os.getenv "FENNEL_LINT_MODULES"))
45        module-pattern (module-name:find module-pattern))))
46
47(fn descend [target [part & parts]]
48  (if (= nil part) target
49      (= :table (type target)) (match (. target part)
50                                 new-target (descend new-target parts))
51      target))
52
53(fn min-arity [target last-required name]
54  (match (debug.getlocal target last-required)
55    localname (if (and (localname:match "^_3f") (< 0 last-required))
56                  (min-arity target (- last-required 1))
57                  last-required)
58    _ last-required))
59
60(fn arity-check-call [[f & args] scope]
61  "Perform static arity checks on static function calls in a module."
62  (let [last-arg (. args (length args))
63        arity (if (: (tostring f) :find ":") ; method
64                  (+ (length args) 1)
65                  (length args))
66        [f-local & parts] (or (multi-sym? f) [])
67        module-name (-?> scope.symmeta (. (tostring f-local)) (. :required))
68        module (and module-name (require module-name))
69        field (table.concat parts ".")
70        target (descend module parts)]
71    (when (and (arity-check? module module-name) _G.debug _G.debug.getinfo
72               module (not (varg? last-arg)) (not (list? last-arg)))
73      (assert-compile (= (type target) :function)
74                      (string.format "Missing function %s in module %s"
75                                     (or field :?) module-name) f)
76      (match (_G.debug.getinfo target)
77        {: nparams :what "Lua"}
78        (let [min (min-arity target nparams f)]
79          (assert-compile (<= min arity)
80                          (: "Called %s with %s arguments, expected at least %s"
81                             :format f arity min) f))))))
82
83(fn check-unused [ast scope]
84  (each [symname (pairs scope.symmeta)]
85    (assert-compile (or (. scope.symmeta symname :used) (symname:find "^_"))
86                    (string.format "unused local %s" (or symname :?)) ast)))
87
88{:destructure save-require-meta
89 :symbol-to-expression check-module-fields
90 :call arity-check-call
91 ;; Note that this will only check unused args inside functions and let blocks,
92 ;; not top-level locals of a chunk.
93 :fn check-unused
94 :do check-unused
95 :chunk check-unused
96 :name "fennel/linter"
97 :versions ["1.0.0"]}
98