From 3534c6ea09f0cb2162017660a90c6a4ad704da6b Mon Sep 17 00:00:00 2001 From: Alexander T Date: Fri, 28 Feb 2020 18:49:26 +0200 Subject: [PATCH 01/10] fix(eslint-plugin): [default-param-last] handle param props (#1650) --- .../docs/rules/default-param-last.md | 12 ++ .../src/rules/default-param-last.ts | 8 +- .../tests/rules/default-param-last.test.ts | 135 ++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/default-param-last.md b/packages/eslint-plugin/docs/rules/default-param-last.md index 202723907137..f3eb9f27b9a1 100644 --- a/packages/eslint-plugin/docs/rules/default-param-last.md +++ b/packages/eslint-plugin/docs/rules/default-param-last.md @@ -12,6 +12,12 @@ Examples of **incorrect** code for this rule: function f(a = 0, b: number) {} function f(a: number, b = 0, c: number) {} function f(a: number, b?: number, c: number) {} +class Foo { + constructor(public a = 10, private b: number) {} +} +class Foo { + constructor(public a?: number, private b: number) {} +} ``` Examples of **correct** code for this rule: @@ -24,6 +30,12 @@ function f(a: number, b = 0) {} function f(a: number, b?: number) {} function f(a: number, b?: number, c = 0) {} function f(a: number, b = 0, c?: number) {} +class Foo { + constructor(public a, private b = 0) {} +} +class Foo { + constructor(public a, private b?: number) {} +} ``` Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/default-param-last.md) diff --git a/packages/eslint-plugin/src/rules/default-param-last.ts b/packages/eslint-plugin/src/rules/default-param-last.ts index 956a88479752..dea41116911b 100644 --- a/packages/eslint-plugin/src/rules/default-param-last.ts +++ b/packages/eslint-plugin/src/rules/default-param-last.ts @@ -51,7 +51,11 @@ export default createRule({ ): void { let hasSeenPlainParam = false; for (let i = node.params.length - 1; i >= 0; i--) { - const param = node.params[i]; + const current = node.params[i]; + const param = + current.type === AST_NODE_TYPES.TSParameterProperty + ? current.parameter + : current; if (isPlainParam(param)) { hasSeenPlainParam = true; @@ -63,7 +67,7 @@ export default createRule({ (isOptionalParam(param) || param.type === AST_NODE_TYPES.AssignmentPattern) ) { - context.report({ node: param, messageId: 'shouldBeLast' }); + context.report({ node: current, messageId: 'shouldBeLast' }); } } } diff --git a/packages/eslint-plugin/tests/rules/default-param-last.test.ts b/packages/eslint-plugin/tests/rules/default-param-last.test.ts index dd70b3b05cd3..d965df3c77fa 100644 --- a/packages/eslint-plugin/tests/rules/default-param-last.test.ts +++ b/packages/eslint-plugin/tests/rules/default-param-last.test.ts @@ -42,6 +42,51 @@ ruleTester.run('default-param-last', rule, { 'const foo = (a: number, b = 1, c?: number) => {}', 'const foo = (a: number, b?: number, c = 1) => {}', 'const foo = (a: number, b = 1, ...c) => {}', + ` +class Foo { + constructor(a: number, b: number, c: number) {} +} + `, + ` +class Foo { + constructor(a: number, b?: number, c = 1) {} +} + `, + ` +class Foo { + constructor(a: number, b = 1, c?: number) {} +} + `, + ` +class Foo { + constructor(public a: number, protected b: number, private c: number) {} +} + `, + ` +class Foo { + constructor(public a: number, protected b?: number, private c = 10) {} +} + `, + ` +class Foo { + constructor(public a: number, protected b = 10, private c?: number) {} +} + `, + ` +class Foo { + constructor(a: number, protected b?: number, private c = 0) {} +} + `, + ` +class Foo { + constructor(a: number, b?: number, private c = 0) {} +} + `, + ` +class Foo { + constructor(a: number, private b?: number, c = 0) {} +} + `, ], invalid: [ { @@ -527,5 +572,95 @@ ruleTester.run('default-param-last', rule, { }, ], }, + { + code: ` +class Foo { + constructor(public a: number, protected b?: number, private c: number) {} +} + `, + errors: [ + { + messageId: 'shouldBeLast', + line: 3, + column: 33, + endColumn: 53, + }, + ], + }, + { + code: ` +class Foo { + constructor(public a: number, protected b = 0, private c: number) {} +} + `, + errors: [ + { + messageId: 'shouldBeLast', + line: 3, + column: 33, + endColumn: 48, + }, + ], + }, + { + code: ` +class Foo { + constructor(public a?: number, private b: number) {} +} + `, + errors: [ + { + messageId: 'shouldBeLast', + line: 3, + column: 15, + endColumn: 32, + }, + ], + }, + { + code: ` +class Foo { + constructor(public a = 0, private b: number) {} +} + `, + errors: [ + { + messageId: 'shouldBeLast', + line: 3, + column: 15, + endColumn: 27, + }, + ], + }, + { + code: ` +class Foo { + constructor(a = 0, b: number) {} +} + `, + errors: [ + { + messageId: 'shouldBeLast', + line: 3, + column: 15, + endColumn: 20, + }, + ], + }, + { + code: ` +class Foo { + constructor(a?: number, b: number) {} +} + `, + errors: [ + { + messageId: 'shouldBeLast', + line: 3, + column: 15, + endColumn: 25, + }, + ], + }, ], }); From ae7f7c5637124b1167efd63755df92e219bbbb24 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Fri, 28 Feb 2020 09:47:08 -0800 Subject: [PATCH 02/10] fix(eslint-plugin): [ban-types] add option extendDefaults (#1379) Co-authored-by: Brad Zacher --- .../eslint-plugin/docs/rules/ban-types.md | 17 +++++ packages/eslint-plugin/src/rules/ban-types.ts | 70 +++++++++++-------- .../tests/rules/ban-types.test.ts | 25 +++++++ 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/ban-types.md b/packages/eslint-plugin/docs/rules/ban-types.md index d47643284488..e92e57b521f2 100644 --- a/packages/eslint-plugin/docs/rules/ban-types.md +++ b/packages/eslint-plugin/docs/rules/ban-types.md @@ -58,6 +58,23 @@ The banned type can either be a type name literal (`Foo`), a type name with gene } ``` +By default, this rule includes types which are likely to be mistakes, such as `String` and `Number`. If you don't want these enabled, set the `extendDefaults` option to `false`: + +```CJSON +{ + "@typescript-eslint/ban-types": ["error", { + "types": { + // add a custom message, AND tell the plugin how to fix it + "String": { + "message": "Use string instead", + "fixWith": "string" + } + }, + "extendDefaults": false + }] +} +``` + ### Example ```json diff --git a/packages/eslint-plugin/src/rules/ban-types.ts b/packages/eslint-plugin/src/rules/ban-types.ts index 429b5fd6895c..7e056959a3f7 100644 --- a/packages/eslint-plugin/src/rules/ban-types.ts +++ b/packages/eslint-plugin/src/rules/ban-types.ts @@ -13,7 +13,8 @@ type Types = Record< type Options = [ { - types: Types; + types?: Types; + extendDefaults?: boolean; }, ]; type MessageIds = 'bannedTypeMessage'; @@ -51,6 +52,35 @@ function getCustomMessage( return ''; } +/* + Defaults for this rule should be treated as an "all or nothing" + merge, so we need special handling here. + + See: https://github.com/typescript-eslint/typescript-eslint/issues/686 + */ +const defaultTypes = { + String: { + message: 'Use string instead', + fixWith: 'string', + }, + Boolean: { + message: 'Use boolean instead', + fixWith: 'boolean', + }, + Number: { + message: 'Use number instead', + fixWith: 'number', + }, + Object: { + message: 'Use Record instead', + fixWith: 'Record', + }, + Symbol: { + message: 'Use symbol instead', + fixWith: 'symbol', + }, +}; + export default util.createRule({ name: 'ban-types', meta: { @@ -85,38 +115,22 @@ export default util.createRule({ ], }, }, + extendDefaults: { + type: 'boolean', + }, }, additionalProperties: false, }, ], }, - defaultOptions: [ - { - types: { - String: { - message: 'Use string instead', - fixWith: 'string', - }, - Boolean: { - message: 'Use boolean instead', - fixWith: 'boolean', - }, - Number: { - message: 'Use number instead', - fixWith: 'number', - }, - Object: { - message: 'Use Record instead', - fixWith: 'Record', - }, - Symbol: { - message: 'Use symbol instead', - fixWith: 'symbol', - }, - }, - }, - ], - create(context, [{ types }]) { + defaultOptions: [{}], + create(context, [options]) { + const extendDefaults = options.extendDefaults ?? true; + const customTypes = options.types ?? {}; + const types: Types = { + ...(extendDefaults ? defaultTypes : {}), + ...customTypes, + }; const bannedTypes = new Map( Object.entries(types).map(([type, data]) => [removeSpaces(type), data]), ); diff --git a/packages/eslint-plugin/tests/rules/ban-types.test.ts b/packages/eslint-plugin/tests/rules/ban-types.test.ts index aad95c06e4d9..0edeb74f8d1c 100644 --- a/packages/eslint-plugin/tests/rules/ban-types.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-types.test.ts @@ -72,6 +72,21 @@ ruleTester.run('ban-types', rule, { code: 'let a: NS.Bad._', options, }, + // Replace default options instead of merging with extendDefaults: false + { + code: 'let a: String;', + options: [ + { + types: { + Number: { + message: 'Use number instead.', + fixWith: 'number', + }, + }, + extendDefaults: false, + }, + ], + }, { code: 'let a: undefined', options: options2, @@ -82,6 +97,16 @@ ruleTester.run('ban-types', rule, { }, ], invalid: [ + { + code: 'let a: String;', + errors: [ + { + messageId: 'bannedTypeMessage', + line: 1, + column: 8, + }, + ], + }, { code: 'let a: Object;', errors: [ From baf7c98aa24cbc0a6c69ab41701acd3cf4fc36ad Mon Sep 17 00:00:00 2001 From: Matt Browne Date: Fri, 28 Feb 2020 18:35:46 -0500 Subject: [PATCH 03/10] docs(eslint-plugin): [consistent-type-assertion] improve docs (#1651) --- .../docs/rules/consistent-type-assertions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/consistent-type-assertions.md b/packages/eslint-plugin/docs/rules/consistent-type-assertions.md index eb0fb5213743..cdf0fb7656cb 100644 --- a/packages/eslint-plugin/docs/rules/consistent-type-assertions.md +++ b/packages/eslint-plugin/docs/rules/consistent-type-assertions.md @@ -6,6 +6,8 @@ This rule aims to standardize the use of type assertion style across the codebas Type assertions are also commonly referred as "type casting" in TypeScript (even though it is technically slightly different to what is understood by type casting in other languages), so you can think of type assertions and type casting referring to the same thing. It is essentially you saying to the TypeScript compiler, "in this case, I know better than you!". +In addition to ensuring that type assertions are written in a consistent way, this rule also helps make your codebase more type-safe. + ## Options ```ts @@ -44,10 +46,16 @@ The compiler will warn for excess properties with this syntax, but not missing _ The const assertion `const x = { foo: 1 } as const`, introduced in TypeScript 3.4, is considered beneficial and is ignored by this option. +Assertions to `any` are also ignored by this option. + Examples of **incorrect** code for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }` (and for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' }`) ```ts const x = { ... } as T; + +function foo() { + return { ... } as T; +} ``` Examples of **correct** code for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }`. @@ -56,6 +64,10 @@ Examples of **correct** code for `{ assertionStyle: 'as', objectLiteralTypeAsser const x: T = { ... }; const y = { ... } as any; const z = { ... } as unknown; + +function foo(): T { + return { ... }; +} ``` Examples of **correct** code for `{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' }`. From dd233b52dcd5a39d842123af6fc775574abf2bc2 Mon Sep 17 00:00:00 2001 From: Pavel Birukov Date: Sat, 29 Feb 2020 04:21:51 +0200 Subject: [PATCH 04/10] feat(eslint-plugin): [explicit-member-accessibility] autofix no-public (#1548) --- packages/eslint-plugin/README.md | 2 +- .../rules/explicit-member-accessibility.ts | 49 +++ .../explicit-member-accessibility.test.ts | 339 ++++++++++++++++++ 3 files changed, 389 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index fcc422386b4c..c560ebb6761b 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -102,7 +102,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | -| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | +| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | | | [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index ebfbae27329d..7c49f0d6a318 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -1,6 +1,8 @@ import { AST_NODE_TYPES, TSESTree, + AST_TOKEN_TYPES, + TSESLint, } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; @@ -38,6 +40,7 @@ export default util.createRule({ // too opinionated to be recommended recommended: false, }, + fixable: 'code', messages: { missingAccessibility: 'Missing accessibility modifier on {{type}} {{name}}.', @@ -91,6 +94,7 @@ export default util.createRule({ nodeType: string, node: TSESTree.Node, nodeName: string, + fix: TSESLint.ReportFixFunction | null = null, ): void { context.report({ node: node, @@ -99,6 +103,7 @@ export default util.createRule({ type: nodeType, name: nodeName, }, + fix: fix, }); } @@ -140,6 +145,7 @@ export default util.createRule({ nodeType, methodDefinition, methodName, + getUnwantedPublicAccessibilityFixer(methodDefinition), ); } else if (check === 'explicit' && !methodDefinition.accessibility) { reportIssue( @@ -151,6 +157,47 @@ export default util.createRule({ } } + /** + * Creates a fixer that removes a "public" keyword with following spaces + */ + function getUnwantedPublicAccessibilityFixer( + node: + | TSESTree.MethodDefinition + | TSESTree.ClassProperty + | TSESTree.TSParameterProperty, + ): TSESLint.ReportFixFunction { + return function(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { + const tokens = sourceCode.getTokens(node); + let rangeToRemove: TSESLint.AST.Range; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + if ( + token.type === AST_TOKEN_TYPES.Keyword && + token.value === 'public' + ) { + const commensAfterPublicKeyword = sourceCode.getCommentsAfter( + token, + ); + if (commensAfterPublicKeyword.length) { + // public /* Hi there! */ static foo() + // ^^^^^^^ + rangeToRemove = [ + token.range[0], + commensAfterPublicKeyword[0].range[0], + ]; + break; + } else { + // public static foo() + // ^^^^^^^ + rangeToRemove = [token.range[0], tokens[i + 1].range[0]]; + break; + } + } + } + return fixer.removeRange(rangeToRemove!); + }; + } + /** * Checks if property has an accessibility modifier. * @param classProperty The node representing a ClassProperty. @@ -170,6 +217,7 @@ export default util.createRule({ nodeType, classProperty, propertyName, + getUnwantedPublicAccessibilityFixer(classProperty), ); } else if (propCheck === 'explicit' && !classProperty.accessibility) { reportIssue( @@ -217,6 +265,7 @@ export default util.createRule({ nodeType, node, nodeName, + getUnwantedPublicAccessibilityFixer(node), ); } break; diff --git a/packages/eslint-plugin/tests/rules/explicit-member-accessibility.test.ts b/packages/eslint-plugin/tests/rules/explicit-member-accessibility.test.ts index b5477c062a10..fe0a09dd4c16 100644 --- a/packages/eslint-plugin/tests/rules/explicit-member-accessibility.test.ts +++ b/packages/eslint-plugin/tests/rules/explicit-member-accessibility.test.ts @@ -344,6 +344,11 @@ export class XXXX { line: 3, }, ], + output: ` +export class XXXX { + public constructor(readonly value: string) {} +} + `, }, { filename: 'test.ts', @@ -354,6 +359,11 @@ export class WithParameterProperty { `, options: [{ accessibility: 'explicit' }], errors: [{ messageId: 'missingAccessibility' }], + output: ` +export class WithParameterProperty { + public constructor(readonly value: string) {} +} + `, }, { filename: 'test.ts', @@ -372,6 +382,11 @@ export class XXXX { }, ], errors: [{ messageId: 'missingAccessibility' }], + output: ` +export class XXXX { + public constructor(readonly samosa: string) {} +} + `, }, { filename: 'test.ts', @@ -387,6 +402,11 @@ class Test { }, ], errors: [{ messageId: 'missingAccessibility' }], + output: ` +class Test { + public constructor(readonly foo: string) {} +} + `, }, { filename: 'test.ts', @@ -409,6 +429,14 @@ class Test { column: 3, }, ], + output: ` +class Test { + x: number + public getX () { + return this.x + } +} + `, }, { filename: 'test.ts', @@ -431,6 +459,14 @@ class Test { column: 3, }, ], + output: ` +class Test { + private x: number + getX () { + return this.x + } +} + `, }, { filename: 'test.ts', @@ -462,6 +498,14 @@ class Test { column: 3, }, ], + output: ` +class Test { + x?: number + getX? () { + return this.x + } +} + `, }, { filename: 'test.ts', @@ -486,6 +530,15 @@ class Test { column: 3, }, ], + output: ` +class Test { + protected name: string + protected foo?: string + getX () { + return this.x + } +} + `, }, { filename: 'test.ts', @@ -510,6 +563,15 @@ class Test { column: 3, }, ], + output: ` +class Test { + protected name: string + foo?: string + getX () { + return this.x + } +} + `, }, { filename: 'test.ts', @@ -534,6 +596,14 @@ class Test { }, ], options: [{ accessibility: 'no-public' }], + output: ` +class Test { + x: number + getX () { + return this.x + } +} + `, }, { filename: 'test.ts', @@ -564,6 +634,20 @@ class Test { }, ], options: [{ overrides: { constructors: 'no-public' } }], + output: ` +class Test { + private x: number; + constructor (x: number) { + this.x = x; + } + get internalValue() { + return this.x; + } + set internalValue(value: number) { + this.x = value; + } +} + `, }, { filename: 'test.ts', @@ -598,6 +682,20 @@ class Test { column: 3, }, ], + output: ` +class Test { + private x: number; + constructor (x: number) { + this.x = x; + } + get internalValue() { + return this.x; + } + set internalValue(value: number) { + this.x = value; + } +} + `, }, { filename: 'test.ts', @@ -621,6 +719,14 @@ class Test { overrides: { parameterProperties: 'no-public' }, }, ], + output: ` +class Test { + constructor(public x: number){} + public foo(): string { + return 'foo'; + } +} + `, }, { filename: 'test.ts', @@ -636,6 +742,11 @@ class Test { column: 3, }, ], + output: ` +class Test { + constructor(public x: number){} +} + `, }, { filename: 'test.ts', @@ -657,6 +768,11 @@ class Test { column: 15, }, ], + output: ` +class Test { + constructor(readonly x: number){} +} + `, }, { filename: 'test.ts', @@ -674,6 +790,7 @@ class Test { column: 14, }, ], + output: 'class Test { x = 2 }', }, { filename: 'test.ts', @@ -694,6 +811,10 @@ class Test { column: 9, }, ], + output: `class Test { + x = 2 + private x = 2 + }`, }, { code: 'class Test { constructor(public ...x: any[]) { }}', @@ -705,6 +826,224 @@ class Test { column: 14, }, ], + output: 'class Test { constructor(public ...x: any[]) { }}', + }, + { + filename: 'test.ts', + code: ` +class Test { + @public + public /*public*/constructor(private foo: string) {} +} + `, + options: [ + { + accessibility: 'no-public', + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 3, + column: 3, + }, + ], + output: ` +class Test { + @public + /*public*/constructor(private foo: string) {} +} + `, + }, + { + filename: 'test.ts', + code: ` +class Test { + @public + public foo() {} +} + `, + options: [ + { + accessibility: 'no-public', + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 3, + column: 3, + }, + ], + output: ` +class Test { + @public + foo() {} +} + `, + }, + + { + filename: 'test.ts', + code: ` +class Test { + @public + public foo; +} + `, + options: [ + { + accessibility: 'no-public', + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 3, + column: 3, + }, + ], + output: ` +class Test { + @public + foo; +} + `, + }, + { + filename: 'test.ts', + code: ` +class Test { + public foo = ""; +} + `, + options: [ + { + accessibility: 'no-public', + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 3, + column: 3, + }, + ], + output: ` +class Test { + foo = ""; +} + `, + }, + + { + filename: 'test.ts', + code: ` +class Test { + contructor(public/* Hi there */ readonly foo) +} + `, + options: [ + { + accessibility: 'no-public', + overrides: { parameterProperties: 'no-public' }, + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 3, + column: 14, + }, + ], + output: ` +class Test { + contructor(/* Hi there */ readonly foo) +} + `, + }, + { + filename: 'test.ts', + code: ` +class Test { + contructor(public readonly foo: string) +} + `, + options: [ + { + accessibility: 'no-public', + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 3, + column: 14, + }, + ], + output: ` +class Test { + contructor(readonly foo: string) +} + `, + }, + { + filename: 'test.ts', + code: + 'class EnsureWhiteSPaceSpan { public constructor() {}}', + options: [ + { + accessibility: 'no-public', + overrides: { parameterProperties: 'no-public' }, + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 1, + column: 30, + }, + ], + output: 'class EnsureWhiteSPaceSpan { constructor() {}}', + }, + { + filename: 'test.ts', + code: + 'class EnsureWhiteSPaceSpan { public /* */ constructor() {}}', + options: [ + { + accessibility: 'no-public', + overrides: { parameterProperties: 'no-public' }, + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 1, + column: 30, + }, + ], + output: + 'class EnsureWhiteSPaceSpan { /* */ constructor() {}}', + }, + { + filename: 'test.ts', + code: + 'class EnsureWhiteSPaceSpan { public /* */ constructor() {}}', + options: [ + { + accessibility: 'no-public', + overrides: { parameterProperties: 'no-public' }, + }, + ], + errors: [ + { + messageId: 'unwantedPublicAccessibility', + line: 1, + column: 30, + }, + ], + output: 'class EnsureWhiteSPaceSpan { /* */ constructor() {}}', }, ], }); From 8333d41d5d472ef338fb41a29ccbfc6b16e47627 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 28 Feb 2020 21:26:57 -0500 Subject: [PATCH 05/10] feat(eslint-plugin): add new no-base-to-string rule (#1522) --- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-base-to-string.md | 59 ++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-base-to-string.ts | 121 +++++++++++++ .../tests/rules/no-base-to-string.test.ts | 170 ++++++++++++++++++ 6 files changed, 354 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-base-to-string.md create mode 100644 packages/eslint-plugin/src/rules/no-base-to-string.ts create mode 100644 packages/eslint-plugin/tests/rules/no-base-to-string.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index c560ebb6761b..778e159d6ea5 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -107,6 +107,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | | [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | +| [`@typescript-eslint/no-base-to-string`](./docs/rules/no-base-to-string.md) | Requires that `.toString()` is only called on objects which provide useful information when stringified | | | :thought_balloon: | | [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | | | [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/no-base-to-string.md b/packages/eslint-plugin/docs/rules/no-base-to-string.md new file mode 100644 index 000000000000..4050047673b7 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.md @@ -0,0 +1,59 @@ +# Requires that `.toString()` is only called on objects which provide useful information when stringified (`no-base-to-string`) + +JavaScript will call `toString()` on an object when it is converted to a string, such as when `+` adding to a string or in `${}` template literals. + +The default Object `.toString()` returns `"[object Object]"`, so this rule requires stringified objects define a more useful `.toString()` method. + +Note that `Function` provides its own `.toString()` that returns the function's code. +Functions are not flagged by this rule. + +This rule has some overlap with with [`restrict-plus-operands`](./restrict-plus-operands.md) and [`restrict-template-expressions`](./restrict-template-expressions.md). + +## Rule Details + +This rule prevents accidentally defaulting to the base Object `.toString()` method. + +Examples of **incorrect** code for this rule: + +```ts +// Passing an object or class instance to string concatenation: +'' + {}; + +class MyClass {} +const value = new MyClass(); +value + ''; + +// Interpolation and manual .toString() calls too: +`Value: ${value}`; +({}.toString()); +``` + +Examples of **correct** code for this rule: + +```ts +// These types all have useful .toString()s +'Text' + true; +`Value: ${123}`; +`Arrays too: ${[1, 2, 3]}`; +(() => {}).toString(); + +// Defining a custom .toString class is considered acceptable +class CustomToString { + toString() { + return 'Hello, world!'; + } +} +`Value: ${new CustomToString()}`; + +const literalWithToString = { + toString: () => 'Hello, world!', +}; + +`Value: ${literalWithToString}`; +``` + +## When Not To Use It + +If you don't mind `"[object Object]"` in your strings, then you will not need this rule. + +- [`Object.prototype.toString()` MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 23e9db2d9740..bf5844653ef8 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -26,6 +26,7 @@ "@typescript-eslint/naming-convention": "error", "no-array-constructor": "off", "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-base-to-string": "error", "no-dupe-class-members": "off", "@typescript-eslint/no-dupe-class-members": "error", "@typescript-eslint/no-dynamic-delete": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 16a4c705ca5e..0c3a5d9f0d1e 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -23,6 +23,7 @@ import memberNaming from './member-naming'; import memberOrdering from './member-ordering'; import namingConvention from './naming-convention'; import noArrayConstructor from './no-array-constructor'; +import noBaseToString from './no-base-to-string'; import noDupeClassMembers from './no-dupe-class-members'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; @@ -93,6 +94,7 @@ export default { 'ban-ts-ignore': banTsIgnore, 'ban-ts-comment': banTsComment, 'ban-types': banTypes, + 'no-base-to-string': noBaseToString, 'brace-style': braceStyle, camelcase: camelcase, 'class-name-casing': classNameCasing, diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts new file mode 100644 index 000000000000..fa4042728953 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -0,0 +1,121 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +enum Usefulness { + Always, + Never = 'will', + Sometimes = 'may', +} + +export default util.createRule({ + name: 'no-base-to-string', + meta: { + docs: { + description: + 'Requires that `.toString()` is only called on objects which provide useful information when stringified', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + baseToString: + "'{{name}} {{certainty}} evaluate to '[Object object]' when stringified.", + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + const parserServices = util.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + + function checkExpression(node: TSESTree.Expression, type?: ts.Type): void { + if (node.type === AST_NODE_TYPES.Literal) { + return; + } + + const certainty = collectToStringCertainty( + type ?? + typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node), + ), + ); + if (certainty === Usefulness.Always) { + return; + } + + context.report({ + data: { + certainty, + name: context.getSourceCode().getText(node), + }, + messageId: 'baseToString', + node, + }); + } + + function collectToStringCertainty(type: ts.Type): Usefulness { + const toString = typeChecker.getPropertyOfType(type, 'toString'); + if (toString === undefined || toString.declarations.length === 0) { + return Usefulness.Always; + } + + if ( + toString.declarations.every( + ({ parent }) => + !ts.isInterfaceDeclaration(parent) || parent.name.text !== 'Object', + ) + ) { + return Usefulness.Always; + } + + if (!type.isUnion()) { + return Usefulness.Never; + } + + for (const subType of type.types) { + if (collectToStringCertainty(subType) !== Usefulness.Never) { + return Usefulness.Sometimes; + } + } + + return Usefulness.Never; + } + + return { + 'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'( + node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression, + ): void { + const leftType = typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.left), + ); + const rightType = typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.right), + ); + + if (util.getTypeName(typeChecker, leftType) === 'string') { + checkExpression(node.right, rightType); + } else if (util.getTypeName(typeChecker, rightType) === 'string') { + checkExpression(node.left, leftType); + } + }, + 'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'( + node: TSESTree.Expression, + ): void { + const memberExpr = node.parent as TSESTree.MemberExpression; + checkExpression(memberExpr.object); + }, + + TemplateLiteral(node: TSESTree.TemplateLiteral): void { + for (const expression of node.expressions) { + checkExpression(expression); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts new file mode 100644 index 000000000000..3be0828791df --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -0,0 +1,170 @@ +import rule from '../../src/rules/no-base-to-string'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-base-to-string', rule, { + valid: [ + `\`\${""}\``, + `\`\${true}\``, + `\`\${[]}\``, + `\`\${function () {}}\``, + `"" + ""`, + `"" + true`, + `"" + []`, + `true + true`, + `true + ""`, + `true + []`, + `[] + []`, + `[] + true`, + `[] + ""`, + `({}).constructor()`, + `"text".toString()`, + `false.toString()`, + `let value = 1; + value.toString()`, + `let value = 1n; + value.toString()`, + `function someFunction() { } + someFunction.toString();`, + 'unknownObject.toString()', + 'unknownObject.someOtherMethod()', + '(() => {}).toString();', + `class CustomToString { toString() { return "Hello, world!"; } } + "" + (new CustomToString());`, + `const literalWithToString = { + toString: () => "Hello, world!", + }; + "" + literalToString;`, + `let _ = {} * {}`, + `let _ = {} / {}`, + `let _ = {} *= {}`, + `let _ = {} /= {}`, + `let _ = {} = {}`, + `let _ = {} == {}`, + `let _ = {} === {}`, + `let _ = {} in {}`, + `let _ = {} & {}`, + `let _ = {} ^ {}`, + `let _ = {} << {}`, + `let _ = {} >> {}`, + ], + invalid: [ + { + code: `\`\${{}})\``, + errors: [ + { + data: { + certainty: 'will', + name: '{}', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: `({}).toString()`, + errors: [ + { + data: { + certainty: 'will', + name: '{}', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: `"" + {}`, + errors: [ + { + data: { + certainty: 'will', + name: '{}', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: `"" += {}`, + errors: [ + { + data: { + certainty: 'will', + name: '{}', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + let someObjectOrString = Math.random() ? { a: true } : "text"; + someObjectOrString.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'someObjectOrString', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + let someObjectOrString = Math.random() ? { a: true } : "text"; + someObjectOrString + ""; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'someObjectOrString', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + let someObjectOrObject = Math.random() ? { a: true, b: true } : { a: true }; + someObjectOrObject.toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'someObjectOrObject', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + let someObjectOrObject = Math.random() ? { a: true, b: true } : { a: true }; + someObjectOrObject + ""; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'someObjectOrObject', + }, + messageId: 'baseToString', + }, + ], + }, + ], +}); From fc0a55e8b78203972d01a7c9b79ed6b470c5c1a0 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Sat, 29 Feb 2020 04:31:28 +0200 Subject: [PATCH 06/10] feat(eslint-plugin): [typedef] add variable-declaration-ignore-function (#1578) --- packages/eslint-plugin/docs/rules/typedef.md | 27 +++- packages/eslint-plugin/src/rules/typedef.ts | 17 ++- .../eslint-plugin/tests/rules/typedef.test.ts | 120 ++++++++++++++++++ 3 files changed, 162 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/typedef.md b/packages/eslint-plugin/docs/rules/typedef.md index 17a57412bc22..07f4bb1eb3ab 100644 --- a/packages/eslint-plugin/docs/rules/typedef.md +++ b/packages/eslint-plugin/docs/rules/typedef.md @@ -37,7 +37,8 @@ This rule has an object option that may receive any of the following as booleans - `"objectDestructuring"` - `"parameter"`: `true` by default - `"propertyDeclaration"`: `true` by default -- `"variableDeclaration"` +- `"variableDeclaration"`, +- `"variableDeclarationIgnoreFunction"` For example, with the following configuration: @@ -254,6 +255,30 @@ let initialText: string = 'text'; let delayedText: string; ``` +### `variableDeclarationIgnoreFunction` + +Ignore variable declarations for non-arrow and arrow functions. + +Examples of **incorrect** code with `{ "variableDeclaration": true, "variableDeclarationIgnoreFunction": true }`: + +```ts +const text = 'text'; +``` + +Examples of **correct** code with `{ "variableDeclaration": true, "variableDeclarationIgnoreFunction": true }`: + +```ts +const a = (): void => {}; +const b = function (): void => {}; +const c: () => void = (): void => {}; + +class Foo { + a = (): void => {}; + b = function (): void => {}; + c = () => void = (): void => {}; +} +``` + ## When Not To Use It If you are using stricter TypeScript compiler options, particularly `--noImplicitAny` and/or `--strictPropertyInitialization`, you likely don't need this rule. diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index dd7bcc29c61f..7e6f4f14965c 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -12,6 +12,7 @@ const enum OptionKeys { Parameter = 'parameter', PropertyDeclaration = 'propertyDeclaration', VariableDeclaration = 'variableDeclaration', + VariableDeclarationIgnoreFunction = 'variableDeclarationIgnoreFunction', } type Options = { [k in OptionKeys]?: boolean }; @@ -41,6 +42,7 @@ export default util.createRule<[Options], MessageIds>({ [OptionKeys.Parameter]: { type: 'boolean' }, [OptionKeys.PropertyDeclaration]: { type: 'boolean' }, [OptionKeys.VariableDeclaration]: { type: 'boolean' }, + [OptionKeys.VariableDeclarationIgnoreFunction]: { type: 'boolean' }, }, }, ], @@ -125,6 +127,14 @@ export default util.createRule<[Options], MessageIds>({ } } + function isVariableDeclarationIgnoreFunction(node: TSESTree.Node): boolean { + return ( + !!options[OptionKeys.VariableDeclarationIgnoreFunction] && + (node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.ArrowFunctionExpression) + ); + } + return { ArrayPattern(node): void { if ( @@ -141,6 +151,10 @@ export default util.createRule<[Options], MessageIds>({ } }, ClassProperty(node): void { + if (node.value && isVariableDeclarationIgnoreFunction(node.value)) { + return; + } + if ( options[OptionKeys.MemberVariableDeclaration] && !node.typeAnnotation @@ -188,7 +202,8 @@ export default util.createRule<[Options], MessageIds>({ (node.id.type === AST_NODE_TYPES.ArrayPattern && !options[OptionKeys.ArrayDestructuring]) || (node.id.type === AST_NODE_TYPES.ObjectPattern && - !options[OptionKeys.ObjectDestructuring]) + !options[OptionKeys.ObjectDestructuring]) || + (node.init && isVariableDeclarationIgnoreFunction(node.init)) ) { return; } diff --git a/packages/eslint-plugin/tests/rules/typedef.test.ts b/packages/eslint-plugin/tests/rules/typedef.test.ts index 679ed1a8a519..bbb1a49c0186 100644 --- a/packages/eslint-plugin/tests/rules/typedef.test.ts +++ b/packages/eslint-plugin/tests/rules/typedef.test.ts @@ -302,6 +302,57 @@ ruleTester.run('typedef', rule, { }, ], }, + // variable declaration ignore function + { + code: `const foo = function(): void {};`, + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + }, + ], + }, + { + code: `const foo = (): void => {};`, + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + }, + ], + }, + { + code: `const foo: () => void = (): void => {};`, + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + }, + ], + }, + { + code: `const foo: () => void = function (): void {};`, + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + }, + ], + }, + { + code: ` +class Foo { + a = (): void => {}; + b = function (): void {}; +} + `, + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + }, + ], + }, ], invalid: [ // Array destructuring @@ -652,5 +703,74 @@ ruleTester.run('typedef', rule, { }, ], }, + { + code: `const foo = 'foo'`, + errors: [ + { + messageId: 'expectedTypedefNamed', + data: { name: 'foo' }, + }, + ], + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + }, + ], + }, + { + code: `const foo = function(): void {};`, + errors: [ + { + messageId: 'expectedTypedefNamed', + data: { name: 'foo' }, + }, + ], + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: false, + }, + ], + }, + { + code: `const foo = (): void => {};`, + errors: [ + { + messageId: 'expectedTypedefNamed', + data: { name: 'foo' }, + }, + ], + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: false, + }, + ], + }, + { + code: ` +class Foo { + a = (): void => {}; + b = function (): void {}; +} + `, + errors: [ + { + messageId: 'expectedTypedefNamed', + data: { name: 'a' }, + }, + { + messageId: 'expectedTypedefNamed', + data: { name: 'b' }, + }, + ], + options: [ + { + variableDeclaration: true, + variableDeclarationIgnoreFunction: false, + }, + ], + }, ], }); From 33e3e6f79ea21792ccb60b7f1ada74057ceb52e9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 29 Feb 2020 12:17:52 -0800 Subject: [PATCH 07/10] fix(eslint-plugin): [no-implied-eval] correct logic for ts3.8 (#1652) --- .../src/rules/no-implied-eval.ts | 15 +-- .../tests/rules/no-implied-eval.test.ts | 126 ++++++------------ 2 files changed, 49 insertions(+), 92 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-implied-eval.ts b/packages/eslint-plugin/src/rules/no-implied-eval.ts index fe50beba0502..0f6118597868 100644 --- a/packages/eslint-plugin/src/rules/no-implied-eval.ts +++ b/packages/eslint-plugin/src/rules/no-implied-eval.ts @@ -3,6 +3,7 @@ import { TSESTree, AST_NODE_TYPES, } from '@typescript-eslint/experimental-utils'; +import * as tsutils from 'tsutils'; import * as util from '../util'; const FUNCTION_CONSTRUCTOR = 'Function'; @@ -68,8 +69,11 @@ export default util.createRule({ const symbol = type.getSymbol(); if ( - symbol?.flags === ts.SymbolFlags.Function || - symbol?.flags === ts.SymbolFlags.Method + symbol && + tsutils.isSymbolFlagSet( + symbol, + ts.SymbolFlags.Function | ts.SymbolFlags.Method, + ) ) { return true; } @@ -79,12 +83,7 @@ export default util.createRule({ ts.SignatureKind.Call, ); - if (signatures.length) { - const [{ declaration }] = signatures; - return declaration?.kind === ts.SyntaxKind.FunctionType; - } - - return false; + return signatures.length > 0; } function isFunction(node: TSESTree.Node): boolean { diff --git a/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts b/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts index 947044727d51..b92b61eff6aa 100644 --- a/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts +++ b/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts @@ -36,38 +36,31 @@ ruleTester.run('no-implied-eval', rule, { `window.execScript(() => {});`, `window['execScript'](() => {});`, - { - code: ` + ` const foo = () => {}; setTimeout(foo, 0); setInterval(foo, 0); setImmediate(foo); execScript(foo); - `, - }, - { - code: ` + `, + ` const foo = function () {}; setTimeout(foo, 0); setInterval(foo, 0); setImmediate(foo); execScript(foo); - `, - }, - { - code: ` + `, + ` function foo() {}; setTimeout(foo, 0); setInterval(foo, 0); setImmediate(foo); execScript(foo); - `, - }, - { - code: ` + `, + ` const foo = { fn: () => {}, } @@ -76,10 +69,8 @@ setTimeout(foo.fn, 0); setInterval(foo.fn, 0); setImmediate(foo.fn); execScript(foo.fn); - `, - }, - { - code: ` + `, + ` const foo = { fn: function () {}, } @@ -88,10 +79,8 @@ setTimeout(foo.fn, 0); setInterval(foo.fn, 0); setImmediate(foo.fn); execScript(foo.fn); - `, - }, - { - code: ` + `, + ` const foo = { fn: function foo() {}, } @@ -100,10 +89,8 @@ setTimeout(foo.fn, 0); setInterval(foo.fn, 0); setImmediate(foo.fn); execScript(foo.fn); - `, - }, - { - code: ` + `, + ` const foo = { fn() {}, } @@ -112,10 +99,8 @@ setTimeout(foo.fn, 0); setInterval(foo.fn, 0); setImmediate(foo.fn); execScript(foo.fn); - `, - }, - { - code: ` + `, + ` const foo = { fn: () => {}, } @@ -125,10 +110,8 @@ setTimeout(foo[fn], 0); setInterval(foo[fn], 0); setImmediate(foo[fn]); execScript(foo[fn]); - `, - }, - { - code: ` + `, + ` const foo = { fn: () => {}, } @@ -137,10 +120,8 @@ setTimeout(foo['fn'], 0); setInterval(foo['fn'], 0); setImmediate(foo['fn']); execScript(foo['fn']); - `, - }, - { - code: ` + `, + ` const foo: () => void = () => { }; @@ -148,10 +129,8 @@ setTimeout(foo, 0); setInterval(foo, 0); setImmediate(foo); execScript(foo); - `, - }, - { - code: ` + `, + ` const foo: (() => () => void) = () => { return () => {}; } @@ -160,30 +139,24 @@ setTimeout(foo(), 0); setInterval(foo(), 0); setImmediate(foo()); execScript(foo()); - `, - }, - { - code: ` + `, + ` const foo: (() => () => void) = () => () => {}; setTimeout(foo(), 0); setInterval(foo(), 0); setImmediate(foo()); execScript(foo()); - `, - }, - { - code: ` + `, + ` const foo = () => () => {}; setTimeout(foo(), 0); setInterval(foo(), 0); setImmediate(foo()); execScript(foo()); - `, - }, - { - code: ` + `, + ` const foo = function foo () { return function foo() {} } @@ -192,10 +165,8 @@ setTimeout(foo(), 0); setInterval(foo(), 0); setImmediate(foo()); execScript(foo()); - `, - }, - { - code: ` + `, + ` const foo = function () { return function () { return ''; @@ -206,10 +177,8 @@ setTimeout(foo(), 0); setInterval(foo(), 0); setImmediate(foo()); execScript(foo()); - `, - }, - { - code: ` + `, + ` const foo: (() => () => void) = function foo () { return function foo() {} } @@ -218,10 +187,8 @@ setTimeout(foo(), 0); setInterval(foo(), 0); setImmediate(foo()); execScript(foo()); - `, - }, - { - code: ` + `, + ` function foo() { return function foo() { return () => {}; @@ -232,10 +199,8 @@ setTimeout(foo()(), 0); setInterval(foo()(), 0); setImmediate(foo()()); execScript(foo()()); - `, - }, - { - code: ` + `, + ` class Foo { static fn = () => {}; } @@ -244,10 +209,8 @@ setTimeout(Foo.fn, 0); setInterval(Foo.fn, 0); setImmediate(Foo.fn); execScript(Foo.fn); - `, - }, - { - code: ` + `, + ` class Foo { fn() {} } @@ -258,10 +221,8 @@ setTimeout(foo.fn, 0); setInterval(foo.fn, 0); setImmediate(foo.fn); execScript(foo.fn); - `, - }, - { - code: ` + `, + ` class Foo { fn() {} } @@ -272,18 +233,15 @@ setTimeout(fn.bind(null), 0); setInterval(fn.bind(null), 0); setImmediate(fn.bind(null)); execScript(fn.bind(null)); - `, - }, - { - code: ` + `, + ` const fn = (foo: () => void) => { setTimeout(foo, 0); setInterval(foo, 0); setImmediate(foo); execScript(foo); } - `, - }, + `, ], invalid: [ From b097245df35114906b1f9c60c3ad4cd698d957b8 Mon Sep 17 00:00:00 2001 From: Christoph Kettelhoit Date: Mon, 2 Mar 2020 06:12:41 +0100 Subject: [PATCH 08/10] =?UTF-8?q?feat(eslint-plugin):=20additional=20annot?= =?UTF-8?q?ation=20spacing=20rules=20for=20va=E2=80=A6=20(#1496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/rules/type-annotation-spacing.md | 2 +- .../src/rules/type-annotation-spacing.ts | 128 ++++++++++++---- packages/eslint-plugin/src/util/astUtils.ts | 95 ++++++++++++ .../rules/type-annotation-spacing.test.ts | 140 ++++++++++++++++++ 4 files changed, 332 insertions(+), 33 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/type-annotation-spacing.md b/packages/eslint-plugin/docs/rules/type-annotation-spacing.md index c3a889cf36fd..a7c43a704ada 100644 --- a/packages/eslint-plugin/docs/rules/type-annotation-spacing.md +++ b/packages/eslint-plugin/docs/rules/type-annotation-spacing.md @@ -41,7 +41,7 @@ This rule has an object option: - `"before": true`, (default for arrow) requires a space before the colon/arrow. - `"after": true`, (default) requires a space after the colon/arrow. - `"after": false`, disallows spaces after the colon/arrow. -- `"overrides"`, overrides the default options for type annotations with `colon` (e.g. `const foo: string`) and function types with `arrow` (e.g. `type Foo = () => {}`). +- `"overrides"`, overrides the default options for type annotations with `colon` (e.g. `const foo: string`) and function types with `arrow` (e.g. `type Foo = () => {}`). Additionally allows granular overrides for `variable` (`const foo: string`),`parameter` (`function foo(bar: string) {...}`),`property` (`interface Foo { bar: string }`) and `returnType` (`function foo(): string {...}`) annotations. ### defaults diff --git a/packages/eslint-plugin/src/rules/type-annotation-spacing.ts b/packages/eslint-plugin/src/rules/type-annotation-spacing.ts index 48ec12cfff81..5b9dbfbaa4ad 100644 --- a/packages/eslint-plugin/src/rules/type-annotation-spacing.ts +++ b/packages/eslint-plugin/src/rules/type-annotation-spacing.ts @@ -1,22 +1,35 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; +import { + isClassOrTypeElement, + isFunction, + isFunctionOrFunctionType, + isIdentifier, + isTSFunctionType, + isVariableDeclarator, +} from '../util'; -type Options = [ - { - before?: boolean; - after?: boolean; - overrides?: { - colon?: { - before?: boolean; - after?: boolean; - }; - arrow?: { - before?: boolean; - after?: boolean; - }; - }; - }?, -]; +interface WhitespaceRule { + readonly before?: boolean; + readonly after?: boolean; +} + +interface WhitespaceOverride { + readonly colon?: WhitespaceRule; + readonly arrow?: WhitespaceRule; + readonly variable?: WhitespaceRule; + readonly property?: WhitespaceRule; + readonly parameter?: WhitespaceRule; + readonly returnType?: WhitespaceRule; +} + +interface Config extends WhitespaceRule { + readonly overrides?: WhitespaceOverride; +} + +type WhitespaceRules = Required; + +type Options = [Config?]; type MessageIds = | 'expectedSpaceAfter' | 'expectedSpaceBefore' @@ -32,6 +45,67 @@ const definition = { additionalProperties: false, }; +function createRules(options?: Config): WhitespaceRules { + const globals = { + ...(options?.before !== undefined ? { before: options.before } : {}), + ...(options?.after !== undefined ? { after: options.after } : {}), + }; + const override = options?.overrides ?? {}; + const colon = { + ...{ before: false, after: true }, + ...globals, + ...override?.colon, + }; + const arrow = { + ...{ before: true, after: true }, + ...globals, + ...override?.arrow, + }; + + return { + colon: colon, + arrow: arrow, + variable: { ...colon, ...override?.variable }, + property: { ...colon, ...override?.property }, + parameter: { ...colon, ...override?.parameter }, + returnType: { ...colon, ...override?.returnType }, + }; +} + +function getIdentifierRules( + rules: WhitespaceRules, + node: TSESTree.Node | undefined, +): WhitespaceRule { + const scope = node?.parent; + + if (isVariableDeclarator(scope)) { + return rules.variable; + } else if (isFunctionOrFunctionType(scope)) { + return rules.parameter; + } else { + return rules.colon; + } +} + +function getRules( + rules: WhitespaceRules, + node: TSESTree.TypeNode, +): WhitespaceRule { + const scope = node?.parent?.parent; + + if (isTSFunctionType(scope)) { + return rules.arrow; + } else if (isIdentifier(scope)) { + return getIdentifierRules(rules, scope); + } else if (isClassOrTypeElement(scope)) { + return rules.property; + } else if (isFunction(scope)) { + return rules.returnType; + } else { + return rules.colon; + } +} + export default util.createRule({ name: 'type-annotation-spacing', meta: { @@ -59,6 +133,10 @@ export default util.createRule({ properties: { colon: definition, arrow: definition, + variable: definition, + parameter: definition, + property: definition, + returnType: definition, }, additionalProperties: false, }, @@ -76,20 +154,7 @@ export default util.createRule({ const punctuators = [':', '=>']; const sourceCode = context.getSourceCode(); - const overrides = options?.overrides ?? { colon: {}, arrow: {} }; - - const colonOptions = Object.assign( - {}, - { before: false, after: true }, - options, - overrides.colon, - ); - const arrowOptions = Object.assign( - {}, - { before: true, after: true }, - options, - overrides.arrow, - ); + const ruleSet = createRules(options); /** * Checks if there's proper spacing around type annotations (no space @@ -108,8 +173,7 @@ export default util.createRule({ return; } - const before = type === ':' ? colonOptions.before : arrowOptions.before; - const after = type === ':' ? colonOptions.after : arrowOptions.after; + const { before, after } = getRules(ruleSet, typeAnnotation); if (type === ':' && previousToken.value === '?') { // shift the start to the ? diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index 29dcdc0b459f..a92f00eaa168 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -82,6 +82,95 @@ function isTypeAssertion( ); } +function isVariableDeclarator( + node: TSESTree.Node | undefined, +): node is TSESTree.VariableDeclarator { + return node?.type === AST_NODE_TYPES.VariableDeclarator; +} + +function isFunction( + node: TSESTree.Node | undefined, +): node is + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression { + if (!node) { + return false; + } + + return [ + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + ].includes(node.type); +} + +function isFunctionType( + node: TSESTree.Node | undefined, +): node is + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSConstructSignatureDeclaration + | TSESTree.TSEmptyBodyFunctionExpression + | TSESTree.TSFunctionType + | TSESTree.TSMethodSignature { + if (!node) { + return false; + } + + return [ + AST_NODE_TYPES.TSCallSignatureDeclaration, + AST_NODE_TYPES.TSConstructSignatureDeclaration, + AST_NODE_TYPES.TSEmptyBodyFunctionExpression, + AST_NODE_TYPES.TSFunctionType, + AST_NODE_TYPES.TSMethodSignature, + ].includes(node.type); +} + +function isFunctionOrFunctionType( + node: TSESTree.Node | undefined, +): node is + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSConstructSignatureDeclaration + | TSESTree.TSEmptyBodyFunctionExpression + | TSESTree.TSFunctionType + | TSESTree.TSMethodSignature { + return isFunction(node) || isFunctionType(node); +} + +function isTSFunctionType( + node: TSESTree.Node | undefined, +): node is TSESTree.TSFunctionType { + return node?.type === AST_NODE_TYPES.TSFunctionType; +} + +function isClassOrTypeElement( + node: TSESTree.Node | undefined, +): node is TSESTree.ClassElement | TSESTree.TypeElement { + if (!node) { + return false; + } + + return [ + // ClassElement + AST_NODE_TYPES.ClassProperty, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.MethodDefinition, + AST_NODE_TYPES.TSAbstractClassProperty, + AST_NODE_TYPES.TSAbstractMethodDefinition, + AST_NODE_TYPES.TSEmptyBodyFunctionExpression, + AST_NODE_TYPES.TSIndexSignature, + // TypeElement + AST_NODE_TYPES.TSCallSignatureDeclaration, + AST_NODE_TYPES.TSConstructSignatureDeclaration, + // AST_NODE_TYPES.TSIndexSignature, + AST_NODE_TYPES.TSMethodSignature, + AST_NODE_TYPES.TSPropertySignature, + ].includes(node.type); +} + /** * Checks if a node is a constructor method. */ @@ -136,6 +225,10 @@ export { isAwaitExpression, isAwaitKeyword, isConstructor, + isClassOrTypeElement, + isFunction, + isFunctionOrFunctionType, + isFunctionType, isIdentifier, isLogicalOrOperator, isNonNullAssertionPunctuator, @@ -145,6 +238,8 @@ export { isOptionalOptionalChain, isSetter, isTokenOnSameLine, + isTSFunctionType, isTypeAssertion, + isVariableDeclarator, LINEBREAK_MATCHER, }; diff --git a/packages/eslint-plugin/tests/rules/type-annotation-spacing.test.ts b/packages/eslint-plugin/tests/rules/type-annotation-spacing.test.ts index 04abdc81a472..e496743afc3e 100644 --- a/packages/eslint-plugin/tests/rules/type-annotation-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/type-annotation-spacing.test.ts @@ -1034,6 +1034,146 @@ type Bar = Record ], }, 'let resolver: (() => PromiseLike) | PromiseLike;', + { + code: 'const foo:string;', + options: [ + { + overrides: { + colon: { + after: false, + before: true, + }, + variable: { + before: false, + }, + }, + }, + ], + }, + { + code: 'const foo:string;', + options: [ + { + before: true, + overrides: { + colon: { + after: true, + before: false, + }, + variable: { + after: false, + }, + }, + }, + ], + }, + { + code: ` +interface Foo { + greet():string; +} + `, + options: [ + { + overrides: { + colon: { + after: false, + before: true, + }, + property: { + before: false, + }, + }, + }, + ], + }, + { + code: ` +interface Foo { + name:string; +} + `, + options: [ + { + before: true, + overrides: { + colon: { + after: true, + before: false, + }, + property: { + after: false, + }, + }, + }, + ], + }, + { + code: 'function foo(name:string) {}', + options: [ + { + overrides: { + colon: { + after: false, + before: true, + }, + parameter: { + before: false, + }, + }, + }, + ], + }, + { + code: 'function foo(name:string) {}', + options: [ + { + before: true, + overrides: { + colon: { + after: true, + before: false, + }, + parameter: { + after: false, + }, + }, + }, + ], + }, + { + code: 'function foo():string {}', + options: [ + { + overrides: { + colon: { + after: false, + before: true, + }, + returnType: { + before: false, + }, + }, + }, + ], + }, + { + code: 'function foo():string {}', + options: [ + { + before: true, + overrides: { + colon: { + after: true, + before: false, + }, + returnType: { + after: false, + }, + }, + }, + ], + }, ], invalid: [ { From 3be98542afd7553cbbec66c4be215173d7f7ffcf Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 1 Mar 2020 21:23:58 -0800 Subject: [PATCH 09/10] feat(eslint-plugin): add prefer-readonly-parameters (#1513) --- packages/eslint-plugin/README.md | 1 + .../rules/prefer-readonly-parameter-types.md | 163 ++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 24 +- .../rules/prefer-readonly-parameter-types.ts | 98 ++++ packages/eslint-plugin/src/util/index.ts | 1 + .../eslint-plugin/src/util/isTypeReadonly.ts | 155 ++++++ .../prefer-readonly-parameter-types.test.ts | 507 ++++++++++++++++++ .../eslint-plugin/typings/typescript.d.ts | 21 +- 9 files changed, 952 insertions(+), 19 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md create mode 100644 packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts create mode 100644 packages/eslint-plugin/src/util/isTypeReadonly.ts create mode 100644 packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 778e159d6ea5..19ee2638a8e7 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -142,6 +142,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | :wrench: | | | [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-readonly-parameter-types`](./docs/rules/prefer-readonly-parameter-types.md) | Requires that function parameters are typed as readonly to prevent accidental mutation of inputs | | | :thought_balloon: | | [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md b/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md new file mode 100644 index 000000000000..c69b3b21c345 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md @@ -0,0 +1,163 @@ +# Requires that function parameters are typed as readonly to prevent accidental mutation of inputs (`prefer-readonly-parameter-types`) + +Mutating function arguments can lead to confusing, hard to debug behavior. +Whilst it's easy to implicitly remember to not modify function arguments, explicitly typing arguments as readonly provides clear contract to consumers. +This contract makes it easier for a consumer to reason about if a function has side-effects. + +## Rule Details + +This rule allows you to enforce that function parameters resolve to readonly types. +A type is considered readonly if: + +- it is a primitive type (`string`, `number`, `boolean`, `symbol`, or an enum), +- it is a function signature type, +- it is a readonly array type whose element type is considered readonly. +- it is a readonly tuple type whose elements are all considered readonly. +- it is an object type whose properties are all marked as readonly, and whose values are all considered readonly. + +Examples of **incorrect** code for this rule: + +```ts +function array1(arg: string[]) {} // array is not readonly +function array2(arg: readonly string[][]) {} // array element is not readonly +function array3(arg: [string, number]) {} // tuple is not readonly +function array4(arg: readonly [string[], number]) {} // tuple element is not readonly +// the above examples work the same if you use ReadonlyArray instead + +function object1(arg: { prop: string }) {} // property is not readonly +function object2(arg: { readonly prop: string; prop2: string }) {} // not all properties are readonly +function object3(arg: { readonly prop: { prop2: string } }) {} // nested property is not readonly +// the above examples work the same if you use Readonly instead + +interface CustomArrayType extends ReadonlyArray { + prop: string; // note: this property is mutable +} +function custom1(arg: CustomArrayType) {} + +interface CustomFunction { + (): void; + prop: string; // note: this property is mutable +} +function custom2(arg: CustomFunction) {} + +function union(arg: string[] | ReadonlyArray) {} // not all types are readonly + +// rule also checks function types +interface Foo { + (arg: string[]): void; +} +interface Foo { + new (arg: string[]): void; +} +const x = { foo(arg: string[]): void; }; +function foo(arg: string[]); +type Foo = (arg: string[]) => void; +interface Foo { + foo(arg: string[]): void; +} +``` + +Examples of **correct** code for this rule: + +```ts +function array1(arg: readonly string[]) {} +function array2(arg: readonly (readonly string[])[]) {} +function array3(arg: readonly [string, number]) {} +function array4(arg: readonly [readonly string[], number]) {} +// the above examples work the same if you use ReadonlyArray instead + +function object1(arg: { readonly prop: string }) {} +function object2(arg: { readonly prop: string; readonly prop2: string }) {} +function object3(arg: { readonly prop: { readonly prop2: string } }) {} +// the above examples work the same if you use Readonly instead + +interface CustomArrayType extends ReadonlyArray { + readonly prop: string; +} +function custom1(arg: CustomArrayType) {} + +interface CustomFunction { + (): void; + readonly prop: string; +} +function custom2(arg: CustomFunction) {} + +function union(arg: readonly string[] | ReadonlyArray) {} + +function primitive1(arg: string) {} +function primitive2(arg: number) {} +function primitive3(arg: boolean) {} +function primitive4(arg: unknown) {} +function primitive5(arg: null) {} +function primitive6(arg: undefined) {} +function primitive7(arg: any) {} +function primitive8(arg: never) {} +function primitive9(arg: string | number | undefined) {} + +function fnSig(arg: () => void) {} + +enum Foo { a, b } +function enum(arg: Foo) {} + +function symb1(arg: symbol) {} +const customSymbol = Symbol('a'); +function symb2(arg: typeof customSymbol) {} + +// function types +interface Foo { + (arg: readonly string[]): void; +} +interface Foo { + new (arg: readonly string[]): void; +} +const x = { foo(arg: readonly string[]): void; }; +function foo(arg: readonly string[]); +type Foo = (arg: readonly string[]) => void; +interface Foo { + foo(arg: readonly string[]): void; +} +``` + +## Options + +```ts +interface Options { + checkParameterProperties?: boolean; +} + +const defaultOptions: Options = { + checkParameterProperties: true, +}; +``` + +### `checkParameterProperties` + +This option allows you to enable or disable the checking of parameter properties. +Because parameter properties create properties on the class, it may be undesirable to force them to be readonly. + +Examples of **incorrect** code for this rule with `{checkParameterProperties: true}`: + +```ts +class Foo { + constructor(private paramProp: string[]) {} +} +``` + +Examples of **correct** code for this rule with `{checkParameterProperties: true}`: + +```ts +class Foo { + constructor(private paramProp: readonly string[]) {} +} +``` + +Examples of **correct** code for this rule with `{checkParameterProperties: false}`: + +```ts +class Foo { + constructor( + private paramProp1: string[], + private paramProp2: readonly string[], + ) {} +} +``` diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index bf5844653ef8..590ef81df8d5 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -79,6 +79,7 @@ "@typescript-eslint/prefer-nullish-coalescing": "error", "@typescript-eslint/prefer-optional-chain": "error", "@typescript-eslint/prefer-readonly": "error", + "@typescript-eslint/prefer-readonly-parameter-types": "error", "@typescript-eslint/prefer-regexp-exec": "error", "@typescript-eslint/prefer-string-starts-ends-with": "error", "@typescript-eslint/promise-function-async": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 0c3a5d9f0d1e..3568057787b2 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -1,8 +1,8 @@ import adjacentOverloadSignatures from './adjacent-overload-signatures'; import arrayType from './array-type'; import awaitThenable from './await-thenable'; -import banTsIgnore from './ban-ts-ignore'; import banTsComment from './ban-ts-comment'; +import banTsIgnore from './ban-ts-ignore'; import banTypes from './ban-types'; import braceStyle from './brace-style'; import camelcase from './camelcase'; @@ -29,10 +29,10 @@ import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; import noExplicitAny from './no-explicit-any'; +import noExtraneousClass from './no-extraneous-class'; import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraParens from './no-extra-parens'; import noExtraSemi from './no-extra-semi'; -import noExtraneousClass from './no-extraneous-class'; import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; import noImpliedEval from './no-implied-eval'; @@ -41,8 +41,8 @@ import noMagicNumbers from './no-magic-numbers'; import noMisusedNew from './no-misused-new'; import noMisusedPromises from './no-misused-promises'; import noNamespace from './no-namespace'; -import noNonNullAssertion from './no-non-null-assertion'; import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chain'; +import noNonNullAssertion from './no-non-null-assertion'; import noParameterProperties from './no-parameter-properties'; import noRequireImports from './no-require-imports'; import noThisAlias from './no-this-alias'; @@ -51,7 +51,7 @@ import noTypeAlias from './no-type-alias'; import noUnnecessaryBooleanLiteralCompare from './no-unnecessary-boolean-literal-compare'; import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; -import useDefaultTypeParameter from './no-unnecessary-type-arguments'; +import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; import noUntypedPublicSignature from './no-untyped-public-signature'; import noUnusedExpressions from './no-unused-expressions'; @@ -68,6 +68,7 @@ import preferNamespaceKeyword from './prefer-namespace-keyword'; import preferNullishCoalescing from './prefer-nullish-coalescing'; import preferOptionalChain from './prefer-optional-chain'; import preferReadonly from './prefer-readonly'; +import preferReadonlyParameterTypes from './prefer-readonly-parameter-types'; import preferRegexpExec from './prefer-regexp-exec'; import preferStringStartsEndsWith from './prefer-string-starts-ends-with'; import promiseFunctionAsync from './promise-function-async'; @@ -91,8 +92,8 @@ export default { 'adjacent-overload-signatures': adjacentOverloadSignatures, 'array-type': arrayType, 'await-thenable': awaitThenable, - 'ban-ts-ignore': banTsIgnore, 'ban-ts-comment': banTsComment, + 'ban-ts-ignore': banTsIgnore, 'ban-types': banTypes, 'no-base-to-string': noBaseToString, 'brace-style': braceStyle, @@ -125,28 +126,28 @@ export default { 'no-extraneous-class': noExtraneousClass, 'no-floating-promises': noFloatingPromises, 'no-for-in-array': noForInArray, - 'no-inferrable-types': noInferrableTypes, 'no-implied-eval': noImpliedEval, + 'no-inferrable-types': noInferrableTypes, 'no-magic-numbers': noMagicNumbers, 'no-misused-new': noMisusedNew, 'no-misused-promises': noMisusedPromises, 'no-namespace': noNamespace, - 'no-non-null-assertion': noNonNullAssertion, 'no-non-null-asserted-optional-chain': noNonNullAssertedOptionalChain, + 'no-non-null-assertion': noNonNullAssertion, 'no-parameter-properties': noParameterProperties, 'no-require-imports': noRequireImports, 'no-this-alias': noThisAlias, - 'no-type-alias': noTypeAlias, 'no-throw-literal': noThrowLiteral, + 'no-type-alias': noTypeAlias, 'no-unnecessary-boolean-literal-compare': noUnnecessaryBooleanLiteralCompare, 'no-unnecessary-condition': noUnnecessaryCondition, 'no-unnecessary-qualifier': noUnnecessaryQualifier, - 'no-unnecessary-type-arguments': useDefaultTypeParameter, + 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-untyped-public-signature': noUntypedPublicSignature, - 'no-unused-vars': noUnusedVars, - 'no-unused-vars-experimental': noUnusedVarsExperimental, 'no-unused-expressions': noUnusedExpressions, + 'no-unused-vars-experimental': noUnusedVarsExperimental, + 'no-unused-vars': noUnusedVars, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, @@ -157,6 +158,7 @@ export default { 'prefer-namespace-keyword': preferNamespaceKeyword, 'prefer-nullish-coalescing': preferNullishCoalescing, 'prefer-optional-chain': preferOptionalChain, + 'prefer-readonly-parameter-types': preferReadonlyParameterTypes, 'prefer-readonly': preferReadonly, 'prefer-regexp-exec': preferRegexpExec, 'prefer-string-starts-ends-with': preferStringStartsEndsWith, diff --git a/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts b/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts new file mode 100644 index 000000000000..07e8e69940cc --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts @@ -0,0 +1,98 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type Options = [ + { + checkParameterProperties?: boolean; + }, +]; +type MessageIds = 'shouldBeReadonly'; + +export default util.createRule({ + name: 'prefer-readonly-parameter-types', + meta: { + type: 'suggestion', + docs: { + description: + 'Requires that function parameters are typed as readonly to prevent accidental mutation of inputs', + category: 'Possible Errors', + recommended: false, + requiresTypeChecking: true, + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + checkParameterProperties: { + type: 'boolean', + }, + }, + }, + ], + messages: { + shouldBeReadonly: 'Parameter should be a read only type', + }, + }, + defaultOptions: [ + { + checkParameterProperties: true, + }, + ], + create(context, [{ checkParameterProperties }]) { + const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); + const checker = program.getTypeChecker(); + + return { + [[ + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.TSCallSignatureDeclaration, + AST_NODE_TYPES.TSConstructSignatureDeclaration, + AST_NODE_TYPES.TSDeclareFunction, + AST_NODE_TYPES.TSEmptyBodyFunctionExpression, + AST_NODE_TYPES.TSFunctionType, + AST_NODE_TYPES.TSMethodSignature, + ].join(', ')]( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSConstructSignatureDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.TSEmptyBodyFunctionExpression + | TSESTree.TSFunctionType + | TSESTree.TSMethodSignature, + ): void { + for (const param of node.params) { + if ( + !checkParameterProperties && + param.type === AST_NODE_TYPES.TSParameterProperty + ) { + continue; + } + + const actualParam = + param.type === AST_NODE_TYPES.TSParameterProperty + ? param.parameter + : param; + const tsNode = esTreeNodeToTSNodeMap.get(actualParam); + const type = checker.getTypeAtLocation(tsNode); + const isReadOnly = util.isTypeReadonly(checker, type); + + if (!isReadOnly) { + context.report({ + node: actualParam, + messageId: 'shouldBeReadonly', + }); + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 7fde0f41e715..381012acfec1 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -2,6 +2,7 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; export * from './astUtils'; export * from './createRule'; +export * from './isTypeReadonly'; export * from './misc'; export * from './nullThrows'; export * from './types'; diff --git a/packages/eslint-plugin/src/util/isTypeReadonly.ts b/packages/eslint-plugin/src/util/isTypeReadonly.ts new file mode 100644 index 000000000000..99df07b153a7 --- /dev/null +++ b/packages/eslint-plugin/src/util/isTypeReadonly.ts @@ -0,0 +1,155 @@ +import { + isObjectType, + isUnionType, + isUnionOrIntersectionType, + unionTypeParts, + isPropertyReadonlyInType, +} from 'tsutils'; +import * as ts from 'typescript'; +import { nullThrows, NullThrowsReasons } from '.'; + +/** + * Returns: + * - null if the type is not an array or tuple, + * - true if the type is a readonly array or readonly tuple, + * - false if the type is a mutable array or mutable tuple. + */ +function isTypeReadonlyArrayOrTuple( + checker: ts.TypeChecker, + type: ts.Type, +): boolean | null { + function checkTypeArguments(arrayType: ts.TypeReference): boolean { + const typeArguments = checker.getTypeArguments(arrayType); + /* istanbul ignore if */ if (typeArguments.length === 0) { + // this shouldn't happen in reality as: + // - tuples require at least 1 type argument + // - ReadonlyArray requires at least 1 type argument + return true; + } + + // validate the element types are also readonly + if (typeArguments.some(typeArg => !isTypeReadonly(checker, typeArg))) { + return false; + } + return true; + } + + if (checker.isArrayType(type)) { + const symbol = nullThrows( + type.getSymbol(), + NullThrowsReasons.MissingToken('symbol', 'array type'), + ); + const escapedName = symbol.getEscapedName(); + if (escapedName === 'Array' && escapedName !== 'ReadonlyArray') { + return false; + } + + return checkTypeArguments(type); + } + + if (checker.isTupleType(type)) { + if (!type.target.readonly) { + return false; + } + + return checkTypeArguments(type); + } + + return null; +} + +/** + * Returns: + * - null if the type is not an object, + * - true if the type is an object with only readonly props, + * - false if the type is an object with at least one mutable prop. + */ +function isTypeReadonlyObject( + checker: ts.TypeChecker, + type: ts.Type, +): boolean | null { + function checkIndexSignature(kind: ts.IndexKind): boolean | null { + const indexInfo = checker.getIndexInfoOfType(type, kind); + if (indexInfo) { + return indexInfo.isReadonly ? true : false; + } + + return null; + } + + const properties = type.getProperties(); + if (properties.length) { + // ensure the properties are marked as readonly + for (const property of properties) { + if (!isPropertyReadonlyInType(type, property.getEscapedName(), checker)) { + return false; + } + } + + // all properties were readonly + // now ensure that all of the values are readonly also. + + // do this after checking property readonly-ness as a perf optimization, + // as we might be able to bail out early due to a mutable property before + // doing this deep, potentially expensive check. + for (const property of properties) { + const propertyType = nullThrows( + checker.getTypeOfPropertyOfType(type, property.getName()), + NullThrowsReasons.MissingToken(`property "${property.name}"`, 'type'), + ); + if (!isTypeReadonly(checker, propertyType)) { + return false; + } + } + } + + const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String); + if (isStringIndexSigReadonly === false) { + return isStringIndexSigReadonly; + } + + const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number); + if (isNumberIndexSigReadonly === false) { + return isNumberIndexSigReadonly; + } + + return true; +} + +/** + * Checks if the given type is readonly + */ +function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean { + if (isUnionType(type)) { + // all types in the union must be readonly + return unionTypeParts(type).every(t => isTypeReadonly(checker, t)); + } + + // all non-object, non-intersection types are readonly. + // this should only be primitive types + if (!isObjectType(type) && !isUnionOrIntersectionType(type)) { + return true; + } + + // pure function types are readonly + if ( + type.getCallSignatures().length > 0 && + type.getProperties().length === 0 + ) { + return true; + } + + const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type); + if (isReadonlyArray !== null) { + return isReadonlyArray; + } + + const isReadonlyObject = isTypeReadonlyObject(checker, type); + /* istanbul ignore else */ if (isReadonlyObject !== null) { + return isReadonlyObject; + } + + throw new Error('Unhandled type'); +} + +export { isTypeReadonly }; diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts new file mode 100644 index 000000000000..b869e0c15eda --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts @@ -0,0 +1,507 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; +import rule from '../../src/rules/prefer-readonly-parameter-types'; +import { + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, +} from '../../src/util'; + +type MessageIds = InferMessageIdsTypeFromRule; +type Options = InferOptionsTypeFromRule; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +const primitives = [ + 'boolean', + 'true', + 'string', + "'a'", + 'number', + '1', + 'symbol', + 'any', + 'unknown', + 'never', + 'null', + 'undefined', +]; +const arrays = [ + 'readonly string[]', + 'Readonly', + 'ReadonlyArray', + 'readonly [string]', + 'Readonly<[string]>', +]; +const objects = [ + '{ foo: "" }', + '{ foo: readonly string[] }', + '{ foo(): void }', +]; +const weirdIntersections = [ + ` + interface Test { + (): void + readonly property: boolean + } + function foo(arg: Test) {} + `, + ` + type Test = (() => void) & { + readonly property: boolean + }; + function foo(arg: Test) {} + `, +]; + +ruleTester.run('prefer-readonly-parameter-types', rule, { + valid: [ + 'function foo() {}', + + // primitives + ...primitives.map(type => `function foo(arg: ${type}) {}`), + ` + const symb = Symbol('a'); + function foo(arg: typeof symb) {} + `, + ` + enum Enum { a, b } + function foo(arg: Enum) {} + `, + + // arrays + ...arrays.map(type => `function foo(arg: ${type}) {}`), + // nested arrays + 'function foo(arg: readonly (readonly string[])[]) {}', + 'function foo(arg: Readonly[]>) {}', + 'function foo(arg: ReadonlyArray>) {}', + + // functions + 'function foo(arg: () => void) {}', + + // unions + 'function foo(arg: string | null) {}', + 'function foo(arg: string | ReadonlyArray) {}', + 'function foo(arg: string | (() => void)) {}', + 'function foo(arg: ReadonlyArray | ReadonlyArray) {}', + + // objects + ...objects.map(type => `function foo(arg: Readonly<${type}>) {}`), + ` + function foo(arg: { + readonly foo: { + readonly bar: string + } + }) {} + `, + ` + function foo(arg: { + readonly [k: string]: string, + }) {} + `, + ` + function foo(arg: { + readonly [k: number]: string, + }) {} + `, + ` + interface Empty {} + function foo(arg: Empty) {} + `, + + // weird other cases + ...weirdIntersections.map(code => code), + ` + interface Test extends ReadonlyArray { + readonly property: boolean + } + function foo(arg: Readonly) {} + `, + ` + type Test = (readonly string[]) & { + readonly property: boolean + }; + function foo(arg: Readonly) {} + `, + ` + type Test = string & number; + function foo(arg: Test) {} + `, + + // declaration merging + ` + class Foo { + readonly bang = 1; + } + interface Foo { + readonly prop: string; + } + interface Foo { + readonly prop2: string; + } + function foo(arg: Foo) {} + `, + // method made readonly via Readonly + ` + class Foo { + method() {} + } + function foo(arg: Readonly) {} + `, + + // parameter properties should work fine + { + code: ` + class Foo { + constructor( + private arg1: readonly string[], + public arg2: readonly string[], + protected arg3: readonly string[], + readonly arg4: readonly string[], + ) {} + } + `, + options: [ + { + checkParameterProperties: true, + }, + ], + }, + { + code: ` + class Foo { + constructor( + private arg1: string[], + public arg2: string[], + protected arg3: string[], + readonly arg4: string[], + ) {} + } + `, + options: [ + { + checkParameterProperties: false, + }, + ], + }, + + // type functions + 'interface Foo { (arg: readonly string[]): void }', // TSCallSignatureDeclaration + 'interface Foo { new (arg: readonly string[]): void }', // TSConstructSignatureDeclaration + 'const x = { foo(arg: readonly string[]): void }', // TSEmptyBodyFunctionExpression + 'function foo(arg: readonly string[]);', // TSDeclareFunction + 'type Foo = (arg: readonly string[]) => void;', // TSFunctionType + 'interface Foo { foo(arg: readonly string[]): void }', // TSMethodSignature + ], + invalid: [ + // arrays + ...arrays.map>(baseType => { + const type = baseType + .replace(/readonly /g, '') + .replace(/Readonly<(.+?)>/g, '$1') + .replace(/ReadonlyArray/g, 'Array'); + return { + code: `function foo(arg: ${type}) {}`, + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 19 + type.length, + }, + ], + }; + }), + // nested arrays + { + code: 'function foo(arg: readonly (string[])[]) {}', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 40, + }, + ], + }, + { + code: 'function foo(arg: Readonly) {}', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 39, + }, + ], + }, + { + code: 'function foo(arg: ReadonlyArray>) {}', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 47, + }, + ], + }, + + // objects + ...objects.map>(type => { + return { + code: `function foo(arg: ${type}) {}`, + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 19 + type.length, + }, + ], + }; + }), + { + code: ` + function foo(arg: { + readonly foo: { + bar: string + } + }) {} + `, + errors: [ + { + messageId: 'shouldBeReadonly', + line: 2, + column: 22, + endLine: 6, + endColumn: 10, + }, + ], + }, + // object index signatures + { + code: ` + function foo(arg: { + [key: string]: string + }) {} + `, + errors: [ + { + messageId: 'shouldBeReadonly', + line: 2, + column: 22, + endLine: 4, + endColumn: 10, + }, + ], + }, + { + code: ` + function foo(arg: { + [key: number]: string + }) {} + `, + errors: [ + { + messageId: 'shouldBeReadonly', + line: 2, + column: 22, + endLine: 4, + endColumn: 10, + }, + ], + }, + + // weird intersections + ...weirdIntersections.map>( + baseCode => { + const code = baseCode.replace(/readonly /g, ''); + return { + code, + errors: [{ messageId: 'shouldBeReadonly' }], + }; + }, + ), + { + code: ` + interface Test extends Array { + readonly property: boolean + } + function foo(arg: Test) {} + `, + errors: [ + { + messageId: 'shouldBeReadonly', + line: 5, + column: 22, + endLine: 5, + endColumn: 31, + }, + ], + }, + { + code: ` + interface Test extends Array { + property: boolean + } + function foo(arg: Test) {} + `, + errors: [ + { + messageId: 'shouldBeReadonly', + line: 5, + column: 22, + endLine: 5, + endColumn: 31, + }, + ], + }, + + // parameter properties should work fine + { + code: ` + class Foo { + constructor( + private arg1: string[], + public arg2: string[], + protected arg3: string[], + readonly arg4: string[], + ) {} + } + `, + options: [ + { + checkParameterProperties: true, + }, + ], + errors: [ + { + messageId: 'shouldBeReadonly', + line: 4, + column: 23, + endLine: 4, + endColumn: 37, + }, + { + messageId: 'shouldBeReadonly', + line: 5, + column: 23, + endLine: 5, + endColumn: 37, + }, + { + messageId: 'shouldBeReadonly', + line: 6, + column: 23, + endLine: 6, + endColumn: 37, + }, + { + messageId: 'shouldBeReadonly', + line: 7, + column: 23, + endLine: 7, + endColumn: 37, + }, + ], + }, + { + code: ` + class Foo { + constructor( + private arg1: readonly string[], + public arg2: readonly string[], + protected arg3: readonly string[], + readonly arg4: readonly string[], + arg5: string[], + ) {} + } + `, + options: [ + { + checkParameterProperties: false, + }, + ], + errors: [ + { + messageId: 'shouldBeReadonly', + line: 8, + column: 13, + endLine: 8, + endColumn: 27, + }, + ], + }, + + // type functions + { + // TSCallSignatureDeclaration + code: 'interface Foo { (arg: string[]): void }', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 18, + endColumn: 31, + }, + ], + }, + { + // TSConstructSignatureDeclaration + code: 'interface Foo { new (arg: string[]): void }', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 22, + endColumn: 35, + }, + ], + }, + { + // TSEmptyBodyFunctionExpression + code: 'const x = { foo(arg: string[]): void }', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 17, + endColumn: 30, + }, + ], + }, + { + // TSDeclareFunction + code: 'function foo(arg: string[]);', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 27, + }, + ], + }, + { + // TSFunctionType + code: 'type Foo = (arg: string[]) => void', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 13, + endColumn: 26, + }, + ], + }, + { + // TSMethodSignature + code: 'interface Foo { foo(arg: string[]): void }', + errors: [ + { + messageId: 'shouldBeReadonly', + column: 21, + endColumn: 34, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts index 6d9a098b538b..7c9089158b4b 100644 --- a/packages/eslint-plugin/typings/typescript.d.ts +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -1,4 +1,4 @@ -import { TypeChecker, Type } from 'typescript'; +import 'typescript'; declare module 'typescript' { interface TypeChecker { @@ -6,16 +6,21 @@ declare module 'typescript' { /** * @returns `true` if the given type is an array type: - * - Array - * - ReadonlyArray - * - foo[] - * - readonly foo[] + * - `Array` + * - `ReadonlyArray` + * - `foo[]` + * - `readonly foo[]` */ - isArrayType(type: Type): boolean; + isArrayType(type: Type): type is TypeReference; /** * @returns `true` if the given type is a tuple type: - * - [foo] + * - `[foo]` + * - `readonly [foo]` */ - isTupleType(type: Type): boolean; + isTupleType(type: Type): type is TupleTypeReference; + /** + * Return the type of the given property in the given type, or undefined if no such property exists + */ + getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined; } } From 5a097d316fb084dc4b13e87d68fe9bf43d8a9548 Mon Sep 17 00:00:00 2001 From: James Henry Date: Mon, 2 Mar 2020 18:01:22 +0000 Subject: [PATCH 10/10] chore: publish v2.22.0 --- CHANGELOG.md | 22 ++++++++++++++++++++ lerna.json | 2 +- packages/eslint-plugin-internal/CHANGELOG.md | 8 +++++++ packages/eslint-plugin-internal/package.json | 4 ++-- packages/eslint-plugin-tslint/CHANGELOG.md | 8 +++++++ packages/eslint-plugin-tslint/package.json | 6 +++--- packages/eslint-plugin/CHANGELOG.md | 22 ++++++++++++++++++++ packages/eslint-plugin/package.json | 4 ++-- packages/experimental-utils/CHANGELOG.md | 8 +++++++ packages/experimental-utils/package.json | 4 ++-- packages/parser/CHANGELOG.md | 8 +++++++ packages/parser/package.json | 8 +++---- packages/shared-fixtures/CHANGELOG.md | 8 +++++++ packages/shared-fixtures/package.json | 2 +- packages/typescript-estree/CHANGELOG.md | 8 +++++++ packages/typescript-estree/package.json | 4 ++-- 16 files changed, 109 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a78733e77cb5..ef30e7d79104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + + +### Bug Fixes + +* **eslint-plugin:** [ban-types] add option extendDefaults ([#1379](https://github.com/typescript-eslint/typescript-eslint/issues/1379)) ([ae7f7c5](https://github.com/typescript-eslint/typescript-eslint/commit/ae7f7c5637124b1167efd63755df92e219bbbb24)) +* **eslint-plugin:** [default-param-last] handle param props ([#1650](https://github.com/typescript-eslint/typescript-eslint/issues/1650)) ([3534c6e](https://github.com/typescript-eslint/typescript-eslint/commit/3534c6ea09f0cb2162017660a90c6a4ad704da6b)) +* **eslint-plugin:** [no-implied-eval] correct logic for ts3.8 ([#1652](https://github.com/typescript-eslint/typescript-eslint/issues/1652)) ([33e3e6f](https://github.com/typescript-eslint/typescript-eslint/commit/33e3e6f79ea21792ccb60b7f1ada74057ceb52e9)) + + +### Features + +* **eslint-plugin:** [explicit-member-accessibility] autofix no-public ([#1548](https://github.com/typescript-eslint/typescript-eslint/issues/1548)) ([dd233b5](https://github.com/typescript-eslint/typescript-eslint/commit/dd233b52dcd5a39d842123af6fc775574abf2bc2)) +* **eslint-plugin:** [typedef] add variable-declaration-ignore-function ([#1578](https://github.com/typescript-eslint/typescript-eslint/issues/1578)) ([fc0a55e](https://github.com/typescript-eslint/typescript-eslint/commit/fc0a55e8b78203972d01a7c9b79ed6b470c5c1a0)) +* **eslint-plugin:** add new no-base-to-string rule ([#1522](https://github.com/typescript-eslint/typescript-eslint/issues/1522)) ([8333d41](https://github.com/typescript-eslint/typescript-eslint/commit/8333d41d5d472ef338fb41a29ccbfc6b16e47627)) +* **eslint-plugin:** add prefer-readonly-parameters ([#1513](https://github.com/typescript-eslint/typescript-eslint/issues/1513)) ([3be9854](https://github.com/typescript-eslint/typescript-eslint/commit/3be98542afd7553cbbec66c4be215173d7f7ffcf)) +* **eslint-plugin:** additional annotation spacing rules for va… ([#1496](https://github.com/typescript-eslint/typescript-eslint/issues/1496)) ([b097245](https://github.com/typescript-eslint/typescript-eslint/commit/b097245df35114906b1f9c60c3ad4cd698d957b8)) + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) diff --git a/lerna.json b/lerna.json index f38de2345022..cf1d022fc59c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.21.0", + "version": "2.22.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md index 9ccbe0f64764..893f85ee9678 100644 --- a/packages/eslint-plugin-internal/CHANGELOG.md +++ b/packages/eslint-plugin-internal/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 1a10da87b029..2cb24bd5db5c 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-internal", - "version": "2.21.0", + "version": "2.22.0", "private": true, "main": "dist/index.js", "scripts": { @@ -12,6 +12,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.21.0" + "@typescript-eslint/experimental-utils": "2.22.0" } } diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index 10645006953a..5aa674c15128 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index 74c97b0cd0c8..f53880af4572 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-tslint", - "version": "2.21.0", + "version": "2.22.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -31,7 +31,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.21.0", + "@typescript-eslint/experimental-utils": "2.22.0", "lodash": "^4.17.15" }, "peerDependencies": { @@ -41,6 +41,6 @@ }, "devDependencies": { "@types/lodash": "^4.14.149", - "@typescript-eslint/parser": "2.21.0" + "@typescript-eslint/parser": "2.22.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 99f1a97c32bc..09fdc41818dd 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + + +### Bug Fixes + +* **eslint-plugin:** [ban-types] add option extendDefaults ([#1379](https://github.com/typescript-eslint/typescript-eslint/issues/1379)) ([ae7f7c5](https://github.com/typescript-eslint/typescript-eslint/commit/ae7f7c5637124b1167efd63755df92e219bbbb24)) +* **eslint-plugin:** [default-param-last] handle param props ([#1650](https://github.com/typescript-eslint/typescript-eslint/issues/1650)) ([3534c6e](https://github.com/typescript-eslint/typescript-eslint/commit/3534c6ea09f0cb2162017660a90c6a4ad704da6b)) +* **eslint-plugin:** [no-implied-eval] correct logic for ts3.8 ([#1652](https://github.com/typescript-eslint/typescript-eslint/issues/1652)) ([33e3e6f](https://github.com/typescript-eslint/typescript-eslint/commit/33e3e6f79ea21792ccb60b7f1ada74057ceb52e9)) + + +### Features + +* **eslint-plugin:** [explicit-member-accessibility] autofix no-public ([#1548](https://github.com/typescript-eslint/typescript-eslint/issues/1548)) ([dd233b5](https://github.com/typescript-eslint/typescript-eslint/commit/dd233b52dcd5a39d842123af6fc775574abf2bc2)) +* **eslint-plugin:** [typedef] add variable-declaration-ignore-function ([#1578](https://github.com/typescript-eslint/typescript-eslint/issues/1578)) ([fc0a55e](https://github.com/typescript-eslint/typescript-eslint/commit/fc0a55e8b78203972d01a7c9b79ed6b470c5c1a0)) +* **eslint-plugin:** add new no-base-to-string rule ([#1522](https://github.com/typescript-eslint/typescript-eslint/issues/1522)) ([8333d41](https://github.com/typescript-eslint/typescript-eslint/commit/8333d41d5d472ef338fb41a29ccbfc6b16e47627)) +* **eslint-plugin:** add prefer-readonly-parameters ([#1513](https://github.com/typescript-eslint/typescript-eslint/issues/1513)) ([3be9854](https://github.com/typescript-eslint/typescript-eslint/commit/3be98542afd7553cbbec66c4be215173d7f7ffcf)) +* **eslint-plugin:** additional annotation spacing rules for va… ([#1496](https://github.com/typescript-eslint/typescript-eslint/issues/1496)) ([b097245](https://github.com/typescript-eslint/typescript-eslint/commit/b097245df35114906b1f9c60c3ad4cd698d957b8)) + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 595fab7aa6cb..acee148c5f99 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "2.21.0", + "version": "2.22.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -41,7 +41,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.21.0", + "@typescript-eslint/experimental-utils": "2.22.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 6ed1898fe0ba..fc8b6a8f1d67 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + +**Note:** Version bump only for package @typescript-eslint/experimental-utils + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) **Note:** Version bump only for package @typescript-eslint/experimental-utils diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 927154687061..72b9c0436b3c 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "2.21.0", + "version": "2.22.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -37,7 +37,7 @@ }, "dependencies": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.21.0", + "@typescript-eslint/typescript-estree": "2.22.0", "eslint-scope": "^5.0.0" }, "peerDependencies": { diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index 039e2d78eda6..963551cf59c0 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index 13918ec8784d..6377cef8e36c 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "2.21.0", + "version": "2.22.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -43,13 +43,13 @@ }, "dependencies": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.21.0", - "@typescript-eslint/typescript-estree": "2.21.0", + "@typescript-eslint/experimental-utils": "2.22.0", + "@typescript-eslint/typescript-estree": "2.22.0", "eslint-visitor-keys": "^1.1.0" }, "devDependencies": { "@types/glob": "^7.1.1", - "@typescript-eslint/shared-fixtures": "2.21.0", + "@typescript-eslint/shared-fixtures": "2.22.0", "glob": "*" }, "peerDependenciesMeta": { diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index 5eef3db60f0f..33f42d16e034 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) **Note:** Version bump only for package @typescript-eslint/shared-fixtures diff --git a/packages/shared-fixtures/package.json b/packages/shared-fixtures/package.json index 8b9f8ebf381d..69a34e083b3e 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "2.21.0", + "version": "2.22.0", "private": true, "scripts": { "build": "tsc -b tsconfig.build.json", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index 681a0d9a72a3..ffd7ae3936bb 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.22.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.21.0...v2.22.0) (2020-03-02) + +**Note:** Version bump only for package @typescript-eslint/typescript-estree + + + + + # [2.21.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.20.0...v2.21.0) (2020-02-24) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index a9697fc59cad..e58be5a1d73c 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "2.21.0", + "version": "2.22.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -58,7 +58,7 @@ "@types/lodash": "^4.14.149", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", - "@typescript-eslint/shared-fixtures": "2.21.0", + "@typescript-eslint/shared-fixtures": "2.22.0", "tmp": "^0.1.0", "typescript": "*" },