# Migrating from TSLint to ESLint

With Angular 11 release it was announced that the TSlint (deprecated in 2019) linter was to be replaced by ESLint and there was a 3rd-party solution to help with the migration as well as specific Angular linting rules for ESLint.

Head over to the [official project page](https://github.com/angular-eslint/angular-eslint#migrating-from-codelyzer-and-tslint) for more informations on how to incorporate angular-eslint in a project and migrate from TSLint.

## Migration

To do the migration we first need to install the convert-tslint-to-eslint schematic. Run the following in the root folder of your project(s):

```bash
ng add @angular-eslint/schematics
```

Now you have to choose the project you want to migrate. Then run the schematic as follows, replacing the {{}} bit for your project name:

```bash
ng g @angular-eslint/schematics:convert-tslint-to-eslint {{YOUR_PROJECT_NAME_GOES_HERE}}
```

Add new `.eslintrc.json` to git:

```bash
git add .
```

In your `angular.json` file:

- Update the lint `builder` for other projects such as `skeleton-e2e` for example and invoke `@angular-eslint/builder:lint` instead of tslint builder.
- Then replace `tsConfig` and `exclude` options with `lintFilePatterns` option and add path to your .html and .ts files

Before:

```json5
{
    // ...other attributes
    'skeleton-e2e': {
        // ...other attributes
        lint: {
            builder: '@angular-devkit/build-angular:tslint',
            options: {
                tsConfig: ['e2e/tsconfig.e2e.json'],
                exclude: ['**/node_modules/**']
            }
        }
    }
}
```

After:

```json5
{
    // ...other attributes
    'skeleton-e2e': {
        // ...other attributes
        lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
                lintFilePatterns: ['e2e/**/*.ts', 'e2e/**/*.html']
            }
        }
    }
}
```

## Adding `import` ,`jasmine` ,`jsdoc` ,`simple-import-sort` ,`rxjs` and `rxjs-angular` as plugins

```bash
yarn add eslint-plugin-import eslint-plugin-jasmine eslint-plugin-jsdoc eslint-plugin-simple-import-sort eslint-plugin-rxjs eslint-plugin-rxjs-angular --dev
```

## Customizing ESLint (inspired from OTIA and prestations-ng's files for rules)

You can replace the content of your `.eslintrc.json` with the following `json`. You will maybe need to adjust your "_ignorePatterns_"

```json5
{
    root: true,
    env: {
        browser: true,
        es6: true,
        node: true
    },
    parserOptions: {
        ecmaVersion: 8
    },
    ignorePatterns: [
        '/projects/**/*',
        '/package.json',
        '/angular.json',
        '/karma.conf.js',
        '/.eslintrc.json',
        '/tsconfig*.json',
        '/src/main.ts',
        '/src/typings.d.ts'
    ],
    reportUnusedDisableDirectives: true,
    settings: {
        'import/extensions': ['.js', '.ts', '.mjs', '.mts', '.d.ts'],
        'import/internal-regex': '^@dsivd/',
        'import/resolver': {
            node: { extensions: ['.js', '.ts', '.d.ts'] },
            typescript: {}
        }
    },
    overrides: [
        // Typescript
        {
            files: ['*.ts'],
            parserOptions: {
                ecmaVersion: 2020,
                project: ['tsconfig.json'],
                createDefaultProgram: true
            },
            plugins: ['import', 'rxjs', 'rxjs-angular', 'simple-import-sort'],
            extends: [
                'plugin:@angular-eslint/recommended',
                'plugin:@angular-eslint/template/process-inline-templates',
                'plugin:rxjs/recommended',
                'plugin:@typescript-eslint/recommended',
                'plugin:import/recommended',
                'plugin:import/errors',
                'plugin:import/warnings',
                'plugin:import/typescript'
            ],
            rules: {
                /* @angular-eslint */
                '@angular-eslint/component-selector': [
                    'error',
                    {
                        type: 'element',
                        prefix: 'app',
                        style: 'kebab-case'
                    }
                ],
                '@angular-eslint/directive-selector': [
                    'error',
                    {
                        type: 'attribute',
                        prefix: 'app',
                        style: 'camelCase'
                    }
                ],
                '@angular-eslint/no-empty-lifecycle-method': 'error',
                '@angular-eslint/use-lifecycle-interface': 'error',
                /* @typescript-eslint */
                '@typescript-eslint/array-type': [
                    'error',
                    {
                        default: 'array',
                        readonly: 'array'
                    }
                ],
                '@typescript-eslint/ban-ts-comment': 'error',
                '@typescript-eslint/ban-tslint-comment': 'error',
                '@typescript-eslint/brace-style': [
                    'error',
                    '1tbs',
                    {
                        allowSingleLine: true
                    }
                ],
                '@typescript-eslint/comma-dangle': [
                    'error',
                    {
                        objects: 'never',
                        arrays: 'always-multiline',
                        functions: 'never',
                        enums: 'never',
                        generics: 'always-multiline',
                        tuples: 'always-multiline'
                    }
                ],
                '@typescript-eslint/consistent-type-assertions': 'error',
                '@typescript-eslint/dot-notation': 'error',
                '@typescript-eslint/explicit-function-return-type': [
                    'error',
                    {
                        allowExpressions: true,
                        allowTypedFunctionExpressions: true,
                        allowHigherOrderFunctions: true,
                        allowDirectConstAssertionInArrowFunctions: true,
                        allowConciseArrowFunctionExpressionsStartingWithVoid: false
                    }
                ],
                '@typescript-eslint/explicit-member-accessibility': [
                    'error',
                    {
                        accessibility: 'no-public'
                    }
                ],
                '@typescript-eslint/indent': [
                    'error',
                    4,
                    {
                        SwitchCase: 1,
                        VariableDeclarator: 'first',
                        MemberExpression: 1,
                        FunctionDeclaration: { parameters: 'first', body: 1 },
                        FunctionExpression: { parameters: 'first', body: 1 },
                        CallExpression: { arguments: 'first' },
                        ArrayExpression: 1,
                        ObjectExpression: 1,
                        ImportDeclaration: 1,
                        flatTernaryExpressions: false,
                        offsetTernaryExpressions: true,
                        ignoreComments: false
                    }
                ],
                '@typescript-eslint/interface-name-prefix': 'off',
                '@typescript-eslint/member-delimiter-style': 'error',
                '@typescript-eslint/member-ordering': 'error',
                '@typescript-eslint/no-base-to-string': 'warn',
                '@typescript-eslint/no-empty-function': 'warn',
                '@typescript-eslint/no-inferrable-types': [
                    'error',
                    {
                        ignoreParameters: true
                    }
                ],
                '@typescript-eslint/no-shadow': [
                    'error',
                    {
                        hoist: 'all'
                    }
                ],
                '@typescript-eslint/no-unused-expressions': 'error',
                '@typescript-eslint/no-use-before-define': 'off',
                '@typescript-eslint/no-var-requires': 'error',
                '@typescript-eslint/object-curly-spacing': ['error', 'always'],
                '@typescript-eslint/prefer-for-of': 'error',
                '@typescript-eslint/prefer-function-type': 'error',
                '@typescript-eslint/prefer-includes': 'warn',
                '@typescript-eslint/prefer-readonly': 'off',
                '@typescript-eslint/return-await': ['error', 'never'],
                '@typescript-eslint/semi': ['error', 'always'],
                '@typescript-eslint/triple-slash-reference': [
                    'error',
                    {
                        path: 'always',
                        types: 'prefer-import',
                        lib: 'always'
                    }
                ],
                '@typescript-eslint/type-annotation-spacing': 'error',
                '@typescript-eslint/typedef': [
                    'error',
                    {
                        parameter: true
                    }
                ],
                '@typescript-eslint/unified-signatures': 'error',
                /* @eslint */
                'array-bracket-spacing': ['error', 'never'],
                'arrow-body-style': 'error',
                'arrow-parens': ['error', 'as-needed'],
                'brace-style': 'off',
                'comma-dangle': 'off',
                complexity: 'off',
                'constructor-super': 'error',
                curly: 'error',
                'eol-last': 'error',
                eqeqeq: ['error', 'always'],
                'guard-for-in': 'error',
                'id-match': 'error',
                'max-classes-per-file': 'off',
                'max-len': ['error', { code: 140 }],
                'new-parens': 'error',
                'no-bitwise': 'error',
                'no-caller': 'error',
                'no-cond-assign': 'error',
                'no-console': [
                    'error',
                    {
                        allow: ['log', 'warn', 'error', 'assert', 'clear']
                    }
                ],
                'no-debugger': 'error',
                'no-empty': ['error', { allowEmptyCatch: true }],
                'no-eval': 'error',
                'no-invalid-this': 'error',
                'no-new-wrappers': 'error',
                'no-multiple-empty-lines': 'error',
                'no-restricted-imports': [
                    'error',
                    {
                        paths: ['rxjs/Rx', 'primeng/primeng', 'primeng']
                    }
                ],
                'no-return-assign': 'error',
                'no-shadow': 'off',
                'no-throw-literal': 'error',
                'no-trailing-spaces': 'error',
                'no-undef-init': 'error',
                'no-underscore-dangle': 'off',
                'no-unsafe-finally': 'error',
                'no-unused-labels': 'error',
                'no-var': 'error',
                'object-curly-spacing': 'off',
                'object-shorthand': [
                    'error',
                    'always',
                    {
                        avoidQuotes: true,
                        avoidExplicitReturnArrows: false
                    }
                ],
                'one-var': ['error', 'never'],
                'prefer-arrow-callback': 'error',
                'prefer-template': 'error',
                'quote-props': ['error', 'as-needed'],
                quotes: ['error', 'single'],
                radix: 'error',
                'sort-keys': 'off',
                'space-before-function-paren': [
                    'error',
                    {
                        anonymous: 'never',
                        asyncArrow: 'always',
                        named: 'never'
                    }
                ],
                'space-in-parens': ['error', 'never'],
                'spaced-comment': [
                    'error',
                    'always',
                    {
                        markers: ['/']
                    }
                ],
                'use-isnan': 'error',
                'valid-typeof': 'off',
                /* @rx */
                'rxjs-angular/prefer-async-pipe': 'warn',
                'rxjs/no-sharereplay': 'off',
                /* @import */
                'import/no-cycle': [
                    'warn',
                    {
                        maxDepth: 3,
                        ignoreExternal: true
                    }
                ],
                'import/no-deprecated': 'warn',
                'simple-import-sort/imports': 'error',
                'simple-import-sort/exports': 'error',
                'import/first': 'error',
                'import/newline-after-import': 'error',
                'import/no-duplicates': 'error'
            }
        },
        // HTML
        {
            files: ['*.html'],
            extends: ['plugin:@angular-eslint/template/recommended'],
            rules: {
                '@angular-eslint/template/no-duplicate-attributes': 'error',
                '@angular-eslint/template/no-negated-async': 'off',
                'max-len': [
                    'error',
                    {
                        code: 200
                    }
                ],
                '@angular-eslint/template/button-has-type': 'error'
            }
        },
        // Jasmine
        {
            files: ['src/**/*.spec.ts', 'src/**/*.d.ts'],
            parserOptions: {
                project: 'tsconfig.spec.json'
            },
            extends: ['plugin:jasmine/recommended'],
            plugins: ['jasmine'],
            env: { jasmine: true },
            rules: {
                '@typescript-eslint/no-unused-vars': 'off',
                'jasmine/new-line-before-expect': 'off'
            }
        },
        // Javasript (setup ultra-basique)
        {
            files: ['*.js'],
            extends: ['eslint:recommended'],
            rules: {
                quotes: ['error', 'single']
            }
        }
    ]
}
```

## Fixing files

First, you can run the automatic lint fixing:

```bash
ng lint --fix=true
```

Then manually fix remaining errors and run your tests to be sure you didn't break anything. Good luck!

**NB: Be aware when replacing `any` by `unknown`**

The difference between `unknown` and `any` is described as:
Much like `any`, `any` value is assignable to `unknown`; however, unlike `any`, you cannot access any properties on values with the type `unknown`, nor can you call/construct them. Furthermore, values of type `unknown` can only be assigned to `unknown` or `any`.

For more information on **Subtype vs Assignment**, see https://www.typescriptlang.org/docs/handbook/type-compatibility.html#subtype-vs-assignment

## Cleaning up your project

When you are done with your migration and happy with the result, you can delete `tslint.json` file, useless dependencies and upgrade your project.

**tslint.json**

- Remove file because replaced by `.eslintrc.json`

**package.json**

- removed `tslint`, `codelyzer` and `@angular-eslint/schematics` dependencies

**.editorconfig**

- replace `tslint.json` with `.eslintrc.json`
- replace `.angular-cli.json` with `angular.json` (**if needed**)
- remove `indent_style = space` (**if redundant**)

## Configurating IntelliJ IDEA to use ESLint as your JavaScript linter

- To configure ESLint automatically in the current project, open "_File -> Settings..._" (`Ctrl+Alt+S`) and either:

    - Search for `ESLint`

    or

    - Navigate to "_Languages & Frameworks -> Code Quality Tools -> ESLint_"

    and check radio box "_Automatic ESLint configuration_"

- To configure ESLint automatically in all new projects, open "_File -> Other Settings -> Setting for New Projects..._" and follow step just above when configuration in a current project.

- Fix problems automatically on save (**requires Intellij 2020.1 or higher**): See:
    - [IntelliJ IDEA 2020.1](https://www.jetbrains.com/help/idea/2020.1/eslint.html#ws_js_eslint_activate) and read section "_Detecting and fixing problems on save_"
    - [IntelliJ IDEA 2020.2](https://www.jetbrains.com/help/idea/2020.2/eslint.html#ws_js_eslint_activate) and read section "_Activating and configuring ESLint in IntelliJ IDEA_" bullet point 6
    - [IntelliJ IDEA 2020.3](https://www.jetbrains.com/help/idea/2020.3/eslint.html#ws_js_eslint_activate) and read section "_Fix problems automatically on save_"
