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..94745d2c71c9 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-unary-minus.md @@ -0,0 +1,50 @@ +--- +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 +declare const a: string; +-a; + +declare const b: {}; +-b; +``` + +### ✅ Correct + +```ts +-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; +``` diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index f7a3cc3dbcb0..7717b386cc9f 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -125,6 +125,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 9d87b8cac412..5423a7b82075 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -87,6 +87,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'; @@ -224,6 +225,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..05d489d47fab --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts @@ -0,0 +1,58 @@ +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import * as util from '../util'; + +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 = util.getConstrainedTypeAtLocation( + services, + node.argument, + ); + const checker = services.program.getTypeChecker(); + if ( + tsutils + .unionTypeParts(argType) + .some( + type => + !tsutils.isTypeFlagSet( + type, + ts.TypeFlags.Any | + ts.TypeFlags.Never | + ts.TypeFlags.BigIntLike | + ts.TypeFlags.NumberLike, + ), + ) + ) { + 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..d3ea7b067fbc --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-unary-minus.test.ts @@ -0,0 +1,47 @@ +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;', + '-42;', + '-42n;', + '(a: number) => -a;', + '(a: bigint) => -a;', + '(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' }] }, + ], +}); 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 = [];" +`; 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