diff --git a/baselines/packages/mimir/test/prefer-exponentiation-operator/default/test.ts.fix b/baselines/packages/mimir/test/prefer-exponentiation-operator/default/test.ts.fix new file mode 100644 index 000000000..031be9a5f --- /dev/null +++ b/baselines/packages/mimir/test/prefer-exponentiation-operator/default/test.ts.fix @@ -0,0 +1,42 @@ +1** 2; +(1)** (2); +1 + 1** 2; + +2 ** 3** 2; +(2** 3) ** 2; +2** 3** 2; // TODO add parens + +({prop: 1}.prop** 2); + +(1 + 1)**(2 + 2); +(1** 2); +1** 2 as number; +(1)** (2 as number); +1** (Boolean() ? 1 : 2); +!(1** 2); +(1** 2).toString(); +(1** 2); + +-((1 + 1)** (2 + 2)); + +-1** 2; // TODO TS bug + +declare var args: [number, number]; +Math.pow(...args); +Math.pow(1); +Math.pow(1, ...[2]); + +async function* fn() { + await 1** await 2; // TODO TS bug + await (1** 2); + await (await 1** await 2); // TODO TS bug + (yield)** yield; // TODO TS bug + (yield 1)** yield 1; // TODO TS bug + yield 1** 2; +} + +declare namespace foo { + function pow(a: number, b: number): number; +} + +foo.pow(1, 2); diff --git a/baselines/packages/mimir/test/prefer-exponentiation-operator/default/test.ts.lint b/baselines/packages/mimir/test/prefer-exponentiation-operator/default/test.ts.lint new file mode 100644 index 000000000..e896d794b --- /dev/null +++ b/baselines/packages/mimir/test/prefer-exponentiation-operator/default/test.ts.lint @@ -0,0 +1,66 @@ +Math.pow(1, 2); +~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow((1), (2)); +~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +1 + Math.pow(1, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + +2 ** Math.pow(3, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(2, 3) ** 2; +~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(2, Math.pow(3, 2)); // TODO add parens +~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + +Math.pow({prop: 1}.prop, 2); +~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + +Math.pow(1 + 1,2 + 2); +~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(1, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(1, 2) as number; +~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(1, 2 as number); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(1, Boolean() ? 1 : 2); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +!Math.pow(1, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(1, 2).toString(); +~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +Math.pow(1, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + +-Math.pow(1 + 1, 2 + 2); + ~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + +Math.pow(-1, 2); // TODO TS bug +~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + +declare var args: [number, number]; +Math.pow(...args); +Math.pow(1); +Math.pow(1, ...[2]); + +async function* fn() { + Math.pow(await 1, await 2); // TODO TS bug + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + await Math.pow(1, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + await Math.pow(await 1, await 2); // TODO TS bug + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + Math.pow(yield, yield); // TODO TS bug + ~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + Math.pow(yield 1, yield 1); // TODO TS bug + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] + yield Math.pow(1, 2); + ~~~~~~~~~~~~~~ [error prefer-exponentiation-operator: Prefer the exponentiation operator '**' over 'Math.pow'.] +} + +declare namespace foo { + function pow(a: number, b: number): number; +} + +foo.pow(1, 2); diff --git a/packages/mimir/README.md b/packages/mimir/README.md index fab60dabf..2434bff67 100644 --- a/packages/mimir/README.md +++ b/packages/mimir/README.md @@ -52,6 +52,7 @@ Rule | Description | Difference to TSLint rule / Why you should use it `parameter-properties` | Enforces or disallows the use of parameter properties. This rule is **not** enabled in `wotan:recommended`. | TSlint only has `no-parameter-properties` to disallow all parameter properties and has no autofixer. `prefer-const` | Prefer `const` for variables that are never reassigned. Use option `{destructuring: "any"}` if you want to see failures for each identifier of a destructuring, even if not all of them can be constants. The default is `{destructuring: "all"}`. | TSLint's `prefer-const` rule gives some false positives for merged declarations and variables used in before being declared which results in a compiler error after fixing. `prefer-dot-notation` | Prefer `obj.foo` over `obj['foo']` where possible. | Same as TSLint's `no-string-literal` rule, but more performant. +`prefer-exponentiation-operator` | Prefer `a ** b` over `Math.pow(a, b)`. | No similar TSlint rule. `prefer-for-of` | Prefer `for-of` loops over regular `for` loops where possible. *requires type information* | Avoids the false positives of TSLint's `prefer-for-of` rule. `prefer-namespace-keyword` | Prefer `namespace foo {}` over `module foo {}` to avoid confusion with ECMAScript modules. | Same as TSLint's `no-internal-module`. `prefer-number-methods` | Prefer ES2015's `Number.isNaN` and `Number.isFinite` over the global `isNaN` and `isFinite` mainly for performance. *requires type information* | No similar rule in TSLint. diff --git a/packages/mimir/recommended.yaml b/packages/mimir/recommended.yaml index c7bfcf3c5..934309ce6 100644 --- a/packages/mimir/recommended.yaml +++ b/packages/mimir/recommended.yaml @@ -31,6 +31,7 @@ rules: no-useless-spread: error prefer-const: error prefer-dot-notation: error + prefer-exponentiation-operator: error prefer-for-of: error prefer-namespace-keyword: error prefer-number-methods: error diff --git a/packages/mimir/src/rules/prefer-exponentiation-operator.ts b/packages/mimir/src/rules/prefer-exponentiation-operator.ts new file mode 100644 index 000000000..4a476f453 --- /dev/null +++ b/packages/mimir/src/rules/prefer-exponentiation-operator.ts @@ -0,0 +1,71 @@ +import { excludeDeclarationFiles, AbstractRule, Replacement } from '@fimbul/ymir'; +import { WrappedAst, getWrappedNodeAtPosition, isIdentifier, isPropertyAccessExpression, isCallExpression, isSpreadElement } from 'tsutils'; +import { expressionNeedsParensWhenReplacingNode } from '../utils'; +import * as ts from 'typescript'; + +@excludeDeclarationFiles +export class Rule extends AbstractRule { + public apply() { + const re = /\bpow\s*[/(]/g; + let wrappedAst: WrappedAst | undefined; + for (let match = re.exec(this.sourceFile.text); match !== null; match = re.exec(this.sourceFile.text)) { + const {node} = getWrappedNodeAtPosition(wrappedAst || (wrappedAst = this.context.getWrappedAst()), match.index)!; + if (!isIdentifier(node) || node.end !== match.index + 3 || node.text !== 'pow') + continue; + const parent = node.parent!; + if (!isPropertyAccessExpression(parent) || !isIdentifier(parent.expression) || parent.expression.text !== 'Math') + continue; + const grandparent = parent.parent!; + if ( + !isCallExpression(grandparent) || + grandparent.expression !== parent || + grandparent.arguments.length !== 2 || + grandparent.arguments.some(isSpreadElement) + ) + continue; + const fix = [Replacement.replace(grandparent.arguments[1].pos - 1, grandparent.arguments[1].pos, '**')]; + const fixed = ts.createBinary( + grandparent.arguments[0], + ts.SyntaxKind.AsteriskAsteriskToken, + grandparent.arguments[1], + ); + if ( + expressionNeedsParensWhenReplacingNode(fixed, grandparent) || + grandparent.parent!.kind === ts.SyntaxKind.PropertyAccessExpression || + grandparent.parent!.kind === ts.SyntaxKind.ElementAccessExpression && + (grandparent.parent).expression === grandparent || + grandparent.parent!.kind === ts.SyntaxKind.PrefixUnaryExpression || + grandparent.parent!.kind === ts.SyntaxKind.AwaitExpression || + grandparent.parent!.kind === ts.SyntaxKind.VoidExpression || + grandparent.parent!.kind === ts.SyntaxKind.TypeOfExpression || + grandparent.parent!.kind === ts.SyntaxKind.TypeAssertionExpression || + grandparent.parent!.kind === ts.SyntaxKind.BinaryExpression && + (grandparent.parent).operatorToken.kind === ts.SyntaxKind.AsteriskAsteriskToken && + (grandparent.parent).left === grandparent + ) { + fix.push(Replacement.delete(grandparent.getStart(this.sourceFile), grandparent.arguments[0].pos - 1)); + } else { + fix.push( + Replacement.delete(grandparent.getStart(this.sourceFile), grandparent.arguments[0].getStart(this.sourceFile)), + Replacement.delete(grandparent.end - 1, grandparent.end), + ); + } + if (fixed.left !== grandparent.arguments[0]) + fix.push( + Replacement.append(grandparent.arguments[0].getStart(this.sourceFile), '('), + Replacement.append(grandparent.arguments[0].end, ')'), + ); + if (fixed.right !== grandparent.arguments[1]) + fix.push( + Replacement.append(grandparent.arguments[1].getStart(this.sourceFile), '('), + Replacement.append(grandparent.arguments[1].end, ')'), + ); + + this.addFailureAtNode( + grandparent, + "Prefer the exponentiation operator '**' over 'Math.pow'.", + fix, + ); + } + } +} diff --git a/packages/mimir/src/utils.ts b/packages/mimir/src/utils.ts index 512ea359b..992b2849e 100644 --- a/packages/mimir/src/utils.ts +++ b/packages/mimir/src/utils.ts @@ -110,6 +110,10 @@ function getLeadingExpressionWithPossibleParsingAmbiguity(expr: ts.Node): ts.Exp } } +export function expressionNeedsParens(expr: ts.Expression): boolean { + return expressionNeedsParensWhenReplacingNode(expr, expr); +} + export function expressionNeedsParensWhenReplacingNode(expr: ts.Expression, replaced: ts.Expression): boolean { // this currently doesn't handle the following cases // (yield) as any diff --git a/packages/mimir/test/prefer-exponentiation-operator/.wotanrc.yaml b/packages/mimir/test/prefer-exponentiation-operator/.wotanrc.yaml new file mode 100644 index 000000000..f706856ff --- /dev/null +++ b/packages/mimir/test/prefer-exponentiation-operator/.wotanrc.yaml @@ -0,0 +1,2 @@ +rules: + prefer-exponentiation-operator: error diff --git a/packages/mimir/test/prefer-exponentiation-operator/default.test.json b/packages/mimir/test/prefer-exponentiation-operator/default.test.json new file mode 100644 index 000000000..9b5cd77fa --- /dev/null +++ b/packages/mimir/test/prefer-exponentiation-operator/default.test.json @@ -0,0 +1,3 @@ +{ + "files": ["test.ts"] +} diff --git a/packages/mimir/test/prefer-exponentiation-operator/test.ts b/packages/mimir/test/prefer-exponentiation-operator/test.ts new file mode 100644 index 000000000..6c243a02e --- /dev/null +++ b/packages/mimir/test/prefer-exponentiation-operator/test.ts @@ -0,0 +1,42 @@ +Math.pow(1, 2); +Math.pow((1), (2)); +1 + Math.pow(1, 2); + +2 ** Math.pow(3, 2); +Math.pow(2, 3) ** 2; +Math.pow(2, Math.pow(3, 2)); // TODO add parens + +Math.pow({prop: 1}.prop, 2); + +Math.pow(1 + 1,2 + 2); +Math.pow(1, 2); +Math.pow(1, 2) as number; +Math.pow(1, 2 as number); +Math.pow(1, Boolean() ? 1 : 2); +!Math.pow(1, 2); +Math.pow(1, 2).toString(); +Math.pow(1, 2); + +-Math.pow(1 + 1, 2 + 2); + +Math.pow(-1, 2); // TODO TS bug + +declare var args: [number, number]; +Math.pow(...args); +Math.pow(1); +Math.pow(1, ...[2]); + +async function* fn() { + Math.pow(await 1, await 2); // TODO TS bug + await Math.pow(1, 2); + await Math.pow(await 1, await 2); // TODO TS bug + Math.pow(yield, yield); // TODO TS bug + Math.pow(yield 1, yield 1); // TODO TS bug + yield Math.pow(1, 2); +} + +declare namespace foo { + function pow(a: number, b: number): number; +} + +foo.pow(1, 2);