(local faith (require :faith)) (local {: view} (require :fennel)) (local {: create-client} (require :test.utils)) (fn find [diagnostics e] "returns the index of the diagnostic that matches `e`" (accumulate [result nil i d (ipairs diagnostics) &until result] (if (and (or (= e.message nil) (if (= (type e.message) "function") (e.message d.message) (= e.message d.message))) (or (= e.code nil) (= e.code d.code)) (or (= e.range nil) (and (= e.range.start.line d.range.start.line) (= e.range.start.character d.range.start.character) (= e.range.end.line d.range.end.line) (= e.range.end.character d.range.end.character)))) i))) (fn check [file-contents expected unexpected ?opts] (let [{: uri : client} (create-client file-contents nil ?opts) [{:result {:items diagnostics}}] (client:diagnostic uri)] (each [_ e (ipairs unexpected)] (let [i (find diagnostics e)] (faith.= nil i (.. "Lint matching " (view e) "\n" "from: " (view file-contents) "\n" (view (. diagnostics i) {:escape-newlines? true}))))) (each [_ e (ipairs expected)] (let [i (find diagnostics e)] (faith.is i (.. "No lint matching " (view e) "\n" "from: " (view file-contents) "\n" (view diagnostics {:empty-as-sequence? true :escape-newlines? true}))) (table.remove diagnostics i))))) (fn test-compile-error [] (check "(do do)" [{:message "tried to reference a special form without calling it" :range {:start {:character 4 :line 0} :end {:character 6 :line 0}}}] []) nil) (fn test-parse-error [] (check "(do (print :hello(]" [{:message "expected whitespace before opening delimiter (" :range {:start {:character 17 :line 0} :end {:character 17 :line 0}}}] []) nil) (fn test-macro-error [] (check "(match)" [{:range {:start {:character 0 :line 0} :end {:character 7 :line 0}}}] []) nil) (fn test-multiple-errors [] ;; compiler recovery for every fennel.friend error (check "(let [x.y.z 10] (print +))" [{:message "unexpected multi symbol x.y.z"} {:message "tried to reference a special form without calling it"}] []) ;; use of global .* is aliased by a local (check "(let [+ 10] (print +))" [{:message "local + was overshadowed by a special form or macro"} {:message "tried to reference a special form without calling it"}] []) (check "(let [x 10] (set x 20) (print x +))" [{:message "expected var x"} {:message "tried to reference a special form without calling it"}] []) ;; expected macros to be table ;; expected each macro to be a function ;; macro tried to bind .* without gensym (check "(do unknown-global (print +))" [{:message "unknown identifier: unknown-global"} {:message "tried to reference a special form without calling it"}] []) (check "(let [x ()] (print x +))" [{:message "expected a function, macro, or special to call"} {:message "tried to reference a special form without calling it"}] []) (check "(let [x (3)] (print x +))" [{:message "cannot call literal value 3"} {:message "tried to reference a special form without calling it"}] []) (check "(let [x (fn [] ...)] (print x +))" [{:message "unexpected vararg"} {:message "tried to reference a special form without calling it"}] []) (check "(let [x #...] (print x +))" [{:message "use $... in hashfn"} {:message "tried to reference a special form without calling it"}] []) (check "(let [x unknown-global" [{:message "unknown identifier: unknown-global"} {:message "expected body expression"} {:message "expected closing delimiters )]"}] []) ;; recovers from mismatched let (check "(let [x] (print x +))" [{:message "expected even number of name/value bindings"} {:message "tried to reference a special form without calling it"}] []) ;; recovers from missing condition (if) (check "(let [x (if)] (print x +))" [{:message "expected condition and body"} {:message "tried to reference a special form without calling it"}] []) ;; TODO: recovers from missing condition (when) ;; (check "(let [x (when)] ;; (print x +))" ;; [{:message #($:find ".*macros.fnl:%d+: expected body")} ;; {:message "tried to reference a special form without calling it"}] []) ;; TODO: recovers from missing body (when) ;; (check "(let [x (when (< (+ 9 10) 21))] ;; (print x +))" ;; [;; {:message #($:find ".*macros.fnl:%d+: expected body")} ;; {:message "tried to reference a special form without calling it"}] []) (check "(let [x {:mismatched :curly :braces}] (print x +))" [{:message "expected even number of values in table literal"} {:message "tried to reference a special form without calling it"}] []) nil) (fn test-lua-versions [] (check "(local t (unpack []))" [] [{:message "unknown identifier: unpack"}] {:lua-version "lua5.1"}) (check "(local t (unpack []))" [{:message "unknown identifier: unpack"}] [] {:lua-version "lua5.2"}) (check "(local t (unpack []))" [{:message "unknown identifier: unpack"}] [] {:lua-version "lua52"}) ;; intersection should have only the things present in every version (check "(print (unpack [_VERSION]))" [{:message "unknown identifier: unpack"}] [{:message "unknown identifier: _VERSION"}] {:lua-version "intersection"}) (check "(warn (unpack [_VERSION]))" [] [{:message "unknown identifier: unpack"} {:message "unknown identifier: warn"}] {:lua-version "union"}) ;; can reference table.unpack in an or (check "(print (or table.unpack _G.unpack))" [] [{:message "unknown field: table.unpack"}] {:lua-version "intersection"}) ;; deprecated functions still work (check "(print (math.atan2 (tonumber (io.read))))" [] [{:message "unknown field: math.atan2"}] {:lua-version "lua5.3"})) (fn test-macro-environment [] (check {:main.fnl "(macro not-any [subject & vals] \"Returns `true` if `subject` is not equal to any of the remaining values.\" (let [conds# (icollect [_ v# (ipairs vals)] `(not= ,subject ,v#))] `(and ,(unpack conds#))))"} [] [{}]) (check {:main.fnl ";; fennel-ls: macro-file (fn not-any [subject & vals] \"Returns `true` if `subject` is not equal to any of the remaining values.\" (let [conds# (icollect [_ v# (ipairs vals)] `(not= ,subject ,v#))] `(and ,(unpack conds#)))) {: not-any}"} [] [{}]) nil) (fn test-warnings [] (check "(print \"hello\"table)" [{:message "expected whitespace before token"}] [])) (fn test-after-edit [] (let [{: uri : client : locations} (create-client "(fn abc [] (+ a====)) {: abc}" ;; wants push notifications {:capabilities {:textDocument {:publishDiagnostics true}}}) [{:params {: diagnostics}}] (client:change-text! uri [{:range (. locations 1 :range) :text " b"}])] (faith.= diagnostics [{:code "compiler-error" :message "unknown identifier: a" :range {:end {:character 15 :line 0} :start {:character 14 :line 0}} :severity 1} {:code "compiler-error" :message "unknown identifier: b" :range {:end {:character 17 :line 0} :start {:character 16 :line 0}} :severity 1}]) nil)) {: test-lua-versions : test-macro-environment : test-compile-error : test-parse-error : test-macro-error : test-multiple-errors : test-warnings : test-after-edit}