Skip to content

Commit be95eb3

Browse files
committed
Tighten pattern parser, allow (? ?foo) binds
1 parent 90ea522 commit be95eb3

File tree

5 files changed

+340
-193
lines changed

5 files changed

+340
-193
lines changed

.clj-kondo/imports/io.github.noahtheduke/splint/hooks/noahtheduke/splint/rules.clj_kondo

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,15 @@
180180
finds (atom [])]
181181
(doseq [pattern (cons pattern patterns)]
182182
(check-pattern finds pattern))
183-
@finds)
183+
(seq @finds))
184184
new-node (api/list-node
185185
[(with-meta (api/token-node 'def) (meta defrule))
186186
(with-meta (api/token-node (symbol (name (api/sexpr rule-name))))
187187
(meta rule-name))
188188
docs
189-
(update m :children into [(api/token-node :used-predicates)
190-
(api/vector-node used-preds)])])]
189+
(if used-preds
190+
(update m :children into [(api/token-node :used-predicates)
191+
(api/vector-node used-preds)])
192+
m)])]
191193
(validate-rule m)
192194
{:node (with-meta new-node (meta node))}))

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ This changelog is loose. Versions are not semantic, they are incremental. Splint
44

55
## Unreleased
66

7+
BREAKING CHANGES:
8+
9+
* The pattern parser is less forgiving of mistakes, so custom patterns might break.
10+
* The pattern parser no longer resolves predicates, so `noahtheduke.splint.rules.helpers` functions must be referred in namespaces where they're used.
11+
712
### New Rules
813

914
- `lint/update-with-swap`: Prefer `(swap! (:counter state) + 5)` over `(update state :counter swap! + 5)`. (See [#30](https://github.com/NoahTheDuke/splint/issues/30).)
@@ -25,6 +30,12 @@ Update rules:
2530
- `performance/into-transducer`: remove `cat` as it can't be used in the incorrect form.
2631
- `performance/into-transducer`: add configurable fn list with `:fn-0-arg` and `:fn-1-arg` (depending on how many arguments the fn accepts).
2732

33+
Patterns:
34+
35+
- Binds can be written with a prepended question mark inside of special patterns: `(? ?foo)` is now equivalent to `(? foo)`.
36+
- Add additional checks to macroexpansion of patterns (incorrect number of args, invalid places to use a special pattern, etc).
37+
- No longer resolve predicates before usage when matching with a predicate. Now all predicates must be referred in a `:use` or `:require` call, matching normal Clojure behavior. This is done to simplify implementation and to make references work with clj-kondo/clojure-lsp.
38+
2839
Others:
2940

3041
- Bump `edamame` to `1.4.31` to support `#^` metadata and no-op reader conditionals.

resources/clj-kondo.exports/io.github.noahtheduke/splint/hooks/noahtheduke/splint/rules.clj_kondo

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,15 @@
180180
finds (atom [])]
181181
(doseq [pattern (cons pattern patterns)]
182182
(check-pattern finds pattern))
183-
@finds)
183+
(seq @finds))
184184
new-node (api/list-node
185185
[(with-meta (api/token-node 'def) (meta defrule))
186186
(with-meta (api/token-node (symbol (name (api/sexpr rule-name))))
187187
(meta rule-name))
188188
docs
189-
(update m :children into [(api/token-node :used-predicates)
190-
(api/vector-node used-preds)])])]
189+
(if used-preds
190+
(update m :children into [(api/token-node :used-predicates)
191+
(api/vector-node used-preds)])
192+
m)])]
191193
(validate-rule m)
192194
{:node (with-meta new-node (meta node))}))

src/noahtheduke/splint/pattern.clj

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
(ns noahtheduke.splint.pattern
66
(:require
7+
[clojure.string :as str]
78
[noahtheduke.splint.clojure-ext.core :refer [postwalk* vary-meta*]]
89
[noahtheduke.splint.utils :refer [drop-quote simple-type]]))
910

