Repository: clojure-lsp/clojure-lsp
Pull request #239 (2021-01-07 03:45:49)

Support for Add missing import code action
Add code action: "Add missing 'java.ultil.Date' import"

ATM Check only for java.util package.

![clojure-add-missing-import](https://user-images.githubusercontent.com/7820865/103848667-a8ec6500-5081-11eb-8c06-318a2f3774a4.gif)



Patch:
diff --git a/src/clojure_lsp/clojure_core.clj b/src/clojure_lsp/clojure_core.clj
index 9dbae6ab..dc2dfc58 100644
--- a/src/clojure_lsp/clojure_core.clj
+++ b/src/clojure_lsp/clojure_core.clj
@@ -20,7 +20,7 @@
      ClassValue,
      Compiler,
      Double,
-     Enum<E extends Enum<E>>,
+     Enum,
      Float,
      InheritableThreadLocal,
      Integer,
@@ -105,6 +105,136 @@
      SafeVarargs,
      SuppressWarnings})
 
+(def java-util-syms
+  '#{Collection,
+     Comparator,
+     Deque,
+     Enumeration,
+     EventListener,
+     Formattable,
+     Iterator,
+     List,
+     ListIterator,
+     Map,
+     Map.Entry,
+     NavigableMap,
+     NavigableSet,
+     Observer,
+     PrimitiveIterator,
+     PrimitiveIterator.OfDouble,
+     PrimitiveIterator.OfInt,
+     PrimitiveIterator.OfLong,
+     Queue,
+     RandomAccess,
+     Set,
+     SortedMap,
+     SortedSet,
+     Spliterator,
+     Spliterator.OfDouble,
+     Spliterator.OfInt,
+     Spliterator.OfLong,
+     Spliterator.OfPrimitive,
+     AbstractCollection,
+     AbstractList,
+     AbstractMap,
+     AbstractMap.SimpleEntry,
+     AbstractMap.SimpleImmutableEntry,
+     AbstractQueue,
+     AbstractSequentialList,
+     AbstractSet,
+     ArrayDeque,
+     ArrayList,
+     Arrays,
+     Base64,
+     Base64.Decoder,
+     Base64.Encoder,
+     BitSet,
+     Calendar,
+     Calendar.Builder,
+     Collections,
+     Currency,
+     Date,
+     Dictionary,
+     DoubleSummaryStatistics,
+     EnumMap,
+     EnumSet,
+     EventListenerProxy,
+     EventObject,
+     FormattableFlags,
+     Formatter,
+     GregorianCalendar,
+     HashMap,
+     HashSet,
+     Hashtable,
+     IdentityHashMap,
+     IntSummaryStatistics,
+     LinkedHashMap,
+     LinkedHashSet,
+     LinkedList,
+     ListResourceBundle,
+     Locale,
+     Locale.Builder,
+     Locale.LanguageRange,
+     LongSummaryStatistics,
+     Objects,
+     Observable,
+     Optional,
+     OptionalDouble,
+     OptionalInt,
+     OptionalLong,
+     PriorityQueue,
+     Properties,
+     PropertyPermission,
+     PropertyResourceBundle,
+     Random,
+     ResourceBundle,
+     ResourceBundle.Control,
+     Scanner,
+     ServiceLoader,
+     SimpleTimeZone,
+     Spliterators,
+     Spliterators.AbstractDoubleSpliterator,
+     Spliterators.AbstractIntSpliterator,
+     Spliterators.AbstractLongSpliterator,
+     Spliterators.AbstractSpliterator,
+     SplittableRandom,
+     Stack,
+     StringJoiner,
+     StringTokenizer,
+     Timer,
+     TimerTask,
+     TimeZone,
+     TreeMap,
+     TreeSet,
+     UUID,
+     Vector,
+     WeakHashMap,
+     Formatter.BigDecimalLayoutForm,
+     Locale.Category,
+     Locale.FilteringMode,
+     ConcurrentModificationException,
+     DuplicateFormatFlagsException,
+     EmptyStackException,
+     FormatFlagsConversionMismatchException,
+     FormatterClosedException,
+     IllegalFormatCodePointException,
+     IllegalFormatConversionException,
+     IllegalFormatException,
+     IllegalFormatFlagsException,
+     IllegalFormatPrecisionException,
+     IllegalFormatWidthException,
+     IllformedLocaleException,
+     InputMismatchException,
+     InvalidPropertiesFormatException,
+     MissingFormatArgumentException,
+     MissingFormatWidthException,
+     MissingResourceException,
+     NoSuchElementException,
+     TooManyListenersException,
+     UnknownFormatConversionException,
+     UnknownFormatFlagsException,
+     ServiceConfigurationError})
+
 (def core-syms
   '#{*
      *'
@@ -1194,3 +1324,23 @@
      uuid
      write-all})
 
