diff --git a/README.md b/README.md index 3607a0a..a572914 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ get a warning instead. Some of the checks send what a particular client sends, the way it sends it, e.g. `cider.connect` sends the requests CIDER sends while connecting. -That way the report tells you directly whether CIDER, Calva, Conjure and -vim-fireplace will work with your server. +That way the report tells you directly whether CIDER, Calva, Conjure, +vim-fireplace, REPLy and neorepl will work with your server. ## Checking Clients @@ -128,8 +128,8 @@ the replies). `-like jank` turns on all of jank's scenarios at once, and proof is still in its early days. Right now it covers the core of the protocol (`describe`, unknown ops, `eval`, sessions, `stdin` and the wire format), -what CIDER, Calva, Conjure and vim-fireplace send while connecting and -evaluating code, the requests +what CIDER, Calva, Conjure, vim-fireplace, REPLy and neorepl send while +connecting and evaluating code, the requests clients send and the server differences clients have to deal with, and `proof list` will show you all the checks. diff --git a/doc/design.md b/doc/design.md index 4ac5b70..5233230 100644 --- a/doc/design.md +++ b/doc/design.md @@ -132,8 +132,8 @@ it. Each situation becomes a check that sends those requests, the way the client sends them, and fails at the first reply the client couldn't use. Replies that only bother the client (e.g. an error the client just shows to the user) get a note. Right now there are profiles for CIDER, -Calva, Conjure and vim-fireplace, built from what they send with their -default settings. +Calva, Conjure, vim-fireplace, REPLy and neorepl, built from what they +send with their default settings. A new profile starts with a recording, as `proof proxy -record` saves what a client sent as a profile with a check for each connection. What diff --git a/doc/profiles.md b/doc/profiles.md index ed05d7b..7c3e941 100644 --- a/doc/profiles.md +++ b/doc/profiles.md @@ -121,7 +121,7 @@ snippet is missing, all the checks that need it are skipped. | Snippet | Options | What it should do | Checks that use it | |---|---|---|---| -| `value` | `code`, `value` | evaluate to `value` | `eval.value`, `eval.survives-error`, `eval.ns`, `eval.unknown-ns`, `session.ephemeral`, `session.unknown`, `session.closed`, `cider.eval`, `cider.repl`, `calva.eval`, `conjure.eval`, `fireplace.eval` | +| `value` | `code`, `value` | evaluate to `value` | `eval.value`, `eval.survives-error`, `eval.ns`, `eval.unknown-ns`, `session.ephemeral`, `session.unknown`, `session.closed`, `cider.eval`, `cider.repl`, `calva.eval`, `conjure.eval`, `fireplace.eval`, `neorepl.eval` | | `stdout` | `code`, `out` | print `out` to the standard output | `eval.stdout`, `eval.stdout-order` | | `stderr` | `code`, `err` | print `err` to the standard error | `eval.stderr` | | `throw` | `code` | raise an error | `eval.error-status`, `eval.error-report`, `eval.survives-error` | diff --git a/doc/usage.md b/doc/usage.md index ccde6be..b546e4f 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -89,16 +89,18 @@ eval see: nrepl/nrepl#147 https://github.com/nrepl/nrepl/issues/147 ``` -The checks in the `cider`, `calva`, `conjure` and `fireplace` groups send -what those clients send while connecting and evaluating code, with all -the extra fields they put in their requests. If one of them fails, the -client won't work properly with your server, and the report says what -the client does with the reply it got (e.g. "CIDER gives up -connecting"). The code they evaluate for the user is the `value` -snippet of your profile. Before evaluating code from a source file, -CIDER, Calva and Conjure evaluate an `ns` form for the file's namespace, -so those checks also need the `clojure` capability. vim-fireplace finds the classpath -with Java interop while connecting, so its checks need `java`. +The checks in the `cider`, `calva`, `conjure`, `fireplace`, `reply` and +`neorepl` groups send what those clients send while connecting and +evaluating code, with all the extra fields they put in their requests. +If one of them fails, the client won't work properly with your server, +and the report says what the client does with the reply it got (e.g. +"CIDER gives up connecting"). The code they evaluate for the user is the +`value` snippet of your profile. (REPLy sends nothing but the code and +the session, so `eval.value` covers it.) Before evaluating code from a +source file, CIDER, Calva and Conjure evaluate an `ns` form for the +file's namespace, so those checks also need the `clojure` capability. +vim-fireplace finds the classpath with Java interop while connecting, so +its checks need `java`. Each check gets one of the following verdicts: diff --git a/internal/checks/checks_test.go b/internal/checks/checks_test.go index de0de19..55a66e5 100644 --- a/internal/checks/checks_test.go +++ b/internal/checks/checks_test.go @@ -48,8 +48,9 @@ func TestWellBehavedServerPassesEverything(t *testing.T) { // other check and rule must still pass. func TestChecksCatchMisbehaviour(t *testing.T) { F, W, S := check.Failed, check.Warned, check.Skipped - // What every client goes through first. - connects := map[string]check.Verdict{"cider.connect": F, "calva.connect": F, "conjure.connect": F, "fireplace.connect": F} + // What every client but REPLy goes through first. + connects := map[string]check.Verdict{"cider.connect": F, "calva.connect": F, "conjure.connect": F, "fireplace.connect": F, + "neorepl.connect": F} cases := []struct { name string q quirks @@ -71,7 +72,8 @@ func TestChecksCatchMisbehaviour(t *testing.T) { {"no session-closed", quirks{noSessionClosed: true}, map[string]check.Verdict{"session.close": F}}, // Only clients put dicts and lists in their requests. {"flat fields only", quirks{flatFields: true}, map[string]check.Verdict{ - "cider.connect": F, "cider.eval": F, "cider.repl": F, "calva.connect": F, "calva.eval": F, "conjure.eval": F}}, + "cider.connect": F, "cider.eval": F, "cider.repl": F, "calva.connect": F, "calva.eval": F, "conjure.eval": F, + "neorepl.eval": F}}, {"any session accepted", quirks{acceptAnySession: true}, map[string]check.Verdict{"session.unknown": F, "session.closed": F}}, {"shared session state", quirks{sharedState: true}, map[string]check.Verdict{"session.isolated": F}}, {"sessions tied to sockets", quirks{socketSessions: true}, map[string]check.Verdict{"session.across-connections": W}}, @@ -82,6 +84,8 @@ func TestChecksCatchMisbehaviour(t *testing.T) { {"no eval-error", quirks{noEvalError: true}, map[string]check.Verdict{"eval.error-status": F}}, {"no ex", quirks{noEx: true}, map[string]check.Verdict{"eval.error-report": W}}, {"no no-code", quirks{noNoCode: true}, map[string]check.Verdict{"eval.no-code": W}}, + // REPLy evaluates nothing before its first prompt. + {"empty code never answered", quirks{emptyCodeHangs: true}, map[string]check.Verdict{"reply.connect": F}}, {"no ns", quirks{noNs: true}, map[string]check.Verdict{"eval.ns": W, "fireplace.connect": F}}, {"ns fallback", quirks{nsFallback: true}, map[string]check.Verdict{"eval.unknown-ns": F}}, {"two dones", quirks{twoDones: true}, map[string]check.Verdict{"wire.one-done": W}}, @@ -93,7 +97,8 @@ func TestChecksCatchMisbehaviour(t *testing.T) { "wire.id": W, "eval.stdout": F, "eval.stderr": F, "eval.error-report": W}}, {"integer value", quirks{intValue: true}, map[string]check.Verdict{ "wire.field-types": F, "eval.value": F, "eval.survives-error": F, "eval.multiple-forms": F, - "session.ephemeral": F, "session.persistent": F, "cider.eval": F, "cider.repl": F, "calva.eval": F, "conjure.eval": F, "fireplace.eval": F}}, + "session.ephemeral": F, "session.persistent": F, "cider.eval": F, "cider.repl": F, "calva.eval": F, "conjure.eval": F, "fireplace.eval": F, + "neorepl.eval": F}}, {"unsorted keys", quirks{unsortedKeys: true}, map[string]check.Verdict{"wire.canonical": W}}, {"invalid UTF-8", quirks{badUTF8: true}, map[string]check.Verdict{"wire.utf8": W, "eval.stdout": F}}, {"no stdin op", quirks{noStdinOp: true}, map[string]check.Verdict{ diff --git a/internal/checks/client_profiles_test.go b/internal/checks/client_profiles_test.go index 8743c38..07c4969 100644 --- a/internal/checks/client_profiles_test.go +++ b/internal/checks/client_profiles_test.go @@ -22,7 +22,8 @@ func TestClientProfiles(t *testing.T) { pinned := regexp.MustCompile(`^https://github\.com/[^/]+/[^/]+/blob/[0-9a-f]{40}/`) // The links in refs.go to the same clients. bases := map[string]string{"CIDER": ciderBase, "Calva": strings.TrimSuffix(calvaBase, "nrepl/index.ts"), - "Conjure": strings.TrimSuffix(conjureBase, "client/clojure/nrepl/server.fnl"), "vim-fireplace": fireplaceBase} + "Conjure": strings.TrimSuffix(conjureBase, "client/clojure/nrepl/server.fnl"), "vim-fireplace": fireplaceBase, + "REPLy": strings.TrimSuffix(replyBase, "eval_modes/nrepl.clj")} for _, p := range clientProfiles() { if p.Name == "" || !pinned.MatchString(p.Code) { t.Errorf("%q needs a name and links pinned to a commit, got %q", p.Name, p.Code) diff --git a/internal/checks/clients/neorepl.toml b/internal/checks/clients/neorepl.toml new file mode 100644 index 0000000..0467c3d --- /dev/null +++ b/internal/checks/clients/neorepl.toml @@ -0,0 +1,54 @@ +# What neorepl's REPL sends while it starts and when it evaluates code, +# with its default settings and without a terminal. In a terminal it also +# looks things up and completes names as the user types, but it carries +# on when that fails. The links point at the code that needs what the +# steps check. +name = "neorepl" +code = "https://github.com/nrepl/neorepl/blob/462a182c885ff56d65acfc73ad1fa92f02fc7181/src/neorepl/" + +[[checks]] +id = "neorepl.connect" +title = "neorepl can start its REPL" +why = "neorepl asks for describe, clones a session and asks for describe again, in that session, before it shows a prompt, so a server that gets any of it wrong can't be used from neorepl at all." + +# The ops tell neorepl what the server can do, e.g. look things up. +[[checks.steps]] +send = { op = "describe" } +dicts = [["ops"], ["versions"]] +why = "neorepl takes the server for one that isn't running, and says so or starts a server of its own for the project" +refs = ["nrepl.clj#L153-L168", "discovery.clj#L38-L45"] + +[[checks.steps]] +send = { op = "clone" } +new-session = "repl" +why = "neorepl can't start its REPL" +refs = ["main.clj#L735-L737", "nrepl.clj#L185-L188"] + +# For the namespace of the first prompt, which is user when the reply +# doesn't say. +[[checks.steps]] +send = { op = "describe", session = "$repl" } +why = "neorepl can't start its REPL" +refs = ["repl.clj#L194", "nrepl.clj#L199"] + +[[checks]] +id = "neorepl.eval" +title = "neorepl can evaluate code" +why = "neorepl sends what the user types with options for pretty printing the values, so a server that can't handle them leaves neorepl users without the results of their code." + +[[checks.steps]] +send = { op = "clone" } +new-session = "repl" +why = "neorepl can't start its REPL" +refs = ["main.clj#L735-L737", "nrepl.clj#L185-L188"] + +[[checks.steps]] +snippet = "value" +why = "users don't see the result of their code" +refs = ["repl.clj#L16-L24", "repl.clj#L270", "evaluation.clj#L183-L200"] + +[checks.steps.send] +op = "eval" +session = "$repl" +"nrepl.middleware.print/print" = "nrepl.util.print/pprint" +"nrepl.middleware.print/options" = { right-margin = 80 } diff --git a/internal/checks/clients/reply.toml b/internal/checks/clients/reply.toml new file mode 100644 index 0000000..29fbcee --- /dev/null +++ b/internal/checks/clients/reply.toml @@ -0,0 +1,50 @@ +# What REPLy 0.5.1, the version `lein repl` runs, sends while it starts. +# Leiningen only changes the help text in the startup code. REPLy +# evaluates the user's code with nothing but the code and its session, +# like eval.value, and files every reply that isn't output by its session, +# which wire.session-echo checks. The links point at the code that needs +# what the steps check. +name = "REPLy" +code = "https://github.com/trptcolin/reply/blob/2ee3e3a193e9df62d5dc965d25efd0521a289b70/src/clj/reply/" + +# REPLy sends its startup code to any server, so this check runs on +# servers for other languages too. +[[checks]] +id = "reply.connect" +title = "REPLy can start" +why = "REPLy clones a session for the REPL and one for completion, and waits for an eval in the completion session, its startup code and an empty eval before it shows a prompt, so a server that gets any of it wrong leaves REPLy without a REPL." + +[[checks.steps]] +send = { op = "clone" } +new-session = "repl" +why = "REPLy can't start" +refs = ["eval_modes/nrepl.clj#L203-L213"] + +[[checks.steps]] +send = { op = "clone" } +new-session = "completion" +why = "REPLy can't start" +refs = ["eval_modes/nrepl.clj#L203-L213"] + +# REPLy waits for the reply without a timeout. An error is fine, as REPLy +# then reads the value as nil, but a value read-string can't read stops +# it. +[[checks.steps]] +send = { op = "eval", session = "$completion", code = "(clojure.core/binding [clojure.core/*ns* (clojure.core/or (clojure.core/find-ns (clojure.core/symbol \"reply.eval-modes.nrepl\")) clojure.core/*ns*)] (set! *print-length* nil))" } +refs = ["eval_modes/nrepl.clj#L171-L182", "eval_modes/nrepl.clj#L228", "eval_modes/nrepl.clj#L59-L70"] + +# The startup code, which REPLy waits for too. Errors are fine here as well. +[[checks.steps]] +refs = ["eval_modes/nrepl.clj#L229-L236", "initialization.clj#L113-L164", "eval_modes/nrepl.clj#L59-L70"] + +[checks.steps.send] +op = "eval" +session = "$repl" +code = '''(do (do (do (clojure.core/println (clojure.core/str "REPL-y " "0.5.1" ", nREPL " (:version-string nrepl.core/version))) (clojure.core/println "Clojure" (clojure.core/clojure-version)) (clojure.core/println (java.lang.System/getProperty "java.vm.name") (java.lang.System/getProperty "java.runtime.version")) (clojure.core/use (quote [clojure.repl :only [source apropos dir]])) (clojure.core/when (clojure.core/ns-resolve (quote clojure.repl) (quote pst)) (clojure.core/refer (quote clojure.repl) :only (quote [pst doc find-doc]))) (clojure.core/use (quote [clojure.java.javadoc :only [javadoc]])) (clojure.core/use (quote [clojure.pprint :only [pp pprint]])) (clojure.core/create-ns (quote reply.exports)) (intern (quote reply.exports) (quote original-ns) *ns*) (clojure.core/in-ns (quote reply.exports)) (clojure.core/refer (quote clojure.core)) (defn intern-with-meta [ns sym value-var] (intern ns (with-meta sym (meta value-var)) (clojure.core/deref value-var))) (defn help "Prints a list of helpful commands." [] (println " Exit: Control+D or (exit) or (quit)") (println " Commands: (user/help)") (println " Docs: (doc function-name-here)") (println " (find-doc \"part-of-name-here\")") (println "Find by Name: (find-name \"part-of-name-here\")") (println " Source: (source function-name-here)") (println " Javadoc: (javadoc java-object-or-class-here)")) (intern-with-meta (quote user) (quote help) (var help)) (defn unresolve "Given a var, return a sequence of all symbols that resolve to the\n var from the current namespace *ns*." [var] (when-not (instance? clojure.lang.Var var) (throw (Exception. (format "unresolve: first arg must be Var")))) (let [home-ns (.ns var) sym-name-str (second (re-find #"/(.*)$" (str var)))] (sort-by (fn* [p1__2834#] (count (str p1__2834#))) (concat (->> (ns-map *ns*) (filter (fn [[k v]] (= var v))) (map first)) (list (symbol (str home-ns) sym-name-str)) (->> (ns-aliases *ns*) (filter (fn [[ns-alias ns]] (= ns home-ns))) (map first) (map (fn [ns-alias-symbol] (symbol (str ns-alias-symbol) sym-name-str)))))))) (defn apropos-better "Similar to clojure.repl/apropos, but provides enough context (in the form of\n namespaces where the vars live, when necessary) to actually use the results\n in a REPL.\n\n Given a regular expression or stringable thing, calculate a\n sequence of all symbols in all currently-loaded namespaces such that\n it matches the str-or-pattern, with at most one such symbol per Var.\n The sequence returned contains symbols that map to those Vars, and are\n the shortest symbols that map to the Var, when qualified with the\n namespace name or alias, if that qualification is necessary to name\n the Var. Note that it is possible the symbol returned does not match\n the str-or-pattern itself, e.g. if the symbol-to-var mapping was\n created with :rename.\n\n Searches through all non-Java symbols in the current namespace, but\n only public symbols of other namespaces." [str-or-pattern] (let [matches? (if (instance? java.util.regex.Pattern str-or-pattern) (fn* [p1__2838#] (re-find str-or-pattern (str p1__2838#))) (fn* [p1__2839#] (.contains (str p1__2839#) (str str-or-pattern))))] (sort (map (fn* [p1__2840#] (first ((ns-resolve (quote reply.exports) (quote unresolve)) p1__2840#))) (set (mapcat (fn [ns] (map second (filter (fn [[s v]] (matches? s)) (if (= ns *ns*) (concat (ns-interns ns) (ns-refers ns)) (ns-publics ns))))) (all-ns))))))) (intern-with-meta (quote user) (quote apropos-better) (var apropos-better)) (intern-with-meta (quote user) (quote find-name) (var apropos-better)) (try (clojure.core/require (quote [incomplete.core])) (catch java.lang.Exception e__2211__auto__ (try (clojure.core/eval (quote (do (ns incomplete.core "Code completion functionality extracted from nREPL.\n\n The functionality here is experimental and\n the API is subject to changes." {:author "Bozhidar Batsov"} (:require [clojure.main] [clojure.java.io :as io]) (:import [java.util.jar JarFile] [java.io File] [java.lang.reflect Field Member] [java.util.jar JarEntry] [java.util.concurrent ConcurrentHashMap])) (defn annotate-keyword [kw] {:candidate kw, :type :keyword}) (defn all-keywords [] (let [field (.getDeclaredField clojure.lang.Keyword "table")] (.setAccessible field true) (.keySet (.get field nil)))) (defn- resolve-namespace [sym ns] (get (ns-aliases ns) sym (find-ns sym))) (defn qualified-auto-resolved-keywords "Given a namespace alias, a prefix, and a namespace, return completion\n candidates for qualified, auto-resolved keywords (e.g. ::foo/bar)." [ns-alias prefix ns] (let [ns-alias-name (str (resolve-namespace (symbol ns-alias) ns))] (sequence (comp (filter (fn* [p1__2841#] (= (namespace p1__2841#) ns-alias-name))) (filter (fn* [p1__2842#] (.startsWith (name p1__2842#) prefix))) (map (fn* [p1__2843#] (str "::" ns-alias "/" (name p1__2843#)))) (map annotate-keyword)) (all-keywords)))) (defn unqualified-auto-resolved-keywords "Given a prefix and a namespace, return completion candidates for\n keywords that belong to the given namespace." [prefix ns] (sequence (comp (filter (fn* [p1__2844#] (= (namespace p1__2844#) (str ns)))) (filter (fn* [p1__2845#] (.startsWith (name p1__2845#) (subs prefix 2)))) (map (fn* [p1__2846#] (str "::" (name p1__2846#)))) (map annotate-keyword)) (all-keywords))) (defn keyword-namespace-aliases "Given a prefix and a namespace, return completion candidates for namespace\n aliases as auto-resolved keywords." [prefix ns] (sequence (comp (map (comp name first)) (filter (fn [alias-name] (.startsWith alias-name (subs prefix 2)))) (map (fn* [p1__2847#] (str "::" (name p1__2847#)))) (map annotate-keyword)) (ns-aliases ns))) (defn single-colon-keywords "Given a prefix, return completion candidates for keywords that are either\n unqualified or qualified with a synthetic namespace." [prefix] (sequence (comp (filter (fn* [p1__2848#] (.startsWith (str p1__2848#) (subs prefix 1)))) (map (fn* [p1__2849#] (str ":" p1__2849#))) (map annotate-keyword)) (all-keywords))) (defn keyword-candidates [prefix ns] (assert (string? prefix)) (let [double-colon? (.startsWith prefix "::") single-colon? (.startsWith prefix ":") slash-pos (.indexOf prefix "/")] (cond (and double-colon? (pos? slash-pos)) (let [ns-alias (subs prefix 2 slash-pos) prefix (subs prefix (inc slash-pos))] (qualified-auto-resolved-keywords ns-alias prefix ns)) double-colon? (into (unqualified-auto-resolved-keywords prefix ns) (keyword-namespace-aliases prefix ns)) single-colon? (single-colon-keywords prefix)))) (defn namespaces "Returns a list of potential namespace completions for a given namespace" [ns] (concat (map ns-name (all-ns)) (keys (ns-aliases ns)))) (defn ns-public-vars "Returns a list of potential public var name completions for a given namespace" [ns] (vals (ns-publics ns))) (defn ns-vars "Returns a list of all potential var name completions for a given namespace" [ns] (filter var? (vals (ns-map ns)))) (defn ns-classes "Returns a list of potential class name completions for a given namespace" [ns] (keys (ns-imports ns))) (def special-forms (quote [def if do let quote var fn loop recur throw try monitor-enter monitor-exit dot new set!])) (defn- static? [member] (java.lang.reflect.Modifier/isStatic (.getModifiers member))) (defn ns-java-methods "Returns a list of Java method names for a given namespace." [ns] (distinct (for [class (vals (ns-imports ns)) method (.getMethods class) :when (static? method)] (str "." (.getName method))))) (defn static-members "Returns a list of potential static members for a given class" [class] (->> (concat (.getMethods class) (.getDeclaredFields class)) (filter static?) (map (fn* [p1__2850#] (.getName p1__2850#))) (distinct))) (defn path-files [path] (cond (.endsWith path "/*") (for [jar (.listFiles (File. path)) :when (.endsWith (.getName jar) ".jar") file (path-files (.getPath jar))] file) (.endsWith path ".jar") (try (for [entry (enumeration-seq (.entries (JarFile. path)))] (.getName entry)) (catch Exception _e)) :else (for [file (file-seq (File. path))] (.replace (.getPath file) path "")))) (def classfiles (for [prop (filter (fn* [p1__2851#] (System/getProperty p1__2851#)) ["sun.boot.class.path" "java.ext.dirs" "java.class.path"]) path (.split (System/getProperty prop) File/pathSeparator) file (path-files path) :when (and (.endsWith file ".class") (not (.contains file "__")))] file)) (defn- classname [file] (.. file (replace ".class" "") (replace File/separator "."))) (def top-level-classes (doall (for [file classfiles :when (re-find #"^[^\$]+\.class" file)] (classname file)))) (def nested-classes (doall (for [file classfiles :when (re-find #"^[^\$]+(\$[^\d]\w*)+\.class" file)] (classname file)))) (defn resolve-class [ns sym] (try (let [val (ns-resolve ns sym)] (when (class? val) val)) (catch Exception e (when (not= ClassNotFoundException (class (clojure.main/repl-exception e))) (throw e))))) (def safe-var-metadata "A list of var metadata attributes are safe to return to the clients.\n We need to guard ourselves against EDN data that's not encodeable/decodable\n with bencode. We also optimize the response payloads by not returning\n redundant metadata." [:ns :name :doc :file :arglists :forms :macro :special-form :protocol :line :column :added :deprecated :resource]) (defn- handle-file-meta "Convert :file metadata to string.\n Typically `value` would be a string, a File or an URL." [value] (when value (str (if (string? value) (or (io/resource value) value) value)))) (defn- sanitize-meta "Sanitize a Clojure metadata map such that it can be bencoded." [m] (-> m (select-keys safe-var-metadata) (update :ns str) (update :name str) (update :protocol str) (update :file handle-file-meta) (cond-> (:macro m) (update :macro str)) (cond-> (:special-form m) (update :special-form str)) (assoc :arglists-str (str (:arglists m))) (cond-> (:arglists m) (update :arglists str)))) (defn annotate-var [var {:keys [extra-metadata]}] (let [{macro :macro, arglists :arglists, var-name :name, doc :doc} (-> var meta sanitize-meta) type (cond macro :macro arglists :function :else :var)] (cond-> {:candidate (name var-name), :type type} (and (contains? extra-metadata :doc) doc) (assoc :doc doc) (and (contains? extra-metadata :arglists) arglists) (assoc :arglists arglists)))) (defn annotate-class [cname] {:candidate cname, :type :class}) (def special-form-candidates (map (fn* [p1__2852#] (hash-map :candidate (name p1__2852#) :type :special-form :ns "clojure.core")) special-forms)) (defn ns-candidates [ns {:keys [extra-metadata]}] (map (fn* [p1__2853#] (let [doc (some-> p1__2853# find-ns meta :doc)] (cond-> {:candidate (name p1__2853#), :type :namespace} (and (contains? extra-metadata :doc) doc) (assoc :doc doc)))) (namespaces ns))) (defn ns-var-candidates [ns options] (map (fn* [p1__2854#] (annotate-var p1__2854# options)) (ns-vars ns))) (defn ns-public-var-candidates [ns options] (map (fn* [p1__2855#] (annotate-var p1__2855# options)) (ns-public-vars ns))) (defn ns-class-candidates [ns] (map (fn* [p1__2856#] (hash-map :candidate (name p1__2856#) :type :class)) (ns-classes ns))) (defn ns-java-method-candidates [ns] (for [method (ns-java-methods ns)] {:candidate method, :type :method})) (defn static-member-candidates [class] (for [name (static-members class)] {:candidate name, :type :static-method})) (defn scoped-candidates [prefix ns options] (when-let [prefix-scope (first (.split prefix "/"))] (let [scope (symbol prefix-scope)] (map (fn* [p1__2857#] (update p1__2857# :candidate (fn [c] (str scope "/" c)))) (if-let [class (resolve-class ns scope)] (static-member-candidates class) (when-let [ns (or (find-ns scope) (scope (ns-aliases ns)))] (ns-public-var-candidates ns options))))))) (defn class-candidates [prefix _ns] (map annotate-class (if (.contains prefix "$") nested-classes top-level-classes))) (defn generic-candidates [ns options] (concat special-form-candidates (ns-candidates ns options) (ns-var-candidates ns options) (ns-class-candidates ns))) (defn completion-candidates [prefix ns options] (cond (.startsWith prefix ":") (keyword-candidates prefix ns) (.startsWith prefix ".") (ns-java-method-candidates ns) (.contains prefix "/") (scoped-candidates prefix ns options) (.contains prefix ".") (concat (ns-candidates ns options) (class-candidates prefix ns)) :else (generic-candidates ns options))) (defn completions "Return a sequence of matching completion candidates given a prefix string and an optional current namespace." ([prefix] (completions prefix *ns*)) ([prefix ns] (completions prefix ns nil)) ([prefix ns options] (let [candidates (completion-candidates prefix ns options)] (sort-by :candidate (filter (fn* [p1__2858#] (.startsWith (:candidate p1__2858#) prefix)) candidates)))))))) (catch java.lang.Exception f__2212__auto__ (clojure.core/intern (clojure.core/create-ns (quote incomplete.core)) (quote completions) (clojure.core/fn [prefix__2213__auto__ ns__2214__auto__] [])) (clojure.core/println "Unable to initialize completions."))))) (clojure.core/in-ns (clojure.core/ns-name reply.exports/original-ns)) (user/help) nil) nil nil nil))''' + +# REPLy evaluates nothing for the namespace of the first prompt. When no +# reply has had an ns yet (e.g. the startup code failed), it then switches +# to user with another eval, which servers for other languages get. +[[checks.steps]] +send = { op = "eval", session = "$repl", code = "" } +refs = ["eval_modes/nrepl.clj#L118-L119"] diff --git a/internal/checks/fake_test.go b/internal/checks/fake_test.go index c944a55..ef62c44 100644 --- a/internal/checks/fake_test.go +++ b/internal/checks/fake_test.go @@ -31,6 +31,7 @@ type quirks struct { noEvalError bool // failed evals get no eval-error status noEx bool noNoCode bool // missing code gets eval-error instead of no-code + emptyCodeHangs bool // an eval of empty code never gets a reply noNs bool // replies don't carry ns nsFallback bool // unknown ns silently falls back to user twoDones bool @@ -298,6 +299,9 @@ func (s *fakeServer) eval(c net.Conn, req nrepl.Message, local map[string]bool) s.send(c, req, map[string]any{"status": status}) return } + if code == "" && q.emptyCodeHangs { + return + } s.mu.Lock() if name, ok := strings.CutPrefix(code, "(ns "); ok { s.namespaces[strings.TrimRight(name, ") \n")] = true diff --git a/internal/checks/refs.go b/internal/checks/refs.go index e5b1371..5227f61 100644 --- a/internal/checks/refs.go +++ b/internal/checks/refs.go @@ -15,7 +15,7 @@ const ( fireplaceBase = "https://github.com/tpope/vim-fireplace/blob/5e66509599de92550762cf2681338fc4cd1e71cf/autoload/" nreplBase = "https://github.com/nrepl/nrepl/blob/16cc5cb0f2adae44d389ba23c864a02eaa4e71a5/src/clojure/nrepl/" specBase = "https://github.com/nrepl/spec.nrepl.org/blob/67796e34ac34f2f28c3af685fc3ab432fe5eb03f/spec.md" - replyBase = "https://github.com/trptcolin/reply/blob/2b28587004aa3b5cd5d4548eb87f7674eb999b13/src/reply/eval_modes/nrepl.clj" + replyBase = "https://github.com/trptcolin/reply/blob/2ee3e3a193e9df62d5dc965d25efd0521a289b70/src/clj/reply/eval_modes/nrepl.clj" rebelBase = "https://github.com/bhauman/rebel-readline/blob/d8573a61aad5cbbd83532e2050b6595cfb5b13bb/rebel-readline-nrepl/src/rebel_readline/nrepl/service/nrepl.clj" bbNreplBase = "https://github.com/babashka/babashka.nrepl/blob/fb44874128d8cc2622f164e6a499ed4f00e5aae9/src/babashka/nrepl/" basilispBase = "https://github.com/basilisp-lang/basilisp/blob/2f993f9e53b4195316426140384c38728304341e/src/basilisp/contrib/nrepl_server.lpy" @@ -47,7 +47,8 @@ var ( conjureNsMissing = ref("Conjure namespace-not-found handling", conjureBase+"#L415-L416") fireplaceClosed = ref("vim-fireplace session-closed handling", fireplaceBase+"fireplace/transport.vim#L147") fireplaceNsMissing = ref("vim-fireplace namespace-not-found handling", fireplaceBase+"fireplace.vim#L486") - replyTerminal = ref("REPLy stops at error/eval-error", replyBase+"#L57-L66") + replyTerminal = ref("REPLy stops at error/eval-error", replyBase+"#L59-L70") + replySessions = ref("REPLy files replies by their session", replyBase+"#L184-L201") rebelTerminal = ref("rebel-readline stops at error/eval-error", rebelBase+"#L70-L83") nreplUnknownOp = ref("nREPL unknown-op reply", nreplBase+"server.clj#L109-L112") nreplNoCode = ref("nREPL no-code reply", nreplBase+"middleware/interruptible_eval.clj#L190") diff --git a/internal/checks/wire.go b/internal/checks/wire.go index e43dc84..7055559 100644 --- a/internal/checks/wire.go +++ b/internal/checks/wire.go @@ -134,8 +134,8 @@ func WireRules() []*check.Rule { ID: "wire.session-echo", Title: "Replies to a request with a session carry that session", Severity: check.Fail, - Why: "Calva only hands a reply to a session when it carries that session's id; everything else is dropped.", - Refs: []check.Ref{calvaRouting}, + Why: "Calva only hands a reply to a session when it carries that session's id; everything else is dropped. REPLy files replies by their session too, and stops reading replies altogether at one without a session it knows.", + Refs: []check.Ref{calvaRouting, replySessions}, Inspect: eachReply(func(r reply, report check.Reporter) { if !r.Req.Has("session") || r.Msg.Str("session") == r.Req.Str("session") { return