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.
Firstly, lets try and state the problem in english
highlight, as an error, any
VariableDeclaration
of kindvar
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
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.
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.
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.
Top comments (2)
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.
No reason they are not a series - they should be!
AST is a fun cliff to fall off alright!