Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npm install @e18e/web-features-codemods --save-dev
| `arrayIncludes` | Convert `indexOf` checks to `includes()` | `array.indexOf(item) !== -1` → `array.includes(item)` |
| `arrayToReversed` | Convert copy-and-reverse patterns to `toReversed()` | `array.slice().reverse()` → `array.toReversed()` |
| `arrayToSorted` | Convert copy-and-sort patterns to `toSorted()` | `array.slice().sort()` → `array.toSorted()` |
| `arrayToSpliced` | Convert copy-and-splice patterns to `toSpliced()` | `array.slice().splice(...)` → `array.toSpliced(...)` |
| `arrayToSpliced` | Convert copy-and-splice patterns to `toSpliced()` | `const copy = arr.slice(); copy.splice(0, 1);` → `const copy = arr.toSpliced(0, 1);` |
| `exponentiation` | Convert `Math.pow()` to exponentiation operator | `Math.pow(base, exp)` → `base ** exp` |
| `nullishCoalescing` | Convert null/undefined checks to nullish coalescing | `value != null ? value : default` → `value ?? default` |
| `objectHasOwn` | Convert `hasOwnProperty` to `Object.hasOwn()` | `obj.hasOwnProperty(prop)` → `Object.hasOwn(obj, prop)` |
Expand Down
63 changes: 41 additions & 22 deletions src/codemods/array-to-spliced.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,74 @@ import {codemod} from './array-to-spliced.js';
const {apply} = codemod;

