From 241326f1776a6015aa695f0cf676c901856cb3a9 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 21 Mar 2021 00:59:38 +0100 Subject: [PATCH 1/3] feat(eslint-plugin): [prefer-regexp-exec] add autofix --- packages/eslint-plugin/README.md | 2 +- .../src/rules/prefer-regexp-exec.ts | 96 +++++++-- .../src/util/getWrappingFixer.ts | 188 ++++++++++++------ .../tests/rules/prefer-regexp-exec.test.ts | 76 +++++-- .../rules/strict-boolean-expressions.test.ts | 31 +-- 5 files changed, 294 insertions(+), 99 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 69ea06c2308..6b1fa7bbd0f 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -161,7 +161,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@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-reduce-type-parameter`](./docs/rules/prefer-reduce-type-parameter.md) | Prefer using type parameter when calling `Array#reduce` instead of casting | | :wrench: | :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-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: | :wrench: | :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 | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-ts-expect-error`](./docs/rules/prefer-ts-expect-error.md) | Recommends using `@ts-expect-error` over `@ts-ignore` | | :wrench: | | | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: | diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index dda6f15d3a5..d78c4496164 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -1,9 +1,13 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; import { createRule, getParserServices, getStaticValue, getTypeName, + getWrappingFixer, } from '../util'; export default createRule({ @@ -12,6 +16,7 @@ export default createRule({ meta: { type: 'suggestion', + fixable: 'code', docs: { description: 'Enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided', @@ -27,44 +32,103 @@ export default createRule({ create(context) { const globalScope = context.getScope(); - const service = getParserServices(context); - const typeChecker = service.program.getTypeChecker(); + const parserServices = getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + const sourceCode = context.getSourceCode(); /** * Check if a given node is a string. * @param node The node to check. */ - function isStringType(node: TSESTree.LeftHandSideExpression): boolean { + function isStringType(node: TSESTree.Expression): boolean { const objectType = typeChecker.getTypeAtLocation( - service.esTreeNodeToTSNodeMap.get(node), + parserServices.esTreeNodeToTSNodeMap.get(node), ); return getTypeName(typeChecker, objectType) === 'string'; } + /** + * Check if a given node is a RegExp. + * @param node The node to check. + */ + function isRegExpType(node: TSESTree.Expression): boolean { + const objectType = typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node), + ); + return getTypeName(typeChecker, objectType) === 'RegExp'; + } + return { "CallExpression[arguments.length=1] > MemberExpression.callee[property.name='match'][computed=false]"( - node: TSESTree.MemberExpression, + memberNode: TSESTree.MemberExpression, ): void { - const callNode = node.parent as TSESTree.CallExpression; - const arg = callNode.arguments[0]; - const evaluated = getStaticValue(arg, globalScope); + const objectNode = memberNode.object; + const callNode = memberNode.parent as TSESTree.CallExpression; + const argumentNode = callNode.arguments[0]; + const argumentValue = getStaticValue(argumentNode, globalScope); + + if (!isStringType(objectNode)) { + return; + } // Don't report regular expressions with global flag. if ( - evaluated && - evaluated.value instanceof RegExp && - evaluated.value.flags.includes('g') + argumentValue && + argumentValue.value instanceof RegExp && + argumentValue.value.flags.includes('g') ) { return; } - if (isStringType(node.object)) { - context.report({ - node: callNode, + if ( + argumentNode.type === AST_NODE_TYPES.Literal && + typeof argumentNode.value == 'string' + ) { + const regExp = RegExp(argumentNode.value); + return context.report({ + node: memberNode.property, messageId: 'regExpExecOverStringMatch', + fix: getWrappingFixer({ + sourceCode, + node: callNode, + innerNode: [objectNode], + wrap: objectCode => `${regExp.toString()}.exec(${objectCode})`, + }), + }); + } + + if (isRegExpType(argumentNode)) { + return context.report({ + node: memberNode.property, + messageId: 'regExpExecOverStringMatch', + fix: getWrappingFixer({ + sourceCode, + node: callNode, + innerNode: [objectNode, argumentNode], + wrap: (objectCode, argumentCode) => + `${argumentCode}.exec(${objectCode})`, + }), }); - return; } + + if (isStringType(argumentNode)) { + return context.report({ + node: memberNode.property, + messageId: 'regExpExecOverStringMatch', + fix: getWrappingFixer({ + sourceCode, + node: callNode, + innerNode: [objectNode, argumentNode], + wrap: (objectCode, argumentCode) => + `RegExp(${argumentCode}).exec(${objectCode})`, + }), + }); + } + + return context.report({ + node: memberNode.property, + messageId: 'regExpExecOverStringMatch', + }); }, }; }, diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index 4a9edcfcc98..72b0c86b181 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -3,6 +3,7 @@ import { TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; +import { SourceCode } from '@typescript-eslint/experimental-utils/src/ts-eslint'; import * as util from '../util'; interface WrappingFixerParams { @@ -14,13 +15,15 @@ interface WrappingFixerParams { * Descendant of `node` we want to preserve. * Use this to replace some code with another. * By default it's the node we are modifying (so nothing is removed). + * You can pass multiple nodes as an array. */ - innerNode?: TSESTree.Node; + innerNode?: TSESTree.Node | TSESTree.Node[]; /** * The function which gets the code of the `innerNode` and returns some code around it. + * Receives multiple arguments if there are multiple innerNodes. * E.g. ``code => `${code} != null` `` */ - wrap: (code: string) => string; + wrap: (...code: string[]) => string; } /** @@ -31,38 +34,27 @@ export function getWrappingFixer( params: WrappingFixerParams, ): TSESLint.ReportFixFunction { const { sourceCode, node, innerNode = node, wrap } = params; + const innerNodes = Array.isArray(innerNode) ? innerNode : [innerNode]; + return (fixer): TSESLint.RuleFix => { - let code = sourceCode.getText(innerNode); - - // check the inner expression's precedence - if ( - innerNode.type !== AST_NODE_TYPES.Literal && - innerNode.type !== AST_NODE_TYPES.Identifier && - innerNode.type !== AST_NODE_TYPES.MemberExpression && - innerNode.type !== AST_NODE_TYPES.CallExpression - ) { - // we are wrapping something else than a simple variable or function call - // the code we are adding might have stronger precedence than our wrapped node - // let's wrap our node in parens in case it has a weaker precedence than the code we are wrapping it in - code = `(${code})`; - } + const innerCodes = innerNodes.map(innerNode => { + let code = sourceCode.getText(innerNode); - // do the wrapping - code = wrap(code); + // check the inner expression's precedence + if (!isStrongPrecedenceNode(innerNode)) { + // the code we are adding might have stronger precedence than our wrapped node + // let's wrap our node in parens in case it has a weaker precedence than the code we are wrapping it in + code = `(${code})`; + } - let parent = util.nullThrows( - node.parent, - util.NullThrowsReasons.MissingParent, - ); + return code; + }); + + // do the wrapping + let code = wrap(...innerCodes); // check the outer expression's precedence - if ( - parent.type !== AST_NODE_TYPES.IfStatement && - parent.type !== AST_NODE_TYPES.ForStatement && - parent.type !== AST_NODE_TYPES.WhileStatement && - parent.type !== AST_NODE_TYPES.DoWhileStatement - ) { - // the whole expression's parent is something else than condition of if/for/while + if (isWeakPrecedenceParent(node)) { // we wrapped the node in some expression which very likely has a different precedence than original wrapped node // let's wrap the whole expression in parens just in case if (!util.isParenthesized(node, sourceCode)) { @@ -71,39 +63,123 @@ export function getWrappingFixer( } // check if we need to insert semicolon - for (;;) { - const prevParent = parent; - parent = parent.parent!; + if (/^[`([]/.exec(code) && isMissingSemicolonBefore(node, sourceCode)) { + code = `;${code}`; + } + + return fixer.replaceText(node, code); + }; +} + +/** + * Check if a node will always have the same precedence if it's parent changes. + */ +function isStrongPrecedenceNode(innerNode: TSESTree.Node): boolean { + return ( + innerNode.type === AST_NODE_TYPES.Literal || + innerNode.type === AST_NODE_TYPES.Identifier || + innerNode.type === AST_NODE_TYPES.ArrayExpression || + innerNode.type === AST_NODE_TYPES.ObjectExpression || + innerNode.type === AST_NODE_TYPES.MemberExpression || + innerNode.type === AST_NODE_TYPES.CallExpression || + innerNode.type === AST_NODE_TYPES.NewExpression || + innerNode.type === AST_NODE_TYPES.TaggedTemplateExpression + ); +} + +/** + * Check if a node's parent could have different precedence if the node changes. + */ +function isWeakPrecedenceParent(node: TSESTree.Node): boolean { + const parent = node.parent!; + + if ( + parent.type === AST_NODE_TYPES.UnaryExpression || + parent.type === AST_NODE_TYPES.BinaryExpression || + parent.type === AST_NODE_TYPES.LogicalExpression || + parent.type === AST_NODE_TYPES.ConditionalExpression || + parent.type === AST_NODE_TYPES.AwaitExpression || + parent.type === AST_NODE_TYPES.MemberExpression + ) { + return true; + } + + if ( + (parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.NewExpression) && + parent.callee === node + ) { + return true; + } + + if ( + parent.type === AST_NODE_TYPES.TaggedTemplateExpression && + parent.tag === node + ) { + return true; + } + + return false; +} + +/** + * Returns true if a node is at the beginning of expression statement and the statement above doesn't end with semicolon. + * Doesn't check if the node begins with `(`, `[` or `` ` ``. + */ +function isMissingSemicolonBefore( + node: TSESTree.Node, + sourceCode: SourceCode, +): boolean { + for (;;) { + const parent = node.parent!; + + if (parent.type === AST_NODE_TYPES.ExpressionStatement) { + const block = parent.parent!; if ( - parent.type === AST_NODE_TYPES.LogicalExpression || - parent.type === AST_NODE_TYPES.BinaryExpression + block.type === AST_NODE_TYPES.Program || + block.type === AST_NODE_TYPES.BlockStatement ) { - if (parent.left === prevParent) { - // the next parent is a binary expression and current node is on the left - continue; - } - } - if (parent.type === AST_NODE_TYPES.ExpressionStatement) { - const block = parent.parent!; + // parent is an expression statement in a block + const statementIndex = block.body.indexOf(parent); + const previousStatement = block.body[statementIndex - 1]; if ( - block.type === AST_NODE_TYPES.Program || - block.type === AST_NODE_TYPES.BlockStatement + statementIndex > 0 && + sourceCode.getLastToken(previousStatement)!.value !== ';' ) { - // the next parent is an expression in a block - const statementIndex = block.body.indexOf(parent); - const previousStatement = block.body[statementIndex - 1]; - if ( - statementIndex > 0 && - sourceCode.getLastToken(previousStatement)!.value !== ';' - ) { - // the previous statement in a block doesn't end with a semicolon - code = `;${code}`; - } + return true; } } - break; } - return fixer.replaceText(node, code); - }; + if (!isLeftHandSide(node)) { + return false; + } + + node = parent; + } +} + +/** + * Checks if a node is LHS of a binary/ternary expression. + */ +function isLeftHandSide(node: TSESTree.Node): boolean { + const parent = node.parent!; + + if ( + (parent.type === AST_NODE_TYPES.BinaryExpression || + parent.type === AST_NODE_TYPES.LogicalExpression || + parent.type === AST_NODE_TYPES.AssignmentExpression) && + node === parent.left + ) { + return true; + } + + if ( + parent.type === AST_NODE_TYPES.ConditionalExpression && + node === parent.test + ) { + return true; + } + + return false; } diff --git a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts index fc97766f9f7..243c4e065b9 100644 --- a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts @@ -41,9 +41,21 @@ function f(s: string | string[]) { { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 13, }, ], + output: "/thing/.exec('something');", + }, + { + code: "'something'.match('^[a-z]+thing/?$');", + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 13, + }, + ], + output: "/^[a-z]+thing\\/?$/.exec('something');", }, { code: ` @@ -55,9 +67,33 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 4, - column: 1, + column: 6, }, ], + output: ` +const text = 'something'; +const search = /thing/; +search.exec(text); + `, + }, + { + code: ` +const text = 'something'; +const search = 'thing'; +text.match(search); + `, + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 4, + column: 6, + }, + ], + output: ` +const text = 'something'; +const search = 'thing'; +RegExp(search).exec(text); + `, }, { code: "'212'.match(2);", @@ -65,7 +101,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 7, }, ], }, @@ -75,7 +111,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 7, }, ], }, @@ -85,7 +121,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 9, }, ], }, @@ -96,7 +132,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 60, }, ], }, @@ -107,7 +143,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 60, }, ], }, @@ -118,7 +154,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 60, }, ], }, @@ -128,7 +164,7 @@ text.match(search); { messageId: 'regExpExecOverStringMatch', line: 1, - column: 1, + column: 17, }, ], }, @@ -142,9 +178,14 @@ function f(s: 'a' | 'b') { { messageId: 'regExpExecOverStringMatch', line: 3, - column: 3, + column: 5, }, ], + output: ` +function f(s: 'a' | 'b') { + /a/.exec(s); +} + `, }, { code: ` @@ -157,9 +198,15 @@ function f(s: SafeString) { { messageId: 'regExpExecOverStringMatch', line: 4, - column: 3, + column: 5, }, ], + output: ` +type SafeString = string & { __HTML_ESCAPED__: void }; +function f(s: SafeString) { + /thing/.exec(s); +} + `, }, { code: ` @@ -171,9 +218,14 @@ function f(s: T) { { messageId: 'regExpExecOverStringMatch', line: 3, - column: 3, + column: 5, }, ], + output: ` +function f(s: T) { + /thing/.exec(s); +} + `, }, ], }); diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index d24de92ac65..2ad11229b0b 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -464,16 +464,16 @@ if (x) { { messageId: 'conditionFixCompareZero', // TODO: fix compare zero suggestion for bigint - output: ` (x: bigint) => (x === 0);`, + output: ` (x: bigint) => x === 0;`, }, { // TODO: remove check NaN suggestion for bigint messageId: 'conditionFixCompareNaN', - output: ` (x: bigint) => (Number.isNaN(x));`, + output: ` (x: bigint) => Number.isNaN(x);`, }, { messageId: 'conditionFixCastBoolean', - output: ` (x: bigint) => (!Boolean(x));`, + output: ` (x: bigint) => !Boolean(x);`, }, ], }, @@ -503,15 +503,15 @@ if (x) { suggestions: [ { messageId: 'conditionFixCompareZero', - output: ` ([]["length"] === 0); // doesn't count as array.length when computed`, + output: ` []["length"] === 0; // doesn't count as array.length when computed`, }, { messageId: 'conditionFixCompareNaN', - output: ` (Number.isNaN([]["length"])); // doesn't count as array.length when computed`, + output: ` Number.isNaN([]["length"]); // doesn't count as array.length when computed`, }, { messageId: 'conditionFixCastBoolean', - output: ` (!Boolean([]["length"])); // doesn't count as array.length when computed`, + output: ` !Boolean([]["length"]); // doesn't count as array.length when computed`, }, ], }, @@ -607,7 +607,7 @@ if (x) { }, { messageId: 'conditionFixCompareFalse', - output: ` (x?: boolean) => (x === false);`, + output: ` (x?: boolean) => x === false;`, }, ], }, @@ -644,7 +644,7 @@ if (x) { ], output: noFormat` declare const x: object | null; if (x != null) {} - (x?: { a: number }) => (x == null); + (x?: { a: number }) => x == null; (x: T) => (x != null) ? 1 : 0; `, }), @@ -683,7 +683,7 @@ if (x) { suggestions: [ { messageId: 'conditionFixCompareNullish', - output: ' (x?: string) => (x == null);', + output: ' (x?: string) => x == null;', }, { messageId: 'conditionFixDefaultEmptyString', @@ -691,7 +691,7 @@ if (x) { }, { messageId: 'conditionFixCastBoolean', - output: ' (x?: string) => (!Boolean(x));', + output: ' (x?: string) => !Boolean(x);', }, ], }, @@ -754,7 +754,7 @@ if (x) { suggestions: [ { messageId: 'conditionFixCompareNullish', - output: ' (x?: number) => (x == null);', + output: ' (x?: number) => x == null;', }, { messageId: 'conditionFixDefaultZero', @@ -762,7 +762,7 @@ if (x) { }, { messageId: 'conditionFixCastBoolean', - output: ' (x?: number) => (!Boolean(x));', + output: ' (x?: number) => !Boolean(x);', }, ], }, @@ -865,18 +865,21 @@ if (x) { options: [{ allowNullableObject: false }], code: noFormat` declare const obj: { x: number } | null; + !obj ? 1 : 0 !obj obj || 0 obj && 1 || 0 `, errors: [ { messageId: 'conditionErrorNullableObject', line: 3, column: 10 }, - { messageId: 'conditionErrorNullableObject', line: 4, column: 9 }, + { messageId: 'conditionErrorNullableObject', line: 4, column: 10 }, { messageId: 'conditionErrorNullableObject', line: 5, column: 9 }, + { messageId: 'conditionErrorNullableObject', line: 6, column: 9 }, ], output: noFormat` declare const obj: { x: number } | null; - (obj == null) + (obj == null) ? 1 : 0 + obj == null ;(obj != null) || 0 ;(obj != null) && 1 || 0 `, From c52b6f93d965c38af3258ac3e1f941d46513e262 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 22 Mar 2021 02:05:14 +0100 Subject: [PATCH 2/3] test(eslint-plugin): test getWrappingFixer --- .../src/util/getWrappingFixer.ts | 33 +- .../tests/util/getWrappingFixer.test.ts | 311 ++++++++++++++++++ 2 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 packages/eslint-plugin/tests/util/getWrappingFixer.test.ts diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index 72b0c86b181..00c00e74882 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -94,12 +94,19 @@ function isWeakPrecedenceParent(node: TSESTree.Node): boolean { const parent = node.parent!; if ( + parent.type === AST_NODE_TYPES.UpdateExpression || parent.type === AST_NODE_TYPES.UnaryExpression || parent.type === AST_NODE_TYPES.BinaryExpression || parent.type === AST_NODE_TYPES.LogicalExpression || parent.type === AST_NODE_TYPES.ConditionalExpression || - parent.type === AST_NODE_TYPES.AwaitExpression || - parent.type === AST_NODE_TYPES.MemberExpression + parent.type === AST_NODE_TYPES.AwaitExpression + ) { + return true; + } + + if ( + parent.type === AST_NODE_TYPES.MemberExpression && + parent.object === node ) { return true; } @@ -160,11 +167,17 @@ function isMissingSemicolonBefore( } /** - * Checks if a node is LHS of a binary/ternary expression. + * Checks if a node is LHS of an operator. */ function isLeftHandSide(node: TSESTree.Node): boolean { const parent = node.parent!; + // a++ + if (parent.type === AST_NODE_TYPES.UpdateExpression) { + return true; + } + + // a + b if ( (parent.type === AST_NODE_TYPES.BinaryExpression || parent.type === AST_NODE_TYPES.LogicalExpression || @@ -174,6 +187,7 @@ function isLeftHandSide(node: TSESTree.Node): boolean { return true; } + // a ? b : c if ( parent.type === AST_NODE_TYPES.ConditionalExpression && node === parent.test @@ -181,5 +195,18 @@ function isLeftHandSide(node: TSESTree.Node): boolean { return true; } + // a(b) + if (parent.type === AST_NODE_TYPES.CallExpression && node === parent.callee) { + return true; + } + + // a`b` + if ( + parent.type === AST_NODE_TYPES.TaggedTemplateExpression && + node === parent.tag + ) { + return true; + } + return false; } diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts new file mode 100644 index 00000000000..bce06522dd5 --- /dev/null +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -0,0 +1,311 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; +import { createRule, getWrappingFixer } from '../../src/util'; + +const rule = createRule({ + name: 'void-everything', + defaultOptions: [], + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: 'Add void operator in random places for test purposes.', + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + addVoid: 'Please void this', + }, + schema: [], + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const report = (node: TSESTree.Node): void => { + context.report({ + node, + messageId: 'addVoid', + fix: getWrappingFixer({ + sourceCode, + node, + wrap: code => `void ${code}`, + }), + }); + }; + + return { + 'Identifier[name="wrapMe"]': report, + 'Literal[value="wrapMe"]': report, + 'ArrayExpression[elements.0.value="wrapArray"]': report, + 'ObjectExpression[properties.0.value.value="wrapObject"]': report, + 'FunctionExpression[id.name="wrapFunction"]': report, + 'ClassExpression[id.name="wrapClass"]': report, + }; + }, +}); + +const rootPath = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('getWrappingFixer', rule, { + valid: [], + invalid: [ + // should add parens when inner expression might need them + { + code: '(function wrapFunction() {})', + errors: [{ messageId: 'addVoid' }], + output: '(void (function wrapFunction() {}))', + }, + { + code: '(class wrapClass {})', + errors: [{ messageId: 'addVoid' }], + output: '(void (class wrapClass {}))', + }, + + // shouldn't add inner parens when not necessary + { + code: 'wrapMe', + errors: [{ messageId: 'addVoid' }], + output: 'void wrapMe', + }, + { + code: '"wrapMe"', + errors: [{ messageId: 'addVoid' }], + output: 'void "wrapMe"', + }, + { + code: '["wrapArray"]', + errors: [{ messageId: 'addVoid' }], + output: 'void ["wrapArray"]', + }, + { + code: '({ x: "wrapObject" })', + errors: [{ messageId: 'addVoid' }], + output: '(void { x: "wrapObject" })', + }, + + // should add parens when the outer expression might need them + { + code: '!wrapMe', + errors: [{ messageId: 'addVoid' }], + output: '!(void wrapMe)', + }, + { + code: 'wrapMe++', + errors: [{ messageId: 'addVoid' }], + output: '(void wrapMe)++', + }, + { + code: '"wrapMe" + "dontWrap"', + errors: [{ messageId: 'addVoid' }], + output: '(void "wrapMe") + "dontWrap"', + }, + { + code: 'async () => await wrapMe', + errors: [{ messageId: 'addVoid' }], + output: 'async () => await (void wrapMe)', + }, + { + code: 'wrapMe(arg)', + errors: [{ messageId: 'addVoid' }], + output: '(void wrapMe)(arg)', + }, + { + code: 'new wrapMe(arg)', + errors: [{ messageId: 'addVoid' }], + output: 'new (void wrapMe)(arg)', + }, + { + code: 'wrapMe`arg`', + errors: [{ messageId: 'addVoid' }], + output: '(void wrapMe)`arg`', + }, + { + code: 'wrapMe.prop', + errors: [{ messageId: 'addVoid' }], + output: '(void wrapMe).prop', + }, + + // shouldn't add outer parens when not necessary + { + code: 'obj["wrapMe"]', + errors: [{ messageId: 'addVoid' }], + output: 'obj[void "wrapMe"]', + }, + { + code: 'fn(wrapMe)', + errors: [{ messageId: 'addVoid' }], + output: 'fn(void wrapMe)', + }, + { + code: 'new Cls(wrapMe)', + errors: [{ messageId: 'addVoid' }], + output: 'new Cls(void wrapMe)', + }, + { + code: '[wrapMe, ...wrapMe]', + errors: [{ messageId: 'addVoid' }, { messageId: 'addVoid' }], + output: '[void wrapMe, ...void wrapMe]', + }, + { + code: '`${wrapMe}`', + errors: [{ messageId: 'addVoid' }], + output: '`${void wrapMe}`', + }, + { + code: 'tpl`${wrapMe}`', + errors: [{ messageId: 'addVoid' }], + output: 'tpl`${void wrapMe}`', + }, + { + code: '({ ["wrapMe"]: wrapMe, ...wrapMe })', + errors: [ + { messageId: 'addVoid' }, + { messageId: 'addVoid' }, + { messageId: 'addVoid' }, + ], + output: '({ [void "wrapMe"]: void wrapMe, ...void wrapMe })', + }, + { + code: 'function fn() { return wrapMe }', + errors: [{ messageId: 'addVoid' }], + output: 'function fn() { return void wrapMe }', + }, + { + code: 'function fn() { yield wrapMe }', + errors: [{ messageId: 'addVoid' }], + output: 'function fn() { yield void wrapMe }', + }, + { + code: '() => wrapMe', + errors: [{ messageId: 'addVoid' }], + output: '() => void wrapMe', + }, + { + code: 'if (wrapMe) {}', + errors: [{ messageId: 'addVoid' }], + output: 'if (void wrapMe) {}', + }, + + // should detect parens at the beginning of a line and add a semi + { + code: ` + "dontWrap" + "wrapMe" + "!" + `, + errors: [{ messageId: 'addVoid' }], + output: ` + "dontWrap" + ;(void "wrapMe") + "!" + `, + }, + { + code: ` + dontWrap + wrapMe++ + `, + errors: [{ messageId: 'addVoid' }], + output: ` + dontWrap + ;(void wrapMe)++ + `, + }, + { + code: ` + dontWrap() + wrapMe() + `, + errors: [{ messageId: 'addVoid' }], + output: ` + dontWrap() + ;(void wrapMe)() + `, + }, + { + code: ` + dontWrap() + wrapMe\`\` + `, + errors: [{ messageId: 'addVoid' }], + output: ` + dontWrap() + ;(void wrapMe)\`\` + `, + }, + + // shouldn't add a semi when not necessary + { + code: ` + "dontWrap" + test() ? "wrapMe" : "dontWrap" + `, + errors: [{ messageId: 'addVoid' }], + output: ` + "dontWrap" + test() ? (void "wrapMe") : "dontWrap" + `, + }, + { + code: ` + "dontWrap"; + wrapMe && f() + `, + errors: [{ messageId: 'addVoid' }], + output: ` + "dontWrap"; + (void wrapMe) && f() + `, + }, + { + code: ` + new dontWrap + new wrapMe + `, + errors: [{ messageId: 'addVoid' }], + output: ` + new dontWrap + new (void wrapMe) + `, + }, + { + code: ` + wrapMe || f() + `, + errors: [{ messageId: 'addVoid' }], + output: ` + (void wrapMe) || f() + `, + }, + { + code: ` + if (true) wrapMe && f() + `, + errors: [{ messageId: 'addVoid' }], + output: ` + if (true) (void wrapMe) && f() + `, + }, + { + code: ` + dontWrap + if (true) { + wrapMe ?? f() + } + `, + errors: [{ messageId: 'addVoid' }], + output: ` + dontWrap + if (true) { + (void wrapMe) ?? f() + } + `, + }, + ], +}); From e41cf62d9b65237ee1e0096a6746d42df7906112 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 22 Mar 2021 02:24:57 +0100 Subject: [PATCH 3/3] test(eslint-plugin): add generator star --- packages/eslint-plugin/tests/util/getWrappingFixer.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts index bce06522dd5..22306176211 100644 --- a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -179,9 +179,9 @@ ruleTester.run('getWrappingFixer', rule, { output: 'function fn() { return void wrapMe }', }, { - code: 'function fn() { yield wrapMe }', + code: 'function* fn() { yield wrapMe }', errors: [{ messageId: 'addVoid' }], - output: 'function fn() { yield void wrapMe }', + output: 'function* fn() { yield void wrapMe }', }, { code: '() => wrapMe',