Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rules that require type information #250

Merged
merged 15 commits into from
Nov 15, 2022
Merged

Add rules that require type information #250

merged 15 commits into from
Nov 15, 2022

Conversation

Mrtenz
Copy link
Member

@Mrtenz Mrtenz commented Nov 11, 2022

Closes #2.

This adds rules that require type information to the TypeScript ESLint config. I've set it to extend the default configuration, and added some other rules that are not included in the default.

Copy link
Member Author

@Mrtenz Mrtenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below is an explanation of the rules I've added, and the reasoning behind some of them.

Comment on lines +69 to +74
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these have to do with the use of any. While I strongly prefer not using it, we do use it in some areas, so these rules are turned off.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really like to enable these rules, but saving them for a future release might be a good idea anyway, to reduce the disruption these other rules will cause. I've created an issue to track this: #251

'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule prevents the use of non-string types in template expressions. We make use of this in certain places, like key-tree using bip32:${number} for derivation paths.

Copy link
Member

@Gudahtt Gudahtt Nov 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, interesting. I like the idea of requiring explicit type casting (e.g. bip32:{String(number)}). It's not too inconvenient, and can catch cases where you don't realize it's not a string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is that we only want to accept numbers in those cases.

type PathSegment = `bip32:${number}`;

const foo: PathSegment = 'bip32:foo'; // Invalid
const bar: PathSegment = 'bip32:123'; // Valid

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh I misunderstood, that was a string template type not an actual string template with a variable called number.

Yeah that's a great point. Agreed with turning this off.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually used for complex types, or only primitives? If the latter, maybe consider allow-number etc so we can eat the cake and have it without [object Object]s?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primitives and other types like this:

type Hex = `0x${string}`;
type Foo = `foo-${Hex}`;

I'm not sure how this rule handles the second case, I'll give it a try.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I misunderstood the rule slightly. I think it's fine if we use allowNumber, allowBoolean, maybe allowAny?

Copy link
Member

@Gudahtt Gudahtt Nov 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Would we benefit of allowAny? The other two make sense.

Though either way, having this rule enabled with allowAny would still be an improvement compared to today, so I don't feel strongly about us not using allowAny.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote for allowNumber and allowBoolean. Those make the most sense to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed allowAny.

'@typescript-eslint/restrict-template-expressions': 'off',

// Our rules that require type information
'@typescript-eslint/consistent-type-exports': 'error',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up for debate if we want to enable this one. It enforces import type { ... } when importing types. If there are multiple imports from the same location, the imports would be split up in two, e.g.:

// foo.ts
export type Foo = string;
export const bar = 'baz';

// bar.ts
import type { Foo } from './foo';
import { bar } from './foo';

I think personally it makes it more clear if something is just a type or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this seems great to me.

Comment on lines 78 to 115
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: ['camelCase'],
leadingUnderscore: 'forbid',
trailingUnderscore: 'forbid',
},
{
selector: 'enumMember',
format: ['PascalCase'],
},
{
selector: 'variable',
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
{
selector: 'interface',
format: ['PascalCase'],
custom: {
regex: '^I[A-Z]',
match: false,
},
},
],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also up for debate. This enforces consistent naming for different kinds of identifiers. Currently it's configured ilke:

  • By default we use camelCase. This includes things like function names, args, etc.
  • Enum members use PascalCase, e.g.:
    enum Foo {
      Bar,
      Baz
    }
  • Variables can use camelCase, UPPER_CASE, or PascalCase. Reasoning for this is that in certain cases we want to use UPPER_CASE, like for constants, and in other cases we want to use PascalCase, like for structs.
  • "Type like"-identifiers use PascalCase.
  • Interfaces use PascalCase, and cannot start with I.

@brad-decker You might have an opinion on this too.

Copy link
Member Author

@Mrtenz Mrtenz Nov 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added object literal properties and methods, which can be camelCase, UPPER_CASE, and PascalCase as well. This is useful in situations where we don't control the naming (i.e., external libraries).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also changed it so we allow leading underscores in front of variables. We use this occasionally to signal that a variable is unused.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like using PascalCase for variables is not very common. You mentioned that utils uses it for structs, but is that the only case? If so I wonder whether that should be an override.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do it in snaps-monorepo as well, also for structs. I don't know if there's an easy way to override these rules without copying the entire rule?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if the convention is Enums will use PascalCase, then I am in favor of using that. The rules as stated make sense to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, no problem.

},
},
],
'@typescript-eslint/no-confusing-void-expression': 'error',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can catch some bugs, like trying to log a function that returns void.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is causing a lot of errors in snaps-monorepo, especially in functions that do something like return fn() where fn returns void. I think we should not enable this rule.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense.

