diff --git a/docs/rules/prefer-object-has-own.md b/docs/rules/prefer-object-has-own.md new file mode 100644 index 00000000000..4c90078c063 --- /dev/null +++ b/docs/rules/prefer-object-has-own.md @@ -0,0 +1,53 @@ +# Prefer `Object.hasOwn()` over `Object.prototype.hasOwnProperty.call()` (prefer-object-has-own) + +It is very common to write code like: + +```js +if (Object.prototype.hasOwnProperty.call(object, "foo")) { + console.log("has property foo"); +} +``` + +This is a common practice because methods on `Object.prototype` can sometimes be unavailable or redefined (see the [no-prototype-builtins](no-prototype-builtins.md) rule). + +Introduced in ES2022, `Object.hasOwn()` is a shorter alternative to `Object.prototype.hasOwnProperty.call()`: + +```js +if (Object.hasOwn(object, "foo")) { + console.log("has property foo") +} +``` + +## Rule Details + +Examples of **incorrect** code for this rule: + +```js +/*eslint prefer-object-has-own: "error"*/ + +Object.prototype.hasOwnProperty.call(obj, "a"); + +Object.hasOwnProperty.call(obj, "a"); + +({}).hasOwnProperty.call(obj, "a"); + +const hasProperty = Object.prototype.hasOwnProperty.call(object, property); +``` + +Examples of **correct** code for this rule: + +```js +/*eslint prefer-object-has-own: "error"*/ + +Object.hasOwn(obj, "a"); + +const hasProperty = Object.hasOwn(object, property); +``` + +## When Not To Use It + +This rule should not be used unless ES2022 is supported in your codebase. + +## Further Reading + +* [Object.hasOwn()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn) diff --git a/lib/rules/index.js b/lib/rules/index.js index ed322a4120a..130b635c972 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -255,6 +255,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "prefer-exponentiation-operator": () => require("./prefer-exponentiation-operator"), "prefer-named-capture-group": () => require("./prefer-named-capture-group"), "prefer-numeric-literals": () => require("./prefer-numeric-literals"), + "prefer-object-has-own": () => require("./prefer-object-has-own"), "prefer-object-spread": () => require("./prefer-object-spread"), "prefer-promise-reject-errors": () => require("./prefer-promise-reject-errors"), "prefer-reflect": () => require("./prefer-reflect"), diff --git a/lib/rules/prefer-object-has-own.js b/lib/rules/prefer-object-has-own.js new file mode 100644 index 00000000000..dd21e95d47e --- /dev/null +++ b/lib/rules/prefer-object-has-own.js @@ -0,0 +1,97 @@ +/** + * @fileoverview Prefers Object.hasOwn() instead of Object.prototype.hasOwnProperty.call() + * @author Nitin Kumar + * @author Gautam Arora + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + +/** + * Checks if the given node is considered to be an access to a property of `Object.prototype`. + * @param {ASTNode} node `MemberExpression` node to evaluate. + * @returns {boolean} `true` if `node.object` is `Object`, `Object.prototype`, or `{}` (empty 'ObjectExpression' node). + */ +function hasLeftHandObject(node) { + + /* + * ({}).hasOwnProperty.call(obj, prop) - `true` + * ({ foo }.hasOwnProperty.call(obj, prop)) - `false`, object literal should be empty + */ + if (node.object.type === "ObjectExpression" && node.object.properties.length === 0) { + return true; + } + + const objectNodeToCheck = node.object.type === "MemberExpression" && astUtils.getStaticPropertyName(node.object) === "prototype" ? node.object.object : node.object; + + if (objectNodeToCheck.type === "Identifier" && objectNodeToCheck.name === "Object") { + return true; + } + + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('../shared/types').Rule} */ +module.exports = { + meta: { + type: "suggestion", + docs: { + description: + "disallow use of `Object.prototype.hasOwnProperty.call()` and prefer use of `Object.hasOwn()`", + recommended: false, + url: "https://eslint.org/docs/rules/prefer-object-has-own" + }, + schema: [], + messages: { + useHasOwn: "Use 'Object.hasOwn()' instead of 'Object.prototype.hasOwnProperty.call()'." + }, + fixable: "code" + }, + create(context) { + return { + CallExpression(node) { + if (!(node.callee.type === "MemberExpression" && node.callee.object.type === "MemberExpression")) { + return; + } + + const calleePropertyName = astUtils.getStaticPropertyName(node.callee); + const objectPropertyName = astUtils.getStaticPropertyName(node.callee.object); + const isObject = hasLeftHandObject(node.callee.object); + + // check `Object` scope + const scope = context.getScope(); + const variable = astUtils.getVariableByName(scope, "Object"); + + if ( + calleePropertyName === "call" && + objectPropertyName === "hasOwnProperty" && + isObject && + variable && variable.scope.type === "global" + ) { + context.report({ + node, + messageId: "useHasOwn", + fix(fixer) { + const sourceCode = context.getSourceCode(); + + if (sourceCode.getCommentsInside(node.callee).length > 0) { + return null; + } + + return fixer.replaceText(node.callee, "Object.hasOwn"); + } + }); + } + } + }; + } +}; diff --git a/tests/lib/rules/prefer-object-has-own.js b/tests/lib/rules/prefer-object-has-own.js new file mode 100644 index 00000000000..a34cf774555 --- /dev/null +++ b/tests/lib/rules/prefer-object-has-own.js @@ -0,0 +1,349 @@ +/** + * @fileoverview Tests for prefer-object-has-own rule. + * @author Nitin Kumar + * @author Gautam Arora + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/prefer-object-has-own"); +const { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const parserOptions = { + ecmaVersion: 2022 +}; + +const ruleTester = new RuleTester({ parserOptions }); + +ruleTester.run("prefer-object-has-own", rule, { + valid: [ + "Object", + "Object(obj, prop)", + "Object.hasOwnProperty", + "Object.hasOwnProperty(prop)", + "hasOwnProperty(obj, prop)", + "foo.hasOwnProperty(prop)", + "foo.hasOwnProperty(obj, prop)", + "Object.hasOwnProperty.call", + "foo.Object.hasOwnProperty.call(obj, prop)", + "foo.hasOwnProperty.call(obj, prop)", + "foo.call(Object.prototype.hasOwnProperty, Object.prototype.hasOwnProperty.call)", + "Object.foo.call(obj, prop)", + "Object.hasOwnProperty.foo(obj, prop)", + "Object.hasOwnProperty.call.foo(obj, prop)", + "Object[hasOwnProperty].call(obj, prop)", + "Object.hasOwnProperty[call](obj, prop)", + "class C { #hasOwnProperty; foo() { Object.#hasOwnProperty.call(obj, prop) } }", + "class C { #call; foo() { Object.hasOwnProperty.#call(obj, prop) } }", + "(Object) => Object.hasOwnProperty.call(obj, prop)", // not global Object + "Object.prototype", + "Object.prototype(obj, prop)", + "Object.prototype.hasOwnProperty", + "Object.prototype.hasOwnProperty(obj, prop)", + "Object.prototype.hasOwnProperty.call", + "foo.Object.prototype.hasOwnProperty.call(obj, prop)", + "foo.prototype.hasOwnProperty.call(obj, prop)", + "Object.foo.hasOwnProperty.call(obj, prop)", + "Object.prototype.foo.call(obj, prop)", + "Object.prototype.hasOwnProperty.foo(obj, prop)", + "Object.prototype.hasOwnProperty.call.foo(obj, prop)", + "Object.prototype.prototype.hasOwnProperty.call(a, b);", + "Object.hasOwnProperty.prototype.hasOwnProperty.call(a, b);", + "Object.prototype[hasOwnProperty].call(obj, prop)", + "Object.prototype.hasOwnProperty[call](obj, prop)", + "class C { #hasOwnProperty; foo() { Object.prototype.#hasOwnProperty.call(obj, prop) } }", + "class C { #call; foo() { Object.prototype.hasOwnProperty.#call(obj, prop) } }", + "Object[prototype].hasOwnProperty.call(obj, prop)", + "class C { #prototype; foo() { Object.#prototype.hasOwnProperty.call(obj, prop) } }", + "(Object) => Object.prototype.hasOwnProperty.call(obj, prop)", // not global Object + "({})", + "({}(obj, prop))", + "({}.hasOwnProperty)", + "({}.hasOwnProperty(prop))", + "({}.hasOwnProperty(obj, prop))", + "({}.hasOwnProperty.call)", + "({}).prototype.hasOwnProperty.call(a, b);", + "({}.foo.call(obj, prop))", + "({}.hasOwnProperty.foo(obj, prop))", + "({}[hasOwnProperty].call(obj, prop))", + "({}.hasOwnProperty[call](obj, prop))", + "({}).hasOwnProperty[call](object, property)", + "({})[hasOwnProperty].call(object, property)", + "class C { #hasOwnProperty; foo() { ({}.#hasOwnProperty.call(obj, prop)) } }", + "class C { #call; foo() { ({}.hasOwnProperty.#call(obj, prop)) } }", + "({ foo }.hasOwnProperty.call(obj, prop))", // object literal should be empty + "(Object) => ({}).hasOwnProperty.call(obj, prop)", // Object is shadowed, so Object.hasOwn cannot be used here + ` + let obj = {}; + Object.hasOwn(obj,""); + `, + "const hasProperty = Object.hasOwn(object, property);", + `/* global Object: off */ + ({}).hasOwnProperty.call(a, b);` + ], + invalid: [ + { + code: "Object.hasOwnProperty.call(obj, 'foo')", + output: "Object.hasOwn(obj, 'foo')", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 39 + }] + }, + { + code: "Object.hasOwnProperty.call(obj, property)", + output: "Object.hasOwn(obj, property)", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 42 + }] + }, + { + code: "Object.prototype.hasOwnProperty.call(obj, 'foo')", + output: "Object.hasOwn(obj, 'foo')", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 49 + }] + }, + { + code: "({}).hasOwnProperty.call(obj, 'foo')", + output: "Object.hasOwn(obj, 'foo')", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 37 + }] + }, + + // prevent autofixing if there are any comments + { + code: "Object/* comment */.prototype.hasOwnProperty.call(a, b);", + output: null, + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 56 + }] + }, + { + code: "const hasProperty = Object.prototype.hasOwnProperty.call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 75 + }] + }, + { + code: "const hasProperty = (( Object.prototype.hasOwnProperty.call(object, property) ));", + output: "const hasProperty = (( Object.hasOwn(object, property) ));", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 24, + endLine: 1, + endColumn: 78 + }] + }, + { + code: "const hasProperty = (( Object.prototype.hasOwnProperty.call ))(object, property);", + output: "const hasProperty = (( Object.hasOwn ))(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 81 + }] + }, + { + code: "const hasProperty = (( Object.prototype.hasOwnProperty )).call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 81 + }] + }, + { + code: "const hasProperty = (( Object.prototype )).hasOwnProperty.call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 81 + }] + }, + { + code: "const hasProperty = (( Object )).prototype.hasOwnProperty.call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 81 + }] + }, + { + code: "const hasProperty = {}.hasOwnProperty.call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 61 + }] + }, + { + code: "const hasProperty = (( {}.hasOwnProperty.call(object, property) ));", + output: "const hasProperty = (( Object.hasOwn(object, property) ));", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 24, + endLine: 1, + endColumn: 64 + }] + }, + { + code: "const hasProperty = (( {}.hasOwnProperty.call ))(object, property);", + output: "const hasProperty = (( Object.hasOwn ))(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 67 + }] + }, + { + code: "const hasProperty = (( {}.hasOwnProperty )).call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 67 + }] + }, + { + code: "const hasProperty = (( {} )).hasOwnProperty.call(object, property);", + output: "const hasProperty = Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 21, + endLine: 1, + endColumn: 67 + }] + }, + { + code: "function foo(){return {}.hasOwnProperty.call(object, property)}", + output: "function foo(){return Object.hasOwn(object, property)}", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 23, + endLine: 1, + endColumn: 63 + }] + }, + { + code: "Object['prototype']['hasOwnProperty']['call'](object, property);", + output: "Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 64 + }] + }, + { + code: "Object[`prototype`][`hasOwnProperty`][`call`](object, property);", + output: "Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 64 + }] + }, + { + code: "Object['hasOwnProperty']['call'](object, property);", + output: "Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 51 + }] + }, + { + code: "Object[`hasOwnProperty`][`call`](object, property);", + output: "Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 51 + }] + }, + { + code: "({})['hasOwnProperty']['call'](object, property);", + output: "Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 49 + }] + }, + { + code: "({})[`hasOwnProperty`][`call`](object, property);", + output: "Object.hasOwn(object, property);", + errors: [{ + messageId: "useHasOwn", + line: 1, + column: 1, + endLine: 1, + endColumn: 49 + }] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 4a71a8b09ed..85484c49210 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -242,6 +242,7 @@ "prefer-exponentiation-operator": "suggestion", "prefer-named-capture-group": "suggestion", "prefer-numeric-literals": "suggestion", + "prefer-object-has-own": "suggestion", "prefer-object-spread": "suggestion", "prefer-promise-reject-errors": "suggestion", "prefer-reflect": "suggestion",