From 8001c91120d3d90f33307e469d6de0b86fb16b35 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Mon, 5 Jun 2023 19:49:42 -0500 Subject: [PATCH] New "unknown module field" diagnostic, and the groundwork for global completions with documentation --- src/fennel-ls/compiler.fnl | 18 +------------- src/fennel-ls/diagnostics.fnl | 36 +++++++++++++++++++++++++++ src/fennel-ls/docs.fnl | 1 + src/fennel-ls/docs/lua54.fnl | 47 +++++++++++++++++++++++++++++++++++ src/fennel-ls/handlers.fnl | 20 +++++++++------ src/fennel-ls/language.fnl | 13 +++++++--- src/fennel-ls/state.fnl | 7 +++--- src/fennel-ls/utils.fnl | 2 +- 8 files changed, 112 insertions(+), 32 deletions(-) create mode 100644 src/fennel-ls/diagnostics.fnl create mode 100644 src/fennel-ls/docs.fnl create mode 100644 src/fennel-ls/docs/lua54.fnl diff --git a/src/fennel-ls/compiler.fnl b/src/fennel-ls/compiler.fnl index 237b5d5..fc31e81 100644 --- a/src/fennel-ls/compiler.fnl +++ b/src/fennel-ls/compiler.fnl @@ -234,7 +234,7 @@ later by fennel-ls.language to answer requests from the client." (error "__NOT_AN_ERROR"))) (local allowed-globals - (icollect [k v (pairs _G)] + (icollect [k _ (pairs _G)] k)) (table.insert allowed-globals :vim) @@ -273,10 +273,6 @@ later by fennel-ls.language to answer requests from the client." ; {:range (message.pos->range 0 0 0 0) ; :message (.. "unrecoverable compiler error: " err)}) - ;; analyze more things - ;; write things back to the file object - (local deep-references {}) - ; (each [sym target (pairs references)] ; (if ; (sym? target) @@ -284,18 +280,6 @@ later by fennel-ls.language to answer requests from the client." ; (= :table (type target)))) ; ;; base case??? - (each [sym definition (pairs definitions)] - (if (and self.configuration.checks.unused-definition - (= 0 (length definition.referenced-by)) - (not= "_" (: (tostring sym) :sub 1 1))) - (let [range (message.ast->range sym file)] - (table.insert diagnostics - {:range range - :message (.. "unused definition: " (tostring sym)) - :severity message.severity.WARN - :code 301 - :codeDescription "warning error"})))) - (set file.ast ast) (set file.scope scope) (set file.scopes scopes) diff --git a/src/fennel-ls/diagnostics.fnl b/src/fennel-ls/diagnostics.fnl new file mode 100644 index 0000000..268d44a --- /dev/null +++ b/src/fennel-ls/diagnostics.fnl @@ -0,0 +1,36 @@ +(local language (require :fennel-ls.language)) +(local message (require :fennel-ls.message)) +(local utils (require :fennel-ls.utils)) + +(λ unused-definition [self file] + "local variable that is defined but not used" + (icollect [symbol definition (pairs file.definitions) &into file.diagnostics] + (if (and (= 0 (length definition.referenced-by)) + (not= "_" (: (tostring symbol) :sub 1 1))) + {:range (message.ast->range symbol file) + :message (.. "unused definition: " (tostring symbol)) + :severity message.severity.WARN + :code 301 + :codeDescription "warning error"}))) + +(λ unknown-module-field [self file] + "any multisym whose definition can't be found through a (require) call" + (icollect [symbol (pairs file.references) &into file.diagnostics] + (if (. (utils.multi-sym-split symbol) 2) + (let [opts {} + item (language.search self file symbol [] opts)] + (if (and (not item) opts.searched-through-require) + {:range (message.ast->range symbol file) + :message (.. "unknown field " (tostring symbol)) + :severity message.severity.WARN + :code 302 + :codeDescription "field checking I guess"}))))) + +(λ check [self file] + "fill up the file.diagnostics table with linting things" + (if self.configuration.checks.unused-definition + (unused-definition self file)) + (if self.configuration.checks.unknown-module-field + (unknown-module-field self file))) + +{: check} diff --git a/src/fennel-ls/docs.fnl b/src/fennel-ls/docs.fnl new file mode 100644 index 0000000..6225645 --- /dev/null +++ b/src/fennel-ls/docs.fnl @@ -0,0 +1 @@ +{:lua54 (require :fennel-ls.docs.lua54)} diff --git a/src/fennel-ls/docs/lua54.fnl b/src/fennel-ls/docs/lua54.fnl new file mode 100644 index 0000000..663e3e8 --- /dev/null +++ b/src/fennel-ls/docs/lua54.fnl @@ -0,0 +1,47 @@ +(local {: sym} (require :fennel)) +{ + :_G {} + :_VERSION {} + :arg {} + :assert {} + :collectgarbage {} + :coroutine {} + :debug {} + :dofile {} + :error {} + :getmetatable {} + :io {} + :ipairs {} + :load {} + :loadfile {} + :math {} + :next {} + :os {} + :package {} + :pairs {:metadata {:fnl/arglist [:t] + :fnl/docstring "If t has a metamethod __pairs, calls it with t as argument and returns the first three results from the call. + +Otherwise, returns three values: the next function, the table t, and nil, so that the construction +```fnl +(each [k v (pairs t)] ) +``` +will iterate over all key–value pairs of table t. + +See function next for the caveats of modifying the table during its traversal."}} + :pcall {} + :print {} + :rawequal {} + :rawget {} + :rawlen {} + :rawset {} + :require {} + :select {} + :setmetatable {} + :string {} + :table {} + :tonumber {} + :tostring {} + :type {} + :utf8 {} + :warn {} + :xpcall {}} diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 2e06641..2a86a67 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -68,7 +68,7 @@ Every time the client sends a message, it gets handled by a function in the corr (λ requests.textDocument/definition [self send {: position :textDocument {: uri}}] (let [file (state.get-by-uri self uri) - byte (pos->byte file.text position.line position.character)] + byte (utils.pos->byte file.text position.line position.character)] (case-try (language.find-symbol file.ast byte) (symbol parents) ;; TODO unruin this match-try @@ -86,7 +86,7 @@ Every time the client sends a message, it gets handled by a function in the corr :textDocument {: uri} :context {:includeDeclaration ?include-declaration?}}] (let [file (state.get-by-uri self uri) - byte (pos->byte file.text line character)] + byte (utils.pos->byte file.text line character)] (case-try (language.find-symbol file.ast byte) symbol (if (. file.definitions symbol) @@ -107,7 +107,7 @@ Every time the client sends a message, it gets handled by a function in the corr (λ requests.textDocument/hover [self send {: position :textDocument {: uri}}] (let [file (state.get-by-uri self uri) - byte (pos->byte file.text position.line position.character)] + byte (utils.pos->byte file.text position.line position.character)] (case-try (language.find-symbol file.ast byte) symbol (language.search-main self file symbol {} byte) result {:contents (formatter.hover-format result) @@ -152,7 +152,7 @@ Every time the client sends a message, it gets handled by a function in the corr (collect-scope scope :macros #{:label $ :kind kinds.Keyword} result) (collect-scope scope :specials #(make-completion-item self file $ scope) result)) (icollect [_ k (ipairs file.allowed-globals) &into result] - {:label k :kind kinds.Variable}))) + (make-completion-item self file k scope)))) (λ field-completion [self file symbol split] (case (. file.references symbol) @@ -175,24 +175,28 @@ Every time the client sends a message, it gets handled by a function in the corr (λ requests.textDocument/completion [self send {: position :textDocument {: uri}}] (let [file (state.get-by-uri self uri) - byte (pos->byte file.text position.line position.character) + byte (utils.pos->byte file.text position.line position.character) (?symbol parents) (language.find-symbol file.ast byte)] (case (-?> ?symbol utils.multi-sym-split) (where (or nil [_ nil])) (scope-completion self file byte ?symbol parents) [_a _b &as split] (field-completion self file ?symbol split)))) + (λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}] (local file (state.get-by-uri self uri)) - (state.set-uri-contents self uri (apply-changes file.text contentChanges)) + (state.set-uri-contents self uri (utils.apply-changes file.text contentChanges)) + (diagnostics.check self file) (send (message.diagnostics file))) (λ notifications.textDocument/didOpen [self send {:textDocument {: languageId : text : uri}}] (local file (state.set-uri-contents self uri text)) - (set file.open? true) - (send (message.diagnostics file))) + (diagnostics.check self file) + (send (message.diagnostics file)) + (set file.open? true)) (λ notifications.textDocument/didClose [self send {:textDocument {: uri}}] (local file (state.get-by-uri self uri)) + (set file.open? false) ;; TODO only reload from disk if we didn't get a didSave, instead of always (state.flush-uri self uri)) diff --git a/src/fennel-ls/language.fnl b/src/fennel-ls/language.fnl index 771e507..7639d02 100644 --- a/src/fennel-ls/language.fnl +++ b/src/fennel-ls/language.fnl @@ -65,7 +65,7 @@ the data provided by compiler.fnl." (let [newfile (state.get-by-module self mod)] (when newfile (let [newitem (. newfile.ast (length newfile.ast))] - (search self newfile newitem stack opts)))) + (search self newfile newitem stack (doto opts (tset :searched-through-require true)))))) ;; A . form indexes into item 1 with the other items [-dot- & split] (search self file (. split 1) (stack-add-split! stack split) opts) @@ -141,14 +141,21 @@ Returns: (or (. file.definitions-by-scope ?scope name) (find-local-definition file name ?scope.parent)))) +(λ global-info [self name] + (. (require :fennel-ls.docs) + self.configuration.version + name)) + (λ search-name-and-scope [self file name scope ?opts] "find a definition just from the name of the item, and the scope it is in" (assert (= (type name) :string)) (let [stack (stack-add-multisym! [] name)] (case (. METADATA (. SPECIALS name)) metadata {:binding (sym name) : metadata} - _ (case (find-local-definition file name scope) - def (search self file def.definition (stack-add-keys! stack def.keys) (or ?opts {})))))) + _ (case (global-info self name) + global-item global-item + _ (case (find-local-definition file name scope) + def (search self file def.definition (stack-add-keys! stack def.keys) (or ?opts {}))))))) (λ past? [?ast byte] ;; check if a byte is past an ast object diff --git a/src/fennel-ls/state.fnl b/src/fennel-ls/state.fnl index c54a03d..251a363 100644 --- a/src/fennel-ls/state.fnl +++ b/src/fennel-ls/state.fnl @@ -83,7 +83,7 @@ object." (assert (= (type (. default 1)) (type setting))) setting) (= :table (type default)) - (collect [k v (pairs default)] + (collect [k _ (pairs default)] k (make-configuration-from-template (. default k) (?. ?user k) @@ -93,7 +93,9 @@ object." (local default-configuration {:fennel-path (option "./?.fnl;./?/init.fnl;src/?.fnl;src/?/init.fnl") :macro-path (option "./?.fnl;./?/init-macros.fnl;./?/init.fnl;src/?.fnl;src/?/init-macros.fnl;src/?/init.fnl") - :checks {:unused-definition (option true)}}) + :version (option "lua54") + :checks {:unused-definition (option true) + :unknown-module-field (option true)}}) (λ make-configuration [?c] (make-configuration-from-template default-configuration ?c)) @@ -107,7 +109,6 @@ object." (λ write-configuration [self ?configuration] (set self.configuration (make-configuration ?configuration))) - {: flush-uri : get-by-module : get-by-uri diff --git a/src/fennel-ls/utils.fnl b/src/fennel-ls/utils.fnl index 25bc92a..636f945 100644 --- a/src/fennel-ls/utils.fnl +++ b/src/fennel-ls/utils.fnl @@ -25,7 +25,7 @@ These functions are all pure functions, which makes me happy." (λ pos->byte [str line col] "convert a 0-indexed line and column into a 1-indexed byte. Doesn't yet handle UTF8 UTF16 magic from the protocol" (var sofar 1) - (for [i 1 line :until (not sofar)] + (for [_ 1 line :until (not sofar)] (set sofar (next-line str sofar))) (if sofar (+ sofar col)