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-date-now rule #935

Merged
merged 16 commits into from Dec 12, 2020
37 changes: 37 additions & 0 deletions docs/rules/prefer-date-now.md
@@ -0,0 +1,37 @@
# Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch

[`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) is shorter and nicer than [`new Date().getTime()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime).

This rule is fixable.

## Fail

```js
const foo = new Date().getTime();
```

```js
const foo = new Date().valueOf();
```

```js
const foo = +new Date;
```

```js
const foo = Number(new Date());
```

```js
const foo = new Date() * 2;
```

## Pass

```js
const foo = Date.now();
```

```js
const foo = Date.now() * 2;
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -54,6 +54,7 @@ module.exports = {
'unicorn/prefer-add-event-listener': 'error',
'unicorn/prefer-array-find': 'error',
'unicorn/prefer-dataset': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-event-key': 'error',
// TODO: Enable this by default when targeting Node.js 12.
'unicorn/prefer-flat-map': 'off',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -69,6 +69,7 @@ Configure it in `package.json`.
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-array-find": "error",
"unicorn/prefer-dataset": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-event-key": "error",
"unicorn/prefer-flat-map": "error",
"unicorn/prefer-includes": "error",
Expand Down Expand Up @@ -135,6 +136,7 @@ Configure it in `package.json`.
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)*
- [prefer-array-find](docs/rules/prefer-array-find.md) - Prefer `.find(…)` over the first element from `.filter(…)`. *(partly fixable)*
- [prefer-dataset](docs/rules/prefer-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)*
- [prefer-date-now](docs/rules/prefer-date-now.md) - Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. *(fixable)*
- [prefer-event-key](docs/rules/prefer-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)*
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
Expand Down
114 changes: 114 additions & 0 deletions rules/prefer-date-now.js
@@ -0,0 +1,114 @@
'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const methodSelector = require('./utils/method-selector');

const MESSAGE_ID_DEFAULT = 'prefer-date';
const MESSAGE_ID_METHOD = 'prefer-date-now-over-methods';
const MESSAGE_ID_NUMBER = 'prefer-date-now-over-number-data-object';
const messages = {
[MESSAGE_ID_DEFAULT]: 'Prefer `Date.now()` over `new Date()`.',
[MESSAGE_ID_METHOD]: 'Prefer `Date.now()` over `Date#{{method}}()`.',
[MESSAGE_ID_NUMBER]: 'Prefer `Date.now()` over `Number(new Date())`.'
};

const createNewDateSelector = path => {
const prefix = path ? `${path}.` : '';
return [
`[${prefix}type="NewExpression"]`,
`[${prefix}callee.type="Identifier"]`,
`[${prefix}callee.name="Date"]`,
`[${prefix}arguments.length=0]`
].join('');
};

const operatorsSelector = (...operators) => `:matches(${
operators.map(operator => `[operator="${operator}"]`).join(', ')
})`;
const newDateSelector = createNewDateSelector();
const methodsSelector = [
methodSelector({
names: ['getTime', 'valueOf'],
length: 0
}),
createNewDateSelector('callee.object')
].join('');
const builtinObjectSelector = [
'CallExpression',
'[callee.type="Identifier"]',
':matches([callee.name="Number"], [callee.name="BigInt"])',
'[arguments.length=1]',
createNewDateSelector('arguments.0')
].join('');
// https://github.com/estree/estree/blob/master/es5.md#unaryoperator
const unaryExpressionsSelector = [
'UnaryExpression',
operatorsSelector('+', '-'),
createNewDateSelector('argument')
].join('');
const assignmentExpressionSelector = [
'AssignmentExpression',
operatorsSelector('-=', '*=', '/=', '%=', '**='),
'>',
`${newDateSelector}.right`
].join('');
const binaryExpressionSelector = [
'BinaryExpression',
operatorsSelector('-', '*', '/', '%', '**'),
// Both `left` and `right` properties
'>',
newDateSelector
].join('');

