DEV Community

Mark Lennox
Mark Lennox

Posted on

AST selectors rule

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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

[kind='var']
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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']"
    ],
}
Enter fullscreen mode Exit fullscreen mode

This produces the following error in VS Code

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'"
      }
    ],
}
Enter fullscreen mode Exit fullscreen mode

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

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>
Enter fullscreen mode Exit fullscreen mode

You can avoid the span by using this construction instead

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

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>
    :
    :
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

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

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": []
}
Enter fullscreen mode Exit fullscreen mode

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": []
}
Enter fullscreen mode Exit fullscreen mode

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

JSXElement[children='']
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
},
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
jasonsbarr profile image
Jason Barr

Just curious, is there a reason you're not defining these as a series in your front matter so they're linked here on Dev?

Great stuff, by the way. I'm in the process of falling off the AST/parser/language design cliff and it's really fun.

Collapse
 
mlennox profile image
Mark Lennox

No reason they are not a series - they should be!

AST is a fun cliff to fall off alright!