diff --git a/changelog.md b/changelog.md index b7bbd88..94182f1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,8 @@ # Changelog ### Features +* Add `--fix` command-line argument to automatically apply lint fixes +* Add `:legacy-multival` and `legacy-multival-case` lints, disabled by default * Code action "Expand macro" lets you see what a macro expands to * Macro expansion is shown when hovering over a macro * Added support for pull diagnostics, which should help with out of date lints diff --git a/docs/manual.md b/docs/manual.md index 1ec0906..cbc9ed7 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -131,3 +131,11 @@ fennel-ls --lint my-file.fnl f2.fnl # prints diagnostics for the files given This will analyze the given files and print out all compiler errors and lints, without launching a server. A successful exit code indicates no problems found. + +You can also automatically apply fixes from lints that have them: + +```sh +fennel-ls --fix my-file.fnl f2.fnl # applies fixes +my-file.fnl:3:6: warning: unnecessary unary + +Apply fix? [Y/n] Unwrap the expression +``` diff --git a/src/fennel-ls.fnl b/src/fennel-ls.fnl index df813f6..b36c450 100644 --- a/src/fennel-ls.fnl +++ b/src/fennel-ls.fnl @@ -1,17 +1,19 @@ (require :fennel) (local dispatch (require :fennel-ls.dispatch)) (local json-rpc (require :fennel-ls.json-rpc)) +(local lint (require :fennel-ls.lint)) +(local utils (require :fennel-ls.utils)) (local files (require :fennel-ls.files)) (local {: severity->string &as message} (require :fennel-ls.message)) -(fn print-diagnostic [filename msg range ?severity] +(fn print-diagnostic [filename {:message msg : range : severity}] (print (: "%s:%s:%s: %s: %s" :format filename ;; LSP line numbers are zero-indexed, but Emacs and Vim both use ;; 1-indexing for this. (if (= range message.unknown-range) "?" (+ range.start.line 1)) (if (= range message.unknown-range) "?" range.start.character) - (or (. severity->string ?severity) "?") + (or (. severity->string severity) "?") msg))) (fn initialize [server] @@ -26,11 +28,10 @@ {:method "window/showMessage" :params {: message}} (print "WARN:" message))))) -(λ lint [filenames] +(λ lint-files [filenames] "non-interactive mode that gets executed from CLI with --lint. runs lints on each file, then formats and prints them" - (let [lint (require :fennel-ls.lint) - server (doto {} initialize)] + (let [server (doto {} initialize)] (var should-err? false) (each [_ filename (ipairs filenames)] (let [uri (case filename @@ -38,12 +39,39 @@ _ (.. "file://" filename)) file (files.get-by-uri server uri)] (lint.add-lint-diagnostics server file) - (each [_ {: message : range : severity} (ipairs file.diagnostics)] + (each [_ diagnostic (ipairs file.diagnostics)] (set should-err? true) - (print-diagnostic filename message range severity)))) + (print-diagnostic filename diagnostic)))) (when should-err? (os.exit 1)))) +(λ apply-changes [server filename changes] + (let [contents (with-open [f (io.open filename)] (f:read :*a)) + new (utils.apply-edits contents changes server.position-encoding)] + (with-open [f (io.open filename :w)] + (f:write new)))) + +(λ confirm [msg] + (io.write msg) + (case (io.read) (where (or "" "y" "Y" "yes")) true)) + +(λ fix-files [filenames --yes] + (let [server (doto {} initialize)] + (each [_ filename (ipairs filenames)] + (let [uri (case filename + "-" :stdin + _ (.. "file://" filename)) + file (files.get-by-uri server uri)] + (lint.add-lint-diagnostics server file) + (case [(next file.diagnostics)] + [_ {: fix &as diagnostic}] + (let [{: title : changes} (fix) + query (: "Apply fix? [Y/n] %s " :format title)] + (print-diagnostic filename diagnostic) + (when (or --yes (confirm query)) + (apply-changes server filename changes) + (fix-files [filename] --yes)))))))) + (λ main-loop [in out] (local send (partial json-rpc.write out)) (local server {}) @@ -56,22 +84,26 @@ Run fennel-ls, the Fennel language server and linter. - --lint FILES : Run the linter on the provided files - a single dash (-) can be used to read from stdin - --server : Start the language server (stdio mode only) - optional, this is the default with no arguments + --lint FILES : Run the linter on the provided files + a single dash (-) can be used to read from stdin + --fix [-y] FILES : Run suggested fixes from linters on files + --server : Start the language server (stdio mode only) + optional, this is the default with no arguments - --help : Display this text - --version : Show version") + --help : Display this text + --version : Show version") (λ main [] (case arg (where (or ["-h"] ["--help"])) (print help) (where (or ["-v"] ["--version"])) (print version) ;; (where (or ["-l" & filenames] ["--lint" & filenames])) (lint filenames) ;; compile error in fennel <= 1.5.4 - (where [--lint & filenames] (or (= --lint "--lint") (= --lint "-l"))) (lint filenames) + (where [--lint & filenames] (or (= --lint "--lint") (= --lint "-l"))) (lint-files filenames) (where (or ["--server"] [nil])) (main-loop (io.input) (io.output)) + ["--fix" "-y" & filenames] (fix-files filenames true) + ["--fix" "--yes" & filenames] (fix-files filenames true) + ["--fix" & filenames] (fix-files filenames false) _args (do (io.stderr:write help) (io.stderr:write "\n") (os.exit 1)))) diff --git a/src/fennel-ls/utils.fnl b/src/fennel-ls/utils.fnl index 95e7d3b..9ac1e0e 100644 --- a/src/fennel-ls/utils.fnl +++ b/src/fennel-ls/utils.fnl @@ -139,9 +139,7 @@ These functions are all pure functions, which makes me happy." text))) (λ apply-edits [initial-text edits encoding] - "Takes a list of Language-Server-Protocol `TextEdit` or `AnnotatedTextEdit` and applies them to a piece of text. - -WARNING: this is only used in the test code, not in the real language server" + "Takes a list of Language-Server-Protocol `TextEdit` or `AnnotatedTextEdit` and applies them to a piece of text." (accumulate [contents initial-text _ edit (ipairs edits)] diff --git a/test/cli.fnl b/test/cli.fnl index 035a48a..b70911d 100644 --- a/test/cli.fnl +++ b/test/cli.fnl @@ -1,19 +1,35 @@ (local faith (require :faith)) -;; TODO refactor fennel-ls.check to avoid running end-to-end -;; (but keep/write at least some end-to-end tests) -(fn test-lint [] - (let [input-file-name (os.tmpname)] - (doto (io.open input-file-name :w) - (: :write "(local x 1)") +;; alas, no io.popen2 +(macro with-temp-form [[filename form + out cmd] & body] + `(let [,filename (os.tmpname)] + (doto (assert (io.open ,filename :w)) + (: :write ,(view form)) (: :close)) - (let [output-file (io.popen (.. "./fennel-ls --lint " - input-file-name) - :r) - contents (output-file:read :*a)] - (os.remove input-file-name) - (faith.= (.. input-file-name ":1:7: warning: unused definition: x\n") - contents)) + (let [pipe# (io.popen (.. ,cmd " " ,filename)) + ,out (pipe#:read :*a)] + ,body + (os.remove ,filename)) nil)) -{: test-lint} +(fn test-lint [] + (with-temp-form [f (local x 1) + out "./fennel-ls --lint"] + (faith.= (.. f ":1:7: warning: unused definition: x\n") out))) + +(fn test-fix [] + (with-temp-form [f (local x 1) + out "./fennel-ls --fix --yes"] + (faith.= "(local _x 1)" (with-open [file (assert (io.open f))] + (file:read :*a)) out)) + (with-temp-form [f [(let [(a b) (values 1 (+ 2))] (+ a b)) + [(do (print "done doing all the fun things we did!"))]] + out "./fennel-ls --fix --yes"] + (faith.= "[(let [[a b] [(values 1 2)]] (+ a b)) + [(print \"done doing all the fun things we did!\")]]" + (with-open [file (assert (io.open f))] + (file:read :*a)) out))) + +{: test-lint + : test-fix}