I can see how this rule might improve readability, but it would be more verbose, and might make it easier to forget to return.

'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sure we use the generic parameter of Array.prototype.reduce, rather than casting the initial value to something.

const foo = [];
foo.reduce<Bar>(...); // Valid
foo.reduce(reducer, defaultValue as Bar); // Invalid

'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prefers using String.prototype.startsWith and String.prototype.endsWith over other alternatives, like checking a substring from index 0 to n.

'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/promise-function-async': 'error',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sure that functions that return a Promise result are always marked as async functions. That way we cannot throw both regular errors and Promise rejections from the same function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be my favorite rule

'@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sure that all the possible values of a union are covered in a switch statement that does not have a default case.

// This enables support for linting rules that require type information. We
// assume that the project has a `tsconfig.json` file in the directory where
// ESLint is being run.
tsconfigRootDir: process.cwd(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that we always run eslint from the root project here, but that may not always be the case, especially in monorepos. We may have to configure this on a per-project basis.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this in snaps-monorepo, and it does not work out of the box with this option. All monorepo packages need to have their own .eslintrc, and specify the tsconfigRootDir. Otherwise ESLint will complain that the files are not included in tsconfig.json (since they aren't in the root config).

@Mrtenz Mrtenz marked this pull request as ready for review November 11, 2022 18:55
@Mrtenz Mrtenz requested a review from a team as a code owner November 11, 2022 18:55
@Mrtenz
Copy link
Member Author

Mrtenz commented Nov 11, 2022

I did some quick testing in snaps-monorepo, and this is causing a lot of errors that are not automatically fixable. Will have to look into this further.

Gudahtt
Gudahtt previously approved these changes Nov 12, 2022
Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Very excited to see this land.

Can we leave this open a few days to allow others to comment? Since there are a few potentially controversial rules here. i.e. maybe we can merge it on Tuesday.

@Gudahtt Gudahtt dismissed their stale review November 12, 2022 00:16

Missed comment about testing; I will help test these rules out before approving again.

'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/unbound-method': 'off',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule is causing a lot of confusing errors like this:

  14:36  error  Avoid referencing unbound methods which may cause unintentional scoping of `this`.
If your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead  @typescript-eslint/unbound-method

I don't think this is actually an issue, so I have disabled this rule.

Copy link
Contributor

@legobeat legobeat Nov 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may be aware but it's an easy enough mistake to do:

class Foo {
  public logThis() {
    console.log(this);
  }
}
const foo = new Foo();

foo.logThis(); // logs foo
window.setTimeout(foo.logThis); // logs global 

window.setTimeout(foo.logThis.bind()); // do this 
window.setTimeout(() => foo.logThis()); // or this 

Is it worth silencing individually where it's actually desired?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that it causes errors in situations like this:

expect(Math.abs).toStrictEqual(rootRealmGlobal.Math.abs);

Which we happen to do quite a bit in snaps-monorepo. We could just disable it for that repo, but I would guess that there are others with a similar problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Are examples like that widespread? We can disable the rule for specific files/lines. It does seem like what the rule is suggesting is what we are already doing in most places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed to happen quite a bit in snaps-monorepo. Not sure about other repos.

Copy link
Contributor

@legobeat legobeat Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that this can be revisited if/when at least Typescript stdlib is annotating consistently to make those functions work. Or it can be enabled by default and disabled on a per-need (lines or repo) basis.

https://www.typescriptlang.org/docs/handbook/2/functions.html#declaring-this-in-a-function

https://github.com/microsoft/TypeScript/blob/c0f8d1cf75f85132ed46950de884d97ec5312ae8/lib/lib.es5.d.ts#L665

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-enabled it. If it becomes an issue for certain repos, we can disable it for those repos.

@mcmire
Copy link
Contributor

mcmire commented Nov 14, 2022

Looks good to me, minus the bit about getting this to work with snaps-monorepo.

brad-decker
brad-decker previously approved these changes Nov 14, 2022
Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Mrtenz Mrtenz merged commit 32abef3 into main Nov 15, 2022
@Mrtenz Mrtenz deleted the mrtenz/type-rules branch November 15, 2022 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add settings for all of the TypeScript rules that require type information
5 participants