Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
<number>(1** 2);
1** 2 as number;
(<number>1)** (2 as number);
1** (Boolean() ? 1 : 2);
!(1** 2);
(1** 2).toString();
<number>(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);
Original file line number Diff line number Diff line change
@@ -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'.]
<number>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(<number>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'.]
<number>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);
1 change: 1 addition & 0 deletions packages/mimir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/mimir/recommended.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions packages/mimir/src/rules/prefer-exponentiation-operator.ts
Original file line number Diff line number Diff line change
@@ -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 &&
(<ts.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 &&
(<ts.BinaryExpression>grandparent.parent).operatorToken.kind === ts.SyntaxKind.AsteriskAsteriskToken &&
(<ts.BinaryExpression>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,
);
}
}
}
4 changes: 4 additions & 0 deletions packages/mimir/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rules:
prefer-exponentiation-operator: error
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"files": ["test.ts"]
}
42 changes: 42 additions & 0 deletions packages/mimir/test/prefer-exponentiation-operator/test.ts
Original file line number Diff line number Diff line change
@@ -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);
<number>Math.pow(1, 2);
Math.pow(1, 2) as number;
Math.pow(<number>1, 2 as number);
Math.pow(1, Boolean() ? 1 : 2);
!Math.pow(1, 2);
Math.pow(1, 2).toString();
<number>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);