Skip to content

Commit

Permalink
Add prefer-object-has-own rule (#1322)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Jun 2, 2021
1 parent e38769c commit ca34b40
Show file tree
Hide file tree
Showing 11 changed files with 618 additions and 34 deletions.
58 changes: 58 additions & 0 deletions docs/rules/prefer-object-has-own.md
@@ -0,0 +1,58 @@
# Prefer `Object.hasOwn(…)` over `Object.prototype.hasOwnProperty.call(…)`

[`Object.hasOwn(…)`](https://github.com/tc39/proposal-accessible-object-hasownproperty) is more accessible than `Object.prototype.hasOwnProperty.call(…)`.

This rule is fixable.

## Fail

```js
const hasProperty = Object.prototype.hasOwnProperty.call(object, property);
```

```js
const hasProperty = {}.hasOwnProperty.call(object, property);
```

```js
const hasProperty = lodash.has(object, property);
```

## Pass

```js
const hasProperty = Object.hasOwn(object, property);
```

## Options

Type: `object`

### functions

Type: `string[]`

You can also check custom functions that indicating the object has the specified property as its own property.

`_.has()`, `lodash.has()`, and `underscore.has()` are checked by default.

Example:

```js
{
'unicorn/prefer-object-has-own': [
'error',
{
functions: [
'has',
'utils.has',
]
}
]
}
```

```js
// eslint unicorn/prefer-object-has-own: ["error", {"functions": ["utils.has"]}]
const hasProperty = utils.has(object, property); // Fails
```
2 changes: 2 additions & 0 deletions index.js
Expand Up @@ -101,6 +101,8 @@ module.exports = {
'unicorn/prefer-negative-index': 'error',
'unicorn/prefer-node-protocol': 'error',
'unicorn/prefer-number-properties': 'error',
// TODO: Enable this by default when targeting Node.js support `Object.hasOwn`.
'unicorn/prefer-object-has-own': 'off',
'unicorn/prefer-optional-catch-binding': 'error',
'unicorn/prefer-prototype-methods': 'error',
'unicorn/prefer-query-selector': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -97,6 +97,7 @@ Configure it in `package.json`.
"unicorn/prefer-negative-index": "error",
"unicorn/prefer-node-protocol": "error",
"unicorn/prefer-number-properties": "error",
"unicorn/prefer-object-has-own": "off",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-query-selector": "error",
Expand Down Expand Up @@ -196,6 +197,7 @@ Each rule has emojis denoting:
| [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`. || 🔧 | |
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. || 🔧 | |
| [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. || 🔧 | 💡 |
| [prefer-object-has-own](docs/rules/prefer-object-has-own.md) | Prefer `Object.hasOwn(…)` over `Object.prototype.hasOwnProperty.call(…)`. | | 🔧 | |
| [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. || 🔧 | |
| [prefer-prototype-methods](docs/rules/prefer-prototype-methods.md) | Prefer borrowing methods from the prototype instead of the instance. || 🔧 | |
| [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. || 🔧 | |
Expand Down
96 changes: 96 additions & 0 deletions rules/prefer-object-has-own.js
@@ -0,0 +1,96 @@
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const {isNodeMatches, isNodeMatchesNameOrPath} = require('./utils/is-node-matches');
const {
objectPrototypeMethodSelector,
methodCallSelector,
callExpressionSelector
} = require('./selectors');

const MESSAGE_ID = 'prefer-object-has-own';
const messages = {
[MESSAGE_ID]: 'Use `Object.hasOwn(…)` instead of `{{description}}(…)`.'
};

const objectPrototypeHasOwnProperty = [
methodCallSelector({name: 'call', length: 2}),
' > ',
objectPrototypeMethodSelector({
path: 'object',
name: 'hasOwnProperty'
}),
'.callee'
].join('');

const lodashHasFunctions = [
'_.has',
'lodash.has',
'underscore.has'
];

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {functions: configFunctions} = {
functions: [],
...context.options[0]
};
const functions = [...configFunctions, ...lodashHasFunctions];

return Object.fromEntries(
[
{
selector: objectPrototypeHasOwnProperty,
description: 'Object.prototype.hasOwnProperty.call'
},
{
selector: `${callExpressionSelector({length: 2})} > .callee`,
test: node => isNodeMatches(node, functions),
description: node => functions.find(nameOrPath => isNodeMatchesNameOrPath(node, nameOrPath)).trim()
}
].map(({selector, test, description}) => [
selector,
node => {
if (test && !test(node)) {
return;
}

context.report({
node,
messageId: MESSAGE_ID,
data: {
description: typeof description === 'string' ? description : description(node)
},
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => fixer.replaceText(node, 'Object.hasOwn')
});
}
])
);
};

const schema = [
{
type: 'object',
properties: {
functions: {
type: 'array',
uniqueItems: true
}
},
additionalProperties: false
}
];

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `Object.hasOwn(…)` over `Object.prototype.hasOwnProperty.call(…)`.',
url: getDocumentationUrl(__filename)
},
fixable: 'code',
schema,
messages
}
};
33 changes: 0 additions & 33 deletions rules/selectors/array-prototype-method-selector.js

This file was deleted.

11 changes: 11 additions & 0 deletions rules/selectors/empty-object-selector.js
@@ -0,0 +1,11 @@
'use strict';

function emptyObjectSelector(path) {
const prefix = `${path}.`;
return [
`[${prefix}type="ObjectExpression"]`,
`[${prefix}properties.length=0]`
].join('');
}

module.exports = emptyObjectSelector;
3 changes: 2 additions & 1 deletion rules/selectors/index.js
Expand Up @@ -5,7 +5,8 @@ module.exports = {
matches: require('./matches-any'),
not: require('./negation'),

arrayPrototypeMethodSelector: require('./array-prototype-method-selector'),
arrayPrototypeMethodSelector: require('./prototype-method-selector').arrayPrototypeMethodSelector,
objectPrototypeMethodSelector: require('./prototype-method-selector').objectPrototypeMethodSelector,
emptyArraySelector: require('./empty-array-selector'),
memberExpressionSelector: require('./member-expression-selector'),
methodCallSelector: require('./method-call-selector'),
Expand Down
53 changes: 53 additions & 0 deletions rules/selectors/prototype-method-selector.js
@@ -0,0 +1,53 @@
'use strict';
const matches = require('./matches-any');
const memberExpressionSelector = require('./member-expression-selector');
const emptyArraySelector = require('./empty-array-selector');
const emptyObjectSelector = require('./empty-object-selector');

function prototypeMethodSelector(options) {
const {
object,
name,
names,
path
} = {
path: '',
name: '',
...options
};

const objectPath = path ? `${path}.object` : 'object';

const prototypeSelectors = [
memberExpressionSelector({path: objectPath, name: 'prototype', object})
];

switch (object) {
case 'Array':
// `[].method` or `Array.prototype.method`
prototypeSelectors.push(emptyArraySelector(objectPath));
break;
case 'Object':
// `{}.method` or `Object.prototype.method`
prototypeSelectors.push(emptyObjectSelector(objectPath));
break;
// No default
}

return [
memberExpressionSelector({
path,
name,
names
}),
matches(prototypeSelectors)
].join('');
}

const arrayPrototypeMethodSelector = options => prototypeMethodSelector({...options, object: 'Array'});
const objectPrototypeMethodSelector = options => prototypeMethodSelector({...options, object: 'Object'});

module.exports = {
arrayPrototypeMethodSelector,
objectPrototypeMethodSelector
};
93 changes: 93 additions & 0 deletions test/prefer-object-has-own.mjs
@@ -0,0 +1,93 @@
import {getTester} from './utils/test.mjs';

const {test} = getTester(import.meta);

test.snapshot({
valid: [
'const hasProperty = Object.hasOwn(object, property);',

// CallExpression
'Object.prototype.hasOwnProperty.call',
'({}).hasOwnProperty.call',
'foo.call(Object.prototype.hasOwnProperty, Object.prototype.hasOwnProperty.call)',

// Arguments
'Object.prototype.hasOwnProperty.call(object)',
'Object.prototype.hasOwnProperty.call()',
'Object.prototype.hasOwnProperty.call(object, property, extraArgument)',
'Object.prototype.hasOwnProperty.call(...[object, property])',
'({}).hasOwnProperty.call(object)',
'({}).hasOwnProperty.call()',
'({}).hasOwnProperty.call(object, property, extraArgument)',
'({}).hasOwnProperty.call(...[object, property])',

// Optional
'Object.prototype.hasOwnProperty.call?.(object, property)',
'Object.prototype.hasOwnProperty?.call(object, property)',
'Object.prototype?.hasOwnProperty.call(object, property)',
'Object?.prototype.hasOwnProperty.call(object, property)',
'({}).hasOwnProperty.call?.(object, property)',
'({}).hasOwnProperty?.call(object, property)',
'({})?.hasOwnProperty.call(object, property)',

// Computed
'Object.prototype.hasOwnProperty[call](object, property)',
'Object.prototype[hasOwnProperty].call(object, property)',
'Object[prototype].hasOwnProperty.call(object, property)',
'({}).hasOwnProperty[call](object, property)',
'({})[hasOwnProperty].call(object, property)',

// Names
'Object.prototype.hasOwnProperty.notCall(object, property)',
'Object.prototype.notHasOwnProperty.call(object, property)',
'Object.notPrototype.hasOwnProperty.call(object, property)',
'notObject.prototype.hasOwnProperty.call(object, property)',
'({}).hasOwnProperty.notCall(object, property)',
'({}).notHasOwnProperty.call(object, property)',

// Empty object
'({notEmpty}).hasOwnProperty.call(object, property)',
'([]).hasOwnProperty.call(object, property)'
],
invalid: [
'const hasProperty = Object.prototype.hasOwnProperty.call(object, property);',
'const hasProperty = Object.prototype.hasOwnProperty.call(object, property,);',
'const hasProperty = (( Object.prototype.hasOwnProperty.call(object, property) ));',
'const hasProperty = (( Object.prototype.hasOwnProperty.call ))(object, property);',
'const hasProperty = (( Object.prototype.hasOwnProperty )).call(object, property);',
'const hasProperty = (( Object.prototype )).hasOwnProperty.call(object, property);',
'const hasProperty = (( Object )).prototype.hasOwnProperty.call(object, property);',
'const hasProperty = {}.hasOwnProperty.call(object, property);',
'const hasProperty = (( {}.hasOwnProperty.call(object, property) ));',
'const hasProperty = (( {}.hasOwnProperty.call ))(object, property);',
'const hasProperty = (( {}.hasOwnProperty )).call(object, property);',
'const hasProperty = (( {} )).hasOwnProperty.call(object, property);'
]
});

// `functions`
test.snapshot({
valid: [
'_.has(object)',
'_.has()',
'_.has(object, property, extraArgument)',
'_.has',
'_.has?.(object, property)',
'_?.has(object, property)',
'foo.has(object, property)',
'foo._.has(object, property)'
],
invalid: [
'_.has(object, property)',
'lodash.has(object, property)',
'underscore.has(object, property)',
{
code: '_.has(object, property)',
options: [{functions: ['utils.has']}]
},
{
code: 'utils.has(object, property)',
options: [{functions: ['utils.has']}]
}
]
});

0 comments on commit ca34b40

Please sign in to comment.