Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefer-object-has-own rule #1322

Merged
merged 5 commits into from
Jun 2, 2021
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
58 changes: 58 additions & 0 deletions docs/rules/prefer-object-has-own.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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']}]
}
]
});
Loading