x

Webpusher

Mark Lennox

a regrettable tendency to Javascript

Mark Lennox Javascript, C#, Python, machine learning, web, devops, mobile

AST selectors rule

28th April, 2019

10 min read

My previous article on abstract syntax trees ran through a quick, but relatively broad, overview of syntax trees and how to manipulate them.

This second article will show you how to use a basic knowledge of abstract syntax trees to enforce code standards by adding simple ESlint rules implemented only using AST selectors, requiring no javascript!

Rule - 'no-restricted-syntax'

Eslint provides a no-restricted-syntax rule that allows you add simple rules using AST selectors - which are very similar to CSS selectors.

I'll run through a couple of examples in this article

Examples provided here can be found in the AST Selectors folder in the accompanying github repo https://github.com/mlennox/abstractsyntaxforfunandprofit

AST selectors are implemented using esquery. Also, the eslint documentation on selectors is indispensable as a reference.

Const not var

I'll use an example from a previous article - enforce the use of const instead of var. There is already an excellent 'no-var' rule built-in to eslint. This is implemented as an eslint plugin, which requires some effort to write!

However, we can reproduce most of the functionality of the no-var plugin using only AST selectors. As I've already mentioned, AST selectors are based on CSS selectors and won't be a challenge if you have worked with CSS before. I'll explain the construction of the rule in a way that is accessible to those with no knowledge of CSS selectors.

Using the very simple variable declaration below to test against, we'll write an AST selector that will enforce the 'no var' rule in our IDE.

var willIt = true;

To start, we'll need to remind ourselves of the structure of the AST for a simple var variable declaration.

abstract syntax tree of variable declaration showing attribute kind is var

Firstly, lets try and state the problem in english

highlight, as an error, any VariableDeclaration of kind var

Simple enough.

Creating the selector

Firstly, we need to know how to select our variable declaration. Remember, the node type for our variable declaration is simply VariableDeclaration. The AST selector we use is a node type selector - which is simply the type of the node, like so

VariableDeclaration

Next, as we are selecting against all the nodes in the abstract syntax tree for every file in your codebase, we need to refine our selection to only those of kind var.

The kind we refer to is an attribute of the VariableDeclaration node.

We can select all nodes that have a kind attribute using the following selector

[kind]

And to select any kind attribute that has the value var we expand the selector like so

[kind='var']

Now we have a selector that will select all kind attributes with the value var, but we only want to select VariableDeclaration nodes that have that attribute and value, so:

VariableDeclaration[kind='var']

This is our final selector, but how do we add that to our list of eslint rules?

Adding the rule

To apply the rule to our codebase we add the example no-restricted-syntax rule to the rules section of the .eslintrc.js config file

"rules": {
    "no-restricted-syntax": [
      "error", "VariableDeclaration[kind='var']"
    ],
}

This produces the following error in VS Code

no restricted syntax simple

I think you'll agree that Using 'VariableDeclaration[kind='var'] is not allowed is a really bad error message.

Custom error message

Eslint supports a custom message for rule violations, so let's add that

"rules": {
    "no-restricted-syntax": [
      "error",  {
        "selector": "VariableDeclaration[kind='var']",
        "message": "All variables must be declared as 'const', do not use 'var'"
      }
    ],
}

This looks a lot better and the added structure to the configuration has the bonus of easier maintenance of your custom eslint rules.

no restricted syntax with message

What about a more complex example?

React JSX internationalisation - FormattedMessage

If you use react-intl you will be familiar with the FormattedMessage component that facilitates localised messages in your app.

The FormattedMessage component wraps the message in a span by default.

<FormattedMessage id={`someMessageId`} />
// results in : <span>some message text</span>

You can avoid the span by using this construction instead

<FormattedMessage id={`someMessageId`}>{text => text}</FormattedMessage>
// results in : some message text

I don't like it when spurious HTML is added to my layout, so let's write an eslint rule to ensure it doesn't happen. As before we'll state our problem goal in plain english

highlight, as an error, any FormattedMessage that does not contain child elements

We make a very reasonable assumption here that any children will use the general approach that we require, for example

    :
    :
<FormattedMessage id={`someMessageId`}>
  {labelText => (
    <MyComponent
      label={labelText}
      props={this.props}
      />
  )}
</FormattedMessage>
<FormattedMessage id={`anotherMessageId`}>
  {messageText => this.renderSomeStuff(messageText)}
</FormattedMessage>
    :
    :

This saves us from having to consider the types and format of the child components.

AST explorer + JSX = problem

The ever useful AST explorer does not handle JSX so we'll need to use a different approach to visualise the abstract syntax tree.

Babel parser with jsx plugin

The helper file showTree.js is included in the github repo but you cannot run this helper function from the repo root:

cd ASTselectors/FormattedMessage
node showTree.js

This will turn the stateless react component in the file basicReact.js into a JSON abstract syntax tree. We can use this to try and visualise how we might build a selector that selects only the FormattedMessage nodes that have no {text => text} child function.

Visualising the tree structure

The simplified abstract syntax tree for the second FormattedMessage in the file basicReact.js is shown below.

Note that the structure is relatively complex - a generic JSXElement as a parent container with the attributes openingElement and closingElement containing instances of the FormattedMessage tags themselves and the children of the JSXElement are a JSXEXpressionContainer containing the anonymous arrow function AST for {text => text}

