Skip to content

Commit ca3c0a4

Browse files
committed
Add lint/update-with-swap
Closes #30
1 parent 3429029 commit ca3c0a4

File tree

7 files changed

+108
-11
lines changed

7 files changed

+108
-11
lines changed

CHANGELOG.md

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

55
## Unreleased
66

7+
### New Rules
8+
9+
- `lint/update-with-swap`: Prefer `(swap! (:counter state) + 5)` over `(update state :counter swap! + 5)`. (See [#30](https://github.com/NoahTheDuke/splint/issues/30).)
10+
711
### Changed
812

913
- Narrow `lint/existing-constant` to only `clj`, and expand it to cover clojure 1.10 (by using `java.lang.Math/PI` and `java.lang.Math/E`).

resources/config/default.edn

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,13 @@
301301
:added "1.11"
302302
:updated "1.11"}
303303

304+
lint/update-with-swap
305+
{:description "Don't use `swap!` in `update` or `update-in` calls."
306+
:enabled true
307+
:safe false
308+
:added "<<next>>"
309+
:updated "<<next>>"}
310+
304311
lint/warn-on-reflection
305312
{:description "Always set *warn-on-reflection* to avoid reflection in interop."
306313
:enabled false

src/noahtheduke/splint.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
noahtheduke.splint.rules.lint.thread-macro-one-arg
6161
noahtheduke.splint.rules.lint.try-splicing
6262
noahtheduke.splint.rules.lint.underscore-in-namespace
63+
noahtheduke.splint.rules.lint.update-with-swap
6364
noahtheduke.splint.rules.lint.warn-on-reflection
6465

6566
noahtheduke.splint.rules.metrics.fn-length
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
; This Source Code Form is subject to the terms of the Mozilla Public
2+
; License, v. 2.0. If a copy of the MPL was not distributed with this
3+
; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
(ns ^:no-doc noahtheduke.splint.rules.lint.update-with-swap
6+
(:require
7+
[noahtheduke.splint.diagnostic :refer [->diagnostic]]
8+
[noahtheduke.splint.rules :refer [defrule]]
9+
[noahtheduke.splint.clojure-ext.core :refer [->list]]))
10+
11+
(set! *warn-on-reflection* true)
12+
13+
(defrule lint/update-with-swap
14+
"If an atom is stored inside if a map, the atom can be changed using `swap!` within an `update` or `update-in` call. However, `swap!` returns the new value, not the atom itself, so the container map will hold the deref'ed value of the atom, not the original atom. If the result of the `update` call is stored/used, this can lead to bugs.
15+
16+
Additionally, if the return value of the `update` call is ignored, then the `update` form will work as expected (because the return value won't overwrite the existing map and the atom will be updated in place). This should be avoided as it breaks expectations about the value of values and normal behavior.
17+
18+
@safety
19+
If the `update` call's return value isn't ignored (it's used in an assignment or passed to another call), switching to `swap!` will break the expected return value. Care must be exercised when switching.
20+
21+
@examples
22+
23+
; avoid
24+
(update state :counter swap! + 5)
25+
26+
; prefer
27+
(swap! (:counter state) + 5)
28+
"
29+
{:pattern '((?| update [update update-in]) ?map ?key swap! ?+args)
30+
:autocorrect false
31+
:message "swap! in update derefs the value in the map."
32+
:on-match (fn [ctx rule form {:syms [?update ?map ?key ?args]}]
33+
(let [getter (if (= 'update ?update)
34+
(list ?key ?map)
35+
(list 'get-in ?map ?key))
36+
new-form (->list (list* 'swap! getter ?args))]
37+
(->diagnostic ctx rule form {:replace-form new-form})))})

src/noahtheduke/splint/runner.clj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
diagnostic and store it in `ctx`."
101101
[ctx rule-names form]
102102
(when-let [diagnostics (check-form ctx rule-names form)]
103-
(update ctx :diagnostics swap! into diagnostics)))
103+
(swap! (:diagnostics ctx) into diagnostics)
104+
nil))
104105

105106
(defn update-rules [rules-map form]
106107
(if-let [disabled-rules (some-> form meta :splint/disable)]
@@ -213,11 +214,13 @@
213214
(assoc :form-meta {:line (:line data)
214215
:column (:column data)}))
215216
diagnostic (runner-error->diagnostic ex data)]
216-
(update ctx :diagnostics swap! conj diagnostic))
217+
(swap! (:diagnostics ctx) conj diagnostic)
218+
nil)
217219
(let [diagnostic (runner-error->diagnostic
218220
ex {:error-name 'splint/unknown-error
219221
:filename file})]
220-
(update ctx :diagnostics swap! conj diagnostic)))))))
222+
(swap! (:diagnostics ctx) conj diagnostic)
223+
nil))))))
221224

