@ncjamieson

More Lint Rules

August 19, 2020 • 5 minute read

Maginfying glass
Photo by Markus Winkler on Unsplash

Last week, I created two more ESLint rules.

The first is an ESLint-port of a TypeScript-specific Wotan rule — Wotan is yet another linter — named no-misused-generics. It’s a rule that Oliver Ash mentioned in a conversation that stemmed a tweet of Dan Vanderkam’s:

You should read Dan’s blog post. It explains the reasoning behind the rule.

The port was simple, as the Wotan rule’s implementation has a single top-level function. All that had to be done was to call that function whenever ESLint encounters an AST node that has a call TypeScript signature:

return {
  ArrowFunctionExpression: checkSignature,
  FunctionDeclaration: checkSignature,
  FunctionExpression: checkSignature,
  MethodDefinition: checkSignature,
  "Program:exit": () => (usage = undefined),
  TSCallSignatureDeclaration: checkSignature,
  TSConstructorType: checkSignature,
  TSConstructSignatureDeclaration: checkSignature,
  TSDeclareFunction: checkSignature,
  TSFunctionType: checkSignature,
  TSIndexSignature: checkSignature,
  TSMethodSignature: checkSignature,
  TSPropertySignature: checkSignature,
};

and then obtain the corresponding TypeScript AST node via the esTreeNodeToTSNodeMap map that’s provided by the @typescript-eslint/parser:

const { esTreeNodeToTSNodeMap } = getParserServices(context);let usage: Map<ts.Identifier, tsutils.VariableInfo> | undefined;

function checkSignature(node: es.Node) {
  const tsNode = esTreeNodeToTSNodeMap.get(node);  if (
    tsutils.isSignatureDeclaration(tsNode) &&
    tsNode.typeParameters !== undefined
  ) {
    checkTypeParameters(tsNode.typeParameters, tsNode);
  }
}

With the rule enabled, misused generics like this:

function parseYAML<T>(input: string): T {
  /* parsing implementation */
}

will effect a lint failure:

Type parameter 'T' cannot be inferred from any parameter.

Here, the generic is performing the role of a type assertion. There is no guarantee that the runtime return value will be whatever is specified for T. It could be anything.

Instead the function should specify unknown as the return type, requiring the caller to use an explicit type assertion — or, better still, a user-defined type guard.

There are exceptions to the rule — for example, some RxJS operators would fail, as they infer a type parameter from the source observable upon which pipe is called. However, exceptions are relatively rare and my recommendation would be to use the rule as a warning — or as an error with local overrides.

You can find the rule in eslint-plugin-etc. It has same name as the Wotan rule: no-misused-generics.

The second rule has a related tweet, too. It’s this one, from Sophie Alpert:

The problem with the code in the tweet’s snippet is that it effects an additional render and reconciliation. The function passed to useEffect runs after a render is committed and the setState call made within the function effects another render.

Wherever useEffect is used to memoize the synchronous processing of data, it can be replaced with useMemo and the additional render and reconciliation can be avoided, like this:

const processedData = useMemo(() => {
  let processed = /* do something with data */;
  return processed;
}, [data]);

The lint rule that I wrote — prefer-usememo in eslint-plugin-react-etc — uses a simple heuristic to determine whether or not a useEffect call can be replaced with useMemo.

The rule will suggest useMemo whenever the function passed to useEffect:

  • makes an single, unconditional, clearly-synchronous call to a useState setter;
  • does not reference or call additional useState setters;
  • has some dependencies; and
  • does not return a teardown function.

The heuristic identifies the useEffect usage in Sophie’s tweet — and suggests useMemo as a replacement — and it hasn’t effected any false positives in the code bases over which I’ve run the rule. The rule’s tests include some of the use cases for which the rule will not suggest useMemo as a replacement.

The rule is implemented in TypeScript — there are other React lint rules that I’ll be adding to eslint-plugin-react-etc and they will take advantage of information from the type system — but it doesn’t rely upon TypeScript-specific AST nodes, so it will work just fine with ESLint configurations that do not use the TypeScript parser.


Personal blog by Nicholas Jamieson.
Mostly articles about RxJS, TypeScript and React.
GitHubTwitter

© 2020 Nicholas Jamieson All Rights ReservedRSS