diff --git a/src/main/cljs/cljs/core.cljs b/src/main/cljs/cljs/core.cljs index b97f00fa2..e1c7b067e 100644 --- a/src/main/cljs/cljs/core.cljs +++ b/src/main/cljs/cljs/core.cljs @@ -243,7 +243,7 @@ [x] (coercive-= x nil)) -(defn ^boolean array? +(defn array? "Returns true if x is a JavaScript array." [x] (if (identical? *target* "nodejs") @@ -444,7 +444,7 @@ (declare apply) -(defn ^array make-array +(defn make-array "Construct a JavaScript array of the specified dimensions. Accepts ignored type argument for compatibility with Clojure. Note that there is no efficient way to allocate multi-dimensional arrays in JavaScript; as such, this function @@ -1055,8 +1055,8 @@ (bit-xor (-hash o) 0) (number? o) - (if ^boolean (js/isFinite o) - (if-not ^boolean (.isSafeInteger js/Number o) + (if (js/isFinite o) + (if-not (.isSafeInteger js/Number o) (hash-double o) (js-mod (Math/floor o) 2147483647)) (case o @@ -2355,7 +2355,7 @@ reduces them without incurring seq initialization" "Returns true if n is a JavaScript number with no decimal part." [n] (and (number? n) - (not ^boolean (js/isNaN n)) + (not (js/isNaN n)) (not (identical? n js/Infinity)) (== (js/parseFloat n) (js/parseInt n 10)))) @@ -2432,7 +2432,7 @@ reduces them without incurring seq initialization" (or (identical? x js/Number.POSITIVE_INFINITY) (identical? x js/Number.NEGATIVE_INFINITY))) -(defn contains? +(defn ^boolean contains? "Returns true if key is present in the given collection, otherwise returns false. Note that for numerically indexed collections like vectors and arrays, this tests if the numeric key is within the @@ -2462,12 +2462,12 @@ reduces them without incurring seq initialization" (contains? coll k)) (MapEntry. k (get coll k) nil)))) -(defn ^boolean distinct? +(defn distinct? "Returns true if no two of the arguments are =" ([x] true) ([x y] (not (= x y))) ([x y & more] - (if (not (= x y)) + (if (not (= x y)) (loop [s #{x y} xs more] (let [x (first xs) etc (next xs)] @@ -8351,6 +8351,7 @@ reduces them without incurring seq initialization" (if (identical? node root) nil (set! root node)) + ;; FIXME: can we figure out something better here? (if ^boolean (.-val added-leaf?) (set! count (inc count))) tcoll)) @@ -8372,6 +8373,7 @@ reduces them without incurring seq initialization" (if (identical? node root) nil (set! root node)) + ;; FIXME: can we figure out something better here? (if ^boolean (.-val removed-leaf?) (set! count (dec count))) tcoll))) @@ -10562,6 +10564,7 @@ reduces them without incurring seq initialization" (pr-writer (meta obj) writer opts) (-write writer " ")) (cond + ;; FIXME: can we figure out something better here? ;; handle CLJS ctors ^boolean (.-cljs$lang$type obj) (.cljs$lang$ctorPrWriter obj obj writer opts) @@ -10576,7 +10579,7 @@ reduces them without incurring seq initialization" (number? obj) (-write writer (cond - ^boolean (js/isNaN obj) "##NaN" + (js/isNaN obj) "##NaN" (identical? obj js/Number.POSITIVE_INFINITY) "##Inf" (identical? obj js/Number.NEGATIVE_INFINITY) "##-Inf" :else (str_ obj))) @@ -11942,7 +11945,7 @@ reduces them without incurring seq initialization" (fn [x y] (cond (pred x y) -1 (pred y x) 1 :else 0))) -(defn ^boolean special-symbol? +(defn special-symbol? "Returns true if x names a special form" [x] (contains? @@ -12175,6 +12178,8 @@ reduces them without incurring seq initialization" Object (findInternedVar [this sym] (let [k (munge (str_ sym))] + ;; FIXME: this shouldn't need ^boolean due to GCL library analysis, + ;; but not currently working (when ^boolean (gobject/containsKey obj k) (let [var-sym (symbol (str_ name) (str_ sym)) var-meta {:ns this}] @@ -12268,7 +12273,7 @@ reduces them without incurring seq initialization" (when (nil? NS_CACHE) (set! NS_CACHE (atom {}))) (let [ns-str (str_ ns) - ns (if (not ^boolean (gstring/contains ns-str "$macros")) + ns (if (not (gstring/contains ns-str "$macros")) (symbol (str_ ns-str "$macros")) ns) the-ns (get @NS_CACHE ns)] @@ -12292,7 +12297,7 @@ reduces them without incurring seq initialization" [x] (instance? goog.Uri x)) -(defn ^boolean NaN? +(defn NaN? "Returns true if num is NaN, else false" [val] (js/isNaN val)) @@ -12321,6 +12326,7 @@ reduces them without incurring seq initialization" [s] (if (string? s) (cond + ;; FIXME: another cases worth thinking about ^boolean (re-matches #"[\x00-\x20]*[+-]?NaN[\x00-\x20]*" s) ##NaN ^boolean (re-matches #"[\x00-\x20]*[+-]?(Infinity|((\d+\.?\d*|\.\d+)([eE][+-]?\d+)?)[dDfF]?)[\x00-\x20]*" diff --git a/src/main/clojure/cljs/analyzer.cljc b/src/main/clojure/cljs/analyzer.cljc index a13c08545..2a6364c8c 100644 --- a/src/main/clojure/cljs/analyzer.cljc +++ b/src/main/clojure/cljs/analyzer.cljc @@ -980,10 +980,10 @@ (defn normalize-js-tag [x] ;; if not 'js, assume constructor (if-not (= 'js x) - (with-meta 'js - {:prefix (conj (->> (string/split (name x) #"\.") - (map symbol) vec) - 'prototype)}) + (let [props (->> (string/split (name x) #"\.") (map symbol)) + [xs y] ((juxt butlast last) props)] + (with-meta 'js + {:prefix (vec (concat xs [(with-meta y {:ctor true})]))})) x)) (defn ->type-set @@ -1030,46 +1030,89 @@ boolean Boolean symbol Symbol}) -(defn has-extern?* +(defn resolve-extern + "Given a foreign js property list, return a resolved js property list and the + extern var info" + ([pre] + (resolve-extern pre (get-externs))) ([pre externs] - (let [pre (if-some [me (find - (get-in externs '[Window prototype]) - (first pre))] - (if-some [tag (-> me first meta :tag)] - (into [tag 'prototype] (next pre)) - pre) - pre)] - (has-extern?* pre externs externs))) - ([pre externs top] + (resolve-extern pre externs externs {:resolved []})) + ([pre externs top ret] (cond - (empty? pre) true + (empty? pre) ret :else (let [x (first pre) me (find externs x)] (cond - (not me) false + (not me) nil :else (let [[x' externs'] me - xmeta (meta x')] - (if (and (= 'Function (:tag xmeta)) (:ctor xmeta)) - (or (has-extern?* (into '[prototype] (next pre)) externs' top) - (has-extern?* (next pre) externs' top) - ;; check base type if it exists - (when-let [super (:super xmeta)] - (has-extern?* (into [super] (next pre)) externs top))) - (recur (next pre) externs' top)))))))) + info' (meta x') + ret (cond-> ret + ;; we only care about var info for the last property + ;; also if we already added it, don't override it + ;; because we're now resolving type information + ;; not instance information anymore + ;; i.e. [console] -> [Console] but :tag is Console _not_ Function vs. + ;; [console log] -> [Console prototype log] where :tag is Function + (and (empty? (next pre)) + (not (contains? ret :info))) + (assoc :info info'))] + ;; handle actual occurrences of types, i.e. `Console` + (if (and (or (:ctor info') (:iface info')) (= 'Function (:tag info'))) + (or + ;; then check for "static" property + (resolve-extern (next pre) externs' top + (update ret :resolved conj x)) + + ;; first look for a property on the prototype + (resolve-extern (into '[prototype] (next pre)) externs' top + (update ret :resolved conj x)) + + ;; finally check the super class if there is one + (when-let [super (:super info')] + (resolve-extern (into [super] (next pre)) externs top + (assoc ret :resolved [])))) + + (or + ;; If the tag of the property isn't Function or undefined, + ;; try to resolve it similar to the super case above, + ;; this handles singleton cases like `console` + (let [tag (:tag info')] + (when (and tag (not (contains? '#{Function undefined} tag))) + ;; check prefix first, during cljs.externs parsing we always generate prefixes + ;; for tags because of types like webCrypto.Crypto + (resolve-extern (into (or (-> tag meta :prefix) [tag]) (next pre)) externs top + (assoc ret :resolved [])))) + + ;; assume static property + (recur (next pre) externs' top + (update ret :resolved conj x)))))))))) + +(defn normalize-unresolved-prefix + [pre] + (cond-> pre + (< 1 (count pre)) + (cond-> + (-> pre pop peek meta :ctor) + (-> pop + (conj 'prototype) + (conj (peek pre)))))) + +(defn has-extern?* + [pre externs] + (boolean (resolve-extern pre externs))) (defn has-extern? ([pre] (has-extern? pre (get-externs))) ([pre externs] (or (has-extern?* pre externs) - (when (= 1 (count pre)) - (let [x (first pre)] - (or (get-in externs (conj '[Window prototype] x)) - (get-in externs (conj '[Number] x))))) (-> (last pre) str (string/starts-with? "cljs$"))))) +(defn lift-tag-to-js [tag] + (symbol "js" (str (alias->type tag tag)))) + (defn js-tag ([pre] (js-tag pre :tag)) @@ -1078,12 +1121,13 @@ ([pre tag-type externs] (js-tag pre tag-type externs externs)) ([pre tag-type externs top] - (when-let [[p externs' :as me] (find externs (first pre))] - (let [tag (-> p meta tag-type)] - (if (= (count pre) 1) - (when tag (symbol "js" (str (alias->type tag tag)))) - (or (js-tag (next pre) tag-type externs' top) - (js-tag (into '[prototype] (next pre)) tag-type (get top tag) top))))))) + (when-let [tag (get-in (resolve-extern pre externs) [:info tag-type])] + (case tag + ;; don't lift these, analyze-dot will raise them for analysis + ;; representing these types as js/Foo is a hassle as it widens the + ;; return types unnecessarily i.e. #{boolean js/Boolean} + (boolean number string) tag + (lift-tag-to-js tag))))) (defn dotted-symbol? [sym] (let [s (str sym)] @@ -1274,8 +1318,9 @@ (assoc shadowed-by-local :op :local)) :else - (let [pre (->> (string/split (name sym) #"\.") (map symbol) vec)] - (when (and (not (has-extern? pre)) + (let [pre (->> (string/split (name sym) #"\.") (map symbol) vec) + res (resolve-extern (->> (string/split (name sym) #"\.") (map symbol) vec))] + (when (and (not res) ;; ignore exists? usage (not (-> sym meta ::no-resolve))) (swap! env/*compiler* update-in @@ -1284,10 +1329,12 @@ {:name sym :op :js-var :ns 'js - :tag (with-meta (or (js-tag pre) (:tag (meta sym)) 'js) {:prefix pre})} + :tag (with-meta (or (js-tag pre) (:tag (meta sym)) 'js) + {:prefix pre + :ctor (-> res :info :ctor)})} (when-let [ret-tag (js-tag pre :ret-tag)] {:js-fn-var true - :ret-tag ret-tag}))))) + :ret-tag ret-tag}))))) (let [s (str sym) lb (handle-symbol-local sym (get locals sym)) current-ns (-> env :ns :name)] @@ -2585,12 +2632,12 @@ :children [:expr]})) (def js-prim-ctor->tag - '{js/Object object - js/String string - js/Array array - js/Number number + '{js/Object object + js/String string + js/Array array + js/Number number js/Function function - js/Boolean boolean}) + js/Boolean boolean}) (defn prim-ctor? "Test whether a tag is a constructor for a JS primitive" @@ -3543,13 +3590,25 @@ (list* '. dot-form) " with classification " (classify-dot-form dot-form)))))) +;; this only for a smaller set of types that we want to infer +;; we don't generally want to consider function for example, these +;; specific cases are ones we either try to optimize or validate +(def ^{:private true} + tag->js-prim-ctor + '{string js/String + array js/Array + number js/Number + boolean js/Boolean}) + (defn analyze-dot [env target field member+ form] (let [v [target field member+] {:keys [dot-action target method field args]} (build-dot-form v) enve (assoc env :context :expr) targetexpr (analyze enve target) form-meta (meta form) - target-tag (:tag targetexpr) + target-tag (as-> (:tag targetexpr) $ + (or (some-> $ meta :ctor lift-tag-to-js) + (tag->js-prim-ctor $ $))) prop (or field method) tag (or (:tag form-meta) (and (js-tag? target-tag) @@ -3581,7 +3640,8 @@ (let [pre (-> tag meta :prefix)] (when-not (has-extern? pre) (swap! env/*compiler* update-in - (into [::namespaces (-> env :ns :name) :externs] pre) merge {})))) + (into [::namespaces (-> env :ns :name) :externs] + (normalize-unresolved-prefix pre)) merge {})))) (case dot-action ::access (let [children [:target]] {:op :host-field diff --git a/src/main/clojure/cljs/compiler.cljc b/src/main/clojure/cljs/compiler.cljc index b96c09b36..fcc03ab96 100644 --- a/src/main/clojure/cljs/compiler.cljc +++ b/src/main/clojure/cljs/compiler.cljc @@ -641,7 +641,8 @@ (defn safe-test? [env e] (let [tag (ana/infer-tag env e)] - (or (#{'boolean 'seq} tag) (truthy-constant? e)))) + (or ('#{boolean seq} (ana/js-prim-ctor->tag tag tag)) + (truthy-constant? e)))) (defmethod emit* :if [{:keys [test then else env unchecked]}] diff --git a/src/main/clojure/cljs/externs.clj b/src/main/clojure/cljs/externs.clj index c5343e1b1..d25987cde 100644 --- a/src/main/clojure/cljs/externs.clj +++ b/src/main/clojure/cljs/externs.clj @@ -61,12 +61,23 @@ (and (= type :string-lit) (= "undefined" value))) +(defn add-prefix + "Externs inference uses :prefix meta to both resolve externs as well as generate + missing externs information. Google Closure Compiler default externs includes + nested types like webCrypto.Crypto. Add prefix information to the returned symbol to + simplify resolution later." + [type-str] + (with-meta (symbol type-str) + {:prefix (->> (string/split (name type-str) #"\.") + (map symbol) vec)})) + (defn simplify-texpr [texpr] (case (:type texpr) - :string-lit (some-> (:value texpr) symbol) - (:star :qmark) 'any - :bang (simplify-texpr (-> texpr :children first)) + :string-lit (-> texpr :value add-prefix) + :star 'any + ;; TODO: qmark should probably be #{nil T} + (:qmark :bang) (simplify-texpr (-> texpr :children first)) :pipe (let [[x y] (:children texpr)] (if (undefined? y) (simplify-texpr x) diff --git a/src/test/clojure/cljs/compiler_tests.clj b/src/test/clojure/cljs/compiler_tests.clj index bb6a9bfc3..95204e650 100644 --- a/src/test/clojure/cljs/compiler_tests.clj +++ b/src/test/clojure/cljs/compiler_tests.clj @@ -15,7 +15,8 @@ [cljs.util :as util] [cljs.tagged-literals :as tags] [clojure.java.io :as io] - [clojure.string :as str]) + [clojure.string :as str] + [clojure.test :as test]) (:import [java.io File])) (defn analyze @@ -374,6 +375,22 @@ window))]))] (is (re-find #"window__\$1" code))))) +(deftest test-externs-infer-is-nan + (testing "Not calls to truth_ if (.isNaN js/Number ...) is used as a test" + (let [code (env/with-compiler-env (env/default-compiler-env) + (compile-form-seq + '[(if (.isNaN js/Number 1) true false)]))] + (is (nil? (re-find #"truth_" code)))))) + +(deftest test-goog-lib-infer-boolean + (testing "Can infer goog.string/contains returns boolean" + (let [code (env/with-compiler-env (env/default-compiler-env) + (compile-form-seq + '[(ns test.foo + (:require [goog.string :as gstring])) + (if (gstring/contains "foobar" "foo") true false)]))] + (is (nil? (re-find #"truth_" code)))))) + ;; CLJS-1225 (comment diff --git a/src/test/clojure/cljs/externs_infer_tests.clj b/src/test/clojure/cljs/externs_infer_tests.clj index 8ca7ff9aa..967164d1f 100644 --- a/src/test/clojure/cljs/externs_infer_tests.clj +++ b/src/test/clojure/cljs/externs_infer_tests.clj @@ -23,6 +23,35 @@ "goog.isArrayLike;" "Java.type;" "Object.out;" "Object.out.println;" "Object.error;" "Object.error.println;"]) +(deftest test-normalize-js-tag + (is (= 'js (ana/normalize-js-tag 'js))) + (is (= '[Foo] (-> 'js/Foo ana/normalize-js-tag meta :prefix))) + (is (true? (-> 'js/Foo ana/normalize-js-tag meta :prefix last meta :ctor))) + (is (= '[Foo Bar] (-> 'js/Foo.Bar ana/normalize-js-tag meta :prefix))) + (is (true? (-> 'js/Foo.Bar ana/normalize-js-tag meta :prefix last meta :ctor)))) + +(deftest test-normalize-unresolved-prefix + (let [pre (-> (ana/normalize-js-tag 'js/Foo) meta :prefix (conj 'bar))] + (is (= '[Foo prototype bar] (ana/normalize-unresolved-prefix pre)))) + (let [pre '[Foo bar]] + (is (= '[Foo bar] (ana/normalize-unresolved-prefix pre))))) + +(comment + + (test/test-vars [#'test-normalize-js-tag]) + (test/test-vars [#'test-normalize-unresolved-prefix]) + + ) + +(deftest test-resolve-extern + (let [externs + (externs/externs-map + (closure/load-externs + {:externs ["src/test/externs/test.js"] + :use-only-custom-externs true}))] + (is (some? (ana/resolve-extern '[baz] externs))) + (is (nil? (ana/resolve-extern '[Foo gozMethod] externs))))) + (deftest test-has-extern?-basic (let [externs (externs/externs-map (closure/load-externs @@ -35,6 +64,35 @@ (is (true? (ana/has-extern? '[baz] externs))) (is (false? (ana/has-extern? '[Baz] externs))))) +(deftest test-resolve-extern + (let [externs (externs/externs-map)] + (is (= '[Number] + (-> (ana/resolve-extern '[Number] externs) :resolved))) + (is (= '[Number prototype valueOf] + (-> (ana/resolve-extern '[Number valueOf] externs) :resolved))) + (is (= '[Console] + (-> (ana/resolve-extern '[console] externs) :resolved))) + (is (= '[Console prototype log] + (-> (ana/resolve-extern '[console log] externs) :resolved))) + (is (= '[undefined] + (-> (ana/resolve-extern '[undefined] externs) :resolved))) + (is (= '[webCrypto Crypto prototype subtle] + (-> (ana/resolve-extern '[crypto subtle] externs) :resolved))))) + +(comment + (clojure.test/test-vars [#'test-resolve-extern]) + + (def externs (externs/externs-map)) + ;; succeeds + (ana/resolve-extern '[console] externs) + (ana/resolve-extern '[console log] externs) + (ana/resolve-extern '[undefined] externs) + (ana/resolve-extern '[Number] externs) + (ana/resolve-extern '[Number isNaN] externs) + (ana/resolve-extern '[document] externs) + + ) + (deftest test-has-extern?-defaults (let [externs (externs/externs-map)] (is (true? (ana/has-extern? '[console] externs))) @@ -47,9 +105,16 @@ {:externs ["src/test/externs/test.js"]}))] (is (= 'js/Console (ana/js-tag '[console] :tag externs))) (is (= 'js/Function (ana/js-tag '[console log] :tag externs))) - (is (= 'js/Boolean (ana/js-tag '[Number isNaN] :ret-tag externs))) + (is (= 'js/undefined (ana/js-tag '[console log] :ret-tag externs))) + (is (= 'boolean (ana/js-tag '[Number isNaN] :ret-tag externs))) (is (= 'js/Foo (ana/js-tag '[baz] :ret-tag externs))))) +(comment + + (clojure.test/test-vars [#'test-js-tag]) + + ) + (defn infer-test-helper [{:keys [forms externs warnings warn js-dependency-index node-module-index with-core? opts]}] (let [test-cenv (atom @@ -82,6 +147,54 @@ (map (comp :externs second) (get @test-cenv ::ana/namespaces)))))))))))) +(deftest test-externs-type-infer + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(.isNaN js/Number 1)))) + :tag))) + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(js/Number.isNaN 1)))) + :tag))) + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(let [x js/Number] + (.isNaN x 1))))) + :tag))) + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(js/isNaN 1)))) + :tag))) + (is (= 'js/Promise + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(.generateKey js/crypto.subtle)))) + :tag))) + (is (= 'string + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(.toUpperCase "foo")))) + :tag))) + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(.isArray js/Array (array))))) + :tag))) + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(.isSafeInteger js/Number 1)))) + :tag))) + (is (= 'boolean + (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] + (env/with-compiler-env (env/default-compiler-env) + (analyze (ana/empty-env) '(js/isFinite 1)))) + :tag)))) + (deftest test-externs-infer (is (= 'js/Foo (-> (binding [ana/*cljs-ns* ana/*cljs-ns*] @@ -158,9 +271,9 @@ :warnings ws})] (is (= (unsplit-lines ["Foo.Boo.prototype.wozz;"]) res)) (is (= 1 (count @ws))) - (is (string/starts-with? - (first @ws) - "Cannot resolve property wozz for inferred type js/Foo.Boo")))) + (is (some-> @ws first + (string/starts-with? + "Cannot resolve property wozz for inferred type js/Foo.Boo"))))) (deftest test-type-hint-infer-unknown-property-in-chain (let [ws (atom []) @@ -172,9 +285,9 @@ :warnings ws})] (is (= (unsplit-lines ["Foo.Boo.prototype.wozz;"]) res)) (is (= 1 (count @ws))) - (is (string/starts-with? - (first @ws) - "Cannot resolve property wozz for inferred type js/Foo.Boo")))) + (is (some-> @ws first + (string/starts-with? + "Cannot resolve property wozz for inferred type js/Foo.Boo"))))) (deftest test-type-hint-infer-unknown-method (let [ws (atom []) @@ -185,9 +298,24 @@ :warnings ws})] (is (= (unsplit-lines ["Foo.prototype.gozMethod;"]) res)) (is (= 1 (count @ws))) - (is (string/starts-with? - (first @ws) - "Cannot resolve property gozMethod for inferred type js/Foo")))) + (is (some-> @ws first + (string/starts-with? + "Cannot resolve property gozMethod for inferred type js/Foo"))))) + +(comment + + (require '[clojure.java.io :as io] + '[cljs.closure :as cc]) + + (def externs + (-> (cc/js-source-file nil (io/file "src/test/externs/test.js")) + externs/parse-externs externs/index-externs)) + + (ana/resolve-extern '[Foo gozMethod] externs) + + (clojure.test/test-vars [#'test-type-hint-infer-unknown-method]) + + ) (deftest test-infer-unknown-method-from-externs (let [ws (atom []) @@ -197,9 +325,9 @@ :warnings ws})] (is (= (unsplit-lines ["Foo.prototype.gozMethod;"]) res)) (is (= 1 (count @ws))) - (is (string/starts-with? - (first @ws) - "Cannot resolve property gozMethod for inferred type js/Foo")))) + (is (some-> @ws first + (string/starts-with? + "Cannot resolve property gozMethod for inferred type js/Foo"))))) (deftest test-infer-js-require (let [ws (atom []) @@ -211,9 +339,9 @@ :warnings ws})] (is (= (unsplit-lines ["var require;" "Object.Component;"]) res)) (is (= 1 (count @ws))) - (is (string/starts-with? - (first @ws) - "Adding extern to Object for property Component")))) + (is (some-> @ws first + (string/starts-with? + "Adding extern to Object for property Component"))))) (deftest test-set-warn-on-infer (let [ws (atom []) @@ -227,7 +355,9 @@ :warn false :with-core? true})] (is (= 1 (count @ws))) - (is (string/starts-with? (first @ws) "Cannot infer target type")))) + (is (some-> @ws first + (string/starts-with? + "Cannot infer target type"))))) (deftest test-cljs-1970-infer-with-cljs-literals (let [ws (atom []) diff --git a/src/test/clojure/cljs/externs_parsing_tests.clj b/src/test/clojure/cljs/externs_parsing_tests.clj index ed0cfdb70..e5a399c84 100644 --- a/src/test/clojure/cljs/externs_parsing_tests.clj +++ b/src/test/clojure/cljs/externs_parsing_tests.clj @@ -37,6 +37,12 @@ (is (= 'any (get-in ns [:defs 'get :ret-tag]))) (is (= 'array (get-in ns [:defs 'getKeys :ret-tag]))))) +(comment + ;; works + (get-in (externs/analyze-goog-file "goog/object/object.js") + [:defs 'containsKey :ret-tag]) + ) + (deftest test-parse-super (let [info (-> (filter diff --git a/src/test/clojure/cljs/type_inference_tests.clj b/src/test/clojure/cljs/type_inference_tests.clj index c9c5f6343..5435cc90f 100644 --- a/src/test/clojure/cljs/type_inference_tests.clj +++ b/src/test/clojure/cljs/type_inference_tests.clj @@ -307,12 +307,32 @@ (is (= (env/with-compiler-env test-cenv (:tag (analyze test-env '(dissoc {:foo :bar} :foo)))) '#{clj clj-nil})) + (is (= (env/with-compiler-env test-cenv + (:tag (analyze test-env '(distinct? 1)))) + 'boolean)) + (is (= (env/with-compiler-env test-cenv + (:tag (analyze test-env '(special-symbol? 'foo)))) + 'boolean)) + ;; TODO: we can't infer isa?, we get 'any which is a bit surprising + ;(is (= (env/with-compiler-env test-cenv + ; (:tag (analyze test-env '(isa? ::foo :bar)))) + ; 'boolean)) ;; has changed, why does this return #{clj any} ? ;(is (= (env/with-compiler-env test-cenv ; (:tag (analyze test-env '(assoc nil :foo :bar)))) ; 'clj)) ) +(deftest lib-inference-extern-call + (testing "Test return type inference for core fns whose + internal implementation uses standard JS APIs" + (is (= 'boolean + (env/with-compiler-env test-cenv + (:tag (analyze test-env '(array? (array))))))) + (is (= 'array + (env/with-compiler-env test-cenv + (:tag (analyze test-env '(make-array js/String. 10)))))))) + (deftest test-always-true-if (is (= (env/with-compiler-env test-cenv (:tag (analyze test-env '(if 1 2 "foo")))) @@ -374,3 +394,23 @@ (:import [goog.history Html5History])) (Html5History.)] {} true)))))) + +(deftest test-goog-infer + (is (= 'boolean + (:tag (env/with-compiler-env (env/default-compiler-env) + (ana/analyze-form-seq + '[(ns test.foo + (:require [goog.string :as gstring])) + (gstring/contains "foobar" "foo")] + {} true))))) + ;; FIXME: infers any instead of boolean, nothing wrong w/ the externs parsing + ;; but this definitely does not work at the moment + #_(is (= 'boolean + (:tag + (env/with-compiler-env (env/default-compiler-env) + (ana/analyze-form-seq + '[(ns test.foo + (:require [goog.object :as gobject])) + (gobject/containsKey (js-object) "foo")] + {} true)))))) +