From d114b9aa0b1ac9cfaaec540530e96c94eb39ec56 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Mon, 31 Jul 2023 14:04:45 -0400 Subject: [PATCH 1/6] feat: add `no-unsafe-unary-minus` rule --- .../docs/rules/no-unsafe-unary-minus.md | 37 +++++++++++++ packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-unsafe-unary-minus.ts | 55 +++++++++++++++++++ .../tests/rules/no-unsafe-unary-minus.test.ts | 30 ++++++++++ .../no-unsafe-unary-minus.shot | 14 +++++ 6 files changed, 139 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md create mode 100644 packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-unsafe-unary-minus.shot diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md b/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md new file mode 100644 index 000000000000..1288ae60af50 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md @@ -0,0 +1,37 @@ +--- +description: 'Require unary negation to take a number.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-unsafe-unary-minus** for documentation. + +TypeScript does not prevent you from putting a minus sign before things other than numbers: + +```ts +const s = 'hello'; +const x = -s; // x is NaN +``` + +This rule restricts the unary `-` operator to `number | bigint`. + +## Examples + +### ❌ Incorrect + +```ts +const f = (a: string) => -a; +const g = (a: {}) => -a; +``` + +### ✅ Correct + +```ts +const a = -42; +const b = -42n; +const f1 = (a: number) => -a; +const f2 = (a: bigint) => -a; +const f3 = (a: number | bigint) => -a; +const f4 = (a: any) => -a; +const f5 = (a: 1 | 2) => -a; +``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index d0bd265b0996..a5661a0ba8bf 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -123,6 +123,7 @@ export = { '@typescript-eslint/no-unsafe-enum-comparison': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unsafe-unary-minus': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 44aedd6198e1..a143c0554b9f 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -86,6 +86,7 @@ import noUnsafeDeclarationMerging from './no-unsafe-declaration-merging'; import noUnsafeEnumComparison from './no-unsafe-enum-comparison'; import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; +import noUnsafeUnaryMinus from './no-unsafe-unary-minus'; import noUnusedExpressions from './no-unused-expressions'; import noUnusedVars from './no-unused-vars'; import noUseBeforeDefine from './no-use-before-define'; @@ -221,6 +222,7 @@ export default { 'no-unsafe-enum-comparison': noUnsafeEnumComparison, 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, + 'no-unsafe-unary-minus': noUnsafeUnaryMinus, 'no-unused-expressions': noUnusedExpressions, 'no-unused-vars': noUnusedVars, 'no-use-before-define': noUseBeforeDefine, diff --git a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts new file mode 100644 index 000000000000..e9bdf0587524 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts @@ -0,0 +1,55 @@ +import type * as ts from 'typescript'; + +import * as util from '../util'; + +interface TypeChecker extends ts.TypeChecker { + // https://github.com/microsoft/TypeScript/issues/9879 + isTypeAssignableTo(source: ts.Type, target: ts.Type): boolean; + getUnionType(types: ts.Type[]): ts.Type; +} + +type Options = []; +type MessageIds = 'unaryMinus'; + +export default util.createRule({ + name: 'no-unsafe-unary-minus', + meta: { + type: 'problem', + docs: { + description: 'Require unary negation to take a number', + requiresTypeChecking: true, + }, + messages: { + unaryMinus: 'Invalid type "{{type}}" of template literal expression.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + UnaryExpression(node): void { + if (node.operator !== '-') { + return; + } + const services = util.getParserServices(context); + const argType = services.getTypeAtLocation(node.argument); + const checker = services.program.getTypeChecker() as TypeChecker; + if ( + !checker.isTypeAssignableTo( + argType, + checker.getUnionType([ + checker.getNumberType(), // first exposed in TypeScript v5.1 + checker.getBigIntType(), // first added in TypeScript v5.1 + ]), + ) + ) { + context.report({ + messageId: 'unaryMinus', + node, + data: { type: checker.typeToString(argType) }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts new file mode 100644 index 000000000000..914388702ed4 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts @@ -0,0 +1,30 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-unsafe-unary-minus'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-unsafe-unary-minus', rule, { + valid: [ + '-42;', + '-42n;', + '(a: number) => -a;', + '(a: bigint) => -a;', + '(a: number | bigint) => -a;', + '(a: any) => -a;', + '(a: 1 | 2) => -a;', + ], + invalid: [ + { code: '(a: string) => -a;', errors: [{ messageId: 'unaryMinus' }] }, + { code: '(a: {}) => -a;', errors: [{ messageId: 'unaryMinus' }] }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-unary-minus.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-unary-minus.shot new file mode 100644 index 000000000000..e1e805786185 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-unary-minus.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-unsafe-unary-minus 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; From ff17b9ecab0ea4a3fec6901e72df3163626fd890 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Mon, 31 Jul 2023 14:57:49 -0400 Subject: [PATCH 2/6] Cover the early return case --- packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts index 914388702ed4..92556606fe26 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts @@ -15,6 +15,7 @@ const ruleTester = new RuleTester({ ruleTester.run('no-unsafe-unary-minus', rule, { valid: [ + '+42;', '-42;', '-42n;', '(a: number) => -a;', From 3ff8dae899f6cf9b650babfd9d704930753d8106 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sat, 28 Oct 2023 12:54:44 -0400 Subject: [PATCH 3/6] Write more tests --- .../tests/rules/no-unsafe-unary-minus.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts index 92556606fe26..0ab8a4b9bd75 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts @@ -23,9 +23,25 @@ ruleTester.run('no-unsafe-unary-minus', rule, { '(a: number | bigint) => -a;', '(a: any) => -a;', '(a: 1 | 2) => -a;', + '(a: string) => +a;', + '(a: number[]) => -a[0];', + '(t: T & number) => -t;', + '(a: { x: number }) => -a.x;', + '(a: never) => -a;', + '(t: T) => -t;', ], invalid: [ { code: '(a: string) => -a;', errors: [{ messageId: 'unaryMinus' }] }, { code: '(a: {}) => -a;', errors: [{ messageId: 'unaryMinus' }] }, + { code: '(a: number[]) => -a;', errors: [{ messageId: 'unaryMinus' }] }, + { code: "-'hello';", errors: [{ messageId: 'unaryMinus' }] }, + { code: '-`hello`;', errors: [{ messageId: 'unaryMinus' }] }, + { + code: '(a: { x: number }) => -a;', + errors: [{ messageId: 'unaryMinus' }], + }, + { code: '(a: unknown) => -a;', errors: [{ messageId: 'unaryMinus' }] }, + { code: '(a: void) => -a;', errors: [{ messageId: 'unaryMinus' }] }, + { code: '(t: T) => -t;', errors: [{ messageId: 'unaryMinus' }] }, ], }); From 1d431f85ad17ffadfa408749d19c68f486cba215 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sun, 29 Oct 2023 12:59:06 -0400 Subject: [PATCH 4/6] Rewrite to use only public TypeScript API --- .../src/rules/no-unsafe-unary-minus.ts | 27 +++++++++---------- .../tests/rules/no-unsafe-unary-minus.test.ts | 4 +-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts index e9bdf0587524..f78deea008f6 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts @@ -1,13 +1,8 @@ -import type * as ts from 'typescript'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; import * as util from '../util'; -interface TypeChecker extends ts.TypeChecker { - // https://github.com/microsoft/TypeScript/issues/9879 - isTypeAssignableTo(source: ts.Type, target: ts.Type): boolean; - getUnionType(types: ts.Type[]): ts.Type; -} - type Options = []; type MessageIds = 'unaryMinus'; @@ -33,15 +28,17 @@ export default util.createRule({ } const services = util.getParserServices(context); const argType = services.getTypeAtLocation(node.argument); - const checker = services.program.getTypeChecker() as TypeChecker; + const checker = services.program.getTypeChecker(); if ( - !checker.isTypeAssignableTo( - argType, - checker.getUnionType([ - checker.getNumberType(), // first exposed in TypeScript v5.1 - checker.getBigIntType(), // first added in TypeScript v5.1 - ]), - ) + tsutils + .unionTypeParts(argType) + .some( + type => + !tsutils.isTypeFlagSet( + type, + ts.TypeFlags.BigIntLike | ts.TypeFlags.NumberLike, + ), + ) ) { context.report({ messageId: 'unaryMinus', diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts index 0ab8a4b9bd75..d3ea7b067fbc 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts @@ -25,7 +25,7 @@ ruleTester.run('no-unsafe-unary-minus', rule, { '(a: 1 | 2) => -a;', '(a: string) => +a;', '(a: number[]) => -a[0];', - '(t: T & number) => -t;', + '(t: T & number) => -t;', '(a: { x: number }) => -a.x;', '(a: never) => -a;', '(t: T) => -t;', @@ -42,6 +42,6 @@ ruleTester.run('no-unsafe-unary-minus', rule, { }, { code: '(a: unknown) => -a;', errors: [{ messageId: 'unaryMinus' }] }, { code: '(a: void) => -a;', errors: [{ messageId: 'unaryMinus' }] }, - { code: '(t: T) => -t;', errors: [{ messageId: 'unaryMinus' }] }, + { code: '(t: T) => -t;', errors: [{ messageId: 'unaryMinus' }] }, ], }); From bc0430f5824b63bb518d4591c751cc4de2c4ea4a Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sun, 29 Oct 2023 13:53:08 -0400 Subject: [PATCH 5/6] Handle `any`, `never`, and generics --- .../eslint-plugin/src/rules/no-unsafe-unary-minus.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts index f78deea008f6..05d489d47fab 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts @@ -27,7 +27,10 @@ export default util.createRule({ return; } const services = util.getParserServices(context); - const argType = services.getTypeAtLocation(node.argument); + const argType = util.getConstrainedTypeAtLocation( + services, + node.argument, + ); const checker = services.program.getTypeChecker(); if ( tsutils @@ -36,7 +39,10 @@ export default util.createRule({ type => !tsutils.isTypeFlagSet( type, - ts.TypeFlags.BigIntLike | ts.TypeFlags.NumberLike, + ts.TypeFlags.Any | + ts.TypeFlags.Never | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.NumberLike, ), ) ) { From 86b634a5fba26676e9f2d3b04c07f18385dc3aa2 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sun, 29 Oct 2023 15:07:28 -0400 Subject: [PATCH 6/6] Replace functions with `declare` in docs --- .../docs/rules/no-unsafe-unary-minus.md | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md b/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md index 1288ae60af50..94745d2c71c9 100644 --- a/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md +++ b/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md @@ -20,18 +20,31 @@ This rule restricts the unary `-` operator to `number | bigint`. ### ❌ Incorrect ```ts -const f = (a: string) => -a; -const g = (a: {}) => -a; +declare const a: string; +-a; + +declare const b: {}; +-b; ``` ### ✅ Correct ```ts -const a = -42; -const b = -42n; -const f1 = (a: number) => -a; -const f2 = (a: bigint) => -a; -const f3 = (a: number | bigint) => -a; -const f4 = (a: any) => -a; -const f5 = (a: 1 | 2) => -a; +-42; +-42n; + +declare const a: number; +-a; + +declare const b: number; +-b; + +declare const c: number | bigint; +-c; + +declare const d: any; +-d; + +declare const e: 1 | 2; +-e; ``` pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy