Selecting imported identifiers
JavaScript has had different module formats develop over time: AMD, CommonJS, SystemJS and ESM. Because of the varying ways in which imports and exports can be declared, even across a single format, it isn't easy to say whether, or how, a module and its exports are being used. Not definitively, at least.
A codebase can choose to make this problem less severe by adopting a single module format and being consistent about it. For example, if it used CommonJS, then you could in theory search for the text pattern require.*jquery
to get a good idea about the files that are importing the jQuery module. But what about specific exports of that package? What if we wanted to find files that use the ajax
function provided by that module, for example?
Or, what if we wanted to find the calls being made to $.ajax
? Because that's what is ultimately of relevance.
You'd think it would be a fairly basic question to ask and yet it's surprisingly hard to answer. Consider that in CommonJS, you could use ajax
in any of those ways (:and probably others):
const { ajax } = require('jquery')
// or
const $ = require('jquery')
$.ajax
// or
const $ = require('jquery')
const ajaxjustforme = $.ajax
Added to the lack of strong-typing in JavaScript - an aspect TypeScript is helping to improve - we can only go so far in locating imported identifiers and their usage with text-level search.
This is one area where doing a search at the syntax level instead proves to be far more reliable. It still won't be perfect[^1], but good enough to a point where it becomes practical.
Selecting the default export of a module
(import)
selects an identifier imported from another module. It has the following signature:
(import [source] [specifier?])
Specifier is the formal name in the ESM format for the identifier exported by the target module and is being imported into the current one, which defaults to default
when omitted. For example, to select whatever identifier is bound to the default export of the jquery
module:
(import "jquery")
SYNG can select imports made in the ESM format.
Selecting a specific export of a module
The second argument to (import)
selects the specifier for the module being imported, or the ajax
in import { ajax } from "jquery"
:
(import "jquery" ajax)
The pattern in which the specifier is specified (no pun intended) is irrelevant. The following snippet shows (in braces for readability) all the variants that will be selected by SYNG for the previous query:
{ // a named specifier:
import { ajax } from 'jquery'
ajax
}
{ // a named specifier bound to a local identifier:
import { ajax as myajax } from 'jquery'
myajax
}
{ // a string-named specifier bound to a local identifier:
import { "ajax" as myajax } from 'jquery'
myajax
}
{ // identifier accessed through a namespace specifier:
import * as jQuery from 'jquery'
jQuery.ajax
}
{ // identifier accessed through a default specifier:
import $ from 'jquery'
$.ajax
}
If you've come across a pattern that is not supported by SYNG, file an issue to add support for selecting it.
Selecting an import statement
Instead of selecting the imported identifier, SYNG can be instructed to select an import statement through the --mode=match
parameter. This is useful when you want to know which files import a particular module without resorting to dependency graph analysis.
Given the following query, SYNG will match files that import the default specifier of the "jquery" module:
(import jquery)
match
mode has only limited support for composition: while (:or)
is supported, neither (:and)
nor (:not)
are, although we can emulate their behavior.
(:and)
is useful to identify modules that are importing a combination of modules. To emulate its behavior, we can invoke SYNG multiple times and pipe the matching filenames using xargs. For example, to identify modules that import both jQuery and Backbone:
syng -m match '(import jquery)' ./src | xargs \
syng '(import backbone)'
(:or)
behaves as you expect, only that it must appear at the root level since neither source
nor specifier
argument to (import)
supports composition. For example, to find files that import jQuery or Backbone or both:
(:or (import jquery)
(import backbone))
You CANNOT do this:
(import (:or jquery backbone))
(:not)
can be emulated through the invert
mode. Building on the previous example, to find files that import jQuery or Backbone but not React:
syng -m match '(:or (import jquery) (import backbone))' | xargs \
syng -m invert '(import react)'
Example: Selecting calls to imported identifiers
Suppose we're looking to replace our use of the ajax
function provided by the jQuery library with the native fetch function. To start off, let's find out how many calls to $.ajax
our codebase is doing:
$ syng '(call (mem ajax $))' . | wc -l
# 35
What if the object is not called $
, though? It's not uncommon for a different identifier to be used, but we don't know what that might be. (import)
can figure that out for us:
$ syng '(call (import jquery ajax)))' . | wc -l
# 34
This actually went down to 34, implying that one of the initial 35 was accessing a global $
identifier. Oops!
Next, we want to know how many of those calls are specifying onSuccess
callbacks, because they need to be converted into Promises and that may affect callsites. One of the arguments needs to be an object, so we'll use the (obj)
selector:
(call (import jquery ajax))
(arg _ (obj)))
The object must have the property named onSuccess
, so let's put (prop)
to work:
(call (import jquery ajax))
(arg _ (obj (prop onSuccess))))
onSuccess
has to be a function though, which we'll indicate in the second argument to (prop)
:
(call (import jquery ajax))
(arg _ (obj (prop onSuccess (fun)))))
Our query is now complete! Now for a precise translation into English:
"Select a call to the identifier bound to the ajax
export of jquery
made with an argument at any position
of type object
and contains the property identified by onSuccess
and assigned to a value of type function
."
The Lisps aren't looking so bad anymore, huh?
Recap
The versatility of (import)
is evident as I hope you've come to agree. Personally, it's been one of the motivators for building SYNG in the first place. Combining this one selector with others like (call)
gives you a lot of power for analyzing your codebase.
[^1]: References can still be copied. $.ajax
in this example can be bound to some identifier x
that is then used in its place. This is a deep topic that is not exclusive to imports, and we'll explore it in depth in the chapter on following references.