diff --git a/.cspell.json b/.cspell.json index 09466a9d300a..4691cd152db3 100644 --- a/.cspell.json +++ b/.cspell.json @@ -73,8 +73,10 @@ "linebreaks", "lzstring", "markdownlint", + "metastring", "necroing", "nocheck", + "noninteractive", "nullish", "OOM", "OOMs", diff --git a/.prettierignore b/.prettierignore index 896d5c464a35..eda67619c874 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,6 +14,9 @@ packages/eslint-plugin/src/configs/*.json CONTRIBUTORS.md packages/ast-spec/src/*/*/fixtures/_error_/*/fixture.ts +# Syntax not yet supported +packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts + # Ignore CHANGELOG.md files to avoid issues with automated release job CHANGELOG.md diff --git a/docs/linting/MONOREPO.md b/docs/linting/MONOREPO.md index 2f8cbe15f349..4ec2df161362 100644 --- a/docs/linting/MONOREPO.md +++ b/docs/linting/MONOREPO.md @@ -18,22 +18,24 @@ Earlier in our docs on [typed linting](./TYPED_LINTING.md), we showed you how to For example, this is how we specify all of our `tsconfig.json` within this repo. -```diff title=".eslintrc.js" - module.exports = { - root: true, - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: __dirname, -- project: ['./tsconfig.json'], -+ project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'], - }, - plugins: ['@typescript-eslint'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - }; +```js title=".eslintrc.js" +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: __dirname, + // Remove this line + project: ['./tsconfig.json'], + // Add this line + project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'], + }, + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], +}; ``` If you're looking for an example of what the `.eslintrc.js`, and referenced `tsconfig.json` might look like in a real example, look no further than this very repo. We're a multi-package monorepo that uses one `tsconfig.json` per package, that also uses typed linting. diff --git a/docs/linting/README.md b/docs/linting/README.md index dbf08d1e7808..23f0a2b1981a 100644 --- a/docs/linting/README.md +++ b/docs/linting/README.md @@ -136,19 +136,18 @@ If you use [`prettier`](https://www.npmjs.com/package/prettier), there is also a Using this config by adding it to the end of your `extends`: -```diff title=".eslintrc.js" - module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint', - ], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', -+ 'prettier', - ], - }; +```js title=".eslintrc.js" +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + // Add this line + 'prettier', + ], +}; ``` ### Community Configs @@ -163,19 +162,20 @@ A few popular all-in-one configs are: To use one of these complete config packages, you would replace the `extends` with the package name. For example: -```diff title=".eslintrc.js" - module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint', - ], - extends: [ -- 'eslint:recommended', -- 'plugin:@typescript-eslint/recommended', -+ 'airbnb-typescript', - ], - }; +```js title=".eslintrc.js" +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + // Removed lines start + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + // Removed lines end + // Add this line + 'airbnb-typescript', + ], +}; ``` @@ -196,20 +196,22 @@ Below are just a few examples: Every plugin that is out there includes documentation on the various configurations and rules they offer. A typical plugin might be used like: -```diff title=".eslintrc.js" - module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: [ - '@typescript-eslint', -+ 'jest', - ], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', -+ 'plugin:jest/recommended', - ], - }; +```js title=".eslintrc.js" +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint', + // Add this line + 'jest', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + // Add this line + 'plugin:jest/recommended', + ], +}; ``` diff --git a/docs/linting/TROUBLESHOOTING.md b/docs/linting/TROUBLESHOOTING.md index d6cb97486f57..d9c447b4d0e2 100644 --- a/docs/linting/TROUBLESHOOTING.md +++ b/docs/linting/TROUBLESHOOTING.md @@ -42,12 +42,15 @@ See our docs on [type aware linting](./TYPED_LINTING.md#i-get-errors-telling-me- You can use `parserOptions.extraFileExtensions` to specify an array of non-TypeScript extensions to allow, for example: -```diff - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], -+ extraFileExtensions: ['.vue'], - }, +```js title=".eslintrc.js" +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + // Add this line + extraFileExtensions: ['.vue'], + }, +}; ``` ## One of my lint rules isn't working correctly on a pure JavaScript file diff --git a/docs/linting/TYPED_LINTING.md b/docs/linting/TYPED_LINTING.md index 376a0334c174..ae58028e442b 100644 --- a/docs/linting/TYPED_LINTING.md +++ b/docs/linting/TYPED_LINTING.md @@ -8,21 +8,24 @@ Under the hood, the typescript-eslint parser uses TypeScript's compiler APIs to To tap into TypeScript's additional powers, there are two small changes you need to make to your config file: -```diff title=".eslintrc.js" - module.exports = { - root: true, - parser: '@typescript-eslint/parser', -+ parserOptions: { -+ tsconfigRootDir: __dirname, -+ project: ['./tsconfig.json'], -+ }, - plugins: ['@typescript-eslint'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', -+ 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - }; +```js title=".eslintrc.js" +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + // Added lines start + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + // Added lines end + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + // Add this line + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], +}; ``` In more detail: diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index ff85922117e5..60a769945183 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -102,6 +102,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/ban-tslint-comment`](./docs/rules/ban-tslint-comment.md) | Disallow `// tslint:` comments | :lock: | :wrench: | | | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Disallow certain types | :white_check_mark: | :wrench: | | | [`@typescript-eslint/class-literal-property-style`](./docs/rules/class-literal-property-style.md) | Enforce that literals on classes are exposed in a consistent style | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-generic-constructors`](./docs/rules/consistent-generic-constructors.md) | Enforce specifying generic type arguments on type annotation or constructor name of a constructor call | :lock: | :wrench: | | | [`@typescript-eslint/consistent-indexed-object-style`](./docs/rules/consistent-indexed-object-style.md) | Require or disallow the `Record` type | :lock: | :wrench: | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforce consistent usage of type assertions | :lock: | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Enforce type definitions to consistently use either `interface` or `type` | :lock: | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/README.md b/packages/eslint-plugin/docs/rules/README.md index 41cdb518c1cd..805f126f8228 100644 --- a/packages/eslint-plugin/docs/rules/README.md +++ b/packages/eslint-plugin/docs/rules/README.md @@ -24,6 +24,7 @@ See [Configs](/docs/linting/configs) for how to enable recommended rules using c | [`@typescript-eslint/ban-tslint-comment`](./ban-tslint-comment.md) | Disallow `// tslint:` comments | :lock: | :wrench: | | | [`@typescript-eslint/ban-types`](./ban-types.md) | Disallow certain types | :white_check_mark: | :wrench: | | | [`@typescript-eslint/class-literal-property-style`](./class-literal-property-style.md) | Enforce that literals on classes are exposed in a consistent style | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-generic-constructors`](./consistent-generic-constructors.md) | Enforce specifying generic type arguments on type annotation or constructor name of a constructor call | :lock: | :wrench: | | | [`@typescript-eslint/consistent-indexed-object-style`](./consistent-indexed-object-style.md) | Require or disallow the `Record` type | :lock: | :wrench: | | | [`@typescript-eslint/consistent-type-assertions`](./consistent-type-assertions.md) | Enforce consistent usage of type assertions | :lock: | | | | [`@typescript-eslint/consistent-type-definitions`](./consistent-type-definitions.md) | Enforce type definitions to consistently use either `interface` or `type` | :lock: | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/consistent-generic-constructors.md b/packages/eslint-plugin/docs/rules/consistent-generic-constructors.md new file mode 100644 index 000000000000..db717dbcbd3d --- /dev/null +++ b/packages/eslint-plugin/docs/rules/consistent-generic-constructors.md @@ -0,0 +1,82 @@ +# `consistent-generic-constructors` + +Enforces specifying generic type arguments on type annotation or constructor name of a constructor call. + +When constructing a generic class, you can specify the type arguments on either the left-hand side (as a type annotation) or the right-hand side (as part of the constructor call): + +```ts +// Left-hand side +const map: Map = new Map(); + +// Right-hand side +const map = new Map(); +``` + +This rule ensures that type arguments appear consistently on one side of the declaration. + +## Options + +```jsonc +{ + "rules": { + "@typescript-eslint/consistent-generic-constructors": [ + "error", + "constructor" + ] + } +} +``` + +This rule takes a string option: + +- If it's set to `constructor` (default), type arguments that **only** appear on the type annotation are disallowed. +- If it's set to `type-annotation`, type arguments that **only** appear on the constructor are disallowed. + +## Rule Details + +The rule never reports when there are type parameters on both sides, or neither sides of the declaration. It also doesn't report if the names of the type annotation and the constructor don't match. + +### `constructor` + + + +#### ❌ Incorrect + +```ts +const map: Map = new Map(); +const set: Set = new Set(); +``` + +#### ✅ Correct + +```ts +const map = new Map(); +const map: Map = new MyMap(); +const set = new Set(); +const set = new Set(); +const set: Set = new Set(); +``` + +### `type-annotation` + + + +#### ❌ Incorrect + +```ts +const map = new Map(); +const set = new Set(); +``` + +#### ✅ Correct + +```ts +const map: Map = new Map(); +const set: Set = new Set(); +const set = new Set(); +const set: Set = new Set(); +``` + +## When Not To Use It + +You can turn this rule off if you don't want to enforce one kind of generic constructor style over the other. diff --git a/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md index 2b5aa6243542..b25350b60b97 100644 --- a/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md +++ b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md @@ -158,10 +158,9 @@ Examples of code for this rule with `{ allowDirectConstAssertionInArrowFunctions ```ts export const func = (value: number) => ({ type: 'X', value }); -export const foo = () => - ({ - bar: true, - } as const); +export const foo = () => ({ + bar: true, +}); export const bar = () => 1; ``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 5f9562d5bac3..8742d36b2089 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -18,6 +18,7 @@ export = { '@typescript-eslint/comma-dangle': 'error', 'comma-spacing': 'off', '@typescript-eslint/comma-spacing': 'error', + '@typescript-eslint/consistent-generic-constructors': 'error', '@typescript-eslint/consistent-indexed-object-style': 'error', '@typescript-eslint/consistent-type-assertions': 'error', '@typescript-eslint/consistent-type-definitions': 'error', diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index bb51ebfa4f27..a9c91f7c1ca1 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -9,6 +9,7 @@ export = { '@typescript-eslint/ban-tslint-comment': 'warn', '@typescript-eslint/class-literal-property-style': 'warn', '@typescript-eslint/consistent-indexed-object-style': 'warn', + '@typescript-eslint/consistent-generic-constructors': 'warn', '@typescript-eslint/consistent-type-assertions': 'warn', '@typescript-eslint/consistent-type-definitions': 'warn', 'dot-notation': 'off', diff --git a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts new file mode 100644 index 000000000000..0df1412bd7bb --- /dev/null +++ b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts @@ -0,0 +1,104 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { createRule } from '../util'; + +type MessageIds = 'preferTypeAnnotation' | 'preferConstructor'; +type Options = ['type-annotation' | 'constructor']; + +export default createRule({ + name: 'consistent-generic-constructors', + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce specifying generic type arguments on type annotation or constructor name of a constructor call', + recommended: 'strict', + }, + messages: { + preferTypeAnnotation: + 'The generic type arguments should be specified as part of the type annotation.', + preferConstructor: + 'The generic type arguments should be specified as part of the constructor type arguments.', + }, + fixable: 'code', + schema: [ + { + enum: ['type-annotation', 'constructor'], + }, + ], + }, + defaultOptions: ['constructor'], + create(context, [mode]) { + const sourceCode = context.getSourceCode(); + return { + VariableDeclarator(node): void { + const lhs = node.id.typeAnnotation?.typeAnnotation; + const rhs = node.init; + if ( + !rhs || + rhs.type !== AST_NODE_TYPES.NewExpression || + rhs.callee.type !== AST_NODE_TYPES.Identifier + ) { + return; + } + if ( + lhs && + (lhs.type !== AST_NODE_TYPES.TSTypeReference || + lhs.typeName.type !== AST_NODE_TYPES.Identifier || + lhs.typeName.name !== rhs.callee.name) + ) { + return; + } + if (mode === 'type-annotation') { + if (!lhs && rhs.typeParameters) { + const { typeParameters, callee } = rhs; + const typeAnnotation = + sourceCode.getText(callee) + sourceCode.getText(typeParameters); + context.report({ + node, + messageId: 'preferTypeAnnotation', + fix(fixer) { + return [ + fixer.remove(typeParameters), + fixer.insertTextAfter(node.id, ': ' + typeAnnotation), + ]; + }, + }); + } + return; + } + if (mode === 'constructor') { + if (lhs?.typeParameters && !rhs.typeParameters) { + const hasParens = + sourceCode.getTokenAfter(rhs.callee)?.value === '('; + const extraComments = new Set( + sourceCode.getCommentsInside(lhs.parent!), + ); + sourceCode + .getCommentsInside(lhs.typeParameters) + .forEach(c => extraComments.delete(c)); + context.report({ + node, + messageId: 'preferConstructor', + *fix(fixer) { + yield fixer.remove(lhs.parent!); + for (const comment of extraComments) { + yield fixer.insertTextAfter( + rhs.callee, + sourceCode.getText(comment), + ); + } + yield fixer.insertTextAfter( + rhs.callee, + sourceCode.getText(lhs.typeParameters), + ); + if (!hasParens) { + yield fixer.insertTextAfter(rhs.callee, '()'); + } + }, + }); + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 41afc88199f2..29a47ec2384a 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -8,6 +8,7 @@ import braceStyle from './brace-style'; import classLiteralPropertyStyle from './class-literal-property-style'; import commaDangle from './comma-dangle'; import commaSpacing from './comma-spacing'; +import consistentGenericConstructors from './consistent-generic-constructors'; import consistentIndexedObjectStyle from './consistent-indexed-object-style'; import consistentTypeAssertions from './consistent-type-assertions'; import consistentTypeDefinitions from './consistent-type-definitions'; @@ -136,6 +137,7 @@ export default { 'class-literal-property-style': classLiteralPropertyStyle, 'comma-dangle': commaDangle, 'comma-spacing': commaSpacing, + 'consistent-generic-constructors': consistentGenericConstructors, 'consistent-indexed-object-style': consistentIndexedObjectStyle, 'consistent-type-assertions': consistentTypeAssertions, 'consistent-type-definitions': consistentTypeDefinitions, diff --git a/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts b/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts new file mode 100644 index 000000000000..0fe3bcae7fb0 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts @@ -0,0 +1,217 @@ +import rule from '../../src/rules/consistent-generic-constructors'; +import { RuleTester, noFormat } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('consistent-generic-constructors', rule, { + valid: [ + // default: constructor + 'const a = new Foo();', + 'const a = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Foo = Foo();', + 'const a: Foo = Foo();', + 'const a: Foo = Foo();', + // type-annotation + { + code: 'const a = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Bar = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Bar = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = Foo();', + options: ['type-annotation'], + }, + { + code: 'const a = new (class C {})();', + options: ['type-annotation'], + }, + ], + invalid: [ + { + code: 'const a: Foo = new Foo();', + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: 'const a = new Foo();', + }, + { + code: 'const a: Map = new Map();', + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: 'const a = new Map();', + }, + { + code: noFormat`const a: Map = new Map();`, + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new Map();`, + }, + { + code: noFormat`const a: Map< string, number > = new Map();`, + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new Map< string, number >();`, + }, + { + code: noFormat`const a: Map = new Map ();`, + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new Map ();`, + }, + { + code: noFormat`const a: Foo = new Foo;`, + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new Foo();`, + }, + { + code: 'const a: /* comment */ Foo/* another */ = new Foo();', + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new Foo/* comment *//* another */();`, + }, + { + code: 'const a: Foo/* comment */ = new Foo /* another */();', + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new Foo/* comment */ /* another */();`, + }, + { + code: noFormat`const a: Foo = new \n Foo \n ();`, + errors: [ + { + messageId: 'preferConstructor', + }, + ], + output: noFormat`const a = new \n Foo \n ();`, + }, + { + code: 'const a = new Foo();', + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: 'const a: Foo = new Foo();', + }, + { + code: 'const a = new Map();', + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: 'const a: Map = new Map();', + }, + { + code: noFormat`const a = new Map ();`, + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: noFormat`const a: Map = new Map ();`, + }, + { + code: noFormat`const a = new Map< string, number >();`, + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: noFormat`const a: Map< string, number > = new Map();`, + }, + { + code: noFormat`const a = new \n Foo \n ();`, + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: noFormat`const a: Foo = new \n Foo \n ();`, + }, + { + code: 'const a = new Foo/* comment */ /* another */();', + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: noFormat`const a: Foo = new Foo/* comment */ /* another */();`, + }, + { + code: 'const a = new Foo();', + options: ['type-annotation'], + errors: [ + { + messageId: 'preferTypeAnnotation', + }, + ], + output: noFormat`const a: Foo = new Foo();`, + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index fb7eadb47b71..cca98d2a952b 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -739,6 +739,15 @@ export function foo() { return new Promise(); } `, + // https://github.com/typescript-eslint/typescript-eslint/issues/5152 + { + code: noFormat` +function foo(value: T): T { + return { value }; +} +export type Foo = typeof foo; + `, + }, // https://github.com/typescript-eslint/typescript-eslint/issues/2331 { code: ` diff --git a/packages/scope-manager/src/referencer/TypeVisitor.ts b/packages/scope-manager/src/referencer/TypeVisitor.ts index e9abb40f4f42..0d345a149a29 100644 --- a/packages/scope-manager/src/referencer/TypeVisitor.ts +++ b/packages/scope-manager/src/referencer/TypeVisitor.ts @@ -265,6 +265,7 @@ class TypeVisitor extends Visitor { } this.#referencer.currentScope().referenceValue(expr); } + this.visit(node.typeParameters); } protected TSTypeAnnotation(node: TSESTree.TSTypeAnnotation): void { diff --git a/packages/scope-manager/tests/fixtures.test.ts b/packages/scope-manager/tests/fixtures.test.ts index f029367ba06e..b5a2fc70bd67 100644 --- a/packages/scope-manager/tests/fixtures.test.ts +++ b/packages/scope-manager/tests/fixtures.test.ts @@ -13,7 +13,9 @@ const ONLY = [].join(path.sep); const FIXTURES_DIR = path.resolve(__dirname, 'fixtures'); const fixtures = glob - .sync(`${FIXTURES_DIR}/**/*.{js,ts,jsx,tsx}`, { + .sync('**/*.{js,ts,jsx,tsx}', { + cwd: FIXTURES_DIR, + absolute: true, ignore: ['fixtures.test.ts'], }) .map(absolute => { diff --git a/packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts b/packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts new file mode 100644 index 000000000000..e8928f9f914a --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts @@ -0,0 +1,5 @@ +function foo(y: T) { + return { y }; +} + +export type Foo = typeof foo; diff --git a/packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts.shot b/packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts.shot new file mode 100644 index 000000000000..7ec279b55f82 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-declaration/type-query-with-parameters.ts.shot @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-declaration type-query-with-parameters 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + FunctionNameDefinition$1 { + name: Identifier<"foo">, + node: FunctionDeclaration$1, + }, + ], + name: "foo", + references: Array [ + Reference$3 { + identifier: Identifier<"foo">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$2, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$3 { + defs: Array [], + name: "arguments", + references: Array [], + isValueVariable: true, + isTypeVariable: true, + }, + Variable$4 { + defs: Array [ + ParameterDefinition$2 { + name: Identifier<"y">, + node: FunctionDeclaration$1, + }, + ], + name: "y", + references: Array [ + Reference$2 { + identifier: Identifier<"y">, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: false, + resolved: Variable$4, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + Variable$5 { + defs: Array [ + TypeDefinition$3 { + name: Identifier<"T">, + node: TSTypeParameter$2, + }, + ], + name: "T", + references: Array [ + Reference$1 { + identifier: Identifier<"T">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$5, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$6 { + defs: Array [ + TypeDefinition$4 { + name: Identifier<"Foo">, + node: TSTypeAliasDeclaration$3, + }, + ], + name: "Foo", + references: Array [], + isValueVariable: false, + isTypeVariable: true, + }, + Variable$7 { + defs: Array [ + TypeDefinition$5 { + name: Identifier<"T">, + node: TSTypeParameter$4, + }, + ], + name: "T", + references: Array [ + Reference$4 { + identifier: Identifier<"T">, + isRead: true, + isTypeReference: true, + isValueReference: false, + isWrite: false, + resolved: Variable$7, + }, + ], + isValueVariable: false, + isTypeVariable: true, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$5, + isStrict: false, + references: Array [], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "foo" => Variable$2, + "Foo" => Variable$6, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + Variable$6, + ], + }, + FunctionScope$2 { + block: FunctionDeclaration$1, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "arguments" => Variable$3, + "y" => Variable$4, + "T" => Variable$5, + }, + type: "function", + upper: GlobalScope$1, + variables: Array [ + Variable$3, + Variable$4, + Variable$5, + ], + }, + TypeScope$3 { + block: TSTypeAliasDeclaration$3, + isStrict: true, + references: Array [ + Reference$3, + Reference$4, + ], + set: Map { + "T" => Variable$7, + }, + type: "type", + upper: GlobalScope$1, + variables: Array [ + Variable$7, + ], + }, + ], +} +`; diff --git a/packages/utils/src/ts-eslint/SourceCode.ts b/packages/utils/src/ts-eslint/SourceCode.ts index 7ecc7ab1b095..17893d25b50a 100644 --- a/packages/utils/src/ts-eslint/SourceCode.ts +++ b/packages/utils/src/ts-eslint/SourceCode.ts @@ -276,7 +276,7 @@ declare class SourceCodeBase extends TokenStore { * @returns The text representing the AST node. */ getText( - node?: TSESTree.Node, + node?: TSESTree.Node | TSESTree.Token, beforeCount?: number, afterCount?: number, ): string; diff --git a/packages/visitor-keys/src/visitor-keys.ts b/packages/visitor-keys/src/visitor-keys.ts index f7be9f1aed8c..4c3a26d40665 100644 --- a/packages/visitor-keys/src/visitor-keys.ts +++ b/packages/visitor-keys/src/visitor-keys.ts @@ -142,7 +142,7 @@ const additionalKeys: AdditionalKeys = { TSTypeParameterDeclaration: ['params'], TSTypeParameterInstantiation: ['params'], TSTypePredicate: ['typeAnnotation', 'parameterName'], - TSTypeQuery: ['exprName'], + TSTypeQuery: ['exprName', 'typeParameters'], TSTypeReference: ['typeName', 'typeParameters'], TSUndefinedKeyword: [], TSUnionType: ['types'], diff --git a/packages/website/docusaurusConfig.ts b/packages/website/docusaurusConfig.ts index eab99e5d2eb3..ed8d4deaa0cf 100644 --- a/packages/website/docusaurusConfig.ts +++ b/packages/website/docusaurusConfig.ts @@ -139,6 +139,23 @@ const themeConfig: ThemeCommonConfig & AlgoliaThemeConfig = { styles: [], }, additionalLanguages: ['ignore'], + magicComments: [ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: { start: 'highlight-start', end: 'highlight-end' }, + }, + { + className: 'code-block-removed-line', + line: 'Remove this line', + block: { start: 'Removed lines start', end: 'Removed lines end' }, + }, + { + className: 'code-block-added-line', + line: 'Add this line', + block: { start: 'Added lines start', end: 'Added lines end' }, + }, + ], }, tableOfContents: { maxHeadingLevel: 4, diff --git a/packages/website/package.json b/packages/website/package.json index ffa80744244b..a15565d09326 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -26,6 +26,7 @@ "json5": "^2.2.1", "konamimojisplosion": "^0.5.1", "lzstring.ts": "^2.0.2", + "prism-react-renderer": "^1.3.3", "react": "^18.1.0", "react-dom": "^18.1.0", "remark-docusaurus-tabs": "^0.2.0", diff --git a/packages/website/src/components/config/ConfigEditor.module.css b/packages/website/src/components/config/ConfigEditor.module.css index 1329e5af9a74..b2f25dfcbe0f 100644 --- a/packages/website/src/components/config/ConfigEditor.module.css +++ b/packages/website/src/components/config/ConfigEditor.module.css @@ -39,6 +39,10 @@ margin: 0.2rem 0; } +.searchResultDescription { + flex: 1 0 75%; +} + .searchResult:nth-child(even), .searchResultGroup:nth-child(even) { background: var(--ifm-color-emphasis-100); diff --git a/packages/website/src/components/config/ConfigEditor.tsx b/packages/website/src/components/config/ConfigEditor.tsx index 43bfcadd9bcd..4f0f9d939955 100644 --- a/packages/website/src/components/config/ConfigEditor.tsx +++ b/packages/website/src/components/config/ConfigEditor.tsx @@ -7,11 +7,14 @@ import Text from '../inputs/Text'; import Checkbox from '../inputs/Checkbox'; import useFocus from '../hooks/useFocus'; import Modal from '@site/src/components/modals/Modal'; +import Dropdown from '@site/src/components/inputs/Dropdown'; export interface ConfigOptionsField { key: string; + type: 'boolean' | 'string'; label?: string; defaults?: unknown[]; + enum?: string[]; } export interface ConfigOptionsType { @@ -33,6 +36,11 @@ function reducerObject( state: ConfigEditorValues, action: | { type: 'init'; config?: ConfigEditorValues } + | { + type: 'set'; + name: string; + value: unknown; + } | { type: 'toggle'; checked: boolean; @@ -44,6 +52,15 @@ function reducerObject( case 'init': { return action.config ?? {}; } + case 'set': { + const newState = { ...state }; + if (action.value === '') { + delete newState[action.name]; + } else { + newState[action.name] = action.value; + } + return newState; + } case 'toggle': { const newState = { ...state }; if (action.checked) { @@ -110,28 +127,44 @@ function ConfigEditor(props: ConfigEditorProps): JSX.Element {
{group.fields.map(item => ( ))}
diff --git a/packages/website/src/components/config/ConfigEslint.tsx b/packages/website/src/components/config/ConfigEslint.tsx index 3ffc57cb35ff..f14d00ec5b78 100644 --- a/packages/website/src/components/config/ConfigEslint.tsx +++ b/packages/website/src/components/config/ConfigEslint.tsx @@ -1,14 +1,9 @@ import React, { useCallback, useEffect, useState } from 'react'; import ConfigEditor, { ConfigOptionsType } from './ConfigEditor'; -import type { - RulesRecord, - RuleDetails, - RuleEntry, - ConfigModel, -} from '../types'; +import type { RuleDetails, RuleEntry, ConfigModel, EslintRC } from '../types'; import { shallowEqual } from '../lib/shallowEqual'; -import { parseESLintRC, toJsonConfig } from '@site/src/components/config/utils'; +import { parseESLintRC, toJson } from './utils'; export interface ConfigEslintProps { readonly isOpen: boolean; @@ -33,11 +28,11 @@ function checkOptions(rule: [string, unknown]): rule is [string, RuleEntry] { function ConfigEslint(props: ConfigEslintProps): JSX.Element { const [options, updateOptions] = useState([]); - const [configObject, updateConfigObject] = useState({}); + const [configObject, updateConfigObject] = useState(); useEffect(() => { if (props.isOpen) { - updateConfigObject(props.config ? parseESLintRC(props.config) : {}); + updateConfigObject(parseESLintRC(props.config)); } }, [props.isOpen, props.config]); @@ -50,6 +45,7 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element { .map(item => ({ key: item.name, label: item.description, + type: 'boolean', defaults: ['error', 2, 'warn', 1, ['error'], ['warn'], [2], [1]], })), }, @@ -60,6 +56,7 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element { .map(item => ({ key: item.name, label: item.description, + type: 'boolean', defaults: ['error', 2, 'warn', 1, ['error'], ['warn'], [2], [1]], })), }, @@ -77,8 +74,10 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element { ) .filter(checkOptions), ); - if (!shallowEqual(cfg, configObject)) { - props.onClose({ eslintrc: toJsonConfig(cfg, 'rules') }); + if (!shallowEqual(cfg, configObject?.rules)) { + props.onClose({ + eslintrc: toJson({ ...(configObject ?? {}), rules: cfg }), + }); } else { props.onClose(); } @@ -90,7 +89,7 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element { diff --git a/packages/website/src/components/config/ConfigTypeScript.tsx b/packages/website/src/components/config/ConfigTypeScript.tsx index 78f547088b66..91a4781d14fe 100644 --- a/packages/website/src/components/config/ConfigTypeScript.tsx +++ b/packages/website/src/components/config/ConfigTypeScript.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useEffect, useState } from 'react'; import ConfigEditor, { ConfigOptionsType } from './ConfigEditor'; -import type { CompilerFlags, ConfigModel } from '../types'; +import type { ConfigModel, TSConfig } from '../types'; import { shallowEqual } from '../lib/shallowEqual'; -import { getTypescriptOptions, parseTSConfig, toJsonConfig } from './utils'; +import { getTypescriptOptions, parseTSConfig, toJson } from './utils'; interface ConfigTypeScriptProps { readonly isOpen: boolean; @@ -11,17 +11,13 @@ interface ConfigTypeScriptProps { readonly config?: string; } -function checkOptions(item: [string, unknown]): item is [string, boolean] { - return typeof item[1] === 'boolean'; -} - function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { const [tsConfigOptions, updateOptions] = useState([]); - const [configObject, updateConfigObject] = useState({}); + const [configObject, updateConfigObject] = useState(); useEffect(() => { if (props.isOpen) { - updateConfigObject(props.config ? parseTSConfig(props.config) : {}); + updateConfigObject(parseTSConfig(props.config)); } }, [props.isOpen, props.config]); @@ -36,10 +32,20 @@ function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { heading: category, fields: [], }; - group[category].fields.push({ - key: item.name, - label: item.description!.message, - }); + if (item.type === 'boolean') { + group[category].fields.push({ + key: item.name, + type: 'boolean', + label: item.description!.message, + }); + } else if (item.type instanceof Map) { + group[category].fields.push({ + key: item.name, + type: 'string', + label: item.description!.message, + enum: ['', ...Array.from(item.type.keys())], + }); + } return group; }, {}, @@ -51,11 +57,11 @@ function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { const onClose = useCallback( (newConfig: Record) => { - const cfg = Object.fromEntries( - Object.entries(newConfig).filter(checkOptions), - ); - if (!shallowEqual(cfg, configObject)) { - props.onClose({ tsconfig: toJsonConfig(cfg, 'compilerOptions') }); + const cfg = { ...newConfig }; + if (!shallowEqual(cfg, configObject?.compilerOptions)) { + props.onClose({ + tsconfig: toJson({ ...(configObject ?? {}), compilerOptions: cfg }), + }); } else { props.onClose(); } @@ -67,7 +73,7 @@ function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { diff --git a/packages/website/src/components/config/utils.ts b/packages/website/src/components/config/utils.ts index 9ee188b6152e..749487ea1aab 100644 --- a/packages/website/src/components/config/utils.ts +++ b/packages/website/src/components/config/utils.ts @@ -1,4 +1,4 @@ -import type { CompilerFlags, RulesRecord } from '@site/src/components/types'; +import type { EslintRC, TSConfig } from '@site/src/components/types'; import { parse } from 'json5'; import { isRecord } from '@site/src/components/ast/utils'; @@ -8,56 +8,85 @@ export interface OptionDeclarations { type?: unknown; category?: { message: string }; description?: { message: string }; + element?: { + type: unknown; + }; } -export function parseESLintRC(code?: string): RulesRecord { +export function parseESLintRC(code?: string): EslintRC { if (code) { try { const parsed: unknown = parse(code); - if (isRecord(parsed) && 'rules' in parsed && isRecord(parsed.rules)) { - return parsed.rules as RulesRecord; + if (isRecord(parsed)) { + if ('rules' in parsed && isRecord(parsed.rules)) { + return parsed as EslintRC; + } + return { ...parsed, rules: {} }; } } catch (e) { // eslint-disable-next-line no-console console.error(e); } } - return {}; + return { rules: {} }; } -export function parseTSConfig(code?: string): CompilerFlags { +export function parseTSConfig(code?: string): TSConfig { if (code) { try { const parsed: unknown = parse(code); - if ( - isRecord(parsed) && - 'compilerOptions' in parsed && - isRecord(parsed.compilerOptions) - ) { - return parsed.compilerOptions as CompilerFlags; + if (isRecord(parsed)) { + if ('compilerOptions' in parsed && isRecord(parsed.compilerOptions)) { + return parsed as TSConfig; + } + return { ...parsed, compilerOptions: {} }; } } catch (e) { // eslint-disable-next-line no-console console.error(e); } } - return {}; + return { compilerOptions: {} }; +} + +const moduleRegexp = /(module\.exports\s*=)/g; + +function constrainedScopeEval(obj: string): unknown { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function(` + "use strict"; + var module = { exports: {} }; + (${obj}); + return module.exports + `)(); +} + +export function tryParseEslintModule(value: string): string { + try { + if (moduleRegexp.test(value)) { + const newValue = toJson(constrainedScopeEval(value)); + if (newValue !== value) { + return newValue; + } + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + return value; +} + +export function toJson(cfg: unknown): string { + return JSON.stringify(cfg, null, 2); } export function toJsonConfig(cfg: unknown, prop: string): string { - return JSON.stringify( - { - [prop]: cfg, - }, - null, - 2, - ); + return toJson({ [prop]: cfg }); } export function getTypescriptOptions(): OptionDeclarations[] { const allowedCategories = [ 'Command-line Options', - 'Modules', 'Projects', 'Compiler Diagnostics', 'Editor Support', @@ -66,21 +95,33 @@ export function getTypescriptOptions(): OptionDeclarations[] { 'Source Map Options', ]; + const filteredNames = [ + 'moduleResolution', + 'moduleDetection', + 'plugins', + 'typeRoots', + 'jsx', + ]; + // @ts-expect-error: definition is not fully correct return (window.ts.optionDeclarations as OptionDeclarations[]).filter( item => - item.type === 'boolean' && + (item.type === 'boolean' || + item.type === 'list' || + item.type instanceof Map) && item.description && item.category && - !allowedCategories.includes(item.category.message), + !allowedCategories.includes(item.category.message) && + !filteredNames.includes(item.name), ); } -export const defaultTsConfig = toJsonConfig( - { +export const defaultTsConfig = toJson({ + compilerOptions: { strictNullChecks: true, }, - 'compilerOptions', -); +}); -export const defaultEslintConfig = toJsonConfig({}, 'rules'); +export const defaultEslintConfig = toJson({ + rules: {}, +}); diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 0269c62e7f5f..bdc0138f7ac2 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -24,8 +24,7 @@ import { LintCodeAction, } from '../linter/utils'; import { - defaultEslintConfig, - defaultTsConfig, + tryParseEslintModule, parseESLintRC, parseTSConfig, } from '../config/utils'; @@ -62,12 +61,12 @@ export const LoadedEditor: React.FC = ({ const tabsDefault = { code: sandboxInstance.editor.getModel()!, tsconfig: sandboxInstance.monaco.editor.createModel( - defaultTsConfig, + tsconfig, 'json', sandboxInstance.monaco.Uri.file('./tsconfig.json'), ), eslintrc: sandboxInstance.monaco.editor.createModel( - defaultEslintConfig, + eslintrc, 'json', sandboxInstance.monaco.Uri.file('./.eslintrc'), ), @@ -83,7 +82,7 @@ export const LoadedEditor: React.FC = ({ const markers = sandboxInstance.monaco.editor.getModelMarkers({ resource: model.uri, }); - onMarkersChange(parseMarkers(markers, codeActions, model)); + onMarkersChange(parseMarkers(markers, codeActions, sandboxInstance.editor)); }, []); useEffect(() => { @@ -95,20 +94,25 @@ export const LoadedEditor: React.FC = ({ sandboxInstance.monaco.Uri.file(newPath), ); newModel.updateOptions({ tabSize: 2, insertSpaces: true }); - sandboxInstance.editor.setModel(newModel); + if (tabs.code.isAttachedToEditor()) { + sandboxInstance.editor.setModel(newModel); + } tabs.code.dispose(); tabs.code = newModel; } }, [jsx]); useEffect(() => { - const config = createCompilerOptions(jsx, parseTSConfig(tsconfig)); + const config = createCompilerOptions( + jsx, + parseTSConfig(tsconfig).compilerOptions, + ); webLinter.updateCompilerOptions(config); sandboxInstance.setCompilerSettings(config); }, [jsx, tsconfig]); useEffect(() => { - webLinter.updateRules(parseESLintRC(eslintrc)); + webLinter.updateRules(parseESLintRC(eslintrc).rules); }, [eslintrc]); useEffect(() => { @@ -169,9 +173,18 @@ export const LoadedEditor: React.FC = ({ 'typescript', createProvideCodeActions(codeActions), ), + sandboxInstance.editor.onDidPaste(() => { + if (tabs.eslintrc.isAttachedToEditor()) { + const value = tabs.eslintrc.getValue(); + const newValue = tryParseEslintModule(value); + if (newValue !== value) { + tabs.eslintrc.setValue(newValue); + } + } + }), sandboxInstance.editor.onDidChangeCursorPosition( debounce(() => { - if (sandboxInstance.editor.getModel() === tabs.code) { + if (tabs.code.isAttachedToEditor()) { const position = sandboxInstance.editor.getPosition(); if (position) { // eslint-disable-next-line no-console diff --git a/packages/website/src/components/editor/config.ts b/packages/website/src/components/editor/config.ts index 679baa8a0f22..b1d013ff28f6 100644 --- a/packages/website/src/components/editor/config.ts +++ b/packages/website/src/components/editor/config.ts @@ -13,6 +13,12 @@ export function createCompilerOptions( module: window.ts.ModuleKind.ESNext as number, ...tsConfig, jsx: jsx ? window.ts.JsxEmit.Preserve : window.ts.JsxEmit.None, + moduleResolution: undefined, + plugins: undefined, + typeRoots: undefined, + paths: undefined, + moduleDetection: undefined, + baseUrl: undefined, }; } @@ -58,10 +64,27 @@ export function getEslintSchema( export function getTsConfigSchema(): JSONSchema4 { const properties = getTypescriptOptions().reduce((options, item) => { - options[item.name] = { - type: item.type, - description: item.description!.message, - }; + if (item.type === 'boolean') { + options[item.name] = { + type: 'boolean', + description: item.description!.message, + }; + } else if (item.type === 'list' && item.element?.type instanceof Map) { + options[item.name] = { + type: 'array', + items: { + type: 'string', + enum: Array.from(item.element.type.keys()), + }, + description: item.description!.message, + }; + } else if (item.type instanceof Map) { + options[item.name] = { + type: 'string', + description: item.description!.message, + enum: Array.from(item.type.keys()), + }; + } return options; }, {}); diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts index 7c7056a62821..d51d86b0867a 100644 --- a/packages/website/src/components/linter/utils.ts +++ b/packages/website/src/components/linter/utils.ts @@ -48,7 +48,7 @@ export function createEditOperation( export function parseMarkers( markers: Monaco.editor.IMarker[], fixes: Map, - model: Monaco.editor.ITextModel, + editor: Monaco.editor.IStandaloneCodeEditor, ): ErrorItem[] { return markers.map(marker => { const code = @@ -60,7 +60,9 @@ export function parseMarkers( return { message: item.message, fix(): void { - model.applyEdits([createEditOperation(model, item)]); + editor.executeEdits('eslint', [ + createEditOperation(editor.getModel()!, item), + ]); }, }; }) ?? []; diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index c517353a66bf..ef6cf70dcad4 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -42,3 +42,8 @@ export interface ErrorItem { hasFixers: boolean; fixers: { message: string; fix(): void }[]; } + +export type EslintRC = Record & { rules: RulesRecord }; +export type TSConfig = Record & { + compilerOptions: CompilerFlags; +}; diff --git a/packages/website/src/css/custom.css b/packages/website/src/css/custom.css index 2324486fc7e5..dcda800b7ab7 100644 --- a/packages/website/src/css/custom.css +++ b/packages/website/src/css/custom.css @@ -109,7 +109,39 @@ h6 { background-color: var(--code-line-decoration); } -/* indent the nested checklist for the rule doc attributes */ -ul.contains-task-list > li > ul.contains-task-list { - padding-left: 24px; +.code-block-removed-line::before { + content: '-'; + display: inline-block; + width: 0px; + position: relative; + left: -0.7em; + color: red; +} + +.code-block-removed-line { + background-color: #ff000020; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); + user-select: none; +} + +.code-block-added-line::before { + content: '+'; + display: inline-block; + width: 0px; + position: relative; + left: -0.7em; + color: rgb(2, 164, 113); +} + +.code-block-added-line { + background-color: #00ff9540; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +[data-theme='dark'] .code-block-added-line { + background-color: #00ff9510; } diff --git a/packages/website/src/theme/CodeBlock/Content/String.tsx b/packages/website/src/theme/CodeBlock/Content/String.tsx new file mode 100644 index 000000000000..f26a6ec5b9c5 --- /dev/null +++ b/packages/website/src/theme/CodeBlock/Content/String.tsx @@ -0,0 +1,121 @@ +// Change: added `copiedCode` which filters out the removed lines + +import React from 'react'; +import clsx from 'clsx'; +import { + useThemeConfig, + parseCodeBlockTitle, + parseLanguage, + parseLines, + containsLineNumbers, + usePrismTheme, + useCodeWordWrap, +} from '@docusaurus/theme-common'; +import Highlight, { defaultProps, type Language } from 'prism-react-renderer'; +import Line from '@theme/CodeBlock/Line'; +import CopyButton from '@theme/CodeBlock/CopyButton'; +import WordWrapButton from '@theme/CodeBlock/WordWrapButton'; +import Container from '@theme/CodeBlock/Container'; +import type { Props } from '@theme/CodeBlock/Content/String'; + +import styles from './styles.module.css'; + +// eslint-disable-next-line import/no-default-export +export default function CodeBlockString({ + children, + className: blockClassName = '', + metastring, + title: titleProp, + showLineNumbers: showLineNumbersProp, + language: languageProp, +}: Props): JSX.Element { + const { + prism: { defaultLanguage, magicComments }, + } = useThemeConfig(); + const language = + languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage; + const prismTheme = usePrismTheme(); + const wordWrap = useCodeWordWrap(); + + // We still parse the metastring in case we want to support more syntax in the + // future. Note that MDX doesn't strip quotes when parsing metastring: + // "title=\"xyz\"" => title: "\"xyz\"" + const title = parseCodeBlockTitle(metastring) || titleProp; + + const { lineClassNames, code } = parseLines(children, { + metastring, + language, + magicComments, + }); + const showLineNumbers = + showLineNumbersProp ?? containsLineNumbers(metastring); + + const copiedCode = code + .split('\n') + .filter((c, i) => !lineClassNames[i]?.includes('code-block-removed-line')) + .join('\n'); + + return ( + + {title &&
{title}
} +
+ + {({ + className, + tokens, + getLineProps, + getTokenProps, + }): JSX.Element => ( +
+              
+                {tokens.map((line, i) => (
+                  
+                ))}
+              
+            
+ )} +
+
+ {(wordWrap.isEnabled || wordWrap.isCodeScrollable) && ( + wordWrap.toggle()} + isEnabled={wordWrap.isEnabled} + /> + )} + +
+
+
+ ); +} diff --git a/packages/website/src/theme/CodeBlock/Content/styles.module.css b/packages/website/src/theme/CodeBlock/Content/styles.module.css new file mode 100644 index 000000000000..e75d80399a67 --- /dev/null +++ b/packages/website/src/theme/CodeBlock/Content/styles.module.css @@ -0,0 +1,81 @@ +/* No change; copied over so that the .tsx file can get its CSS */ + +.codeBlockContent { + position: relative; + /* rtl:ignore */ + direction: ltr; + border-radius: inherit; +} + +.codeBlockTitle { + border-bottom: 1px solid var(--ifm-color-emphasis-300); + font-size: var(--ifm-code-font-size); + font-weight: 500; + padding: 0.75rem var(--ifm-pre-padding); + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.codeBlock { + --ifm-pre-background: var(--prism-background-color); + margin: 0; + padding: 0; +} + +.codeBlockTitle + .codeBlockContent .codeBlock { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.codeBlockStandalone { + padding: 0; +} + +.codeBlockLines { + font: inherit; + /* rtl:ignore */ + float: left; + min-width: 100%; + padding: var(--ifm-pre-padding); +} + +.codeBlockLinesWithNumbering { + display: table; + padding: var(--ifm-pre-padding) 0; +} + +@media print { + .codeBlockLines { + white-space: pre-wrap; + } +} + +.buttonGroup { + display: flex; + column-gap: 0.2rem; + position: absolute; + right: calc(var(--ifm-pre-padding) / 2); + top: calc(var(--ifm-pre-padding) / 2); +} + +.buttonGroup button { + display: flex; + align-items: center; + background: var(--prism-background-color); + color: var(--prism-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + padding: 0.4rem; + line-height: 0; + transition: opacity 200ms ease-in-out; + opacity: 0; +} + +.buttonGroup button:focus-visible, +.buttonGroup button:hover { + opacity: 1 !important; +} + +:global(.theme-code-block:hover) .buttonGroup button { + opacity: 0.4; +}