+(def core-refers
+  (->> core-syms
+       (mapv (juxt identity (constantly 'clojure.core)))
+       (into {})))
+
+(def cljs-refers
+  (->> core-syms
+       (concat cljs-syms)
+       (mapv (juxt identity (constantly 'cljs.core)))
+       (into {})))
+
+(def java-lang-imports
+  (->> java-lang-syms
+       (mapv (juxt identity #(symbol (str "java.lang." (name %)))))
+       (into {})))
+
+(def java-util-imports
+  (->> java-util-syms
+       (mapv (juxt identity #(symbol (str "java.util." (name %)))))
+       (into {})))
diff --git a/src/clojure_lsp/feature/code_actions.clj b/src/clojure_lsp/feature/code_actions.clj
index 369589cb..3898787f 100644
--- a/src/clojure_lsp/feature/code_actions.clj
+++ b/src/clojure_lsp/feature/code_actions.clj
@@ -1,9 +1,9 @@
 (ns clojure-lsp.feature.code-actions
   (:require
-   [clojure-lsp.feature.definition :as f.definition]
-   [clojure-lsp.feature.refactor :as f.refactor]
-   [clojure-lsp.refactor.transform :as r.transform]
-   [rewrite-clj.zip :as z]))
+    [clojure-lsp.feature.definition :as f.definition]
+    [clojure-lsp.feature.refactor :as f.refactor]
+    [clojure-lsp.refactor.transform :as r.transform]
+    [rewrite-clj.zip :as z]))
 
 (defn all [zloc uri row col diagnostics client-capabilities]
   (let [workspace-edit-capability? (get-in client-capabilities [:workspace :workspace-edit])
@@ -17,21 +17,29 @@
         unresolved-symbol (first (filter #(= (compare "unresolved-symbol" (some-> % .getCode .get)) 0) diagnostics))
         known-refer? (when unresolved-symbol
                        (get r.transform/common-refers->info (z/sexpr zloc)))
-        missing-ns (when (or has-unknown-ns? known-refer?)
-                     (f.refactor/call-refactor {:loc zloc
-                                                :refactoring :add-missing-libspec
-                                                :uri uri
-                                                :version 0
-                                                :row row
-                                                :col col
-                                                :args {:source :code-action}}))]
+        missing-require (when (or has-unknown-ns? known-refer?)
+                          (f.refactor/call-refactor {:loc zloc
+                                                     :refactoring :add-missing-libspec
+                                                     :uri uri
+                                                     :version 0
+                                                     :row row
+                                                     :col col
+                                                     :args {:source :code-action}}))
+        missing-import (when unresolved-symbol
+                          (r.transform/add-common-import-to-namespace zloc))]
     (cond-> []
 
-      (:result missing-ns)
-      (conj {:title          (str "Add missing '" (-> missing-ns :code-action-data :ns-name) "' namespace")
+      (:result missing-require)
+      (conj {:title          (str "Add missing '" (-> missing-require :code-action-data :ns-name) "' require")
              :kind           :quick-fix
              :preferred?     true
-             :workspace-edit (f.refactor/refactor-client-seq-changes uri 0 (:result missing-ns))})
+             :workspace-edit (f.refactor/refactor-client-seq-changes uri 0 (:result missing-require))})
+
+      (:result missing-import)
+      (conj {:title          (str "Add missing '" (-> missing-import :code-action-data :import-name) "' import")
+             :kind           :quick-fix
+             :preferred?     true
+             :workspace-edit (f.refactor/refactor-client-seq-changes uri 0 (:result missing-import))})
 
       inline-symbol?
       (conj {:title   "Inline symbol"
diff --git a/src/clojure_lsp/parser.clj b/src/clojure_lsp/parser.clj
index 51fa0784..ce131383 100644
--- a/src/clojure_lsp/parser.clj
+++ b/src/clojure_lsp/parser.clj
@@ -18,22 +18,6 @@
 (declare find-usages*)
 (declare parse-destructuring)
 
-(def core-refers
-  (->> cc/core-syms
-       (mapv (juxt identity (constantly 'clojure.core)))
-       (into {})))
-
-(def cljs-refers
-  (->> cc/core-syms
-       (concat cc/cljs-syms)
-       (mapv (juxt identity (constantly 'cljs.core)))
-       (into {})))
-
-(def lang-imports
-  (->> cc/java-lang-syms
-       (mapv (juxt identity #(symbol (str "java.lang." (name %)))))
-       (into {})))
-
 (def default-env
   {:ns 'user
    :refer-all-syms {}
@@ -149,15 +133,15 @@
           local-classes' (->> local-classes
                               (map (juxt identity #(symbol (str (name ns) "." %))))
                               (into {}))
-          java-classes (merge local-classes' imports lang-imports)
+          java-classes (merge local-classes' imports cc/java-lang-imports)
           java-sym (get java-classes (symbol (string/replace ident-name #"\.$" "")))
           refered (get refers ident-name)
           required-ns (get requires ident-ns)
           ctr (if (symbol? ident) symbol keyword)
-          core-clj? (and (= :clj file-type) (contains? core-refers ident))
-          core-cljs? (and (= :cljs file-type) (contains? cljs-refers ident))
-          core-clj-symbol (when core-clj? (ctr (name (get core-refers ident)) ident-name))
-          core-cljs-symbol (when core-cljs? (ctr (name (get cljs-refers ident)) ident-name))
+          core-clj? (and (= :clj file-type) (contains? cc/core-refers ident))
+          core-cljs? (and (= :cljs file-type) (contains? cc/cljs-refers ident))
+          core-clj-symbol (when core-clj? (ctr (name (get cc/core-refers ident)) ident-name))
+          core-cljs-symbol (when core-cljs? (ctr (name (get cc/cljs-refers ident)) ident-name))
           core-macro? (and (or core-clj? core-cljs?)
                            (contains? default-macro-defs (or core-clj-symbol core-cljs-symbol)))
           known-macro? (or core-macro?
@@ -195,8 +179,8 @@
               (contains? imports ident-ns)
               {:sym (symbol (name (get imports ident-ns)) ident-name) :tags #{:java :method :norename}}
 
-              (contains? lang-imports ident-ns)
-              {:sym (symbol (name (get lang-imports ident-ns)) ident-name) :tags #{:java :method :norename}}
+              (contains? cc/java-lang-imports ident-ns)
+              {:sym (symbol (name (get cc/java-lang-imports ident-ns)) ident-name) :tags #{:java :method :norename}}
 
               (and (= :cljs file-type) (= 'js ident-ns))
               {:sym ident :tags #{:method :norename}}
diff --git a/src/clojure_lsp/refactor/transform.clj b/src/clojure_lsp/refactor/transform.clj
index af6e687a..3024f204 100644
--- a/src/clojure_lsp/refactor/transform.clj
+++ b/src/clojure_lsp/refactor/transform.clj
@@ -12,7 +12,8 @@
    [rewrite-clj.node :as n]
    [rewrite-clj.zip :as z]
    [rewrite-clj.zip.subedit :as zsub]
-   [clojure.tools.logging :as log]))
+   [clojure.tools.logging :as log]
+   [clojure-lsp.clojure-core :as cc]))
 
 (defn result [zip-edits]
   (mapv (fn [zip-edit]
@@ -394,6 +395,15 @@
 (defn add-import-to-namespace [zloc import-name]
   (add-form-to-namespace zloc (symbol import-name) :import))
 
+(defn add-common-import-to-namespace [zloc]
+  (let [class-with-dot (z/sexpr zloc)
+        class-name (->> class-with-dot str drop-last (string/join "") symbol)]
+    (when-let [import-name (or (get cc/java-util-imports class-name)
+                               (get cc/java-util-imports class-with-dot))]
+      (let [result (add-form-to-namespace zloc (symbol import-name) :import)]
+        {:result result
+         :code-action-data {:import-name import-name}}))))
+
 (defn add-known-libspec
   [zloc ns-to-add qualified-ns-to-add]
   (when (and qualified-ns-to-add ns-to-add)


Test patch:
diff --git a/test/clojure_lsp/handlers_test.clj b/test/clojure_lsp/handlers_test.clj
index 544a2bea..7a3c905d 100644
--- a/test/clojure_lsp/handlers_test.clj
+++ b/test/clojure_lsp/handlers_test.clj
@@ -432,7 +432,9 @@
         c-code   (str "(ns another-ns)\n"
                       "(def bar ons/bar)\n"
                       "(def foo sns/foo)\n"
-                      "(deftest some-test)")
+                      "(deftest some-test)\n"
+                      "MyClass.\n"
+                      "Date.")
         db-state {:documents {"file://a.clj" {:text references-code}
                               "file://b.clj" {:text b-code}
                               "file://c.clj" {:text c-code}}
@@ -454,18 +456,34 @@
       (testing "when it has unresolved-namespace and can find namespace"
         (reset! db/db db-state)
         (let [unknown-ns-diagnostic (Diagnostic. (Range. (Position. 2 10) (Position. 2 16)) "Unknown namespace" DiagnosticSeverity/Error "some source" "unresolved-namespace")]
-          (is (some #(= (:title %) "Add missing 'some-ns' namespace")
+          (is (some #(= (:title %) "Add missing 'some-ns' require")
                     (handlers/code-actions "file://c.clj" [unknown-ns-diagnostic] 2 10)))))
       (testing "when it has unresolved-symbol and it's a known refer"
         (reset! db/db db-state)
         (let [unknown-symbol-diagnostic (Diagnostic. (Range. (Position. 3 1) (Position. 3 8)) "Unresolved symbol deftest" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
-          (is (some #(= (:title %) "Add missing 'clojure.test' namespace")
+          (is (some #(= (:title %) "Add missing 'clojure.test' require")
                     (handlers/code-actions "file://c.clj" [unknown-symbol-diagnostic] 3 1)))))
       (testing "when it has unresolved-symbol but it's not a known refer"
         (reset! db/db db-state)
         (let [unknown-symbol-diagnostic (Diagnostic. (Range. (Position. 3 10) (Position. 3 18)) "Unresolved symbol some-test" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
           (is (not-any? #(string/starts-with? (:title %) "Add missing")
                         (handlers/code-actions "file://c.clj" [unknown-symbol-diagnostic] 3 10))))))
+    (testing "Add common missing import"
+      (testing "when it has no unknown-symbol diagnostic"
+        (reset! db/db db-state)
+        (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                      (handlers/code-actions "file://c.clj" [] 4 1))))
+
+      (testing "when it has unknown-symbol but it's not a common import"
+        (reset! db/db db-state)
+        (let [unresolved-symbol-diagnostic (Diagnostic. (Range. (Position. 4 1) (Position. 4 5)) "Unresolved symbol" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
+          (is (not-any? #(string/starts-with? (:title %) "Add missing")
+                        (handlers/code-actions "file://c.clj" [unresolved-symbol-diagnostic] 4 1)))))
+      (testing "when it has unknown-symbol but it's not a common import"
+        (reset! db/db db-state)
+        (let [unresolved-symbol-diagnostic (Diagnostic. (Range. (Position. 5 1) (Position. 5 5)) "Unresolved symbol" DiagnosticSeverity/Error "some source" "unresolved-symbol")]
+          (is (some #(= (:title %) "Add missing 'java.util.Date' import")
+                    (handlers/code-actions "file://c.clj" [unresolved-symbol-diagnostic] 5 1))))))
     (testing "Inline symbol"
       (testing "when in not a let/def symbol"
         (reset! db/db db-state)
@@ -502,7 +520,7 @@
       (testing "with workspace edit client capability"
         (reset! db/db (assoc-in db-state [:client-capabilities :workspace :workspace-edit] true))
         (is (some #(= (:title %) "Clean namespace")
-                  (handlers/code-actions "file://b.clj" [] 1 1)))))))
+    (handlers/code-actions "file://b.clj" [] 1 1)))))))
 
 (deftest test-code-lens
   (let [references-code (str "(ns some-ns)\n"
diff --git a/test/clojure_lsp/refactor/transform_test.clj b/test/clojure_lsp/refactor/transform_test.clj
index 6f8c4f74..b4493a59 100644
--- a/test/clojure_lsp/refactor/transform_test.clj
+++ b/test/clojure_lsp/refactor/transform_test.clj
@@ -75,6 +75,22 @@
                    "    java.util.Calendar"
                    "    java.util.Date))") (z/root-string loc))))))
 
+(deftest add-common-import-to-namespace-test
+  (testing "when we known the import"
+    (reset! db/db {:file-envs {}})
+    (let [zloc (-> (z/of-string "(ns foo.bar) Date.") (z/find-value z/next 'Date.))
+          {:keys [result code-action-data]} (transform/add-common-import-to-namespace zloc)
+          [{:keys [loc range]}] result]
+      (is (some? range))
+      (is (= 'java.util.Date (:import-name code-action-data)))
+      (is (= (code "(ns foo.bar "
+                   "  (:import"
+                   "    java.util.Date))") (z/root-string loc)))))
+  (testing "when we don't known the import"
+    (reset! db/db {:file-envs {}})
+    (let [zloc (-> (z/of-string "(ns foo.bar) MyClass.") (z/find-value z/next 'Date.))]
+      (is (= nil (transform/add-common-import-to-namespace zloc))))))
+
 (deftest paredit-test
   (let [zloc (edit/raise (z/find-value (z/of-string "(a (b))") z/next 'b))]
     (is (= 'b (z/sexpr zloc)))