Skip to content

Selection refinement

Until now, the examples shown in this manual had all resulted in a selection of the outermost expression that the query was targeting. Or in other words, the result was whatever the very first (outermost) selector matched against. (call ...) always selected a call to a function, (jsx ...) always selected a JSX element, etc.

By design, SYNG's matching algorithm compares the target of your query -- the outermost selector -- to a JavaScript syntax node and checks for matches. This basic design is attractive because it's both effective and makes for a simple mental model that is easy to follow and reason about. It's also part of why SYNG can provide results so quickly. But, it's not always enough.

Suppose what you're looking for is not a call expression in itself, but rather one of the arguments it was made with? Or not a JSX element in itself, but one of its attributes instead?

(:into) refines the selection of a statement into one of its inner statements, and has the following signature:

(:into [val?])

Paired with other selectors, (:into) gives you the power to point exactly at the expression you care about, which might be buried deep inside the selection heirarchy. For example, the following selects not the call to the function translate, but rather every argument it was made with that was a string:

(call translate
      (arg _
           (:into (str))))

Which would match the "a" in translate("a") and "b" in translate({}, "b").

(:into) can be thought of as a cursor into the query: you point it at what is most relevant to you, regardless of how you arrived at it.

Refining to a list of values

Because the matching algorithm operates on one statement at a time, the result is at most that one statement -- that is, when a match is found. It's either 0 or 1. Say you're selecting an array with three elements; the result is always either nothing (no match) or an array (match), regardless of how many elements it has.

Consider this array:

[{}, 1, 'a', {}]

And a query to select it:

(arr (:and (el _ (str))
           (el _ (num))))

The output will be the array itself of all its elements; objects, strings and numbers -- that's what (arr) selects!

Refinement, through (:into), gives you more control over the output, including the ability to refine to lists of values, such as an object's list of properties, or an array's list of elements. Revisiting that query, we can refine the selection to the matching elements (the strings and numbers) of such an array:

(arr (:and (:into (el _ (str))
                  (el _ (num)))))

And now, the output will be 1 and 'a' instead of the entire array.

List refinement applies to every JavaScript type that contains a list of types, including for example imported specifiers:

import {a,b} from 'x'

Running (import _ (:into /a|b/)) would yield both a and b.

Refining to anything

Regarding the very last example shown earlier of the imported specifiers: we had to explicitly match what the imported specifiers were to select them. /a|b/. What if, however, we didn't know that value, and it's what we actually care about?

(:into) can be called with no argument to turn it into an anything selector: that is, it will match and refine to whatever it was instructed to point at. In the case of that import example:

(import _ (:into))

Would yield a and b, even though we didn't specify /a|b/. This ability to cursor into anything is very valuable when you're investigating part of the program that you don't know about. For example, a free (:into) can be used to list all the possible label values every JSX element is provided:

(jsx _ (attr label

Or, all the values provided as the last argument to the function console.log:

(call (mem log console)
      (arg -1 (:into)))

Copyright © 2022-present Semantic Works, Inc.