Skip to content

Selection replacement ​

(:replace) modifies the source code of a matched node by substituting it with a literal string. It's designed for automated refactoring tasks where you want to transform code in a predictable, pattern-driven way.

clojure
(:replace [val?] [sub?])

(:replace) must enclose its target. It cannot reach across to replace something elsewhere in the query -- it replaces exactly what it wraps. This constraint keeps the mental model simple: what you see is what gets replaced.

clojure
; replaces the string "foo" with "bar"
(:replace (str "foo") "bar")

; replaces any call to `x` with the literal `y()`
(:replace (call x) "y()")

Dropping a value ​

When (:replace) is given no substitution string, it drops the matched value entirely:

clojure
; drop the second argument
(call x (arg 2 (:replace)))

Given x(a, b, c), this would produce x(a, c).

Capturing for interpolation ​

Replacements can incorporate dynamic fragments through captures. Captures are recorded either from pattern literals with regex groups, or via the (:capture) operator.

Pattern captures ​

A regex pattern with capture groups makes those groups available for interpolation in the substitution string:

clojure
(:replace (str /load_(.+)/) "load$1Async")

Given "load_user", this produces "loaduserAsync". Named groups work too:

clojure
(:replace (id /get(?<name>.+)/) "fetch$name")

Given getUser, this produces fetchUser.

Node captures ​

[(:capture)] records the span of a matched node for use in substitution. This is useful when you want to move or duplicate an entire expression:

clojure
(call x
  (:and
    (arg 1 (:capture first (str)))
    (arg 2 (:replace _ "$first"))))

Given x("a", "b"), this replaces the second argument with the first, producing x("a", "a"). The capture includes the quotes because it captured the whole string node -- exactly what we want here.

When you need the content of a node rather than its full span, use pattern captures instead:

clojure
(call
  (:replace load "load_$mode")
  (arg 1 (str /(?<mode>.+)/)))

Given load("sync"), this produces load_sync("sync"). The pattern extracts just sync without the surrounding quotes.

Capturing across branches ​

Captures are visible across sibling branches of a query. The example above relies on this: the (:capture) in one (:and) branch is available to the (:replace) in another. This enables transformations where one part of the match informs another's replacement.

Capturing contiguous ranges ​

When capturing multiple list elements, the spans are merged to preserve the original delimiters:

clojure
(call _
  (:and
    (arg 1 (:replace _ "$rest, $first"))
    (arg 1 (:capture first))
    (arg 2.. (:capture rest (:into)))))

Given x("a", "b", "c"), this swaps the first argument to the end: x("b", "c", "a"). The capture $rest spans from "b" to "c", including the comma and whitespace between them.

Limitations ​

(:replace) performs literal text substitution. It doesn't understand syntax, so it's possible to produce invalid code if you're not careful. Always review the output.

Structural transformations -- like moving an argument from one position to another while respecting syntax -- require careful use of captures and may not always be possible with (:replace) alone.

Copyright © 2022-present Semantic Works, Inc.