Skip to content

plow-technologies/rescript-linter

Repository files navigation

ReScript Linter

An AST based linter for ReScript. Write your rule based on ReScript AST.

asciicast

Building the project

opam install . --deps-only --with-doc --with-test
dune build

or with Nix (flakes),

nix build

To run the test

dune runtest

or with Nix (flakes),

nix flake check

Running the parser

Config file

You can set rules that you want to lint using config file. See below for list of available rules.

{
  "rules": [
    {
      "rule": "DisallowOperator",
      "options": {
        "disallowed_operator": "|>",
        "suggested_operator": "->"
      }
    },
    {
      "rule": "DisallowFunction",
      "options": {
        "disallowed_function": "string_of_int",
        "suggested_function": "Belt.Int.fromString"
      }
    },
    {
      "rule": "NoJStringInterpolation"
    },
    {
      "rule": "NoReactComponent",
      "options": {
        "component": "input"
      }
    },
    {
      "rule": "DisallowModule",
      "options": {
        "disallowed_module": "Css",
        "suggested_module": "CssJs"
      }
    }
  ]
}

Once you build the project, you can copy the resulting binary. Or you can also run it with dune

dune exec -- rescript_linter -c config.json foo.res

or run it with Nix (flakes),

nix run .#rescript-linter -- -c config.json foo.res

Disabling lint

You can disable lint per file. Simply add RSLINT_DISABLE comment at the top of your file to disable lint for all rules.

// RSLINT_DISABLE

// this will not throw lint error
let _ = string_of_int(1)

You can also disable certain rules - either the generic rule or specific rule.

// RSLINT_DISABLE_DisallowFunction

// this will not throw lint errors
let _ = string_of_int(0)
let _ = intOfStringOpt("1")
// RSLINT_DISABLE_DisallowFunction[string_of_int]

// this will not throw lint error
let _ = string_of_int(0)

// this will still throw lint error
let _ = intOfStringOpt("1")

You can also disable multiple rules

// RSLINT_DISABLE_DisallowFunction[string_of_int]
// RSLINT_DISABLE_DisallowFunction[intOfStringOpt]

// this will not throw lint errors
let _ = string_of_int(0)
let _ = intOfStringOpt("1")

This style of disabling rules depends on how each rule is implemented. For example, NoJStringInterpolation does not have this feature because the rule itself is generic.

However, you can do the same for DisallowOperator and NoReactComponent rule.

Rules

Rules are built-in in the project. Currently there's no pluggable architecture to add third party rule.

Rules are defined in lib/rules.

Currently, there are five rules available:

  1. DisallowFunction - Disallow the use of certain functions like string_of_int
  2. DisallowOperator - Disallow the use of certain operators like |>
  3. NoJStringInterpolation - Disallow the use of j-string Interpolation
  4. NoReactComponent - Disallow use of certain React component/dom
  5. DisallowModule - Disallow use of certain module

Writing your own rule

By convention, you should write a new rule on its own module in lib/rules.

Rule interface

Each rule module must have the signature of Rule.HASRULE.

type linter =
  | LintExpression of (Parsetree.expression -> lintResult)
  | LintStructure of (Parsetree.structure -> lintResult)
  | LintStructureItem of (Parsetree.structure_item -> lintResult)
  | LintPattern of (Parsetree.pattern -> lintResult)

module type HASRULE = sig
  val meta : meta
  val linters : linter list
end
  • meta allows you to define name and the rule description
  • linters are list of functions that receive AST and these functions should either return LintOk or LintError.
    • it is a list of linters because sometimes it is convenience to be able to parse at different type of AST node.
    • see lib/rules/DisallowModuleRule.ml for an example of this.

Rule with options

Some rule can be designed such a way that it can be generic and user can specify options in order to create a specific rule. For example, our DisallowedFunctionRule is a generic rule and you can specify the function name through its option.

There is a module signature that you would have to follow to add options to a rule.

module type OPTIONS = sig
  type options
  val options : options
end

Then you would a create a module functor that accepts the options as its module argument.

module Options = struct
  type options = {disallowed_function: string; suggested_function: string option}
end

module Make (OPT : Rule.OPTIONS with type options = Options.options) : Rule.HASRULE with type t = Parsetree.expression = struct
  ...

  let function_name = OPT.options.Options.disallowed_function
  ...
end

Using the generic rule

Then you can use the module functor to create a specific rule based on the options that you passed.

module DisallowStringOfIntRule = DisallowedFunctionRule.Make (struct
  type options = DisallowedFunctionRule.Options.options

  let options =
    { DisallowedFunctionRule.Options.disallowed_function= "string_of_int"
    ; DisallowedFunctionRule.Options.suggested_function= Some "Belt.Int.fromString" }
end)

Rule configuration parser

You can add the parser that parses JSON config in ConfigReader.ml. That way the config will read the correct rules that you defined.

Understanding the AST

Printing the AST

It is very useful to print the AST when you're investigating how to write a rule for certain code. Rescript has AST pretty printer that can come handy to convert your Rescript code to its AST.

test.res

let txt = j`hello`

AST

$ rescript -print ast test.res
[
  structure_item (test.res[1,0+0]..[1,0+18])
    Pstr_value Nonrec
    [
      <def>
        pattern (test.res[1,0+4]..[1,0+7])
          Ppat_var "txt" (test.res[1,0+4]..[1,0+7])
        expression (test.res[1,0+11]..[1,0+17])
          attribute "res.template" (_none_[1,0+-1]..[1,0+-1]) ghost
            []
          Pexp_constant PConst_string ("hello",Some "j")
    ]
]

AST

The complete AST types can be found in https://github.com/rescript-lang/syntax/blob/master/compiler-libs-406/parsetree.mli

The linter currently doesn't handle all cases, it only handle few cases. See lib/Rule.ml.

We define a type linter that takes a function of relevant AST type.

type linter =
  | LintExpression of (Parsetree.expression -> lintResult)
  | LintStructure of (Parsetree.structure -> lintResult)
  | LintStructureItem of (Parsetree.structure_item -> lintResult)
  | LintPattern of (Parsetree.pattern -> lintResult)

At the top of the AST, generally you'd have Parsetree.structure_item. With Parsetree.structure_item you can parse basically anything but it can be tedious to drill down to the type of AST you're interested in. This is useful to parse anything at the module level like for example checking module usage, making lint rule such as no open module etc. Note that Parsetree.structure is just a list of Parsetree.structure_item.

For convenience, we exposed Parsetree.expression and Parsetree.pattern. The former allows you to parse any expression and the latter allows you to parse variables.

In most cases, Parsetree.expression is enough to handle all your needs. Printing the AST for the code you're interested in linting is a good step to understand which AST node type that will help.

Walking the AST

Walking throught the AST is done through iterator https://github.com/rescript-lang/syntax/blob/master/compiler-libs-406/ast_iterator.mli.

It allows you to define methods that will be called whenever each of these AST types are traversed.

The default iterator doesn't do anything. Our linter extends this iterator to include the AST that we are interested in (defined by the rules) and attach a callback function that accumulates any lint errors. See lib/Iterator.ml