@@ -71,6 +72,26 @@
7172
`(throw (ex-info "default" {:type ~(read-dispatch ctx pattern form)
7273
:pattern '~pattern})))
7374

75+
(defmethod read-form :?*
76+
read-form--?*
77+
[_ctx _pattern _form]
78+
(throw (IllegalArgumentException. "`:?*` must be used in a surrounding sequence")))
79+
80+
(defmethod read-form :?+
81+
read-form--?*
82+
[_ctx _pattern _form]
83+
(throw (IllegalArgumentException. "`:?+` must be used in a surrounding sequence")))
84+
85+
(defmethod read-form :??
86+
read-form--?*
87+
[_ctx _pattern _form]
88+
(throw (IllegalArgumentException. "`:??` must be used in a surrounding sequence")))
89+
90+
(defmethod read-form :?|
91+
read-form--?*
92+
[_ctx _pattern _form]
93+
(throw (IllegalArgumentException. "`:?|` must be used in a surrounding sequence")))
94+
7495
(defmethod read-form :any
7596
read-form--any
7697
[ctx _pattern _form]
@@ -137,31 +158,39 @@
137158
`(if (#{'~'_ '~'?_} '~bind)
138159
~ctx
139160
(let [~children-form ~form]
140-
(if-let [existing# ^clojure.lang.MapEntry (find ~ctx '~bind)]
161+
(if-let [^clojure.lang.MapEntry existing# (find ~ctx '~bind)]
141162
(when (= (val existing#) ~children-form)
142163
~ctx)
143164
(assoc ~ctx '~bind ~children-form))))))
144165

145166
(defn match-pred
146167
[ctx bind form pred]
147-
(let [pred-name (name pred)
148-
pred (or (requiring-resolve (symbol (or (namespace pred) (str *ns*)) pred-name))
149-
(resolve (symbol "clojure.core" pred-name))
150-
(requiring-resolve (symbol "noahtheduke.splint.rules.helpers" pred-name)))
151-
children-form (gensym "pred-form-")]
168+
(let [children-form (gensym "pred-form-")]
152169
`(let [~children-form ~form]
153170
(when (~pred ~children-form)
154171
~(match-binding ctx bind children-form)))))
155172

173+
(defn coerce-bind
174+
[bind]
175+
(if (str/starts-with? (str bind) "?")
176+
bind
177+
(symbol (str "?" bind))))
178+
156179
(defmethod read-form :?
157180
read-form--?
158181
[ctx pattern form]
159-
(let [[_?sym bind & [pred]] pattern]
182+
(let [pattern (if (symbol? pattern) ['? pattern] pattern)
183+
[_?sym ?bind pred] pattern
184+
bind (coerce-bind ?bind)]
160185
(cond
186+
(not (#{2 3} (count pattern)))
187+
(throw (IllegalArgumentException. "? only accepts 1 or 2 arguments"))
188+
(and (= 3 (count pattern)) (not (symbol? pred)))
189+
(throw (IllegalArgumentException. "? pred must be a symbol"))
161190
(nil? pred)
162-
(match-binding ctx (symbol (str "?" bind)) form)
191+
(match-binding ctx bind form)
163192
(symbol? pred)
164-
(match-pred ctx (symbol (str "?" bind)) form pred)
193+
(match-pred ctx bind form pred)
165194
:else
166195
(throw (ex-info "Predicate must be a symbol" {:pred pred})))))
167196

@@ -170,26 +199,34 @@
170199
(let [[_?sym bind pred] pattern
171200
body-form (gensym "star-rest-form-")
172201
pred-check (if pred `(every? ~pred ~body-form) true)]
202+
(when-not (#{2 3} (count pattern))
203+
(throw (IllegalArgumentException. "?* only accepts 1 or 2 arguments")))
204+
(when (and (= 3 (count pattern)) (not (symbol? pred)))
205+
(throw (IllegalArgumentException. "?* pred must be a symbol")))
173206
[(gensym "star-rest-fn-")
174207
`(fn [~ctx form# cont#]
175208
(let [~body-form (vary-meta (vec form#) assoc ::rest true)]
176209
(when ~pred-check
177-
(let [~ctx ~(match-binding ctx (symbol (str "?" bind)) body-form)]
210+
(let [~ctx ~(match-binding ctx (coerce-bind bind) body-form)]
178211
(cont# ~ctx nil)))))]))
179212

180213
(defn match-star
181214
[ctx pattern]
182215
(let [[_?sym bind pred] pattern
183216
body-form (gensym "star-form-")
184217
pred-check (if pred `(every? ~pred ~body-form) true)]
218+
(when-not (#{2 3} (count pattern))
219+
(throw (IllegalArgumentException. "?* only accepts 1 or 2 arguments")))
220+
(when (and (= 3 (count pattern)) (not (symbol? pred)))
221+
(throw (IllegalArgumentException. "?* pred must be a symbol")))
185222
[(gensym "star-fn-")
186223
`(fn [~ctx form# cont#]
187224
(let [max-len# (count form#)]
188225
(loop [i# 0
189226
~body-form (vary-meta (vec (take i# form#)) assoc ::rest true)]
190227
(when (<= i# max-len#)
191228
(or (and ~pred-check
192-
(let [~ctx ~(match-binding ctx (symbol (str "?" bind)) body-form)]
229+
(let [~ctx ~(match-binding ctx (coerce-bind bind) body-form)]
193230
(cont# ~ctx (drop i# form#))))
194231
(recur (inc i#)
195232
(if (< (count ~body-form) max-len#)
@@ -201,27 +238,35 @@
201238
(let [[_?sym bind pred] pattern
202239
body-form (gensym "plus-rest-form-")
203240
pred-check (if pred `(every? ~pred ~body-form) true)]
241+
(when-not (#{2 3} (count pattern))
242+
(throw (IllegalArgumentException. "?+ only accepts 1 or 2 arguments")))
243+
(when (and (= 3 (count pattern)) (not (symbol? pred)))
244+
(throw (IllegalArgumentException. "?+ pred must be a symbol")))
204245
[(gensym "plus-rest-fn-")
205246
`(fn [~ctx form# cont#]
206247
(when (seq form#)
207248
(let [~body-form (vary-meta (vec form#) assoc ::rest true)]
208249
(when ~pred-check
209-
(let [~ctx ~(match-binding ctx (symbol (str "?" bind)) body-form)]
250+
(let [~ctx ~(match-binding ctx (coerce-bind bind) body-form)]
210251
(cont# ~ctx nil))))))]))
211252

212253
(defn match-plus
213254
[ctx pattern]
214255
(let [[_?sym bind pred] pattern
215256
body-form (gensym "plus-form-")
216257
pred-check (if pred `(every? ~pred ~body-form) true)]
258+
(when-not (#{2 3} (count pattern))
259+
(throw (IllegalArgumentException. "?+ only accepts 1 or 2 arguments")))
260+
(when (and (= 3 (count pattern)) (not (symbol? pred)))
261+
(throw (IllegalArgumentException. "?+ pred must be a symbol")))
217262
[(gensym "plus-fn-")
218263
`(fn [~ctx form# cont#]
219264
(let [max-len# (count form#)]
220265
(loop [i# 1
221266
~body-form (vary-meta (vec (take i# form#)) assoc ::rest true)]
222267
(when (<= i# max-len#)
223268
(or (and ~pred-check
224-
(let [~ctx ~(match-binding ctx (symbol (str "?" bind)) body-form)]
269+
(let [~ctx ~(match-binding ctx (coerce-bind bind) body-form)]
225270
(cont# ~ctx (drop i# form#))))
226271
(recur (inc i#)
227272
(if (< (count ~body-form) max-len#)
@@ -243,26 +288,29 @@
243288
`(fn [~ctx form# cont#]
244289
(let [~body-form (vary-meta () assoc ::rest true)]
245290
(or (and ~pred-check
246-
(let [ctx# ~(match-binding ctx (symbol (str "?" bind)) body-form)]
291+
(let [ctx# ~(match-binding ctx (coerce-bind bind) body-form)]
247292
(cont# ctx# form#)))
248293
(when (seq form#)
249294
(let [~body-form (vary-meta (vec (take 1 form#)) assoc ::rest true)]
250295
(when ~pred-check
251-
(let [ctx# ~(match-binding ctx (symbol (str "?" bind)) body-form)]
296+
(let [ctx# ~(match-binding ctx (coerce-bind bind) body-form)]
252297
(cont# ctx# (drop 1 form#)))))))))]))
253298

254299
(defn match-alt
255300
[ctx pattern]
256301
(let [[_?sym bind alts] pattern]
302+
(when-not (= 3 (count pattern))
303+
(throw (IllegalArgumentException. "?| only accepts 2 arguments")))
257304
(when-not (and (vector? alts)
305+
(seq alts)
258306
(every? #(literal? (read-dispatch %)) alts))
259307
(throw (IllegalArgumentException. "?| alts must be a vector of literals")))
260308
(let [temp-ctx (gensym "temp-ctx-")
261309
body-form (gensym "alt-body-form-")
262310
binds [temp-ctx
263311
`(let [~body-form (first ~body-form)]
264312
(when ((quote ~(set alts)) ~body-form)
265-
~(match-binding ctx (symbol (str "?" bind)) body-form)))
313+
~(match-binding ctx (coerce-bind bind) body-form)))
266314
body-form
267315
`(if ~temp-ctx
268316
(next ~body-form)
@@ -279,9 +327,8 @@
279327
[ctx children-form items]
280328
(mapcat
281329
(fn [item]
282-
[ctx `(when ~ctx
283-
(when (seq ~children-form)
284-
~(read-form ctx item `(first ~children-form))))
330+
[ctx `(when (and ~ctx (seq ~children-form))
331+
~(read-form ctx item `(first ~children-form)))
285332
children-form `(when ~ctx
286333
(next ~children-form))])
287334
items))
@@ -345,7 +392,7 @@
345392
(mapcat #(match-optional ctx %) patterns)
346393
:?|
347394
(mapcat #(match-alt ctx %) patterns)
348-
; else
395+
; else
349396
(match-single ctx patterns)))))
350397
(mapcat identity))
351398
fn-names (take-nth 2 fns)
@@ -384,9 +431,7 @@
384431
(defn non-coll?
385432
"Is a given simple-type a non-collection?"
386433
[t]
387-
(case t
388-
(:nil :boolean :char :number :keyword :string :symbol) true
389-
false))
434+
(#{:nil :boolean :char :number :keyword :string :symbol} t))
390435

391436
(defmethod read-form :map
392437
read-form--map
@@ -467,12 +512,10 @@
467512
(case special-type
468513
(:symbol :any) obj
469514
:? (let [sym-name (str obj)]
470-
(cond
471-
(= 1 (count sym-name)) obj
472-
;; If given `?_`, short-circuit to just _
473-
(.equals "?_" sym-name) '_
474-
:else
475-
(list '? (symbol (subs sym-name 1)))))
515+
;; If given `?_`, short-circuit to just _
516+
(if (.equals "?_" sym-name)
517+
'_
518+
obj))
476519
:?+ (let [sym-name (str obj)]
477520
(if (= 2 (count sym-name))
478521
obj

0 commit comments

Comments
 (0)