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
74 changes: 74 additions & 0 deletions packages/typescript/rules-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,66 @@
{
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/default-param-last": "error",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "default",
"format": ["camelCase"],
"leadingUnderscore": "allow",
"trailingUnderscore": "forbid"
},
{
"selector": "enumMember",
"format": ["PascalCase"]
},
{
"selector": "interface",
"format": ["PascalCase"],
"custom": {
"regex": "^I[A-Z]",
"match": false
}
},
{
"selector": "objectLiteralMethod",
"format": ["camelCase", "PascalCase", "UPPER_CASE"]
},
{
"selector": "objectLiteralProperty",
"format": ["camelCase", "PascalCase", "UPPER_CASE"]
},
{
"selector": "typeLike",
"format": ["PascalCase"]
},
{
"selector": "variable",
"format": ["camelCase", "UPPER_CASE", "PascalCase"],
"leadingUnderscore": "allow"
}
],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-dupe-class-members": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-extra-semi": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-loss-of-precision": "error",
"@typescript-eslint/no-meaningless-void-operator": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-namespace": [
"error",
{
Expand All @@ -33,7 +78,17 @@
}
],
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-throw-literal": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@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",
"@typescript-eslint/no-unused-expressions": [
"error",
{
Expand Down Expand Up @@ -61,9 +116,26 @@
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@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",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/restrict-template-expressions": [
"error",
{
"allowBoolean": true,
"allowNumber": true
}
],
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/unified-signatures": "error",
"constructor-super": "off",
"default-param-last": "off",
Expand All @@ -85,6 +157,7 @@
"no-empty-function": "off",
"no-extra-semi": "off",
"no-func-assign": "off",
"no-implied-eval": "off",
"no-import-assign": "off",
"no-loss-of-precision": "off",
"no-new-symbol": "off",
Expand Down Expand Up @@ -112,5 +185,6 @@
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"require-await": "off",
"valid-typeof": "off"
}
2 changes: 2 additions & 0 deletions packages/typescript/src/__test__/dummy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is only used to test the config.
console.log('Hello, world!');
75 changes: 74 additions & 1 deletion packages/typescript/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ module.exports = {
// (not pre-release) here: https://github.com/tc39/ecma262/releases
ecmaVersion: 2020,
sourceType: 'module',

// 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).

project: ['./tsconfig.json'],
},

plugins: ['@typescript-eslint', 'jsdoc'],

extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:import/typescript',
],

Expand Down Expand Up @@ -59,14 +66,80 @@ module.exports = {
},
],

// Recommended rules that require type information
'@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',
Comment on lines +70 to +74
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


// 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.

'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: ['camelCase'],
leadingUnderscore: 'allow',
trailingUnderscore: 'forbid',
},
{
selector: 'enumMember',
format: ['PascalCase'],
},
{
selector: 'interface',
format: ['PascalCase'],
custom: {
regex: '^I[A-Z]',
match: false,
},
},
{
selector: 'objectLiteralMethod',
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
},
{
selector: 'objectLiteralProperty',
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
{
selector: 'variable',
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allow',
},
],
'@typescript-eslint/no-meaningless-void-operator': '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 disables the use of the void operator in some cases. I'm not sure if we use the void operator at all.

'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
'@typescript-eslint/no-unnecessary-qualifier': '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 don't use a qualifier in enums that reference other enum values. Not sure if we ever do this, but regardless I think it's fine to enable it.

enum Foo {
  Bar,
  Baz = Foo.Bar, // Invalid
  Qux = Baz, // Valid
}

'@typescript-eslint/no-unnecessary-type-arguments': '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.

In functions with generics, this makes sure don't specify the default generic value (if any).

'@typescript-eslint/prefer-includes': '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 is to prefer String.prototype.includes over String.prototype.indexOf (comparing it to -1).

'@typescript-eslint/prefer-nullish-coalescing': '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 ?? rather than ||.

'@typescript-eslint/prefer-readonly': '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.

In classes with variables that are never mutated, this makes sure we define it as readonly. Not sure if we want to use this.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I'm OK with trying this. Not entirely sure how I feel about it yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

Start with warn?

Copy link
Member

Choose a reason for hiding this comment

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

We could 🤔

The problem with warnings is that since they don't get highlighted by CI, people don't notice them and they build up over time. That's why we haven't used warnings for anything here so far.

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 believe there is ways for warnings to show inline in GitHub, but in general I don't think we should use warnings. This rule is auto-fixable, so it's easy to implement regardless.

'@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-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/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/restrict-template-expressions': [
'error',
{
allowBoolean: true,
allowNumber: true,
},
],
'@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.


'default-param-last': 'off',
'@typescript-eslint/default-param-last': 'error',

'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error', { builtinGlobals: true }],

'no-throw-literal': 'off',
// '@typescript-eslint/no-throw-literal' is left disabled because it requires type information
'@typescript-eslint/no-throw-literal': 'error',

'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': [
Expand Down
12 changes: 10 additions & 2 deletions packages/typescript/src/index.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
const { ESLint } = require('eslint');
const { resolve } = require('path');

const config = require('.');

describe('index', () => {
it('is a valid ESLint config', async () => {
const api = new ESLint({
baseConfig: config,
useEslintrc: false,
overrideConfig: {
env: {
node: true,
},
parserOptions: {
tsconfigRootDir: resolve(__dirname, '..'),
project: 'tsconfig.json',
},
},
useEslintrc: false,
});

const result = await api.lintText(`console.log('Hello, world!');\n`);
// In order to test rules that require type information, we need to actually
// compile the file with TypeScript, so rather than using `api.lintText()`,
// we use `api.lintFiles()` and pass in a file that we know will pass.
const result = await api.lintFiles(resolve(__dirname, '__test__/dummy.ts'));

expect(result[0].messages).toStrictEqual([]);
expect(result[0].warningCount).toBe(0);
Expand Down
3 changes: 3 additions & 0 deletions packages/typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["src"]
}