Rules of hooks and Natural Language Linters

On the topic of AI-augmented ReScript, looking at uses of Natural Language Linters, which is in early stage of experimentation.

The idea is to express a linter in natural language, and use AI to turn that informal description into precise linting rules applied to a project. It works like this:

  1. You tell the AI, in English, what you’d like to lint. Example from the video below: Check the rule of hooks that calls to hooks must be at toplevel.
  2. The AI interprets this and generates linting instructions for the general purpose lint engine.
  3. The lint engine runs the generated instruction on your code base and reports any violations.

Here’s a demonstration video of the core functionality (not integrated in the error reporting of the editor yet, but it should give an idea):

There have been questions about supporting rules of hooks for ReScript and React for quite some time, so this is a natural application to experiment with.

I am interested in exploring other linter ideas given that one will be able to quickly craft new rules to be checked by just expressing them in English, with the supporting infra to be built out over time as required by the experiments.
What linters would you like to have on your projects?

7 Likes

That is very cool! Rule of hooks is a great one to start with. What else have you tried?

I have a ton of ESLint rules for my typescript projects, but with ReScript I don’t feel that I need most of them.

I’d be interested in hearing from the community what things they would use this for.

Linters for unit tests are always nice. We use a few for jest/vitest to only allow top level “test” blocks and not use “describe/it” blocks.

We also check that assertions aren’t looking for something useless like “is defined”.

Complexity is another good set of rules. We limit nested ternaries and if/else statements, size of functions, and size of react components.

3 Likes

We also have a very little eslint rules for rescript. There are a few interesting ones:

{
  "selector": ":matches(CallExpression[callee.name='use'], CallExpression[callee.object.property.name='Query'][callee.property.name='use'])[arguments.length=5]:not([arguments.1.value>=0])",
  "message": "Should explicitly set fetchPolicy for the query. Use 'StoreAndNetwork' when the queried data is editable by users and 'StoreOrNetwork' when it's not, eg list of users. You can read more in the relay docs: https://relay.dev/docs/guided-tour/reusing-cached-data/fetch-policies/"
},
{
  "selector": "Property[value.property.name='fragmentRefs']:matches([key.name='fragmentRefs'],[key.name!=/^\\w+Refs$/])",
  "message": "We have a naming convention for fragmentRefs props: `<entityName>Refs`"
},
{
  "selector": "[source.value=/_test\\.bs.mjs$/]",
  "message": "Don't import anything from test files."
},
{
  "selector": "[source.value=/_sandbox\\.bs.mjs$/]",
  "message": "Don't import anything from sandbox files."
},
{
  "selector": "CallExpression[callee.object.name='Stdlib_Debug'][callee.property.name='log']",
  "message": "Debug.log should only be used for debugging purposes."
},
{
  "selector": "CallExpression[callee.object.name='Stdlib_Debug'][callee.property.name='todo']",
  "message": "Debug.todo should only be used for debugging purposes."
},
{
  "selector": "ThrowStatement[argument.type='ObjectExpression'][argument.properties.0.key.name='RE_EXN_ID']",
  "message": "raise should not be used. Use Exn.raiseError instead."
}

It’s the no-restricted-syntax rule.

4 Likes

rescript-react 0.12.0-alpha.3 deprecates use*N hooks and type the hook dependencies as 'a, which is usually nicer but there’s one frustrating corner case, if you have let’s say two dependencies (a, b) and remove one dependency, it becomes (a) which doesn’t compile to an array anymore, so we could add a linter to check that the dependency is always an array or a tuple.

2 Likes

That was a good test to motivate extending the capabilities a bit: recognise when expressions are tuples and arrays.

Thanks for the suggestion @tsnobip

3 Likes