Skip to content

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):

javascript
const { ajax } = require('jquery')

// or
const $ = require('jquery')
$.ajax

// or
const $ = require('jquery')
const ajaxjustforme = $.ajax
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:

clojure
(import [source] [specifier?])
(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:

clojure
(import "jquery")
(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":

clojure
(import "jquery" ajax)
(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:

javascript
{ // 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
}
{ // 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:

clojure
(import jquery)
(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:

bash
syng -m match '(import jquery)' ./src | xargs \
syng          '(import 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:

clojure
(:or (import jquery)
     (import backbone))
(:or (import jquery)
     (import backbone))

You CANNOT do this:

clojure
(import (:or jquery backbone))
(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:

bash
syng -m match  '(:or (import jquery) (import backbone))' | xargs \
syng -m invert '(import 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:

bash
$ syng '(call (mem ajax $))' . | wc -l
# 35
$ 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:

bash
$ syng '(call (import jquery ajax)))' . | wc -l
# 34
$ 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:

clojure
(call (import jquery ajax))
      (arg _ (obj)))
(call (import jquery ajax))
      (arg _ (obj)))

The object must have the property named onSuccess, so let's put (prop) to work:

clojure
(call (import jquery ajax))
      (arg _ (obj (prop onSuccess))))
(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):

clojure
(call (import jquery ajax))
      (arg _ (obj (prop onSuccess (fun)))))
(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.

Copyright © 2022-present Semantic Works, Inc.