Skip to content

CLJS-3429: Handle More Complex Closure Type Annotations #245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions src/main/clojure/cljs/externs.clj
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,43 @@
(into [] (butlast props))
(with-meta (last props) ty))))

(def token->kw
{Token/BANG :bang
Token/BLOCK :block
Token/PIPE :pipe
Token/STRINGLIT :string-lit
Token/QMARK :qmark
Token/STAR :star})

(defn parse-texpr [^Node root]
(when-let [token (get token->kw (.getToken root))]
(let [children (.children root)]
(merge
{:type token}
(when-not (empty? children)
{:children (vec (map parse-texpr (.children root)))})
(when (= :string-lit token)
{:value (.getString root)})))))

(defn undefined?
[{:keys [type value] :as texpr}]
(and (= type :string-lit)
(= "undefined" value)))

(defn simplify-texpr
[texpr]
(case (:type texpr)
:string-lit (some-> (:value texpr) symbol)
(:star :qmark) 'any
:bang (simplify-texpr (-> texpr :children first))
:pipe (let [[x y] (:children texpr)]
(if (undefined? y)
(simplify-texpr x)
'any))
'any))

(defn get-tag [^JSTypeExpression texpr]
(when-let [root (.getRoot texpr)]
(if (.isString root)
(symbol (.getString root))
(if-let [child (.. root getFirstChild)]
(if (.isString child)
(symbol (.. child getString)))))))
(some-> (.getRoot texpr) parse-texpr simplify-texpr))

(defn params->method-params [xs]
(letfn [(not-opt? [x]
Expand Down Expand Up @@ -156,7 +186,7 @@
[lhs])
[]))))

(defmethod parse-extern-node Token/GETPROP [node]
(defmethod parse-extern-node Token/GETPROP [^Node node]
(when-not *ignore-var*
(let [props (map symbol (string/split (.getQualifiedName node) #"\."))]
[(if-let [ty (get-var-info node)]
Expand All @@ -165,7 +195,7 @@

;; JavaScript Object literal
;; { ... }
(defmethod parse-extern-node Token/OBJECTLIT [node]
(defmethod parse-extern-node Token/OBJECTLIT [^Node node]
(when (> (.getChildCount node) 0)
(loop [nodes (.children node)
externs []]
Expand Down Expand Up @@ -215,8 +245,8 @@
(loop [nodes (cond-> nodes
;; handle goog.modules which won't have top-levels
;; need to look at internal children
(= Token/MODULE_BODY (some-> nodes first .getToken))
(-> first .children))
(= Token/MODULE_BODY (some-> nodes ^Node (first) .getToken))
(-> ^Node (first) .children))
externs []]
(if (empty? nodes)
externs
Expand Down Expand Up @@ -313,6 +343,24 @@
(parse-externs (resource->source-file rsrc))
(:module desc))}))))

(defn info
"Helper for grabbing var info from an externs map.
Example:
(info externs '[Number isNaN])
See `externs-map`"
[externs props]
(-> externs
(get-in (butlast props))
(find (last props))
first meta))

(defn filtered-externs [f]
(->>
(filter
#(= f (.getName %))
(default-externs))
first parse-externs index-externs))

(comment
(require '[clojure.java.io :as io]
'[cljs.closure :as closure]
Expand Down
28 changes: 27 additions & 1 deletion src/test/clojure/cljs/externs_parsing_tests.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

(ns cljs.externs-parsing-tests
(:require [cljs.closure :as closure]
[cljs.analyzer :as ana]
[cljs.env :as env]
[cljs.externs :as externs]
[clojure.java.io :as io]
[clojure.test :as test :refer [deftest is]])
[clojure.test :as test :refer [deftest is testing]])
(:import [com.google.javascript.jscomp CommandLineRunner]))

(deftest cljs-3121
Expand Down Expand Up @@ -45,8 +47,32 @@
(find 'HTMLDocument) first meta)]
(is (= 'Document (:super info)))))

(deftest test-parse-closure-type-annotations
(let [externs (::ana/externs @(env/default-compiler-env))]
(testing "JS global console has tag Console"
(let [info (externs/info externs '[console])]
(is (= 'Console (:tag info)))))
(testing "JS global crypto has tag webCrypto.Crypto from:
@type {!webCrypto.Crypto|undefined}"
(let [info (externs/info externs '[crypto])]
(is (= 'webCrypto.Crypto (:tag info)))))
(testing "Generic return type on crypto methods returns ClojureScript relevant
type info:"
(testing "@return {!Promise<!ArrayBuffer>}"
(let [info (externs/info externs '[webCrypto SubtleCrypto prototype encrypt])]
(is (= 'Promise (:ret-tag info)))))
(testing "@return {!Promise<!webCrypto.CryptoKey|!webCrypto.CryptoKeyPair>}"
(let [info (externs/info externs '[webCrypto SubtleCrypto prototype deriveKey])]
(is (= 'Promise (:ret-tag info)))))
(testing "@return {!Int8Array|!Uint8Array|!Uint8ClampedArray|!Int16Array|!Uint16Array|!Int32Array|!Uint32Array|!BigInt64Array|!BigUint64Array}"
(let [info (externs/info externs '[webCrypto Crypto prototype getRandomValues])]
(is (= 'any (:ret-tag info))))))))

(comment

(let [externs (::ana/externs @(env/default-compiler-env))]
(externs/info externs '[webCrypto Crypto prototype getRandomValues]))

(externs/parse-externs
(externs/resource->source-file (io/resource "goog/object/object.js")))

Expand Down