Build a custom Javascript linter in 5 minutes

Search for a command to run...

No comments yet. Be the first to comment.
Up to this point, we've been using simple test databases where the data for each row fits within a single page. However, in the wild, it is quite common for a row to be larger than a single page (typically 4096 bytes), especially when using variable-...

The frontend part of our compiler is complete, and we can parse the source code of a pylite program into an AST. This leaves us with a final task: translating the program described by the AST into assembly code. Technically, we could generate assembl...

It has become common practice to start with a "Hello World" program when learning a new programming language. This is a simple program that outputs the text "Hello, World!" to the screen. While writing such a program is a trivial task with most prog...

Compilers are frustrating. Two decades ago, during a boring afternoon in my teenage room, I set out to discover how the software and games I spent so many hours with were made. I plunged into a Google Search rabbit hole that would yield a puzzling a...

In the previous posts, we've explored the SQLite file format and built a simple SQL parser. It's time to put these pieces together and implement a query evaluator! In this post, we'll lay the groundwork for evaluating SQL queries and build a query ev...

Creating a custom linter can be a great way to enforce coding standards and detect code smells. In this tutorial, we'll use Sylver, a source code query engine to build a custom Javascript linter in just a few lines of code.
Sylver's main interface is a REPL console, in which we can load the source code of our project to query it using a SQL-like query language called SYLQ. Once we'll have authored SYLQ queries expressing our linting rules, we'll be able to save them into a ruleset that can be run like a traditional linter.
If sylver --version doesn't output a version number >= 0.1.9, go to https://sylver.dev to download a fresh copy of the software.
Starting the REPL is as simple as invoking the following command at the root of your project:
sylver query --files="src/**/*.js" --spec=https://github.com/sylver-dev/javascript.git#javascript.yaml
The REPL can be exited by pressing Ctrl+C or typing :quit at the prompt.
We can now execute SYLQ queries by typing the code of the query, followed by a ;.
For instance: to retrieve all the method definitions (denoted by the node type MethodDefinition):
match MethodDefinition;
The results of the query will be formatted as follow:
[...]
$0 [MethodDefinition src/store/createArticles.js:36:5-38:5]
$1 [MethodDefinition src/store/createArticles.js:39:5-41:5]
$2 [MethodDefinition src/store/createArticles.js:42:5-59:5]
$3 [MethodDefinition src/store/createArticles.js:60:5-77:5]
$4 [MethodDefinition src/store/createArticles.js:78:5-83:5]
$5 [MethodDefinition src/store/createArticles.js:84:5-89:5]
[...]
The code of a given method definition can be displayed by typing :print followed by the node alias (for instance: :print $3). The parse tree can be displayed using the :print_ast command (for instance: :print_ast $3).
== operatorFor our first rule, we'd like to detect uses of the unsafe == operator for checking equality.
The first step is to get familiar with the tree structure of Javascript's binary expressions, so let's print a BinaryExpression node along with its AST:
λ> match BinaryExpression;
[...]
$43 [BinaryExpression src/pages/Article/Comments.js:7:31-7:77]
[...]
λ> :print $43
currentUser.username == comment.author.username
λ> :print_ast $43
BinaryExpression {
. ● left: MemberExpression {
. . ● object: Identifier { currentUser }
. . ● property: Identifier { username }
. }
. ● operator: EqEq { == }
. ● right: MemberExpression {
. . ● object: MemberExpression {
. . . ● object: Identifier { comment }
. . . ● property: Identifier { author }
. . }
. . ● property: Identifier { username }
. }
}
It appears that the nodes violating our rule are the BinaryExpression nodes
for which the operator field contains an EqEq node.
This can be easily expressed in SYLQ:
match BinaryExpression(operator: EqEq);
For our second linting rule, we'd like to identify functions that have more than 6 parameters.
Here is the relevant part of the parse tree of a Function node:
Function {
. ● async: AsyncModifier { async }
. ● name: Identifier { send }
. ● parameters: FormalParameters {
. . ● params: List {
. . . FormalParameter {
. . . . ● value: Identifier { method }
. . . }
. . . FormalParameter {
. . . . ● value: Identifier { url }
. . . }
. . . FormalParameter {
. . . . ● value: Identifier { data }
. . . }
. . . FormalParameter {
. . . . ● value: Identifier { resKey }
. . . }
. . }
. }
. ● body: StatementBlock {
[...]
Function parameters are represented by FormalParameters nodes with a params field containing the actual function parameters. In our query, the condition
regarding the length of the params list can be specified in a when clause, as follows:
match f@FormalParameters when f.params.length > 6;
For our last rule, we'd like to identify <img> elements that miss the alt attribute. img elements are self-closing, so we'll start by looking at the parse tree of a JsxSelfClosingElement node:
λ> match JsxSelfClosingElement;
[...]
$73 [JsxSelfClosingElement src/pages/Article/Comments.js:21:11-21:55]
[...]
λ> :print $73
<img src={image} class="comment-author-img"/>
λ> :print_ast $73
JsxSelfClosingElement {
. ● name: Identifier { img }
. ● attribute: List {
. . JsxAttribute {
. . . ● name: Identifier { src }
. . . ● value: JsxExpression {
. . . . Identifier { image }
. . . }
. . }
. . JsxAttribute {
. . . ● name: Identifier { class }
. . . ● value: String { "comment-author-img" }
. . }
. }
}
In order to find the img elements that have no JsxAttribute with named alt in their attribute list, we can use a list quantifying expression, as illustrated in the following query:
match j@JsxSelfClosingElement(name: "img")
when no j.attribute match JsxAttribute(name: "alt");
The following ruleset uses our linting rules:
id: customLinter
language: "https://github.com/sylver-dev/javascript.git#javascript.yaml"
rules:
- id: unsafeEq
message: equality comparison with `==` operator
category: style
query: "match BinaryExpression(operator: EqEq)"
- id: tooManyParams
message: function has too many parameters
category: style
note: According to our style guide, functions should have less than 6 parameters.
query: match f@FormalParameters when f.params.length > 6
- id: missingAlt
message: <img> tags should have an "alt" attribute
category: style
query:
"match j@JsxSelfClosingElement(name: 'img')
when no j.attribute match JsxAttribute(name: 'alt')"
Assuming that it is stored in a file called custom_linter.yaml at the root of our project, we can run it with the following command:
sylver ruleset run --files="src/**/*.js" --rulesets=custom_linter.yaml
For more informations about new features and/or cool SYLQ one-liners, connect with Sylver on Twitter or Discord!