From 3dd1b02ead7a92b479bd83f0f6e872dde34f05b5 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 21 Dec 2019 14:50:41 +1030 Subject: [PATCH] feat(eslint-plugin): more optional chain support in rules (#1363) --- .../src/rules/require-array-sort-compare.ts | 4 +- .../eslint-plugin/src/rules/unbound-method.ts | 6 +- .../rules/require-array-sort-compare.test.ts | 25 +++++++++ .../tests/rules/unbound-method.test.ts | 56 ++++++++++++++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts index e88f2791f8f..1d220702e04 100644 --- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts +++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts @@ -25,8 +25,8 @@ export default util.createRule({ const checker = service.program.getTypeChecker(); return { - "CallExpression[arguments.length=0] > MemberExpression[property.name='sort'][computed=false]"( - node: TSESTree.MemberExpression, + ":matches(CallExpression, OptionalCallExpression)[arguments.length=0] > :matches(MemberExpression, OptionalMemberExpression)[property.name='sort'][computed=false]"( + node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression, ): void { // Get the symbol of the `sort` method. const tsNode = service.esTreeNodeToTSNodeMap.get(node); diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 46bbe779c66..16ee4787760 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -55,7 +55,9 @@ export default util.createRule({ const checker = parserServices.program.getTypeChecker(); return { - MemberExpression(node): void { + 'MemberExpression, OptionalMemberExpression'( + node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression, + ): void { if (isSafeUse(node)) { return; } @@ -103,12 +105,14 @@ function isSafeUse(node: TSESTree.Node): boolean { case AST_NODE_TYPES.IfStatement: case AST_NODE_TYPES.ForStatement: case AST_NODE_TYPES.MemberExpression: + case AST_NODE_TYPES.OptionalMemberExpression: case AST_NODE_TYPES.SwitchStatement: case AST_NODE_TYPES.UpdateExpression: case AST_NODE_TYPES.WhileStatement: return true; case AST_NODE_TYPES.CallExpression: + case AST_NODE_TYPES.OptionalCallExpression: return parent.callee === node; case AST_NODE_TYPES.ConditionalExpression: diff --git a/packages/eslint-plugin/tests/rules/require-array-sort-compare.test.ts b/packages/eslint-plugin/tests/rules/require-array-sort-compare.test.ts index c9b005189fc..1dae0d37f3d 100644 --- a/packages/eslint-plugin/tests/rules/require-array-sort-compare.test.ts +++ b/packages/eslint-plugin/tests/rules/require-array-sort-compare.test.ts @@ -72,6 +72,22 @@ ruleTester.run('require-array-sort-compare', rule, { } } `, + // optional chain + ` + function f(a: any[]) { + a?.sort((a, b) => a - b) + } + `, + ` + namespace UserDefined { + interface Array { + sort(): void + } + function f(a: Array) { + a?.sort() + } + } + `, ], invalid: [ { @@ -123,5 +139,14 @@ ruleTester.run('require-array-sort-compare', rule, { `, errors: [{ messageId: 'requireCompare' }], }, + // optional chain + { + code: ` + function f(a: string[]) { + a?.sort() + } + `, + errors: [{ messageId: 'requireCompare' }], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 520b28fc1d1..634977b477f 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -116,7 +116,8 @@ typeof instance.unbound === 'function'; typeof ContainsMethods.boundStatic === 'function'; typeof ContainsMethods.unboundStatic === 'function'; `, - `interface RecordA { + ` +interface RecordA { readonly type: "A" readonly a: {} } @@ -143,6 +144,29 @@ class CommunicationError { class CommunicationError {} const x = CommunicationError.prototype; `, + // optional chain + ` +class ContainsMethods { + bound?: () => void; + unbound?(): void; + + static boundStatic?: () => void; + static unboundStatic?(): void; +} + +function foo(instance: ContainsMethods | null) { + instance?.bound(); + instance?.unbound(); + + instance?.bound++; + + if (instance?.bound) { } + if (instance?.unbound) { } + + typeof instance?.bound === 'function'; + typeof instance?.unbound === 'function'; +} + `, ], invalid: [ { @@ -154,6 +178,36 @@ class ContainsMethods { static unboundStatic?(): void; } +function foo(instance: ContainsMethods | null) { + const unbound = instance?.unbound; + instance.unbound += 1; + instance?.unbound as any; +} + `, + errors: [ + { + line: 10, + messageId: 'unbound', + }, + { + line: 11, + messageId: 'unbound', + }, + { + line: 12, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class ContainsMethods { + bound?: () => void; + unbound?(): void; + static boundStatic?: () => void; + static unboundStatic?(): void; +} + const instance = new ContainsMethods(); {