fix set rename issue ~xerool/fennel-ls#8

Now unused variables and var-not-set lints use a different system.

References are now of type:
{:symbol sym :target ast :ref-type (or :read :write :mutate)}
instead of ast.

unused var will warn if all the references are :write references.
This commit is contained in:
XeroOl 2024-01-24 22:18:18 -06:00
parent 01035980b0
commit 297f6c4fa9
6 changed files with 106 additions and 48 deletions

View File

@ -3,11 +3,21 @@ This file is responsible for the low level tasks of analysis. Its main job
is to recieve a file object and run all of the basic analysis that will be used is to recieve a file object and run all of the basic analysis that will be used
later by fennel-ls.language to answer requests from the client." later by fennel-ls.language to answer requests from the client."
(local {: sym? : list? : sequence? : table? : sym &as fennel} (require :fennel)) (local {: sym? : list? : sequence? : table? : sym : view &as fennel} (require :fennel))
(local message (require :fennel-ls.message)) (local message (require :fennel-ls.message))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local searcher (require :fennel-ls.searcher)) (local searcher (require :fennel-ls.searcher))
(fn scope? [candidate]
;; just checking a couple of the fields
(and
(= (type candidate) :table)
(= (type candidate.includes) :table)
(= (type candidate.macros) :table)
(= (type candidate.manglings) :table)
(= (type candidate.specials) :table)
(= (type candidate.gensyms) :table)))
;; words surrounded by - are symbols, ;; words surrounded by - are symbols,
;; because fennel doesn't allow 'require in a runtime file ;; because fennel doesn't allow 'require in a runtime file
(local -require- (sym :require)) (local -require- (sym :require))
@ -64,24 +74,34 @@ later by fennel-ls.language to answer requests from the client."
require-calls {}] ; the keys are all the calls that start with `require require-calls {}] ; the keys are all the calls that start with `require
(λ find-definition [name ?scope] (λ find-definition [name ?scope]
(when ?scope (if ?scope
(or (. definitions-by-scope ?scope name) (or (. definitions-by-scope ?scope name)
(find-definition name ?scope.parent)))) (find-definition name ?scope.parent))))
(λ reference [ast scope] (λ reference [symbol scope ref-type]
;; Add a reference to the references (assert (or (= ref-type :read) (= ref-type :write) (= ref-type :mutate)) "wrong ref-type")
(assert (sym? ast)) (assert (sym? symbol) :not-a-symbol)
(assert (scope? scope) :not-a-scope)
;; find reference ;; find reference
(let [name (string.match (tostring ast) "[^%.:]+")] (let [name (string.match (tostring symbol) "[^%.:]+")]
(case (find-definition (tostring name) scope) (case (find-definition (tostring name) scope)
target target
(do (if (. references symbol)
(tset references ast target) (do ;; already exists
(table.insert target.referenced-by ast))))) (assert (= symbol (. references symbol :symbol)) (.. "the symbol should always be the same")))
;; (assert (= target (. references symbol :target)) (.. "different targets: " (view target) (view (. references symbol :target)))))
;; (print "old and new" ref-type (. references symbol :ref-type)))
(let [ref {: symbol : target : ref-type}]
(tset references symbol ref)
(table.insert target.referenced-by ref))))))
(λ symbol-to-expression [ast scope ?reference?] (λ symbol-to-expression [ast scope ?reference?]
(when (or ?reference? (fennel.multi-sym? ast)) (assert (sym? ast) "symbols only")
(reference ast scope))) (reference ast scope (if ?reference?
:read
(not (multisym? ast))
:write
:mutate)))
(λ define [?definition binding scope ?opts] (λ define [?definition binding scope ?opts]
;; Add a definition to the definitions ;; Add a definition to the definitions
@ -124,22 +144,19 @@ later by fennel-ls.language to answer requests from the client."
(recurse binding [])) (recurse binding []))
(λ mutate [_?definition binding scope] (λ mutate [_?definition binding scope]
;; for now, mutating a field counts as a reference I guess
(λ recurse [binding keys] (λ recurse [binding keys]
(if (sym? binding) (if (sym? binding)
(let [ ;; (let [;; future work may need to care about mutations
;; ;; future work may need to care about mutations ;; _mutation
;; _mutation ;; {: binding
;; {: binding ;; :new-definition ?definition
;; :new-definition ?definition ;; :keys (if (< 0 (length keys))
;; :keys (if (< 0 (length keys)) ;; (fcollect [i 1 (length keys)]
;; (fcollect [i 1 (length keys)] ;; (. keys i)))}]
;; (. keys i)))} (when (not (multisym? binding))
name (string.match (tostring binding) "[^%.:]+")] (reference binding scope :write)
(case (find-definition (tostring name) scope) (if (. references binding)
target (if (multisym? binding) (tset (. references binding :target) :var-set true)))
(table.insert target.referenced-by binding)
(set target.var-set true))))
(= :table (type binding)) (= :table (type binding))
(each [k v (iter binding)] (each [k v (iter binding)]
(table.insert keys k) (table.insert keys k)
@ -147,7 +164,7 @@ later by fennel-ls.language to answer requests from the client."
(table.remove keys)))) (table.remove keys))))
(recurse binding [])) (recurse binding []))
(λ destructure [to from scope {:declaration ?declaration? &as opts}] (λ destructure [to from scope {:declaration ?declaration? : symtype &as opts}]
;; I really don't understand symtype ;; I really don't understand symtype
;; I think I need an explanation ;; I think I need an explanation
(if ?declaration? (if ?declaration?
@ -164,9 +181,9 @@ later by fennel-ls.language to answer requests from the client."
(set target.fields (or target.fields {})) (set target.fields (or target.fields {}))
(tset target.fields field (tset target.fields field
{:binding multisym {:binding multisym
:definition ast :definition ast}))))
;; referenced-by inherits from all other symbols ;; ;; referenced-by inherits from all other symbols
:referenced-by (or (?. definitions multisym :referenced-by) [])})))) ;; :referenced-by (or (?. definitions multisym :referenced-by) [])}))))
(λ define-function-name [ast scope] (λ define-function-name [ast scope]
;; add a function definition to the definitions ;; add a function definition to the definitions
@ -221,7 +238,7 @@ later by fennel-ls.language to answer requests from the client."
(tset require-calls ast true) (tset require-calls ast true)
;; fennel expands multisym calls into the `:` special, so we need to reference the symbol while we still can ;; fennel expands multisym calls into the `:` special, so we need to reference the symbol while we still can
(where [sym] (multisym? sym) (: (tostring sym) :find ":")) (where [sym] (multisym? sym) (: (tostring sym) :find ":"))
(reference sym scope))) (reference sym scope :read)))
(λ recoverable? [msg] (λ recoverable? [msg]
(or (= 1 (msg:find "unknown identifier")) (or (= 1 (msg:find "unknown identifier"))
@ -295,6 +312,8 @@ later by fennel-ls.language to answer requests from the client."
(true ?item1 ?item2) (values ?item1 ?item2) (true ?item1 ?item2) (values ?item1 ?item2)
(where (or (nil err) (false err)) (not (err:find "^[^\n]-__NOT_AN_ERROR\n"))) (where (or (nil err) (false err)) (not (err:find "^[^\n]-__NOT_AN_ERROR\n")))
(if (os.getenv :TESTING) (if (os.getenv :TESTING)
(print (.. "\nYou have crashed fennel-ls (or the fennel " component ") with the following message\n:" err
"\n\n^^^ the error message above here is the root problem\n\n"))
(error (.. "\nYou have crashed fennel-ls (or the fennel " component ") with the following message\n:" err (error (.. "\nYou have crashed fennel-ls (or the fennel " component ") with the following message\n:" err
"\n\n^^^ the error message above here is the root problem\n\n")) "\n\n^^^ the error message above here is the root problem\n\n"))
(table.insert diagnostics (table.insert diagnostics

View File

@ -10,8 +10,12 @@ Goes through a file and mutates the `file.diagnostics` field, filling it with di
(λ unused-definition [self file] (λ unused-definition [self file]
"local variable that is defined but not used" "local variable that is defined but not used"
(icollect [symbol definition (pairs file.definitions) &into file.diagnostics] (icollect [symbol definition (pairs file.definitions) &into file.diagnostics]
(if (and (= 0 (length definition.referenced-by)) (if (and (not= "_" (: (tostring symbol) :sub 1 1))
(not= "_" (: (tostring symbol) :sub 1 1))) (not (accumulate [reference false
_ ref (ipairs definition.referenced-by)
&until reference]
(or (= ref.ref-type :read)
(= ref.ref-type :mutate)))))
{:range (message.ast->range self file symbol) {:range (message.ast->range self file symbol)
:message (.. "unused definition: " (tostring symbol)) :message (.. "unused definition: " (tostring symbol))
:severity message.severity.WARN :severity message.severity.WARN
@ -77,9 +81,9 @@ Goes through a file and mutates the `file.diagnostics` field, filling it with di
(icollect [symbol definition (pairs file.definitions) &into file.diagnostics] (icollect [symbol definition (pairs file.definitions) &into file.diagnostics]
(if (and definition.var? (not definition.var-set)) (if (and definition.var? (not definition.var-set))
{:range (message.ast->range self file symbol) {:range (message.ast->range self file symbol)
:message (.. "var is never set: " (tostring symbol)) :message (.. "var is never set: " (tostring symbol) " Consider using (local) instead of (var)")
:severity message.severity.WARN :severity message.severity.WARN
:code 301 :code 305
:codeDescription "var-never-set"}))) :codeDescription "var-never-set"})))
(λ check [self file] (λ check [self file]

View File

@ -90,7 +90,7 @@ Every time the client sends a message, it gets handled by a function in the corr
symbol symbol
(language.find-nearest-definition self file symbol byte) (language.find-nearest-definition self file symbol byte)
(where (definition def-file) (not= definition.referenced-by nil)) (where (definition def-file) (not= definition.referenced-by nil))
(let [result (icollect [_ symbol (ipairs definition.referenced-by)] (let [result (icollect [_ {: symbol} (ipairs definition.referenced-by)]
(message.range-and-uri self def-file symbol))] (message.range-and-uri self def-file symbol))]
(if ?include-declaration? (if ?include-declaration?
(table.insert result (table.insert result
@ -188,7 +188,7 @@ Every time the client sends a message, it gets handled by a function in the corr
(language.find-nearest-definition self file symbol symbol.bytestart) (language.find-nearest-definition self file symbol symbol.bytestart)
;; TODO we are assuming that every reference is in the same file ;; TODO we are assuming that every reference is in the same file
(where (definition def-file) (not= definition.referenced-by nil)) (where (definition def-file) (not= definition.referenced-by nil))
(let [usages (icollect [_ symbol (ipairs definition.referenced-by) (let [usages (icollect [_ {: symbol} (ipairs definition.referenced-by)
&into [{:range (message.multisym->range self def-file definition.binding 1) &into [{:range (message.multisym->range self def-file definition.binding 1)
:newText new-name}]] :newText new-name}]]
(if (not (rawequal symbol definition.binding)) (if (not (rawequal symbol definition.binding))

View File

@ -2,7 +2,7 @@
The high level analysis system that does deep searches following The high level analysis system that does deep searches following
the data provided by compiler.fnl." the data provided by compiler.fnl."
(local {: sym? : list? : sequence? : varg? : sym} (require :fennel)) (local {: sym? : list? : sequence? : varg? : sym &as fennel} (require :fennel))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local state (require :fennel-ls.state)) (local state (require :fennel-ls.state))
@ -35,15 +35,16 @@ the data provided by compiler.fnl."
(stack-add-split! stack (utils.multi-sym-split symbol))) (stack-add-split! stack (utils.multi-sym-split symbol)))
(λ search-assignment [self file assignment stack opts] (λ search-assignment [self file assignment stack opts]
(let [{:binding _ (assert assignment.target (.. "WRONG TYPE" (fennel.traceback)))
:definition ?definition (let [{:target {:binding _
:keys ?keys :definition ?definition
:fields ?fields} assignment] :keys ?keys
:fields ?fields}} assignment]
(if (and (= 0 (length stack)) opts.stop-early?) (if (and (= 0 (length stack)) opts.stop-early?)
(values assignment file) ;; BASE CASE!! (values assignment.target file) ;; BASE CASE!!
;; search a virtual field from :fields ;; search a virtual field from :fields
(and (not= 0 (length stack)) (?. ?fields (. stack (length stack)))) (and (not= 0 (length stack)) (?. ?fields (. stack (length stack))))
(search-assignment self file (. ?fields (table.remove stack)) stack opts) (search-assignment self file {:target (. ?fields (table.remove stack))} stack opts)
(search-ast self file ?definition (stack-add-keys! stack ?keys) opts)))) (search-ast self file ?definition (stack-add-keys! stack ?keys) opts))))
(λ search-symbol [self file symbol stack opts] (λ search-symbol [self file symbol stack opts]

View File

@ -88,7 +88,7 @@
[{:params {: diagnostics}}] [{:params {: diagnostics}}]
(is (find [_ v (ipairs diagnostics)] (is (find [_ v (ipairs diagnostics)]
(match v (match v
{:message "var is never set: x" {:code 305
:range {:start {:character 5 :line 0} :range {:start {:character 5 :line 0}
:end {:character 6 :line 0}}} :end {:character 6 :line 0}}}
v)) v))
@ -112,7 +112,7 @@
(it "does not warn if a field is used" (it "does not warn if a field is used"
(let [self (create-client) (let [self (create-client)
responses (self:open-file! filename "(fn [a b] (set a.x 10) (fn b.f []))")] responses (self:open-file! filename "(fn [a b] (set a.x 10) (fn b.f []))")]
(assert (not (?. responses 1 :params :diagnostics 1))))) (assert (not (?. responses 1 :params :diagnostics 1)) (?. responses 1 :params :diagnostics 1 :message))))
(it "warns when using the : special when a multisym would do" (it "warns when using the : special when a multisym would do"
(let [self (create-client)] (let [self (create-client)]
@ -150,20 +150,42 @@
v))) v)))
_ (error "did not match")))) _ (error "did not match"))))
(it "warns if a var is written but not read" (it "warns 'unused' if a var is written but not read"
(let [self (create-client) (let [self (create-client)
responses (self:open-file! filename "(var x 1) (set x 2) (set [x] [3])")] responses (self:open-file! filename "(var x 1) (set x 2) (set [x] [3])")]
(match responses (match responses
[{:params {: diagnostics}}] [{:params {: diagnostics}}]
(is (find [_ v (ipairs diagnostics)] (is (find [_ v (ipairs diagnostics)]
(match v (match v
{:message "unused definition: x" {:code 301
:range {:start {:character 5 :line 0} :range {:start {:character 5 :line 0}
:end {:character 6 :line 0}}} :end {:character 6 :line 0}}}
v)) v))
"not found") "not found")
_ (error "did not match")))) _ (error "did not match"))))
(it "warns 'var-never-set' if a var is not written"
(let [self (create-client)
responses (self:open-file! filename "(var x 1) (print x)")]
(match responses
[{:params {: diagnostics}}]
(is (find [_ v (ipairs diagnostics)]
(match v
{:code 305
:range {:start {:character 5 :line 0}
:end {:character 6 :line 0}}}
v))
"not found")
_ (error "did not match"))))
(it "does not warn 'var-never-set' if a var is written"
(let [self (create-client)
responses (self:open-file! filename "(var x 1) (set x 2) (print x)")]
(match responses
[{:params {: diagnostics}}]
(is.equal 0 (length diagnostics) "this code has no problems")
_ (error "did not match"))))
(it "does not warn on ampersand in destructuring" (it "does not warn on ampersand in destructuring"
(let [self (create-client) (let [self (create-client)
responses (self:open-file! filename "(let [[x & y] [1 2 3]] (print x (. y 1) (. y 2)))")] responses (self:open-file! filename "(let [[x & y] [1 2 3]] (print x (. y 1) (. y 2)))")]

View File

@ -39,4 +39,16 @@
(it "renames a sym inside of lambda" (it "renames a sym inside of lambda"
(check-rename "(λ [foo] (print foo))" 0 6 :something (check-rename "(λ [foo] (print foo))" 0 6 :something
"(λ [something] (print something))"))) "(λ [something] (print something))"))
(it "renames a sym inside of set"
(check-rename "(var x 10)\n(set x 20)" 1 6 :something
"(var something 10)\n(set something 20)"))
(it "renames a sym inside of set 2"
(check-rename "(var x 10)\n(var m 0)\n(set (m x) (values 10 20))" 2 8 :something
"(var something 10)\n(var m 0)\n(set (m something) (values 10 20))"))
(it "renames a sym inside of macro that uses multiple times"
(check-rename "(var x 10)\n(doto x (set 20) (set 30))" 1 6 :something
"(var something 10)\n(doto something (set 20) (set 30))")))