From a0c3c41a068d06737a54422237f19f054ef5093a Mon Sep 17 00:00:00 2001 From: davidnolen Date: Wed, 9 Jul 2025 08:43:14 -0400 Subject: [PATCH 01/11] wip --- src/main/clojure/cljs/analyzer.cljc | 27 +++++++++++++++++++++++- src/test/clojure/cljs/analyzer_tests.clj | 17 +++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 709531e59..bb31a36db 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3039,6 +3039,24 @@ ret (recur fs ret true))))) +;; this case is like :refer-clojure it cannot be used as a spec parser +(defn parse-global-refer-spec + [env spec] + (let [{referred :refer-global + renamed :rename} (apply hash-map (concat [:refer-global] [spec])) + referred-without-renamed (remove (set (keys renamed)) referred)] + ;; TODO: refer validation, this can be lifted easily into common helper from parse-require-spec + ;; TODO: rename validation, harder to lift + {:use (zipmap referred-without-renamed (repeat 'js)) + :rename (into {} + (map (fn [[orig new-name]] + [new-name (symbol "js" (str orig))])) + renamed)})) + +#_(defn parse-global-require-spec + [env deps aliases spec] + ) + (defn parse-require-spec [env macros? deps aliases spec] (if (or (symbol? spec) (string? spec)) (recur env macros? deps aliases [spec]) @@ -3292,6 +3310,10 @@ (select-keys new deep-merge-keys)))) new)) +(def ns-spec-cases + #{:use :use-macros :require :require-macros + :import :refer-global :require-global}) + (defmethod parse 'ns [_ env [_ name & args :as form] _ opts] (when-not *allow-ns* @@ -3335,7 +3357,10 @@ (partial use->require env)) :use-macros (comp (partial parse-require-spec env true deps aliases) (partial use->require env)) - :import (partial parse-import-spec env deps)} + :import (partial parse-import-spec env deps) + ;:refer-global (partial parse-global-refer-spec env) + ;:require-global #(parse-global-require-spec env deps aliases %) + } valid-forms (atom #{:use :use-macros :require :require-macros :import}) reload (atom {:use nil :require nil :use-macros nil :require-macros nil}) reloads (atom {}) diff --git a/src/test/clojure/cljs/analyzer_tests.clj b/src/test/clojure/cljs/analyzer_tests.clj index f1b639938..71d593c57 100644 --- a/src/test/clojure/cljs/analyzer_tests.clj +++ b/src/test/clojure/cljs/analyzer_tests.clj @@ -1533,3 +1533,20 @@ (ana/gen-constant-id '+))) (is (not= (ana/gen-constant-id 'foo.bar) (ana/gen-constant-id 'foo$bar)))) + +;; ----------------------------------------------------------------------------- +;; :refer-global / :require-global ns parsing tests + +#_(deftest test-refer-global + (binding [ana/*cljs-ns* ana/*cljs-ns*] + (let [parsed-ns (env/with-compiler-env test-cenv + (analyze test-env + '(ns foo.core + (:refer-global [Date] :rename {Date MyDate}))))] + ))) + +(comment + + (clojure.test/test-vars [#'test-refer-global]) + + ) \ No newline at end of file From dfe9c652f740ea5e7c21fe4f479b00025b9f1d1d Mon Sep 17 00:00:00 2001 From: davidnolen Date: Mon, 14 Jul 2025 22:04:05 -0400 Subject: [PATCH 02/11] - parse-global-refer-spec is more like parse-ns-excludes - basic impl test --- src/main/clojure/cljs/analyzer.cljc | 31 +++++++++++++++--------- src/test/clojure/cljs/analyzer_tests.clj | 7 ++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index bb31a36db..f01fee5b0 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3039,19 +3039,26 @@ ret (recur fs ret true))))) -;; this case is like :refer-clojure it cannot be used as a spec parser (defn parse-global-refer-spec - [env spec] - (let [{referred :refer-global - renamed :rename} (apply hash-map (concat [:refer-global] [spec])) - referred-without-renamed (remove (set (keys renamed)) referred)] - ;; TODO: refer validation, this can be lifted easily into common helper from parse-require-spec - ;; TODO: rename validation, harder to lift - {:use (zipmap referred-without-renamed (repeat 'js)) - :rename (into {} - (map (fn [[orig new-name]] - [new-name (symbol "js" (str orig))])) - renamed)})) + [env args] + (let [xs (filter #(-> % first (= :refer-global)) args)] + (if-not (= 1 (count xs)) + (throw (error env "Only one :refer-global form is allowed per namespace definition")) + (let [[_ refers & {:keys [rename] :as renames-only}] (first xs) + err-str "Only [:refer-global (names)] and optionally `:rename {from to}` specs supported"] + (when-not (and (vector? refers) (every? symbol refers)) + (throw (error env err-str))) + (when-not (or (empty? renames-only) + (and (= 1 (count renames-only)) + (contains? renames-only :rename) + (map? rename) + (every? symbol (mapcat identity rename)))) + (throw (error env (str err-str (pr-str renames-only))))) + {:use (zipmap refers (repeat 'js)) + :rename (into {} + (map (fn [[orig new-name]] + [new-name (symbol "js" (str orig))])) + rename)})))) #_(defn parse-global-require-spec [env deps aliases spec] diff --git a/src/test/clojure/cljs/analyzer_tests.clj b/src/test/clojure/cljs/analyzer_tests.clj index 71d593c57..de8573b16 100644 --- a/src/test/clojure/cljs/analyzer_tests.clj +++ b/src/test/clojure/cljs/analyzer_tests.clj @@ -387,6 +387,13 @@ :renames {map clj-map}})) (is (set? (:excludes parsed))))) +(deftest test-parse-global-refer + (let [parsed (ana/parse-global-refer-spec {} + '((:refer-global [Date] :rename {Symbol JSSymbol})))] + (is (= parsed + '{:use {Date js} + :rename {JSSymbol js/Symbol}})))) + (deftest test-cljs-1785-js-shadowed-by-local (let [ws (atom [])] (ana/with-warning-handlers [(collecting-warning-handler ws)] From c2487b80d052a2663c9d4d27cf4fe24f41c090dc Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 07:01:51 -0400 Subject: [PATCH 03/11] - fix parse-global-refer-spec validation - incorporate, untested --- src/main/clojure/cljs/analyzer.cljc | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index f01fee5b0..d52e6f705 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3041,9 +3041,13 @@ (defn parse-global-refer-spec [env args] - (let [xs (filter #(-> % first (= :refer-global)) args)] - (if-not (= 1 (count xs)) - (throw (error env "Only one :refer-global form is allowed per namespace definition")) + (let [xs (filter #(-> % first (= :refer-global)) args) + cnt (count xs)] + (cond + (> cnt 1) + (throw (error env (str "Only one :refer-global form is allowed per namespace definition " (count xs)))) + + (== cnt 1) (let [[_ refers & {:keys [rename] :as renames-only}] (first xs) err-str "Only [:refer-global (names)] and optionally `:rename {from to}` specs supported"] (when-not (and (vector? refers) (every? symbol refers)) @@ -3355,6 +3359,7 @@ core-renames (reduce (fn [m [original renamed]] (assoc m renamed (symbol "cljs.core" (str original)))) {} core-renames) + {global-uses :use global-renames :rename} (parse-global-refer-spec env args) deps (atom []) ;; as-aliases can only be used *once* because they are about the reader aliases (atom {:fns as-aliases :macros as-aliases}) @@ -3416,9 +3421,9 @@ :use-macros use-macros :require-macros require-macros :rename-macros rename-macros - :uses uses + :uses (merge uses global-uses) :requires requires - :renames (merge renames core-renames) + :renames (merge renames core-renames global-renames) :imports imports}] (swap! env/*compiler* update-in [::namespaces name] merge ns-info) (merge {:op :ns From c47146ae4459bc2a80386c6c862156e828e0041a Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 08:27:12 -0400 Subject: [PATCH 04/11] - refactor validation - :refer-global :only semantically more aligned w/ Clojure --- src/main/clojure/cljs/analyzer.cljc | 22 ++++++++++++---------- src/main/clojure/cljs/core.cljc | 7 +++++++ src/test/clojure/cljs/analyzer_tests.clj | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index d52e6f705..91173b69a 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3045,20 +3045,22 @@ cnt (count xs)] (cond (> cnt 1) - (throw (error env (str "Only one :refer-global form is allowed per namespace definition " (count xs)))) + (throw (error env "Only one :refer-global form is allowed per namespace definition")) (== cnt 1) - (let [[_ refers & {:keys [rename] :as renames-only}] (first xs) - err-str "Only [:refer-global (names)] and optionally `:rename {from to}` specs supported"] - (when-not (and (vector? refers) (every? symbol refers)) + (let [[_ & {:keys [only rename] :as parsed-spec}] (first xs) + err-str "Only (:refer-global :only [names]) and optionally `:rename {from to}` specs supported"] + (when-not (or (empty? only) + (and (vector? only) + (every? symbol only))) (throw (error env err-str))) - (when-not (or (empty? renames-only) - (and (= 1 (count renames-only)) - (contains? renames-only :rename) - (map? rename) + (when-not (or (empty? rename) + (and (map? rename) (every? symbol (mapcat identity rename)))) - (throw (error env (str err-str (pr-str renames-only))))) - {:use (zipmap refers (repeat 'js)) + (throw (error env (str err-str (pr-str parsed-spec))))) + (when-not (every? #{:only :rename} (keys parsed-spec)) + (throw (error env (str err-str (pr-str parsed-spec))))) + {:use (zipmap only (repeat 'js)) :rename (into {} (map (fn [[orig new-name]] [new-name (symbol "js" (str orig))])) diff --git a/src/main/clojure/cljs/core.cljc b/src/main/clojure/cljs/core.cljc index 8393a1a67..3b0b01afb 100644 --- a/src/main/clojure/cljs/core.cljc +++ b/src/main/clojure/cljs/core.cljc @@ -3116,6 +3116,13 @@ [& args] `(~'ns* ~(cons :refer-clojure args))) +(core/defmacro refer-global + "Refer global js vars. Supports renaming via :rename. + + (refer-global :only '[Date Symbol] :rename '{Symbol Sym})" + [& args] + `(~'ns* ~(cons :refer-global args))) + ;; INTERNAL - do not use, only for Node.js (core/defmacro load-file* [f] `(goog/nodeGlobalRequire ~f)) diff --git a/src/test/clojure/cljs/analyzer_tests.clj b/src/test/clojure/cljs/analyzer_tests.clj index de8573b16..dd0e24806 100644 --- a/src/test/clojure/cljs/analyzer_tests.clj +++ b/src/test/clojure/cljs/analyzer_tests.clj @@ -389,7 +389,7 @@ (deftest test-parse-global-refer (let [parsed (ana/parse-global-refer-spec {} - '((:refer-global [Date] :rename {Symbol JSSymbol})))] + '((:refer-global :only [Date] :rename {Symbol JSSymbol})))] (is (= parsed '{:use {Date js} :rename {JSSymbol js/Symbol}})))) From c63f305f1a706e1e12f4e4ebaea1ddc14efe5357 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 08:40:39 -0400 Subject: [PATCH 05/11] - works for Date in a Node REPL --- src/main/clojure/cljs/analyzer.cljc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 91173b69a..214865942 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3465,6 +3465,7 @@ core-renames (reduce (fn [m [original renamed]] (assoc m renamed (symbol "cljs.core" (str original)))) {} core-renames) + {global-uses :use global-renames :rename} (parse-global-refer-spec env args) deps (atom []) ;; as-aliases can only be used *once* because they are about the reader aliases (atom {:fns as-aliases :macros as-aliases}) @@ -3495,7 +3496,7 @@ (apply merge-with merge m (map (spec-parsers k) (remove #{:reload :reload-all} libs)))) - {} (remove (fn [[r]] (= r :refer-clojure)) args))] + {} (remove (fn [[r]] (#{:refer-clojure :refer-global} r)) args))] (set! *cljs-ns* name) (let [require-info {:as-aliases as-aliases @@ -3504,9 +3505,9 @@ :use-macros use-macros :require-macros require-macros :rename-macros rename-macros - :uses uses + :uses (merge uses global-uses) :requires requires - :renames (merge renames core-renames) + :renames (merge renames core-renames global-renames) :imports imports}] (swap! env/*compiler* update-in [::namespaces name] merge-ns-info require-info env) (merge {:op :ns* From 34690cc0c1b4de7561202ae87b25eb1c44b39ac6 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 08:53:09 -0400 Subject: [PATCH 06/11] - :refer-global is not a normal spec-parser - copy over logic from ns* --- src/main/clojure/cljs/analyzer.cljc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 214865942..649934e81 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -3372,7 +3372,6 @@ :use-macros (comp (partial parse-require-spec env true deps aliases) (partial use->require env)) :import (partial parse-import-spec env deps) - ;:refer-global (partial parse-global-refer-spec env) ;:require-global #(parse-global-require-spec env deps aliases %) } valid-forms (atom #{:use :use-macros :require :require-macros :import}) @@ -3401,7 +3400,7 @@ (apply merge-with merge m (map (spec-parsers k) (remove #{:reload :reload-all} libs)))) - {} (remove (fn [[r]] (= r :refer-clojure)) args)) + {} (remove (fn [[r]] (#{:refer-clojure :refer-global} r)) args)) ;; patch `require-macros` and `use-macros` in Bootstrap for namespaces ;; that require their own macros #?@(:cljs [[require-macros use-macros] From 688a21656a9ef3d39a9be066df1ddf1205cbf350 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 09:30:53 -0400 Subject: [PATCH 07/11] - refer-clojure exclude duped --- src/main/clojure/cljs/core.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/clojure/cljs/core.cljc b/src/main/clojure/cljs/core.cljc index 3b0b01afb..4ac879d47 100644 --- a/src/main/clojure/cljs/core.cljc +++ b/src/main/clojure/cljs/core.cljc @@ -12,7 +12,7 @@ defprotocol defrecord defstruct deftype delay destructure doseq dosync dotimes doto extend-protocol extend-type fn for future gen-class gen-interface if-let if-not import io! lazy-cat lazy-seq let letfn locking loop - memfn ns or proxy proxy-super pvalues refer-clojure reify sync time + memfn ns or proxy proxy-super pvalues reify sync time when when-first when-let when-not while with-bindings with-in-str with-loading-context with-local-vars with-open with-out-str with-precision with-redefs satisfies? identical? true? false? number? nil? instance? symbol? keyword? string? str get From fdb9c7a634e73c41b52cb79d159239a48fa5a9b3 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 17:39:32 -0400 Subject: [PATCH 08/11] - add case to missing-use for global referred vars - add unit test --- src/main/clojure/cljs/analyzer.cljc | 14 ++++++++------ src/test/clojure/cljs/analyzer_tests.clj | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 649934e81..5edef6d90 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -2821,12 +2821,14 @@ (error-message :undeclared-ns {:ns-sym dep :js-provide (name dep)})))))))))))) (defn missing-use? [lib sym cenv] - (let [js-lib (get-in cenv [:js-dependency-index (name lib)])] - (and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found) - (not (= (get js-lib :group) :goog)) - (not (get js-lib :closure-lib)) - (not (node-module-dep? lib)) - (not (dep-has-global-exports? lib))))) + ;; ignore globals referred via :refer-global + (when-not (= 'js lib) + (let [js-lib (get-in cenv [:js-dependency-index (name lib)])] + (and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found) + (not (= (get js-lib :group) :goog)) + (not (get js-lib :closure-lib)) + (not (node-module-dep? lib)) + (not (dep-has-global-exports? lib)))))) (defn missing-rename? [sym cenv] (let [lib (symbol (namespace sym)) diff --git a/src/test/clojure/cljs/analyzer_tests.clj b/src/test/clojure/cljs/analyzer_tests.clj index dd0e24806..d68692eb9 100644 --- a/src/test/clojure/cljs/analyzer_tests.clj +++ b/src/test/clojure/cljs/analyzer_tests.clj @@ -554,6 +554,12 @@ (analyze test-env '(map #(require '[clojure.set :as set]) [1 2])))))) +(deftest test-analyze-refer-global + (testing "refer-global macro expr return expected AST" + (let [test-env (ana/empty-env)] + (is (= (-> (analyze test-env '(refer-global :only '[Date])) :uses vals set) + '#{js}))))) + (deftest test-gen-user-ns ;; note: can't use `with-redefs` because direct-linking is enabled (let [s "src/cljs/foo.cljs" From 53abd2c7e40142715945d0b3092ff12cdab6477b Mon Sep 17 00:00:00 2001 From: davidnolen Date: Fri, 25 Jul 2025 17:47:13 -0400 Subject: [PATCH 09/11] - missing binding for test --- src/test/clojure/cljs/analyzer_tests.clj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/clojure/cljs/analyzer_tests.clj b/src/test/clojure/cljs/analyzer_tests.clj index d68692eb9..5d87340b5 100644 --- a/src/test/clojure/cljs/analyzer_tests.clj +++ b/src/test/clojure/cljs/analyzer_tests.clj @@ -556,9 +556,11 @@ (deftest test-analyze-refer-global (testing "refer-global macro expr return expected AST" - (let [test-env (ana/empty-env)] - (is (= (-> (analyze test-env '(refer-global :only '[Date])) :uses vals set) - '#{js}))))) + (binding [ana/*cljs-ns* ana/*cljs-ns* + ana/*cljs-warnings* nil] + (let [test-env (ana/empty-env)] + (is (= (-> (analyze test-env '(refer-global :only '[Date])) :uses vals set) + '#{js})))))) (deftest test-gen-user-ns ;; note: can't use `with-redefs` because direct-linking is enabled From dd859199462eb5e1a8e8d8d2d439a0db82e248d4 Mon Sep 17 00:00:00 2001 From: davidnolen Date: Tue, 29 Jul 2025 13:58:21 -0400 Subject: [PATCH 10/11] - fix load-dependencies so we drop global namespaces --- src/main/clojure/cljs/repl.cljc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/clojure/cljs/repl.cljc b/src/main/clojure/cljs/repl.cljc index b685a62e5..0db0ba7d6 100644 --- a/src/main/clojure/cljs/repl.cljc +++ b/src/main/clojure/cljs/repl.cljc @@ -251,12 +251,20 @@ (load-sources repl-env sources opts) sources))) +(defn global-ns? [x] + (or (= 'js x) + (= "js" (namespace x)))) + (defn- load-dependencies "Compile and load the given `requires` and return the compiled sources." ([repl-env requires] (load-dependencies repl-env requires nil)) ([repl-env requires opts] - (doall (mapcat #(load-namespace repl-env % opts) (distinct requires))))) + (->> requires + distinct + (remove global-ns?) + (mapcat #(load-namespace repl-env % opts)) + doall))) (defn ^File js-src->cljs-src "Map a JavaScript output file back to the original ClojureScript source @@ -652,7 +660,7 @@ (defn- wrap-fn [form] (cond (and (seq? form) - (#{'ns 'require 'require-macros + (#{'ns 'require 'require-macros 'refer-global 'use 'use-macros 'import 'refer-clojure} (first form))) identity @@ -673,7 +681,7 @@ (defn- init-wrap-fn [form] (cond (and (seq? form) - (#{'ns 'require 'require-macros + (#{'ns 'require 'require-macros 'refer-global 'use 'use-macros 'import 'refer-clojure} (first form))) identity From f4b6f562e87dd34514f7531ec0876971d7d37efa Mon Sep 17 00:00:00 2001 From: davidnolen Date: Tue, 29 Jul 2025 15:26:28 -0400 Subject: [PATCH 11/11] - move global-ns? predicate to analyzer --- src/main/clojure/cljs/analyzer.cljc | 6 +++++- src/main/clojure/cljs/repl.cljc | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index 5edef6d90..4b6032ffe 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -2820,9 +2820,13 @@ (error env (error-message :undeclared-ns {:ns-sym dep :js-provide (name dep)})))))))))))) +(defn global-ns? [x] + (or (= 'js x) + (= "js" (namespace x)))) + (defn missing-use? [lib sym cenv] ;; ignore globals referred via :refer-global - (when-not (= 'js lib) + (when-not (global-ns? lib) (let [js-lib (get-in cenv [:js-dependency-index (name lib)])] (and (= (get-in cenv [::namespaces lib :defs sym] ::not-found) ::not-found) (not (= (get js-lib :group) :goog)) diff --git a/src/main/clojure/cljs/repl.cljc b/src/main/clojure/cljs/repl.cljc index 0db0ba7d6..f6b584ba9 100644 --- a/src/main/clojure/cljs/repl.cljc +++ b/src/main/clojure/cljs/repl.cljc @@ -251,10 +251,6 @@ (load-sources repl-env sources opts) sources))) -(defn global-ns? [x] - (or (= 'js x) - (= "js" (namespace x)))) - (defn- load-dependencies "Compile and load the given `requires` and return the compiled sources." ([repl-env requires] @@ -262,7 +258,7 @@ ([repl-env requires opts] (->> requires distinct - (remove global-ns?) + (remove ana/global-ns?) (mapcat #(load-namespace repl-env % opts)) doall)))