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
25 changes: 25 additions & 0 deletions src/codemods/__snapshots__/object-hasown.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`object-hasown > should handle both patterns in the same code 1`] = `
"const has1 = Object.hasOwn(obj, 'key');
const has2 = Object.hasOwn(obj, 'key');
"
`;

exports[`object-hasown > should not change code without hasOwnProperty usage 1`] = `
"const has = Object.hasOwn(obj, 'key');
const value = obj.key;
"
`;

exports[`object-hasown > should replace Object.prototype.hasOwnProperty.call() with Object.hasOwn() 1`] = `
"const has = Object.hasOwn(obj, 'key');
const hasAnother = Object.hasOwn(myObject, 'property');
"
`;

exports[`object-hasown > should replace obj.hasOwnProperty() with Object.hasOwn() 1`] = `
"const has = Object.hasOwn(obj, 'key');
const hasAnother = Object.hasOwn(myObject, 'property');
"
`;
63 changes: 63 additions & 0 deletions src/codemods/object-hasown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {describe, it, expect} from 'vitest';
import {codemod} from './object-hasown.js';

describe('object-hasown', () => {
it('should replace obj.hasOwnProperty() with Object.hasOwn()', () => {
const source = `
const has = obj.hasOwnProperty('key');
const hasAnother = myObject.hasOwnProperty('property');
`;
const result = codemod.apply({source});
expect(result).toMatchSnapshot();
});

it('should replace Object.prototype.hasOwnProperty.call() with Object.hasOwn()', () => {
const source = `
const has = Object.prototype.hasOwnProperty.call(obj, 'key');
const hasAnother = Object.prototype.hasOwnProperty.call(myObject, 'property');
`;
const result = codemod.apply({source});
expect(result).toMatchSnapshot();
});

it('should handle both patterns in the same code', () => {
const source = `
const has1 = obj.hasOwnProperty('key');
const has2 = Object.prototype.hasOwnProperty.call(obj, 'key');
`;
const result = codemod.apply({source});
expect(result).toMatchSnapshot();
});

it('should not change code without hasOwnProperty usage', () => {
const source = `
const has = Object.hasOwn(obj, 'key');
const value = obj.key;
`;
const result = codemod.apply({source});
expect(result).toMatchSnapshot();
});

describe('test', () => {
it('should detect obj.hasOwnProperty()', () => {
const source = `
const has = obj.hasOwnProperty('key');
`;
expect(codemod.test({source})).toBe(true);
});

it('should detect Object.prototype.hasOwnProperty.call()', () => {
const source = `
const has = Object.prototype.hasOwnProperty.call(obj, 'key');
`;
expect(codemod.test({source})).toBe(true);
});

it('should not detect when there is no hasOwnProperty usage', () => {
const source = `
const has = Object.hasOwn(obj, 'key');
`;
expect(codemod.test({source})).toBe(false);
});
});
});
40 changes: 40 additions & 0 deletions src/codemods/object-hasown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {parse, Lang, type Edit, type NapiConfig} from '@ast-grep/napi';
import type {Options, CodeMod} from '../shared.js';

const hasOwnPropertyRule: NapiConfig = {
rule: {
any: [
{pattern: '$OBJECT.hasOwnProperty($PROPERTY)'},
{pattern: 'Object.prototype.hasOwnProperty.call($OBJECT, $PROPERTY)'}
]
}
};

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

return root.has(hasOwnPropertyRule);
},
apply(options: Options): string {
const ast = parse(Lang.TypeScript, options.source);
const root = ast.root();
const edits: Edit[] = [];

const hasOwnPropertyCalls = root.findAll(hasOwnPropertyRule);

for (const node of hasOwnPropertyCalls) {
const object = node.getMatch('OBJECT');
const property = node.getMatch('PROPERTY');
if (object && property) {
const edit = node.replace(
`Object.hasOwn(${object.text()}, ${property.text()})`
);
edits.push(edit);
}
}

return root.commitEdits(edits);
}
};
8 changes: 6 additions & 2 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ describe('main', () => {
});

for (const codemod of expectedCodemods) {
expect(codemods).toHaveProperty(codemod);
const codemodObj = codemods[codemod as keyof typeof codemods];
// Find the matching export (case-insensitive)
const actualKey = Object.keys(codemods).find(
(key) => key.toLowerCase() === codemod.toLowerCase()
);
expect(actualKey).toBeDefined();
const codemodObj = codemods[actualKey as keyof typeof codemods];
expect(typeof codemodObj).toBe('object');
expect(typeof codemodObj.test).toBe('function');
expect(typeof codemodObj.apply).toBe('function');
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 objectHasOwn} from './codemods/object-hasown.js';
export {codemod as postcssSignFunctions} from './codemods/postcss-sign-functions.js';
export {codemod as spreadSyntax} from './codemods/spread-syntax.js';
export {codemod as stringIncludes} from './codemods/string-includes.js';