const create = context => {
const report = (node, problem) => context.report({
node,
messageId: MESSAGE_ID_DEFAULT,
fix: fixer => fixer.replaceText(node, 'Date.now()'),
...problem
});

return {
[methodsSelector](node) {
const method = node.callee.property;
report(node, {
node: method,
messageId: MESSAGE_ID_METHOD,
data: {method: method.name}
});
},
[builtinObjectSelector](node) {
const {name} = node.callee;
if (name === 'Number') {
report(node, {
messageId: MESSAGE_ID_NUMBER
});
} else {
report(node.arguments[0]);
}
},
[unaryExpressionsSelector](node) {
report(node.operator === '-' ? node.argument : node);
},
[assignmentExpressionSelector](node) {
report(node);
},
[binaryExpressionSelector](node) {
report(node);
}
};
};

const schema = [];

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
schema,
messages
}
};
104 changes: 104 additions & 0 deletions test/prefer-date-now.js
@@ -0,0 +1,104 @@
import {test} from './utils/test';

test({
valid: [
'const ts = Date.now()',

// Test `new Date()`
// Not `NewExpression`
'+Date()',
'+ Date',
// Not `Identifier`
'+ new window.Date()',
// Not `Date`
'+ new Moments()',
// More arguments
'+ new Date(0)',
'+ new Date(...[])',

// Test `new Date().getTime()` and `new Date().valueOf()`
// Not `CallExpression`
'new Date.getTime()',
// Not `MemberExpression`
'valueOf()',
// `computed`
'new Date()[getTime]()',
// Not `Identifier`
'new Date()["valueOf"]()',
// Not listed names
'new Date().notListed(0)',
// More arguments
'new Date().getTime(0)',
'new Date().valueOf(...[])',

// Test `Number(new Date())` and `BigInt(new Date())`
// Not `CallExpression`
'new Number(new Date())',
// Not `Identifier`
'window.BigInt(new Date())',
// Not listed names
'toNumber(new Date())',
// More/less arguments
'BigInt()',
'Number(new Date(), extraArgument)',
'BigInt([...new Date()])',

// Test `+ new Date()` / `- new Date()`
// Not `UnaryExpression`
'throw new Date()',
// Not `+/-`
'typeof new Date()',

// Test `AssignmentExpression`
// Not `AssignmentExpression`
'const foo = () => {return new Date()}',
// `operator` not listed
'foo += new Date()',

// Test `BinaryExpression`
// Not `BinaryExpression`
'function * foo() {yield new Date()}',
// `operator` not listed
'new Date() + new Date()',

// We are not checking these cases
'foo = new Date() | 0',
'foo &= new Date()',
'foo = new Date() >> 0'
],
invalid: []
});

test.visualize([
// `Date` methods
'const ts = new Date().getTime();',
'const ts = (new Date).getTime();',
'const ts = (new Date()).getTime();',
'const ts = new Date().valueOf();',
'const ts = (new Date).valueOf();',
'const ts = (new Date()).valueOf();',

// `Number()` and `BigInt()`
'const ts = /* 1 */ Number(/* 2 */ new /* 3 */ Date( /* 4 */ ) /* 5 */) /* 6 */',
'const tsBigInt = /* 1 */ BigInt(/* 2 */ new /* 3 */ Date( /* 4 */ ) /* 5 */) /* 6 */',

// `UnaryExpression`
'const ts = + /* 1 */ new Date;',
'const ts = - /* 1 */ new Date();',

// `BinaryExpression`
'const ts = new Date() - 0',
'const foo = bar - new Date',
'const foo = new Date() * bar',
'const ts = new Date() / 1',
'const ts = new Date() % Infinity',
'const ts = new Date() ** 1',
'const zero = (new Date(/* 1 */) /* 2 */) /* 3 */ - /* 4 */new Date',

// `AssignmentExpression`
'foo -= new Date()',
'foo *= new Date()',
'foo /= new Date',
'foo %= new Date()',
'foo **= new Date()'
]);