{
  "type": "JSXElement",
  "openingElement": {
    "type": "JSXOpeningElement",
    "name": {
      "type": "JSXIdentifier",
      "name": "FormattedMessage"
    },
    "attributes": [ /* not important to us */ ],
    "selfClosing": false
  },
  "closingElement": {
    "type": "JSXClosingElement",
    "name": {
      "type": "JSXIdentifier",
      "name": "FormattedMessage"
    }
  },
  "children": [{
    "type": "JSXExpressionContainer",
    "expression": {
      "type": "ArrowFunctionExpression",
      "params": [{
        "type": "Identifier",
        "name": "text"
      }],
      "body": {
        "type": "Identifier",
        "name": "text"
      }
    }
  }]
}

As usual a graphic representation of the simplified abstract syntax tree shows the hierarchy much more clearly.

AST FormattedMessage

We won't be using the correctly structured FormattedMessage AST as a reference when building our selector, I supply this to as a reference to ensure we don't construct a selector that will also select a properly constructed FormattedMessage.

Now Lets compare that to the self-closing FormattedMessage. A simplified version of the JSON AST is shown below

{
  "type": "JSXElement",
  "openingElement": {
    "type": "JSXOpeningElement",
    "name": {
      "type": "JSXIdentifier",
      "name": "FormattedMessage"
    },
    "attributes": [ /* not important to us... */ ],
    "selfClosing": true
  },
  "closingElement": null,
  "children": []
}

Constructing the selector - approach 1 : JSXElement has no child elements

Referring to the JSON AST, we can see the parent JSXElement has no child elements we can select on that basis

{
  "type": "JSXElement",
  "children": []
}

The selector is simple enough, we want to select the JSXElement where the children attribute is empty.

JSXElement[children='']

Its important to note here that the children attribute is slightly confusing as the children it refers to are the children of the openingElement / closingElement. In regard to the AST selectors, the openingElement and closingElement themselves are the direct descendants (yes, children - hence the confusion) of the parent JSXElement. So armed with this information we know we can use descendant selectors to select the JSXOpeningElement

JSXElement[children=''] JSXOpeningElement

This is still too specific. We are still selecting many elements, we only want to select FormattedMessage elements inside a JSXElement that has an empty children attribute.

Once again, some explanation is required. As far as AST selectors are concerned, the direct descendants of the JSXOpeningElement in the abstract syntax tree are not the components referred to in the children attribute of the parent JSXElement but the JSXIdentifier referred to in the name attribute of the JSXOpeningElement.

Because the name attribute of the JSXOpeningElement is not a simple string it is not possible to use the attribute selector, as they only allow simple matching rules. For instance, the example below, or similar variations, would not work

// bad! does not work!
JSXOpeningElement[name='JSXIdentifier.name=FormattedMessage']

As far as the AST selectors are concerned, the name attribute element is a descendant element and can be selected using a descendant selector paired with an attribute selector that matches the all important string FormattedMessage.

JSXElement[children=''] JSXOpeningElement JSXIdentifier[name='FormattedMessage']

This will select the self-closing FormattedString components in the codebase and will ignore those that wrap components. Success!

But wait, there's more - this can be simpler.

The selector does not gain any specificity from using the JSXOpeningElement. We already know that the parent JSXElement indicates there are no child components, so we don't need to worry that our selector is going to select the JSXClosingElement as it is not there. We can simplify the selector by removing the reference to JSXOpeningElement.

JSXElement[children=''] JSXIdentifier[name='FormattedMessage']

And our final rule, in place in the eslint config

"error", {
  "selector": "JSXElement[children=''] JSXIdentifier[name='FormattedMessage']",
  "message": "Please use {text => text} function as child of FormattedMessage to avoid spurious span"
}

Constructing the selector - approach 2 : JSXOpeningElement is self-closing

There is a different approach we can take that only selects against the opening element itself without requiring reference to the parent JSXElement with an empty children attribute. Look at the JSON AST of the JSXOpeningElement.

{
  "type": "JSXOpeningElement",
  "name": {
    "type": "JSXIdentifier",
    "name": "FormattedMessage"
  },
  "attributes": [ /* not important to us */ ],
  "selfClosing": true
},

The important property here is selfClosing if it is true, as it is here, it means there is no closing tag and therefore no child components.

Instead of selecting the parent JSXElement we can now directly select the JSXOpeningElement that is self-closing.

JSXOpeningElement[selfClosing=true]

And we already know how to filter our selected components to a FormattedMessage by using a descendant selector combined with an attribute selector.

JSXOpeningElement[selfClosing=true] JSXIdentifier[name='FormattedMessage']

The final eslint config would be

"error", {
  "selector": "JSXOpeningElement[selfClosing=true] JSXIdentifier[name='FormattedMessage']",
  "message": "Please use {text => text} function as child of FormattedMessage to avoid spurious span"
}

Conclusion

AST selectors can be very useful in providing a simple way of adding a new ESlint rule, and they also leverage any existing CSS selector knowledge you may have. However, they suffer the same limitations as CSS selectors and quickly become cumbersome for what should be relatively simple selections. The selection of a node based on the contents of the attributes of the children of a sibling node is common, but not simple to achieve using AST selectors; while there is an adjacent and descendant selector there is no previous selector.

The next post in this series will look at writing "proper" ESlint plugins that are much more flexible, and useful.

Other blog posts in this series

  • Abstract Syntax Trees for fun and profit

    14 min read

    Part One - an overviewThis is part one of a series of articles about abstract syntax trees and their use in javascript. The scope of this article is a quick introduction to ASTs, babel plugins and…