Skip to content

Commit

Permalink
Add prefer-date-now rule (#935)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Dec 12, 2020
1 parent 2ecc766 commit d0c4826
Show file tree
Hide file tree
Showing 7 changed files with 618 additions and 0 deletions.
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()'
]);

0 comments on commit d0c4826

Please sign in to comment.