New "unknown module field" diagnostic, and the groundwork for global completions with documentation

This commit is contained in:
XeroOl 2023-06-05 19:49:42 -05:00
parent 20b9094592
commit 8001c91120
8 changed files with 112 additions and 32 deletions

View File

@ -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)

View File

@ -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}

1
src/fennel-ls/docs.fnl Normal file
View File

@ -0,0 +1 @@
{:lua54 (require :fennel-ls.docs.lua54)}

View File

@ -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)] <body>)
```
will iterate over all keyvalue 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 {}}

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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)