An AST based linter for ReScript. Write your rule based on ReScript AST.
opam install . --deps-only --with-doc --with-test
dune build
or with Nix (flakes),
nix build
dune runtest
or with Nix (flakes),
nix flake check
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
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 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:
DisallowFunction
- Disallow the use of certain functions likestring_of_int
DisallowOperator
- Disallow the use of certain operators like|>
NoJStringInterpolation
- Disallow the use of j-string InterpolationNoReactComponent
- Disallow use of certain React component/domDisallowModule
- Disallow use of certain module
By convention, you should write a new rule on its own module in lib/rules
.
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 descriptionlinters
are list of functions that receive AST and these functions should either returnLintOk
orLintError
.- 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.
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)
You can add the parser that parses JSON config in ConfigReader.ml
. That way the config will read the correct rules that you defined.
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")
]
]
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 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