Skip to content

Commit 9159fe2

Browse files
committed
Fix NPE in resolve when called at runtime inside macro body
When `resolve` is called at runtime (e.g. inside a macro body during code walking), `lookup` may find bindings not set up by the analyzer (e.g. from `macroexpand-1`'s env merge). Skip the idx machinery (iden->invoke-idx, outer-idens, aget) when idx is nil, returning [k v] directly instead.
1 parent 234cd05 commit 9159fe2

File tree

3 files changed

+45
-30
lines changed

3 files changed

+45
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ SCI is used in [babashka](https://github.com/babashka/babashka),
1919
- `deftype` now macroexpands to `deftype*`, matching JVM Clojure, enabling code walkers like riddley
2020
- `case` now macroexpands to JVM-compatible `case*` format, enabling tools like riddley and cloverage to work with SCI
2121
- Preserve `:tag` metadata in `copy-var`
22+
- Fix NPE in `resolve` when called at runtime (e.g. inside a macro body during code walking) by skipping analysis-only idx machinery
2223
- Fix `.method` on class objects (e.g. `(.getDeclaredField String "value")`) routing to static instead of instance method path
2324
- `macroexpand-1` of `(.method ClassName)` now wraps class targets in `identity`, matching Clojure behavior
2425
- `macroexpand-1` now accepts an optional env map as first argument, like `cljs.analyzer/macroexpand-1`

src/sci/impl/resolve.cljc

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -172,34 +172,40 @@
172172
(if only-var?
173173
[k nil]
174174
(let [idx (or (get (:iden->invoke-idx ctx) v)
175-
(let [oi (:outer-idens ctx)
176-
ob (oi v)]
177-
(update-parents ctx (:closure-bindings ctx) ob)))
178-
#?@(:clj [tag (or (:tag m)
179-
(some-> k meta :tag))])
180-
mutable? (when track-mutable?
181-
(when-let [m (some-> k meta)]
182-
#?(:clj (or (:volatile-mutable m)
183-
(:unsynchronized-mutable m))
184-
:cljs (or (:mutable m)
185-
(:volatile-mutable m)))))
186-
v (if call? ;; resolve-symbol is already handled in the call case
187-
(mark-resolve-sym k idx)
188-
(let [v (cond-> (if mutable?
189-
(let [ext-map (second (lookup ctx '__sci_this false))]
190-
(->Node
191-
(let [this (sci.impl.types/eval ext-map ctx bindings)
192-
inner (sci.impl.types/getVal this)]
193-
(get inner sym))
194-
nil))
195-
(->Node
196-
(aget ^objects bindings idx)
197-
nil))
198-
#?@(:clj [tag (with-meta
199-
{:tag tag})])
200-
mutable? (vary-meta assoc :mutable true))]
201-
v))]
202-
[k v])))
175+
(when-let [oi (:outer-idens ctx)]
176+
(let [ob (oi v)]
177+
(update-parents ctx (:closure-bindings ctx) ob))))]
178+
(if (nil? idx)
179+
;; idx is only set during analysis. When lookup is
180+
;; called at runtime (e.g. resolve inside a macro body),
181+
;; bindings may contain entries not set up by the
182+
;; analyzer, so we skip the idx machinery.
183+
[k v]
184+
(let [#?@(:clj [tag (or (:tag m)
185+
(some-> k meta :tag))])
186+
mutable? (when track-mutable?
187+
(when-let [m (some-> k meta)]
188+
#?(:clj (or (:volatile-mutable m)
189+
(:unsynchronized-mutable m))
190+
:cljs (or (:mutable m)
191+
(:volatile-mutable m)))))
192+
v (if call? ;; resolve-symbol is already handled in the call case
193+
(mark-resolve-sym k idx)
194+
(let [v (cond-> (if mutable?
195+
(let [ext-map (second (lookup ctx '__sci_this false))]
196+
(->Node
197+
(let [this (sci.impl.types/eval ext-map ctx bindings)
198+
inner (sci.impl.types/getVal this)]
199+
(get inner sym))
200+
nil))
201+
(->Node
202+
(aget ^objects bindings idx)
203+
nil))
204+
#?@(:clj [tag (with-meta
205+
{:tag tag})])
206+
mutable? (vary-meta assoc :mutable true))]
207+
v))]
208+
[k v])))))
203209
(when-let [kv (lookup* ctx sym call? only-var?)]
204210
(when (:check-permissions ctx)
205211
(check-permission! ctx sym kv))

test/sci/core_test.cljc

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,15 @@
449449
(is (= [6] (eval* "[(-> 3 inc inc inc)]")))
450450
(is (= [{3 6}] (eval* "[{(->> 2 inc) (-> 3 inc inc inc)}]")))
451451
(is (eval* (str `(#(< 10 % 18) 15))))
452-
(is (eval* (str `(#(and (int? %) (< 10 % 18)))) 15)))
452+
(is (eval* (str `(#(and (int? %) (< 10 % 18)))) 15))
453+
#?(:clj
454+
(testing "macroexpand-1 with env does not NPE when macro calls resolve on a local"
455+
(is (= '(f 1)
456+
(tu/eval* "
457+
(defmacro resolving-macro [form]
458+
(resolve (first form))
459+
form)
460+
(macroexpand-1 '{f nil} '(resolving-macro (f 1)))" {}))))))
453461

454462
(deftest permission-test
455463
(is (tu/eval* "(int? 1)" {:allow '[int?]}))
@@ -1834,4 +1842,4 @@
18341842
(comment
18351843
(eval* 1 '(inc *in*))
18361844
(test-difference "foo" "[10 10]" 0 10)
1837-
(test-difference "rand" #(rand) 0 10))
1845+
(test-difference "rand" #(rand) 0 10))

0 commit comments

Comments
 (0)