clj-kondo

borkdude 2024-08-02T17:18:37.946809Z

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 map

1
🆒 1
😅 1
borkdude 2024-08-03T08:43:14.668009Z

I'm looking for a better name (if possible) for:

:destructured-or-refers-bindings-of-same-map

teodorlu 2024-08-03T09:17:15.071109Z

Perhaps :ambiguous-keys-destructuring or :nondeterministic-keys-destructuring ? Those are less specific, for better or worse. Another alternative: :nondeterministic-keys-or-destructuring

borkdude 2024-08-03T10:31:19.937209Z

this linter is specifically about the :or clause

lread 2024-08-03T11:28:08.361809Z

How about :or-clause-key-bindings-in-destructure-ambiguous

lread 2024-08-03T11:29:54.620309Z

The nice thing about starting with :or is that it matches what it is talking about and won't be misread as a boolean,

lread 2024-08-03T11:31:24.714129Z

:or-destructure-ambiguous might be too vague?

lread 2024-08-03T11:43:32.232989Z

:or-on-destructured-map-ambiguous?

lread 2024-08-03T12:05:08.378219Z

:or-on-keys-in-destructured-map-ambiguous?

borkdude 2024-08-05T08:58:34.748389Z

I think I'll land on :destructured-or-binding-of-same-map. Thanks for the input!

👍 2
2024-08-02T19:57:42.760319Z

same issue with {:keys [flush] :or {flush flush}} right?

2024-08-02T20:01:13.632409Z

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.

borkdude 2024-08-02T20:19:09.348689Z

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

borkdude 2024-08-02T20:20:22.079849Z

that case is actually explicitly supported here: https://github.com/clj-kondo/clj-kondo/blob/c7b00f056fb79ed7207f67a67e4c34b718a41c24/src/clj_kondo/impl/analyzer.clj#L121

borkdude 2024-08-02T20:21:34.886959Z

https://github.com/clj-kondo/clj-kondo/issues/915 see also Alex's comment in there

2024-08-02T20:44:47.365819Z

on first glance Alex's comment is self-contradictory. I'll look into it.

2024-08-02T21:01:25.704089Z

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)

borkdude 2024-08-02T21:15:35.149219Z

you mean, it works only with the current impl? yeah, fair enough

2024-08-02T21:23:33.706549Z

yeah pretty much. you're probably making the right call though.

2024-08-02T21:23:46.013429Z

I don't have to like it of course 😉

borkdude 2024-08-02T21:24:40.876459Z

what would be the thing you would like, btw?

borkdude 2024-08-02T21:24:50.985039Z

I'm not 100% sure if I've understood that ;)

2024-08-02T21:25:24.525849Z

a huge document describing what is undefined behavior in a "clojure" 😉

➕ 1
2024-08-02T21:26:43.141889Z

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.

borkdude 2024-08-02T21:28:05.952629Z

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

2024-08-02T21:37:44.247239Z

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.

borkdude 2024-08-02T21:40:48.310859Z

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})

borkdude 2024-08-02T21:41:01.918249Z

posted by Sean in that issue

2024-08-02T21:41:24.204639Z

blows up for me

2024-08-02T21:41:34.125749Z

does it sometimes work?

borkdude 2024-08-02T21:42:21.637739Z

it does when you change :or to :or {d (inc a)}

2024-08-02T21:42:33.711549Z

ahh!

borkdude 2024-08-02T21:42:36.446759Z

it again has to do with the ordering of the locals, I guess

2024-08-02T21:43:20.927769Z

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]}] ...)

2024-08-02T21:44:09.106669Z

clj-kondo said it was fine. hard to disagree

2024-08-02T21:45:07.619949Z

the correct syntax is:

(s/defn a [{:keys [a]} :- {:a s/Num}] ...)

borkdude 2024-08-02T21:46:03.385289Z

perhaps it's more correct to say that clj-kondo didn't say it was wrong ;)

2024-08-02T21:47:02.911469Z

haha! completely deterministic, obviously take a {:a 1, :- 2, :s/Num 3}

😆 1
borkdude 2024-08-02T21:47:15.015579Z

is this about plumatic or is this also about (defn a [a] a)?

2024-08-02T21:48:21.194889Z

oh, no sorry that's a subject change.

2024-08-02T21:48:50.912229Z

or, elaborating my thoughts on error-prone vs deterministic.

borkdude 2024-08-02T21:49:10.570599Z

hmm:

user=> (defn foo [{:keys [s/Num]}] Num)
#'user/foo
user=> (foo {:s/Num 1})
1

borkdude 2024-08-02T21:49:24.249369Z

yeah I guess this is valid ;)

💯 1
borkdude 2024-08-02T21:49:38.301979Z

I'm not sure if plumatic itself allows this?

2024-08-02T21:49:43.338689Z

it does

2024-08-02T21:49:52.002009Z

as you said, it's valid xD

borkdude 2024-08-02T21:50:34.370279Z

now let's wait for someone to abuse this to make a kinda typed clojure with existing syntax

2024-08-02T21:50:57.186939Z

noooooooo

borkdude 2024-08-02T21:50:57.368069Z

which is valid normal clojure

2024-08-02T21:51:06.208509Z

what have I done

borkdude 2024-08-02T21:51:26.244979Z

don't tell #malli

2024-08-02T21:51:59.256989Z

there's a wild experimental defn macro in malli that probably already does this.