From 43ad6db0ddeefb7e9150f9e0271bf18c9efddfa9 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:47:18 +0000 Subject: [PATCH] feat: add exponentiation Adds a `Math.pow(a, b)` to `a ** b` codemod. --- .../__snapshots__/exponentiation.test.ts.snap | 27 +++++++++ src/codemods/exponentiation.test.ts | 57 +++++++++++++++++++ src/codemods/exponentiation.ts | 51 +++++++++++++++++ src/main.ts | 1 + 4 files changed, 136 insertions(+) create mode 100644 src/codemods/__snapshots__/exponentiation.test.ts.snap create mode 100644 src/codemods/exponentiation.test.ts create mode 100644 src/codemods/exponentiation.ts diff --git a/src/codemods/__snapshots__/exponentiation.test.ts.snap b/src/codemods/__snapshots__/exponentiation.test.ts.snap new file mode 100644 index 0000000..c26d3b8 --- /dev/null +++ b/src/codemods/__snapshots__/exponentiation.test.ts.snap @@ -0,0 +1,27 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`exponentiation > should handle Math.pow in expressions 1`] = ` +"const result = (a + b) ** (c - d) * 2; + const value = 10 + (x) ** (y); + " +`; + +exports[`exponentiation > should handle nested Math.pow calls 1`] = ` +"const result = ((2) ** (3)) ** (4); + " +`; + +exports[`exponentiation > should not change code without Math.pow 1`] = ` +" + const result = x ** y; + const squared = 2 ** 3; + const value = Math.sqrt(4); + " +`; + +exports[`exponentiation > should replace Math.pow with exponentiation operator 1`] = ` +"const result = (2) ** (3); + const squared = (x) ** (2); + const complex = (base) ** (exponent); + " +`; diff --git a/src/codemods/exponentiation.test.ts b/src/codemods/exponentiation.test.ts new file mode 100644 index 0000000..98818b9 --- /dev/null +++ b/src/codemods/exponentiation.test.ts @@ -0,0 +1,57 @@ +import {describe, it, expect} from 'vitest'; +import {codemod} from './exponentiation.js'; + +describe('exponentiation', () => { + it('should replace Math.pow with exponentiation operator', () => { + const source = ` + const result = Math.pow(2, 3); + const squared = Math.pow(x, 2); + const complex = Math.pow(base, exponent); + `; + const result = codemod.apply({source}); + expect(result).toMatchSnapshot(); + }); + + it('should handle nested Math.pow calls', () => { + const source = ` + const result = Math.pow(Math.pow(2, 3), 4); + `; + const result = codemod.apply({source}); + expect(result).toMatchSnapshot(); + }); + + it('should handle Math.pow in expressions', () => { + const source = ` + const result = Math.pow(a + b, c - d) * 2; + const value = 10 + Math.pow(x, y); + `; + const result = codemod.apply({source}); + expect(result).toMatchSnapshot(); + }); + + it('should not change code without Math.pow', () => { + const source = ` + const result = x ** y; + const squared = 2 ** 3; + const value = Math.sqrt(4); + `; + const result = codemod.apply({source}); + expect(result).toMatchSnapshot(); + }); + + describe('test', () => { + it('should detect Math.pow usage', () => { + const source = ` + const result = Math.pow(2, 3); + `; + expect(codemod.test({source})).toBe(true); + }); + + it('should not detect when there is no Math.pow', () => { + const source = ` + const result = x ** y; + `; + expect(codemod.test({source})).toBe(false); + }); + }); +}); diff --git a/src/codemods/exponentiation.ts b/src/codemods/exponentiation.ts new file mode 100644 index 0000000..755fb31 --- /dev/null +++ b/src/codemods/exponentiation.ts @@ -0,0 +1,51 @@ +import {parse, Lang, type Edit, type NapiConfig} from '@ast-grep/napi'; +import type {Options, CodeMod} from '../shared.js'; + +const mathPowRule: NapiConfig = { + rule: { + pattern: 'Math.pow($BASE, $EXPONENT)' + } +}; + +export const codemod: CodeMod = { + test(options: Options): boolean { + const ast = parse(Lang.TypeScript, options.source); + const root = ast.root(); + + return root.has(mathPowRule); + }, + apply(options: Options): string { + let source = options.source; + + while (true) { + const ast = parse(Lang.TypeScript, source); + const root = ast.root(); + + const mathPowCalls = root.findAll(mathPowRule); + if (mathPowCalls.length === 0) { + break; + } + + const edits: Edit[] = []; + + for (const node of mathPowCalls) { + const base = node.getMatch('BASE'); + const exponent = node.getMatch('EXPONENT'); + if (base && exponent) { + const baseText = base.text(); + const exponentText = exponent.text(); + const edit = node.replace(`(${baseText}) ** (${exponentText})`); + edits.push(edit); + } + } + + const newSource = root.commitEdits(edits); + if (newSource === source) { + break; + } + source = newSource; + } + + return source; + } +}; diff --git a/src/main.ts b/src/main.ts index b70bf3a..ed54c29 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ export {codemod as arrayIncludes} from './codemods/array-includes.js'; export {codemod as arrayToReversed} from './codemods/array-to-reversed.js'; export {codemod as arrayToSorted} from './codemods/array-to-sorted.js'; export {codemod as arrayToSpliced} from './codemods/array-to-spliced.js'; +export {codemod as exponentiation} from './codemods/exponentiation.js'; export {codemod as nullishCoalescing} from './codemods/nullish-coalescing.js'; export {codemod as postcssSignFunctions} from './codemods/postcss-sign-functions.js'; export {codemod as stringIncludes} from './codemods/string-includes.js';