From def09f88cdb4a85cebb8619b45931f7e2c88dfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 15 Jun 2023 12:17:51 -0400 Subject: [PATCH] feat(eslint-plugin): [restrict-plus-operands] add allow* options (#6161) * feat(eslint-plugin): [restrict-plus-operands] add allow* options from restrict-template-expressions * Warn explicitly * Update packages/eslint-plugin/docs/rules/restrict-plus-operands.md Co-authored-by: Brad Zacher * Brad suggestion * Fix lint-markdown --------- Co-authored-by: Brad Zacher --- .../docs/rules/restrict-plus-operands.md | 160 +++- .../src/rules/restrict-plus-operands.ts | 303 ++++--- .../rules/restrict-template-expressions.ts | 16 +- .../rules/restrict-plus-operands.test.ts | 836 ++++++++++++++---- 4 files changed, 975 insertions(+), 340 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/restrict-plus-operands.md b/packages/eslint-plugin/docs/rules/restrict-plus-operands.md index fc823f6da9f..7abf7dc38c3 100644 --- a/packages/eslint-plugin/docs/rules/restrict-plus-operands.md +++ b/packages/eslint-plugin/docs/rules/restrict-plus-operands.md @@ -18,70 +18,180 @@ This rule reports when a `+` operation combines two values of different types, o ### ❌ Incorrect ```ts -var foo = '5.5' + 5; -var foo = 1n + 1; +let foo = '5.5' + 5; +let foo = 1n + 1; ``` ### ✅ Correct ```ts -var foo = parseInt('5.5', 10) + 10; -var foo = 1n + 1n; +let foo = parseInt('5.5', 10) + 10; +let foo = 1n + 1n; ``` ## Options -### `checkCompoundAssignments` +:::caution +We generally recommend against using these options, as they limit which varieties of incorrect `+` usage can be checked. +This in turn severely limits the validation that the rule can do to ensure that resulting strings and numbers are correct. + +Safer alternatives to using the `allow*` options include: + +- Using variadic forms of logging APIs to avoid needing to `+` values. + ```ts + // Remove this line + console.log('The result is ' + true); + // Add this line + console.log('The result is', true); + ``` +- Using `.toFixed()` to coerce numbers to well-formed string representations: + ```ts + const number = 1.123456789; + const result = 'The number is ' + number.toFixed(2); + // result === 'The number is 1.12' + ``` +- Calling `.toString()` on other types to mark explicit and intentional string coercion: + ```ts + const arg = '11'; + const regex = /[0-9]/; + const result = + 'The result of ' + + regex.toString() + + '.test("' + + arg + + '") is ' + + regex.test(arg).toString(); + // result === 'The result of /[0-9]/.test("11") is true' + ``` + +::: -Examples of code for this rule with `{ checkCompoundAssignments: true }`: +### `allowAny` + +Examples of code for this rule with `{ allowAny: true }`: #### ❌ Incorrect ```ts -/*eslint @typescript-eslint/restrict-plus-operands: ["error", { "checkCompoundAssignments": true }]*/ +let fn = (a: number, b: []) => a + b; +let fn = (a: string, b: []) => a + b; +``` -let foo: string | undefined; -foo += 'some data'; +#### ✅ Correct -let bar: string = ''; -bar += 0; +```ts +let fn = (a: number, b: any) => a + b; +let fn = (a: string, b: any) => a + b; +``` + +### `allowBoolean` + +Examples of code for this rule with `{ allowBoolean: true }`: + + + +#### ❌ Incorrect + +```ts +let fn = (a: number, b: unknown) => a + b; +let fn = (a: string, b: unknown) => a + b; ``` #### ✅ Correct ```ts -/*eslint @typescript-eslint/restrict-plus-operands: ["error", { "checkCompoundAssignments": true }]*/ +let fn = (a: number, b: boolean) => a + b; +let fn = (a: string, b: boolean) => a + b; +``` -let foo: number = 0; -foo += 1; +### `allowNullish` -let bar = ''; -bar += 'test'; +Examples of code for this rule with `{ allowNullish: true }`: + + + +#### ❌ Incorrect + +```ts +let fn = (a: number, b: unknown) => a + b; +let fn = (a: number, b: never) => a + b; +let fn = (a: string, b: unknown) => a + b; +let fn = (a: string, b: never) => a + b; ``` -### `allowAny` +#### ✅ Correct -Examples of code for this rule with `{ allowAny: true }`: +```ts +let fn = (a: number, b: undefined) => a + b; +let fn = (a: number, b: null) => a + b; +let fn = (a: string, b: undefined) => a + b; +let fn = (a: string, b: null) => a + b; +``` + +### `allowNumberAndString` + +Examples of code for this rule with `{ allowNumberAndString: true }`: #### ❌ Incorrect ```ts -var fn = (a: any, b: boolean) => a + b; -var fn = (a: any, b: []) => a + b; -var fn = (a: any, b: {}) => a + b; +let fn = (a: number, b: unknown) => a + b; +let fn = (a: number, b: never) => a + b; ``` #### ✅ Correct ```ts -var fn = (a: any, b: any) => a + b; -var fn = (a: any, b: string) => a + b; -var fn = (a: any, b: bigint) => a + b; -var fn = (a: any, b: number) => a + b; +let fn = (a: number, b: string) => a + b; +let fn = (a: number, b: number | string) => a + b; +``` + +### `allowRegExp` + +Examples of code for this rule with `{ allowRegExp: true }`: + + + +#### ❌ Incorrect + +```ts +let fn = (a: number, b: RegExp) => a + b; +``` + +#### ✅ Correct + +```ts +let fn = (a: string, b: RegExp) => a + b; +``` + +### `checkCompoundAssignments` + +Examples of code for this rule with `{ checkCompoundAssignments: true }`: + + + +#### ❌ Incorrect + +```ts +let foo: string | undefined; +foo += 'some data'; + +let bar: string = ''; +bar += 0; +``` + +#### ✅ Correct + +```ts +let foo: number = 0; +foo += 1; + +let bar = ''; +bar += 'test'; ``` ## When Not To Use It diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index ad5d0a832b7..2170bf44cc7 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -1,20 +1,21 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import * as tsutils from 'tsutils'; import * as ts from 'typescript'; import * as util from '../util'; type Options = [ { - checkCompoundAssignments?: boolean; allowAny?: boolean; + allowBoolean?: boolean; + allowNullish?: boolean; + allowNumberAndString?: boolean; + allowRegExp?: boolean; + checkCompoundAssignments?: boolean; }, ]; -type MessageIds = - | 'notNumbers' - | 'notStrings' - | 'notBigInts' - | 'notValidAnys' - | 'notValidTypes'; + +type MessageIds = 'bigintAndNumber' | 'invalid' | 'mismatched'; export default util.createRule({ name: 'restrict-plus-operands', @@ -27,29 +28,44 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - notNumbers: - "Operands of '+' operation must either be both strings or both numbers.", - notStrings: - "Operands of '+' operation must either be both strings or both numbers. Consider using a template literal.", - notBigInts: "Operands of '+' operation must be both bigints.", - notValidAnys: - "Operands of '+' operation with any is possible only with string, number, bigint or any", - notValidTypes: - "Operands of '+' operation must either be one of string, number, bigint or any (if allowed by option)", + bigintAndNumber: + "Numeric '+' operations must either be both bigints or both numbers. Got `{{left}}` + `{{right}}`.", + invalid: + "Invalid operand for a '+' operation. Operands must each be a number or {{stringLike}}. Got `{{type}}`.", + mismatched: + "Operands of '+' operations must be a number or {{stringLike}}. Got `{{left}}` + `{{right}}`.", }, schema: [ { type: 'object', additionalProperties: false, properties: { - checkCompoundAssignments: { - description: 'Whether to check compound assignments such as `+=`.', - type: 'boolean', - }, allowAny: { description: 'Whether to allow `any` typed values.', type: 'boolean', }, + allowBoolean: { + description: 'Whether to allow `boolean` typed values.', + type: 'boolean', + }, + allowNullish: { + description: + 'Whether to allow potentially `null` or `undefined` typed values.', + type: 'boolean', + }, + allowNumberAndString: { + description: + 'Whether to allow `bigint`/`number` typed values and `string` typed values to be added together.', + type: 'boolean', + }, + allowRegExp: { + description: 'Whether to allow `regexp` typed values.', + type: 'boolean', + }, + checkCompoundAssignments: { + description: 'Whether to check compound assignments such as `+=`.', + type: 'boolean', + }, }, }, ], @@ -57,136 +73,159 @@ export default util.createRule({ defaultOptions: [ { checkCompoundAssignments: false, - allowAny: false, }, ], - create(context, [{ checkCompoundAssignments, allowAny }]) { + create( + context, + [ + { + checkCompoundAssignments, + allowAny, + allowBoolean, + allowNullish, + allowNumberAndString, + allowRegExp, + }, + ], + ) { const service = util.getParserServices(context); const typeChecker = service.program.getTypeChecker(); - type BaseLiteral = 'string' | 'number' | 'bigint' | 'invalid' | 'any'; - - /** - * Helper function to get base type of node - */ - function getBaseTypeOfLiteralType(type: ts.Type): BaseLiteral { - if (type.isNumberLiteral()) { - return 'number'; - } - if ( - type.isStringLiteral() || - util.isTypeFlagSet(type, ts.TypeFlags.TemplateLiteral) - ) { - return 'string'; - } - // is BigIntLiteral - if (type.flags & ts.TypeFlags.BigIntLiteral) { - return 'bigint'; - } - if (type.isUnion()) { - const types = type.types.map(getBaseTypeOfLiteralType); - - return types.every(value => value === types[0]) ? types[0] : 'invalid'; - } - - if (type.isIntersection()) { - const types = type.types.map(getBaseTypeOfLiteralType); - - if (types.some(value => value === 'string')) { - return 'string'; - } - - if (types.some(value => value === 'number')) { - return 'number'; - } - - if (types.some(value => value === 'bigint')) { - return 'bigint'; - } - - return 'invalid'; - } + const stringLikes = [ + allowAny && '`any`', + allowBoolean && '`boolean`', + allowNullish && '`null`', + allowRegExp && '`RegExp`', + allowNullish && '`undefined`', + ].filter((value): value is string => typeof value === 'string'); + const stringLike = stringLikes.length + ? stringLikes.length === 1 + ? `string, allowing a string + ${stringLikes[0]}` + : `string, allowing a string + any of: ${stringLikes.join(', ')}` + : 'string'; + + function getTypeConstrained(node: TSESTree.Node): ts.Type { + return typeChecker.getBaseTypeOfLiteralType( + util.getConstrainedTypeAtLocation( + typeChecker, + service.esTreeNodeToTSNodeMap.get(node), + ), + ); + } - const stringType = typeChecker.typeToString(type); + function checkPlusOperands( + node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression, + ): void { + const leftType = getTypeConstrained(node.left); + const rightType = getTypeConstrained(node.right); if ( - stringType === 'number' || - stringType === 'string' || - stringType === 'bigint' || - stringType === 'any' + leftType === rightType && + tsutils.isTypeFlagSet( + leftType, + ts.TypeFlags.BigIntLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.StringLike, + ) ) { - return stringType; + return; } - return 'invalid'; - } - - /** - * Helper function to get base type of node - * @param node the node to be evaluated. - */ - function getNodeType( - node: TSESTree.Expression | TSESTree.PrivateIdentifier, - ): BaseLiteral { - const tsNode = service.esTreeNodeToTSNodeMap.get(node); - const type = util.getConstrainedTypeAtLocation(typeChecker, tsNode); - - return getBaseTypeOfLiteralType(type); - } - function checkPlusOperands( - node: TSESTree.BinaryExpression | TSESTree.AssignmentExpression, - ): void { - const leftType = getNodeType(node.left); - const rightType = getNodeType(node.right); - - if (leftType === rightType) { - if (leftType === 'invalid') { + let hadIndividualComplaint = false; + + for (const [baseNode, baseType, otherType] of [ + [node.left, leftType, rightType], + [node.right, rightType, leftType], + ] as const) { + if ( + isTypeFlagSetInUnion( + baseType, + ts.TypeFlags.ESSymbolLike | + ts.TypeFlags.Never | + ts.TypeFlags.Unknown, + ) || + (!allowAny && isTypeFlagSetInUnion(baseType, ts.TypeFlags.Any)) || + (!allowBoolean && + isTypeFlagSetInUnion(baseType, ts.TypeFlags.BooleanLike)) || + (!allowNullish && + util.isTypeFlagSet( + baseType, + ts.TypeFlags.Null | ts.TypeFlags.Undefined, + )) + ) { context.report({ - node, - messageId: 'notValidTypes', + data: { + stringLike, + type: typeChecker.typeToString(baseType), + }, + messageId: 'invalid', + node: baseNode, }); + hadIndividualComplaint = true; + continue; } - if (!allowAny && leftType === 'any') { - context.report({ - node, - messageId: 'notValidAnys', - }); + // RegExps also contain ts.TypeFlags.Any & ts.TypeFlags.Object + for (const subBaseType of tsutils.unionTypeParts(baseType)) { + const typeName = util.getTypeName(typeChecker, subBaseType); + if ( + typeName === 'RegExp' + ? !allowRegExp || + tsutils.isTypeFlagSet(otherType, ts.TypeFlags.NumberLike) + : (!allowAny && util.isTypeAnyType(subBaseType)) || + isDeeplyObjectType(subBaseType) + ) { + context.report({ + data: { + stringLike, + type: typeChecker.typeToString(subBaseType), + }, + messageId: 'invalid', + node: baseNode, + }); + hadIndividualComplaint = true; + continue; + } } + } + if (hadIndividualComplaint) { return; } - if (leftType === 'any' || rightType === 'any') { - if (!allowAny || leftType === 'invalid' || rightType === 'invalid') { - context.report({ + for (const [baseType, otherType] of [ + [leftType, rightType], + [rightType, leftType], + ] as const) { + if ( + !allowNumberAndString && + isTypeFlagSetInUnion(baseType, ts.TypeFlags.StringLike) && + isTypeFlagSetInUnion(otherType, ts.TypeFlags.NumberLike) + ) { + return context.report({ + data: { + stringLike, + left: typeChecker.typeToString(leftType), + right: typeChecker.typeToString(rightType), + }, + messageId: 'mismatched', node, - messageId: 'notValidAnys', }); } - return; - } - - if (leftType === 'string' || rightType === 'string') { - return context.report({ - node, - messageId: 'notStrings', - }); - } - - if (leftType === 'bigint' || rightType === 'bigint') { - return context.report({ - node, - messageId: 'notBigInts', - }); - } - - if (leftType === 'number' || rightType === 'number') { - return context.report({ - node, - messageId: 'notNumbers', - }); + if ( + isTypeFlagSetInUnion(baseType, ts.TypeFlags.NumberLike) && + isTypeFlagSetInUnion(otherType, ts.TypeFlags.BigIntLike) + ) { + return context.report({ + data: { + left: typeChecker.typeToString(leftType), + right: typeChecker.typeToString(rightType), + }, + messageId: 'bigintAndNumber', + node, + }); + } } } @@ -200,3 +239,15 @@ export default util.createRule({ }; }, }); + +function isDeeplyObjectType(type: ts.Type): boolean { + return type.isIntersection() + ? tsutils.intersectionTypeParts(type).every(tsutils.isObjectType) + : tsutils.unionTypeParts(type).every(tsutils.isObjectType); +} + +function isTypeFlagSetInUnion(type: ts.Type, flag: ts.TypeFlags): boolean { + return tsutils + .unionTypeParts(type) + .some(subType => tsutils.isTypeFlagSet(subType, flag)); +} diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 485ba42378b..21e638e2f85 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -6,10 +6,10 @@ import * as util from '../util'; type Options = [ { - allowNumber?: boolean; - allowBoolean?: boolean; allowAny?: boolean; + allowBoolean?: boolean; allowNullish?: boolean; + allowNumber?: boolean; allowRegExp?: boolean; allowNever?: boolean; }, @@ -34,9 +34,9 @@ export default util.createRule({ { type: 'object', properties: { - allowNumber: { + allowAny: { description: - 'Whether to allow `number` typed values in template expressions.', + 'Whether to allow `any` typed values in template expressions.', type: 'boolean', }, allowBoolean: { @@ -44,14 +44,14 @@ export default util.createRule({ 'Whether to allow `boolean` typed values in template expressions.', type: 'boolean', }, - allowAny: { + allowNullish: { description: - 'Whether to allow `any` typed values in template expressions.', + 'Whether to allow `nullish` typed values in template expressions.', type: 'boolean', }, - allowNullish: { + allowNumber: { description: - 'Whether to allow `nullish` typed values in template expressions.', + 'Whether to allow `number` typed values in template expressions.', type: 'boolean', }, allowRegExp: { diff --git a/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts b/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts index 59215883758..4d9aa64deaa 100644 --- a/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts +++ b/packages/eslint-plugin/tests/rules/restrict-plus-operands.test.ts @@ -13,53 +13,53 @@ const ruleTester = new RuleTester({ ruleTester.run('restrict-plus-operands', rule, { valid: [ - 'var x = 5;', - "var y = '10';", - 'var z = 8.2;', - "var w = '6.5';", - 'var foo = 5 + 10;', - "var foo = '5.5' + '10';", - "var foo = parseInt('5.5', 10) + 10;", - "var foo = parseFloat('5.5', 10) + 10;", - 'var foo = 1n + 1n;', - 'var foo = BigInt(1) + 1n;', + 'let x = 5;', + "let y = '10';", + 'let z = 8.2;', + "let w = '6.5';", + 'let foo = 5 + 10;', + "let foo = '5.5' + '10';", + "let foo = parseInt('5.5', 10) + 10;", + "let foo = parseFloat('5.5', 10) + 10;", + 'let foo = 1n + 1n;', + 'let foo = BigInt(1) + 1n;', ` - var foo = 1n; + let foo = 1n; foo + 2n; `, ` function test(s: string, n: number): number { return 2; } -var foo = test('5.5', 10) + 10; +let foo = test('5.5', 10) + 10; `, ` -var x = 5; -var z = 8.2; -var foo = x + z; +let x = 5; +let z = 8.2; +let foo = x + z; `, ` -var w = '6.5'; -var y = '10'; -var foo = y + w; +let w = '6.5'; +let y = '10'; +let foo = y + w; `, - 'var foo = 1 + 1;', - "var foo = '1' + '1';", + 'let foo = 1 + 1;', + "let foo = '1' + '1';", ` -var pair: { first: number; second: string } = { first: 5, second: '10' }; -var foo = pair.first + 10; +let pair: { first: number; second: string } = { first: 5, second: '10' }; +let foo = pair.first + 10; `, ` -var pair: { first: number; second: string } = { first: 5, second: '10' }; -var foo = pair.first + (10 as number); +let pair: { first: number; second: string } = { first: 5, second: '10' }; +let foo = pair.first + (10 as number); `, ` -var pair: { first: number; second: string } = { first: 5, second: '10' }; -var foo = '5.5' + pair.second; +let pair: { first: number; second: string } = { first: 5, second: '10' }; +let foo = '5.5' + pair.second; `, ` -var pair: { first: number; second: string } = { first: 5, second: '10' }; -var foo = ('5.5' as string) + pair.second; +let pair: { first: number; second: string } = { first: 5, second: '10' }; +let foo = ('5.5' as string) + pair.second; `, ` const foo = @@ -157,6 +157,84 @@ function A(s: string) { } const b = A('') + '!'; `, + ` +declare const a: \`template\${string}\`; +declare const b: ''; +const x = a + b; + `, + ` +const a: \`template\${0}\`; +declare const b: ''; +const x = a + b; + `, + { + code: ` + declare const a: RegExp; + declare const b: string; + const x = a + b; + `, + options: [ + { + allowRegExp: true, + }, + ], + }, + { + code: ` + const a = /regexp/; + declare const b: string; + const x = a + b; + `, + options: [ + { + allowRegExp: true, + }, + ], + }, + // TypeScript handles this case, so we don't have to + { + code: ` +const f = (a: RegExp, b: RegExp) => a + b; + `, + options: [ + { + allowRegExp: true, + }, + ], + }, + { + code: ` +let foo: string | undefined; +foo = foo + 'some data'; + `, + options: [ + { + allowNullish: true, + }, + ], + }, + { + code: ` +let foo: string | null; +foo = foo + 'some data'; + `, + options: [ + { + allowNullish: true, + }, + ], + }, + { + code: ` +let foo: string | null | undefined; +foo = foo + 'some data'; + `, + options: [ + { + allowNullish: true, + }, + ], + }, { code: ` let foo: number = 0; @@ -181,7 +259,7 @@ foo += 'string'; }, { code: ` -export const f = (a: any, b: any) => a + b; +const f = (a: any, b: any) => a + b; `, options: [ { @@ -191,7 +269,7 @@ export const f = (a: any, b: any) => a + b; }, { code: ` -export const f = (a: any, b: string) => a + b; +const f = (a: any, b: string) => a + b; `, options: [ { @@ -201,7 +279,7 @@ export const f = (a: any, b: string) => a + b; }, { code: ` -export const f = (a: any, b: bigint) => a + b; +const f = (a: any, b: bigint) => a + b; `, options: [ { @@ -211,7 +289,7 @@ export const f = (a: any, b: bigint) => a + b; }, { code: ` -export const f = (a: any, b: number) => a + b; +const f = (a: any, b: number) => a + b; `, options: [ { @@ -219,121 +297,230 @@ export const f = (a: any, b: number) => a + b; }, ], }, - ], - invalid: [ { - code: "var foo = '1' + 1;", - errors: [ + code: ` +const f = (a: any, b: boolean) => a + b; + `, + options: [ { - messageId: 'notStrings', - line: 1, - column: 11, + allowAny: true, + allowBoolean: true, }, ], }, { - code: 'var foo = [] + {};', + code: ` +const f = (a: string, b: string | number) => a + b; + `, + options: [ + { + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowNumberAndString: true, + allowRegExp: true, + }, + ], + }, + { + code: ` +const f = (a: string | number, b: number) => a + b; + `, + options: [ + { + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowNumberAndString: true, + allowRegExp: true, + }, + ], + }, + { + code: ` +const f = (a: string | number, b: string | number) => a + b; + `, + options: [ + { + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowNumberAndString: true, + allowRegExp: true, + }, + ], + }, + ], + invalid: [ + { + code: "let foo = '1' + 1;", errors: [ { - messageId: 'notValidTypes', + data: { + left: 'string', + right: 'number', + stringLike: 'string', + }, + messageId: 'mismatched', line: 1, column: 11, }, ], }, { - code: "var foo = 5 + '10';", + code: 'let foo = [] + {};', errors: [ { - messageId: 'notStrings', - line: 1, + data: { + stringLike: 'string', + type: 'never[]', + }, column: 11, + endColumn: 13, + line: 1, + messageId: 'invalid', + }, + { + data: { + stringLike: 'string', + type: '{}', + }, + column: 16, + endColumn: 18, + line: 1, + messageId: 'invalid', }, ], }, { - code: 'var foo = [] + 5;', + code: "let foo = 5 + '10';", errors: [ { - messageId: 'notNumbers', + data: { + left: 'number', + right: 'string', + stringLike: 'string', + }, + messageId: 'mismatched', line: 1, column: 11, }, ], }, { - code: 'var foo = [] + [];', + code: 'let foo = [] + 5;', errors: [ { - messageId: 'notValidTypes', + data: { + stringLike: 'string', + type: 'never[]', + }, + messageId: 'invalid', line: 1, column: 11, + endColumn: 13, }, ], }, { - code: 'var foo = 5 + [];', + code: 'let foo = [] + [];', errors: [ { - messageId: 'notNumbers', + data: { + stringLike: 'string', + type: 'never[]', + }, + messageId: 'invalid', line: 1, + endColumn: 13, column: 11, }, + { + data: { + stringLike: 'string', + type: 'never[]', + }, + messageId: 'invalid', + line: 1, + endColumn: 18, + column: 16, + }, ], }, { - code: "var foo = '5' + {};", + code: 'let foo = 5 + [3];', errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'number[]', + }, + column: 15, + endColumn: 18, line: 1, - column: 11, + messageId: 'invalid', }, ], }, { - code: "var foo = 5.5 + '5';", + code: "let foo = '5' + {};", errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: '{}', + }, + messageId: 'invalid', line: 1, - column: 11, + endColumn: 19, + column: 17, }, ], }, { - code: "var foo = '5.5' + 5;", + code: "let foo = 5.5 + '5';", errors: [ { - messageId: 'notStrings', + data: { + left: 'number', + right: 'string', + stringLike: 'string', + }, + messageId: 'mismatched', line: 1, column: 11, }, ], }, { - code: ` -var x = 5; -var y = '10'; -var foo = x + y; - `, + code: "let foo = '5.5' + 5;", errors: [ { - messageId: 'notStrings', - line: 4, + data: { + left: 'string', + right: 'number', + stringLike: 'string', + }, + messageId: 'mismatched', + line: 1, column: 11, }, ], }, { code: ` -var x = 5; -var y = '10'; -var foo = y + x; +let x = 5; +let y = '10'; +let foo = x + y; `, errors: [ { - messageId: 'notStrings', + data: { + left: 'number', + right: 'string', + stringLike: 'string', + }, + messageId: 'mismatched', line: 4, column: 11, }, @@ -341,38 +528,52 @@ var foo = y + x; }, { code: ` -var x = 5; -var foo = x + {}; +let x = 5; +let y = '10'; +let foo = y + x; `, errors: [ { - messageId: 'notNumbers', - line: 3, + data: { + right: 'number', + left: 'string', + stringLike: 'string', + }, + messageId: 'mismatched', + line: 4, column: 11, }, ], }, { code: ` -var y = '10'; -var foo = [] + y; +let x = 5; +let foo = x + {}; `, errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: '{}', + }, + messageId: 'invalid', line: 3, - column: 11, + column: 15, }, ], }, { code: ` -var pair: { first: number; second: string } = { first: 5, second: '10' }; -var foo = pair.first + '10'; +let y = '10'; +let foo = [] + y; `, errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'never[]', + }, + messageId: 'invalid', line: 3, column: 11, }, @@ -380,55 +581,74 @@ var foo = pair.first + '10'; }, { code: ` -var pair: { first: number; second: string } = { first: 5, second: '10' }; -var foo = 5 + pair.second; +let pair = { first: 5, second: '10' }; +let foo = pair + pair; `, errors: [ { - messageId: 'notStrings', - line: 3, + data: { + stringLike: 'string', + type: '{ first: number; second: string; }', + }, column: 11, + endColumn: 15, + line: 3, + messageId: 'invalid', }, - ], - }, - { - code: "var foo = parseInt('5.5', 10) + '10';", - errors: [ { - messageId: 'notStrings', - line: 1, - column: 11, + data: { + stringLike: 'string', + type: '{ first: number; second: string; }', + }, + column: 18, + endColumn: 22, + line: 3, + messageId: 'invalid', }, ], }, { code: ` -var pair = { first: 5, second: '10' }; -var foo = pair + pair; +type Valued = { value: number }; +let value: Valued = { value: 0 }; +let combined = value + 0; `, errors: [ { - messageId: 'notValidTypes', - line: 3, - column: 11, + data: { + stringLike: 'string', + type: 'Valued', + }, + column: 16, + endColumn: 21, + line: 4, + messageId: 'invalid', }, ], }, { - code: 'var foo = 1n + 1;', + code: 'let foo = 1n + 1;', errors: [ { - messageId: 'notBigInts', + data: { + left: 'bigint', + right: 'number', + }, + messageId: 'bigintAndNumber', line: 1, column: 11, }, ], }, { - code: 'var foo = 1 + 1n;', + code: 'let foo = 1 + 1n;', errors: [ { - messageId: 'notBigInts', + data: { + left: 'number', + right: 'bigint', + }, + messageId: 'bigintAndNumber', line: 1, column: 11, }, @@ -436,12 +656,16 @@ var foo = pair + pair; }, { code: ` - var foo = 1n; + let foo = 1n; foo + 1; `, errors: [ { - messageId: 'notBigInts', + data: { + left: 'bigint', + right: 'number', + }, + messageId: 'bigintAndNumber', line: 3, column: 9, }, @@ -449,12 +673,16 @@ var foo = pair + pair; }, { code: ` - var foo = 1; + let foo = 1; foo + 1n; `, errors: [ { - messageId: 'notBigInts', + data: { + left: 'number', + right: 'bigint', + }, + messageId: 'bigintAndNumber', line: 3, column: 9, }, @@ -469,7 +697,12 @@ function foo(a: T) { `, errors: [ { - messageId: 'notStrings', + data: { + left: 'string', + right: 'number', + stringLike: 'string', + }, + messageId: 'mismatched', line: 3, column: 10, }, @@ -483,7 +716,12 @@ function foo(a: T) { `, errors: [ { - messageId: 'notStrings', + data: { + left: 'string', + right: 'number', + stringLike: 'string', + }, + messageId: 'mismatched', line: 3, column: 10, }, @@ -497,7 +735,12 @@ function foo(a: T) { `, errors: [ { - messageId: 'notStrings', + data: { + left: 'number', + right: 'string', + stringLike: 'string', + }, + messageId: 'mismatched', line: 3, column: 10, }, @@ -511,7 +754,12 @@ function foo(a: T) { `, errors: [ { - messageId: 'notStrings', + data: { + left: 'number', + right: 'string', + stringLike: 'string', + }, + messageId: 'mismatched', line: 3, column: 10, }, @@ -519,13 +767,18 @@ function foo(a: T) { }, { code: ` - declare const a: boolean & string; - declare const b: string; + declare const a: \`template\${number}\`; + declare const b: number; const x = a + b; `, errors: [ { - messageId: 'notStrings', + data: { + left: 'string', + right: 'number', + stringLike: 'string', + }, + messageId: 'mismatched', line: 4, column: 19, }, @@ -533,13 +786,17 @@ function foo(a: T) { }, { code: ` - declare const a: number & string; + declare const a: never; declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'never', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -547,13 +804,17 @@ function foo(a: T) { }, { code: ` - declare const a: symbol & string; + declare const a: never & string; declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'never', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -561,13 +822,17 @@ function foo(a: T) { }, { code: ` - declare const a: object & string; + declare const a: boolean & string; declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'never', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -575,13 +840,17 @@ function foo(a: T) { }, { code: ` - declare const a: never & string; + declare const a: any & string; declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'any', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -589,13 +858,17 @@ function foo(a: T) { }, { code: ` - declare const a: any & string; + declare const a: { a: 1 } & { b: 2 }; declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notValidAnys', + data: { + stringLike: 'string', + type: '{ a: 1; } & { b: 2; }', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -603,55 +876,81 @@ function foo(a: T) { }, { code: ` - declare const a: { a: 1 } & { b: 2 }; + interface A { + a: 1; + } + declare const a: A; declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notStrings', - line: 4, + data: { + stringLike: 'string', + type: 'A', + }, + messageId: 'invalid', + line: 7, column: 19, }, ], }, { code: ` - declare const a: boolean & number; - declare const b: number; + interface A { + a: 1; + } + interface A2 extends A { + b: 2; + } + declare const a: A2; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notNumbers', - line: 4, + data: { + stringLike: 'string', + type: 'A2', + }, + messageId: 'invalid', + line: 10, column: 19, }, ], }, { code: ` - declare const a: symbol & number; - declare const b: number; + type A = { a: 1 } & { b: 2 }; + declare const a: A; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notNumbers', - line: 4, + data: { + stringLike: 'string', + type: 'A', + }, + messageId: 'invalid', + line: 5, column: 19, }, ], }, { code: ` - declare const a: object & number; + declare const a: { a: 1 } & { b: 2 }; declare const b: number; const x = a + b; `, errors: [ { - messageId: 'notNumbers', + data: { + stringLike: 'string', + type: '{ a: 1; } & { b: 2; }', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -659,13 +958,17 @@ function foo(a: T) { }, { code: ` - declare const a: never & number; - declare const b: number; + declare const a: never; + declare const b: bigint; const x = a + b; `, errors: [ { - messageId: 'notNumbers', + data: { + stringLike: 'string', + type: 'never', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -673,13 +976,17 @@ function foo(a: T) { }, { code: ` - declare const a: any & number; - declare const b: number; + declare const a: any; + declare const b: bigint; const x = a + b; `, errors: [ { - messageId: 'notValidAnys', + data: { + stringLike: 'string', + type: 'any', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -688,12 +995,16 @@ function foo(a: T) { { code: ` declare const a: { a: 1 } & { b: 2 }; - declare const b: number; + declare const b: bigint; const x = a + b; `, errors: [ { - messageId: 'notNumbers', + data: { + stringLike: 'string', + type: '{ a: 1; } & { b: 2; }', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -701,13 +1012,17 @@ function foo(a: T) { }, { code: ` - declare const a: boolean & bigint; - declare const b: bigint; + declare const a: RegExp; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notBigInts', + data: { + stringLike: 'string', + type: 'RegExp', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -715,13 +1030,17 @@ function foo(a: T) { }, { code: ` - declare const a: number & bigint; - declare const b: bigint; + const a = /regexp/; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notBigInts', + data: { + stringLike: 'string', + type: 'RegExp', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -729,13 +1048,17 @@ function foo(a: T) { }, { code: ` - declare const a: symbol & bigint; - declare const b: bigint; + declare const a: Symbol; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notBigInts', + data: { + stringLike: 'string', + type: 'Symbol', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -743,13 +1066,17 @@ function foo(a: T) { }, { code: ` - declare const a: object & bigint; - declare const b: bigint; + declare const a: symbol; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notBigInts', + data: { + stringLike: 'string', + type: 'symbol', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -757,13 +1084,17 @@ function foo(a: T) { }, { code: ` - declare const a: never & bigint; - declare const b: bigint; + declare const a: unique symbol; + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notBigInts', + data: { + stringLike: 'string', + type: 'unique symbol', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -771,13 +1102,17 @@ function foo(a: T) { }, { code: ` - declare const a: any & bigint; - declare const b: bigint; + const a = Symbol(''); + declare const b: string; const x = a + b; `, errors: [ { - messageId: 'notValidAnys', + data: { + stringLike: 'string', + type: 'unique symbol', + }, + messageId: 'invalid', line: 4, column: 19, }, @@ -785,21 +1120,29 @@ function foo(a: T) { }, { code: ` - declare const a: { a: 1 } & { b: 2 }; - declare const b: bigint; - const x = a + b; +let foo: string | undefined; +foo += 'some data'; `, + options: [ + { + checkCompoundAssignments: true, + }, + ], errors: [ { - messageId: 'notBigInts', - line: 4, - column: 19, + data: { + stringLike: 'string', + type: 'string | undefined', + }, + messageId: 'invalid', + line: 3, + column: 1, }, ], }, { code: ` -let foo: string | undefined; +let foo: string | null; foo += 'some data'; `, options: [ @@ -809,7 +1152,11 @@ foo += 'some data'; ], errors: [ { - messageId: 'notStrings', + data: { + stringLike: 'string', + type: 'string | null', + }, + messageId: 'invalid', line: 3, column: 1, }, @@ -827,7 +1174,12 @@ foo += 0; ], errors: [ { - messageId: 'notStrings', + data: { + left: 'string', + right: 'number', + stringLike: 'string', + }, + messageId: 'mismatched', line: 3, column: 1, }, @@ -844,9 +1196,9 @@ const f = (a: any, b: boolean) => a + b; ], errors: [ { - messageId: 'notValidAnys', + messageId: 'invalid', line: 2, - column: 35, + column: 39, }, ], }, @@ -861,13 +1213,37 @@ const f = (a: any, b: []) => a + b; ], errors: [ { - messageId: 'notValidAnys', + data: { + stringLike: 'string, allowing a string + `any`', + type: '[]', + }, + messageId: 'invalid', line: 2, - column: 30, + column: 34, + }, + ], + }, + { + code: ` +const f = (a: any, b: boolean) => a + b; + `, + options: [ + { + allowBoolean: true, + }, + ], + errors: [ + { + data: { + stringLike: 'string, allowing a string + `boolean`', + type: 'any', + }, + messageId: 'invalid', + line: 2, + column: 35, }, ], }, - { code: ` const f = (a: any, b: any) => a + b; @@ -879,10 +1255,15 @@ const f = (a: any, b: any) => a + b; ], errors: [ { - messageId: 'notValidAnys', + messageId: 'invalid', line: 2, column: 31, }, + { + messageId: 'invalid', + line: 2, + column: 35, + }, ], }, { @@ -896,7 +1277,7 @@ const f = (a: any, b: string) => a + b; ], errors: [ { - messageId: 'notValidAnys', + messageId: 'invalid', line: 2, column: 34, }, @@ -913,7 +1294,7 @@ const f = (a: any, b: bigint) => a + b; ], errors: [ { - messageId: 'notValidAnys', + messageId: 'invalid', line: 2, column: 34, }, @@ -930,7 +1311,7 @@ const f = (a: any, b: number) => a + b; ], errors: [ { - messageId: 'notValidAnys', + messageId: 'invalid', line: 2, column: 34, }, @@ -940,16 +1321,109 @@ const f = (a: any, b: number) => a + b; code: ` const f = (a: any, b: boolean) => a + b; `, + errors: [ + { + messageId: 'invalid', + line: 2, + column: 35, + }, + { + messageId: 'invalid', + line: 2, + column: 39, + }, + ], options: [ { allowAny: false, }, ], + }, + { + code: ` +const f = (a: number, b: RegExp) => a + b; + `, errors: [ { - messageId: 'notValidAnys', + messageId: 'invalid', line: 2, - column: 35, + column: 41, + }, + ], + options: [ + { + allowRegExp: true, + }, + ], + }, + { + code: ` +let foo: string | boolean; +foo = foo + 'some data'; + `, + errors: [ + { + data: { + stringLike: + 'string, allowing a string + any of: `null`, `undefined`', + type: 'string | boolean', + }, + messageId: 'invalid', + line: 3, + column: 7, + }, + ], + options: [ + { + allowNullish: true, + }, + ], + }, + { + code: ` +let foo: boolean; +foo = foo + 'some data'; + `, + errors: [ + { + data: { + stringLike: + 'string, allowing a string + any of: `null`, `undefined`', + type: 'boolean', + }, + messageId: 'invalid', + line: 3, + column: 7, + }, + ], + options: [ + { + allowNullish: true, + }, + ], + }, + { + code: ` +const f = (a: any, b: unknown) => a + b; + `, + options: [ + { + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowRegExp: true, + }, + ], + errors: [ + { + data: { + stringLike: + 'string, allowing a string + any of: `any`, `boolean`, `null`, `RegExp`, `undefined`', + type: 'unknown', + }, + messageId: 'invalid', + line: 2, + column: 39, }, ], },