describe('array-to-spliced', () => {
it('should transform concat().splice() to toSpliced()', () => {
const source = `arr.concat().splice(0, 1)`;
it('should transform concat() followed by splice() to toSpliced()', () => {
const source = `const copy = arr.concat();\ncopy.splice(0, 1);`;
const result = apply({source});
expect(result).toBe(`arr.toSpliced(0, 1)`);
expect(result).toBe(`const copy = arr.toSpliced(0, 1);\n`);
});

it('should transform slice().splice() to toSpliced()', () => {
const source = `arr.slice().splice(2, 3)`;
it('should transform slice() followed by splice() to toSpliced()', () => {
const source = `const copy = arr.slice();\ncopy.splice(2, 3);`;
const result = apply({source});
expect(result).toBe(`arr.toSpliced(2, 3)`);
expect(result).toBe(`const copy = arr.toSpliced(2, 3);\n`);
});

it('should transform slice(0).splice() to toSpliced()', () => {
const source = `arr.slice(0).splice(1, 2)`;
it('should transform slice(0) followed by splice() to toSpliced()', () => {
const source = `const copy = arr.slice(0);\ncopy.splice(1, 2);`;
const result = apply({source});
expect(result).toBe(`arr.toSpliced(1, 2)`);
expect(result).toBe(`const copy = arr.toSpliced(1, 2);\n`);
});

it('should transform [...arr].splice() to toSpliced()', () => {
const source = `[...arr].splice(3, 4)`;
it('should transform spread followed by splice() to toSpliced()', () => {
const source = `const copy = [...arr];\ncopy.splice(3, 4);`;
const result = apply({source});
expect(result).toBe(`arr.toSpliced(3, 4)`);
expect(result).toBe(`const copy = arr.toSpliced(3, 4);\n`);
});

it('should handle multiple arguments in splice', () => {
const source = `arr.concat().splice(1, 2, 'a', 'b')`;
const source = `const copy = arr.concat();\ncopy.splice(1, 2, 'a', 'b');`;
const result = apply({source});
expect(result).toBe(`arr.toSpliced(1, 2, 'a', 'b')`);
expect(result).toBe(`const copy = arr.toSpliced(1, 2, 'a', 'b');\n`);
});

it('should not transform unrelated code', () => {
const source = `arr.splice(0, 1)`;
it('should not transform when variable names do not match', () => {
const source = `const copy = arr.concat();\nother.splice(0, 1);`;
const result = apply({source});
expect(result).toBe(`arr.splice(0, 1)`);
expect(result).toBe(`const copy = arr.concat();\nother.splice(0, 1);`);
});

it('should not transform direct splice() calls without cloning', () => {
const source = `arr.splice(0, 1);`;
const result = apply({source});
expect(result).toBe(`arr.splice(0, 1);`);
});

it('should handle multiple transformations in the same file', () => {
const source = `const a = arr1.slice();\na.splice(0, 1);\nconst b = arr2.concat();\nb.splice(2, 3);`;
const result = apply({source});
expect(result).toBe(
`const a = arr1.toSpliced(0, 1);\n\nconst b = arr2.toSpliced(2, 3);\n`
);
});

describe('test', () => {
it('should detect slice().splice() pattern', () => {
const source = `arr.slice().splice(0, 1)`;
it('should detect slice() followed by splice() pattern', () => {
const source = `const copy = arr.slice();\ncopy.splice(0, 1);`;
expect(codemod.test({source})).toBe(true);
});

it('should detect [...arr].splice() pattern', () => {
const source = `[...arr].splice(0, 1)`;
it('should detect spread followed by splice() pattern', () => {
const source = `const copy = [...arr];\ncopy.splice(0, 1);`;
expect(codemod.test({source})).toBe(true);
});

it('should not detect direct splice() calls', () => {
const source = `arr.splice(0, 1)`;
const source = `arr.splice(0, 1);`;
expect(codemod.test({source})).toBe(false);
});

it('should not detect when variable names do not match', () => {
const source = `const copy = arr.slice();\nother.splice(0, 1);`;
expect(codemod.test({source})).toBe(false);
});

Expand Down
75 changes: 64 additions & 11 deletions src/codemods/array-to-spliced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,62 @@ import {parse, Lang, type Edit, type NapiConfig} from '@ast-grep/napi';
import type {Options, CodeMod} from '../shared.js';
import {getNodesSourceText} from '../typescript-utils.js';

const arrayClonePatterns = [
{pattern: 'const $NAME = $ARRAY.concat();'},
{pattern: 'const $NAME = $ARRAY.slice();'},
{pattern: 'const $NAME = $ARRAY.slice(0);'},
{pattern: 'const $NAME = [...$ARRAY];'}
];

const arrayToSplicedRule: NapiConfig = {
rule: {
any: [
all: [
{
pattern: '$ARRAY.concat().splice($$$ARGS)'
any: arrayClonePatterns
},
{
pattern: '$ARRAY.slice().splice($$$ARGS)'
},
precedes: {
pattern: '$ARRSPLICE.splice($$$ARGS);'
}
}
]
}
};

const createSpliceStatementRule = (name: string): NapiConfig => ({
rule: {
all: [
{
pattern: '$ARRAY.slice(0).splice($$$ARGS)'
pattern: `${name}.splice($$$ARGS);`
},
{
pattern: '[...$ARRAY].splice($$$ARGS)'
follows: {
any: arrayClonePatterns.map((p) => ({
pattern: p.pattern.replace('$NAME', name)
}))
}
}
]
}
};
});

export const codemod: CodeMod = {
test(options: Options): boolean {
const ast = parse(Lang.TypeScript, options.source);
const root = ast.root();

return root.has(arrayToSplicedRule);
const nodes = root.findAll(arrayToSplicedRule);

for (const node of nodes) {
const name = node.getMatch('NAME');
const arraySplice = node.getMatch('ARRSPLICE');

if (name && arraySplice && name.text() === arraySplice.text()) {
return true;
}
}

return false;
},
apply(options: Options): string {
const ast = parse(Lang.TypeScript, options.source);
Expand All @@ -36,12 +67,34 @@ export const codemod: CodeMod = {
const edits: Edit[] = [];

for (const node of nodes) {
const name = node.getMatch('NAME');
const array = node.getMatch('ARRAY');
const arraySplice = node.getMatch('ARRSPLICE');

if (!name || !array || !arraySplice) {
continue;
}

const args = node.getMultipleMatches('ARGS');
const nameText = name.text();
const arraySpliceText = arraySplice.text();

if (nameText !== arraySpliceText) {
continue;
}

const argsText = getNodesSourceText(options.source, args);
if (array) {
const edit = node.replace(`${array.text()}.toSpliced(${argsText})`);
edits.push(edit);

const replaceEdit = node.replace(
`const ${nameText} = ${array.text()}.toSpliced(${argsText});`
);
edits.push(replaceEdit);

const spliceStatement = root.find(createSpliceStatementRule(nameText));

if (spliceStatement) {
const deleteEdit = spliceStatement.replace('');
edits.push(deleteEdit);
}
}

Expand Down