222225
(defn slurp-file [file-obj]
223226
(if (:contents file-obj)

src/noahtheduke/splint/runners/autocorrect.cljc

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,12 @@
185185
zloc))))
186186
zloc))))
187187
(catch Exception ex
188-
(update ctx :diagnostics swap! conj
189-
(run/runner-error->diagnostic
190-
ex {:error-name 'splint/error
191-
:form zloc
192-
:rule-name (:full-name rule)
193-
:filename (:filename ctx)}))
188+
(swap! (:diagnostics ctx) conj
189+
(run/runner-error->diagnostic
190+
ex {:error-name 'splint/error
191+
:form zloc
192+
:rule-name (:full-name rule)
193+
:filename (:filename ctx)}))
194194
zloc))
195195
zloc)))
196196
zloc
@@ -280,11 +280,13 @@
280280
(assoc :form-meta {:line (:line data)
281281
:column (:column data)}))
282282
diagnostic (run/runner-error->diagnostic ex data)]
283-
(update ctx :diagnostics swap! conj diagnostic))
283+
(swap! (:diagnostics ctx) conj diagnostic)
284+
nil)
284285
(let [diagnostic (run/runner-error->diagnostic
285286
ex {:error-name 'splint/unknown-error
286287
:filename file})]
287-
(update ctx :diagnostics swap! conj diagnostic))))))))
288+
(swap! (:diagnostics ctx) conj diagnostic)
289+
nil)))))))
288290

289291
(comment
290292
(let [config {:clojure-version *clojure-version*
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
; This Source Code Form is subject to the terms of the Mozilla Public
2+
; License, v. 2.0. If a copy of the MPL was not distributed with this
3+
; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
(ns noahtheduke.splint.rules.lint.update-with-swap-test
6+
(:require
7+
[lazytest.core :refer [defdescribe it]]
8+
[noahtheduke.splint.test-helpers :refer [expect-match single-rule-config]]))
9+
10+
(set! *warn-on-reflection* true)
11+
12+
(def rule-name 'lint/update-with-swap)
13+
14+
(defdescribe update-with-swap-test
15+
(it "works with update"
16+
(expect-match
17+
[{:rule-name rule-name
18+
:form '(update state :counter swap! + 5)
19+
:message "swap! in update derefs the value in the map."
20+
:alt '(swap! (:counter state) + 5)}]
21+
"(update state :counter swap! + 5)"
22+
(single-rule-config rule-name)))
23+
(it "works with update-in"
24+
(expect-match
25+
[{:rule-name rule-name
26+
:form '(update-in state [:users :counter] swap! + 5)
27+
:message "swap! in update derefs the value in the map."
28+
:alt '(swap! (get-in state [:users :counter]) + 5)}]
29+
"(update-in state [:users :counter] swap! + 5)"
30+
(single-rule-config rule-name)))
31+
(it "expects at least 1 arg to swap!"
32+
(expect-match
33+
nil
34+
"(update state :counter swap!)"
35+
(single-rule-config rule-name)))
36+
(it "doesn't care about the args to swap!"
37+
(expect-match
38+
[{:rule-name rule-name
39+
:form '(update state :counter swap! (fn [foo] foo))
40+
:message "swap! in update derefs the value in the map."
41+
:alt '(swap! (:counter state) (fn [foo] foo))}]
42+
"(update state :counter swap! (fn [foo] foo))"
43+
(single-rule-config rule-name))))

0 commit comments

Comments
 (0)