Working on this issue now: https://github.com/clj-kondo/clj-kondo/issues/916
tl;dr: you should not refer to a destructured value in the same map, inside of an :or because the order of a map is undefined
example:
{:keys [a b] :or {a b}}
usually this code works, but it's by accident.
if we swap a and b in :keys it already falls apart:
user=> (let [[{:keys [b a] :or {b (inc a)}}] {}])
Syntax error compiling at (REPL:1:28).
Unable to resolve symbol: a in this context
While implementing the linter I found this case: https://github.com/clojure/clojurescript/blob/47a7bfdefd156027b7b98cb483ddd93add71fbb2/src/main/clojure/cljs/repl.cljc#L1051
Not only does this example use a binding in :or from the same destructuring map, the value is even more undeterministic since it depends on a default in the same :or mapI'm looking for a better name (if possible) for:
:destructured-or-refers-bindings-of-same-map
Perhaps :ambiguous-keys-destructuring or :nondeterministic-keys-destructuring ? Those are less specific, for better or worse.
Another alternative: :nondeterministic-keys-or-destructuring
this linter is specifically about the :or clause
How about :or-clause-key-bindings-in-destructure-ambiguous
The nice thing about starting with :or is that it matches what it is talking about and won't be misread as a boolean,
:or-destructure-ambiguous might be too vague?
:or-on-destructured-map-ambiguous?
:or-on-keys-in-destructured-map-ambiguous?
I think I'll land on :destructured-or-binding-of-same-map. Thanks for the input!
same issue with {:keys [flush] :or {flush flush}} right?
the similarities being that in one case it resolves to a var, the other it resolves to a local depending on the order of your map.
your example is supposed to work I think, due to how destructuring expands, flush is only ever defined as a local once and there's a get expression that gets the :or fallback
that case is actually explicitly supported here: https://github.com/clj-kondo/clj-kondo/blob/c7b00f056fb79ed7207f67a67e4c34b718a41c24/src/clj_kondo/impl/analyzer.clj#L121
https://github.com/clj-kondo/clj-kondo/issues/915 see also Alex's comment in there
on first glance Alex's comment is self-contradictory. I'll look into it.
Yeah I see now how it can never be resolved to a local with the current impl. FWIW I think with the license Alex gives in the first sentence this is a valid implementation of destructuring (which the default resolves to a local).
(let [{:keys [a] :or {a a}} {}] a)
=>
(let [_m {}
_not-found (Object.)
a (clojure.core/get _m :a _not-found)
_default a
a (if (identical? a _not-found)
_default
a)]
a)you mean, it works only with the current impl? yeah, fair enough
yeah pretty much. you're probably making the right call though.
I don't have to like it of course 😉
what would be the thing you would like, btw?
I'm not 100% sure if I've understood that ;)
a huge document describing what is undefined behavior in a "clojure" 😉
personally I would disallow this in Typed Clojure, but clj-kondo is a different beast and I think you're the best person to decide.
I think the above thing is just one special case of "a clojure map is unordered" and that's why certain things can go wrong in map destructuring combinations, that's what the check is about. So it very much checks against the current implementation and what can go wrong with that, not against a standard (because there isn't one). Your alternative impl of destructuring would come with yet other problems, clj-kondo can be adapted if clojure might change to a different implementation
I'm with you, I think it's a judgement call. For example, you decided in the OP you need to disallow the first form because swapping the vector would change the semantics. But given that small maps are ordered, in some ways it's deterministic AFAICT. But since it's error-prone, I think we both agree that it should be picked up.
This one is also pretty interesting:
((fn [{:keys [a b c] :foo/keys [d e f] :or {b (inc f)}}] [a b c d e f]) {:a 42 :foo/f 13})posted by Sean in that issue
blows up for me
does it sometimes work?
it does when you change :or to :or {d (inc a)}
ahh!
it again has to do with the ordering of the locals, I guess
here's one that will break your brain I found at work today. is this wrong? (plumatic schema)
(s/defn a [{:keys [a :- s/Num]}] ...)
clj-kondo said it was fine. hard to disagree
the correct syntax is:
(s/defn a [{:keys [a]} :- {:a s/Num}] ...)perhaps it's more correct to say that clj-kondo didn't say it was wrong ;)
haha! completely deterministic, obviously take a {:a 1, :- 2, :s/Num 3}
is this about plumatic or is this also about (defn a [a] a)?
oh, no sorry that's a subject change.
or, elaborating my thoughts on error-prone vs deterministic.
hmm:
user=> (defn foo [{:keys [s/Num]}] Num)
#'user/foo
user=> (foo {:s/Num 1})
1yeah I guess this is valid ;)
I'm not sure if plumatic itself allows this?
it does
as you said, it's valid xD
now let's wait for someone to abuse this to make a kinda typed clojure with existing syntax
noooooooo
which is valid normal clojure
what have I done
don't tell #malli
there's a wild experimental defn macro in malli that probably already does this.