From 8d3acf1e5241260d9a9d7e38e41b39f34067a93e Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 4 Sep 2021 00:48:09 +1000 Subject: [PATCH 1/8] add prefer-number-is-integer rule --- docs/rules/prefer-number-is-integer.md | 36 ++++++ index.js | 120 +++++++++++++++++++- rules/prefer-number-is-integer.js | 151 +++++++++++++++++++++++++ test/prefer-number-is-integer.mjs | 88 ++++++++++++++ 4 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 docs/rules/prefer-number-is-integer.md create mode 100644 rules/prefer-number-is-integer.js create mode 100644 test/prefer-number-is-integer.mjs diff --git a/docs/rules/prefer-number-is-integer.md b/docs/rules/prefer-number-is-integer.md new file mode 100644 index 0000000000..448d65195a --- /dev/null +++ b/docs/rules/prefer-number-is-integer.md @@ -0,0 +1,36 @@ +# Prefer the use of `Number.isInteger` over the other various methods + +Enforces the use of [Number.isInteger()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) for checking if a number is an integer. + +These different implementations have slightly different behaviours. + +For example: + +```js +let number = [['1']]; + +number % 1 === 0; // true +``` + +Due to the difference in behaviours across the different implementations, this rule is fixable via the suggestions API. + +## Fail + +```js +(value^0) === value +(value | 0) === value +Math.round(value) === value +parseInt(value, 10) === value +~~value === value + +// these will all trigger the lint warning +_.isInteger(value); +lodash.isInteger(value); +underscore.isInteger(value); +``` + +## Pass + +```js +Number.isInteger(value); +``` diff --git a/index.js b/index.js index 33b3f5dd65..28c3b49bb5 100644 --- a/index.js +++ b/index.js @@ -29,7 +29,123 @@ module.exports = { ...deprecatedRules, }, configs: { - recommended: recommendedConfig, - all, + recommended: { + env: { + es6: true, + }, + parserOptions: { + ecmaVersion: 2021, + sourceType: 'module', + }, + plugins: [ + 'unicorn', + ], + rules: { + 'unicorn/better-regex': 'error', + 'unicorn/catch-error-name': 'error', + 'unicorn/consistent-destructuring': 'error', + 'unicorn/consistent-function-scoping': 'error', + 'unicorn/custom-error-definition': 'off', + 'unicorn/empty-brace-spaces': 'error', + 'unicorn/error-message': 'error', + 'unicorn/escape-case': 'error', + 'unicorn/expiring-todo-comments': 'error', + 'unicorn/explicit-length-check': 'error', + 'unicorn/filename-case': 'error', + 'unicorn/import-index': 'off', + 'unicorn/import-style': 'error', + 'unicorn/new-for-builtins': 'error', + 'unicorn/no-abusive-eslint-disable': 'error', + 'unicorn/no-array-callback-reference': 'error', + 'unicorn/no-array-for-each': 'error', + 'unicorn/no-array-method-this-argument': 'error', + 'unicorn/no-array-push-push': 'error', + 'unicorn/no-array-reduce': 'error', + 'unicorn/no-console-spaces': 'error', + 'unicorn/no-document-cookie': 'error', + 'unicorn/no-for-loop': 'error', + 'unicorn/no-hex-escape': 'error', + 'unicorn/no-instanceof-array': 'error', + 'unicorn/no-invalid-remove-event-listener': 'error', + 'unicorn/no-keyword-prefix': 'off', + 'unicorn/no-lonely-if': 'error', + 'no-nested-ternary': 'off', + 'unicorn/no-nested-ternary': 'error', + 'unicorn/no-new-array': 'error', + 'unicorn/no-new-buffer': 'error', + 'unicorn/no-null': 'error', + 'unicorn/no-object-as-default-parameter': 'error', + 'unicorn/no-process-exit': 'error', + 'unicorn/no-static-only-class': 'error', + 'unicorn/no-this-assignment': 'error', + 'unicorn/no-unreadable-array-destructuring': 'error', + 'unicorn/no-unsafe-regex': 'off', + 'unicorn/no-unused-properties': 'off', + 'unicorn/no-useless-fallback-in-spread': 'error', + 'unicorn/no-useless-length-check': 'error', + 'unicorn/no-useless-spread': 'error', + 'unicorn/no-useless-undefined': 'error', + 'unicorn/no-zero-fractions': 'error', + 'unicorn/number-literal-case': 'error', + 'unicorn/numeric-separators-style': 'error', + 'unicorn/prefer-add-event-listener': 'error', + 'unicorn/prefer-array-find': 'error', + 'unicorn/prefer-array-flat': 'error', + 'unicorn/prefer-array-flat-map': 'error', + 'unicorn/prefer-array-index-of': 'error', + 'unicorn/prefer-array-some': 'error', + // TODO: Enable this by default when targeting a Node.js version that supports `Array#at`. + 'unicorn/prefer-at': 'off', + 'unicorn/prefer-date-now': 'error', + 'unicorn/prefer-default-parameters': 'error', + 'unicorn/prefer-dom-node-append': 'error', + 'unicorn/prefer-dom-node-dataset': 'error', + 'unicorn/prefer-dom-node-remove': 'error', + 'unicorn/prefer-dom-node-text-content': 'error', + 'unicorn/prefer-includes': 'error', + 'unicorn/prefer-keyboard-event-key': 'error', + 'unicorn/prefer-math-trunc': 'error', + 'unicorn/prefer-modern-dom-apis': 'error', + 'unicorn/prefer-module': 'error', + 'unicorn/prefer-negative-index': 'error', + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/prefer-number-is-integer': 'error', + 'unicorn/prefer-number-properties': 'error', + 'unicorn/prefer-object-from-entries': 'error', + // TODO: Enable this by default when targeting a Node.js version that supports `Object.hasOwn`. + 'unicorn/prefer-object-has-own': 'off', + 'unicorn/prefer-optional-catch-binding': 'error', + 'unicorn/prefer-prototype-methods': 'error', + 'unicorn/prefer-query-selector': 'error', + 'unicorn/prefer-reflect-apply': 'error', + 'unicorn/prefer-regexp-test': 'error', + 'unicorn/prefer-set-has': 'error', + 'unicorn/prefer-spread': 'error', + // TODO: Enable this by default when targeting Node.js 16. + 'unicorn/prefer-string-replace-all': 'off', + 'unicorn/prefer-string-slice': 'error', + 'unicorn/prefer-string-starts-ends-with': 'error', + 'unicorn/prefer-string-trim-start-end': 'error', + 'unicorn/prefer-switch': 'error', + 'unicorn/prefer-ternary': 'error', + // TODO: Enable this by default when targeting Node.js 14. + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prefer-type-error': 'error', + 'unicorn/prevent-abbreviations': 'error', + 'unicorn/require-array-join-separator': 'error', + 'unicorn/require-number-to-fixed-digits-argument': 'error', + 'unicorn/require-post-message-target-origin': 'error', + 'unicorn/string-content': 'off', + 'unicorn/throw-new-error': 'error', + }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + 'unicorn/require-post-message-target-origin': 'off', + }, + }, + ], + }, }, }; diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js new file mode 100644 index 0000000000..5e2fc2207e --- /dev/null +++ b/rules/prefer-number-is-integer.js @@ -0,0 +1,151 @@ +'use strict'; + +const MESSAGE_ID = 'preferNumberIsInteger'; +const MESSAGE_ID_SUGGEST = 'preferNumberIsIntegerSuggestion'; +const messages = { + [MESSAGE_ID]: 'Replace `{{original}}` with `Number.isInteger({{variable}})`.', + [MESSAGE_ID_SUGGEST]: 'Prefer `Number.isInteger()` for integer checks.', +}; + +const equalsSelector = ':matches([operator="==="],[operator="=="])'; + +// Value % 1 === 0 +const modulo1Selector = [ + 'BinaryExpression', + '[left.type="BinaryExpression"]', + '[left.left.type="Identifier"]', + '[left.operator="%"]', + '[left.right.value=1]', + equalsSelector, + '[right.value="0"]', +].join(''); + +// (value^0) === value OR (value | 0) === value +const mathOperatorSelector = [ + 'BinaryExpression', + '[left.type="BinaryExpression"]', + '[left.left.type="Identifier"]', + `:matches(${['^', '|'].map(operator => `[left.operator="${operator}"]`).join(',')})`, + '[left.right.value=0]', + equalsSelector, + '[right.type="Identifier"]', +].join(''); + +// ParseInt(value,10) === value +const parseIntSelector = [ + 'BinaryExpression', + '[left.type="CallExpression"]', + '[left.callee.name="parseInt"]', + '[left.arguments.0.type="Identifier"]', + '[left.arguments.1.value=10]', + equalsSelector, + '[right.type="Identifier"]', +].join(''); + +// _.isInteger(value) +const lodashIsIntegerSelector = [ + 'CallExpression', + `:matches(${['_', 'lodash', 'underscore'].map(callee => `[callee.object.name="${callee}"]`).join(',')})`, + '[callee.property.name="isInteger"]', +].join(''); + +// Math.round(value) === value +const mathRoundSelector = [ + 'BinaryExpression', + '[left.type="CallExpression"]', + '[left.callee.type="MemberExpression"]', + '[left.callee.object.name="Math"]', + '[left.callee.property.name="round"]', + '[left.arguments.0.type="Identifier"]', + equalsSelector, + '[right.type="Identifier"]', +].join(''); + +// ~~value === value +const bitwiseNotSelector = [ + 'BinaryExpression', + '[left.type="UnaryExpression"]', + '[left.operator="~"]', + '[left.argument.type="UnaryExpression"]', + '[left.argument.operator="~"]', + '[left.argument.argument.type="Identifier"]', + equalsSelector, + '[right.type="Identifier"]', +].join(''); + +function createNodeListener(sourceCode, variableGetter) { + return node => { + const variable = variableGetter(node); + + if (!variable) { + return; + } + + return { + node, + messageId: MESSAGE_ID, + data: { + variable, + original: sourceCode.getText(node), + }, + /** @param {import('eslint').Rule.RuleFixer} fixer */ + suggest: [{ + messageId: MESSAGE_ID_SUGGEST, + fix: fixer => fixer.replaceText(node, `Number.isInteger(${variable})`), + }], + }; + }; +} + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + const sourceCode = context.getSourceCode(); + + return { + [modulo1Selector]: createNodeListener(sourceCode, node => node.left.left.name), + [mathOperatorSelector]: createNodeListener(sourceCode, node => { + const variableName = node.right.name; + + if (variableName === node.left.left.name) { + return variableName; + } + }), + [parseIntSelector]: createNodeListener(sourceCode, node => { + const variableName = node.right.name; + + if ( + node.left.arguments[0].name === variableName + ) { + return variableName; + } + }), + [lodashIsIntegerSelector]: createNodeListener(sourceCode, node => node.arguments[0].name), + [mathRoundSelector]: createNodeListener(sourceCode, node => { + const variableName = node.right.name; + + if (node.left.arguments[0].name === variableName) { + return variableName; + } + }), + [bitwiseNotSelector]: createNodeListener(sourceCode, node => { + const variableName = node.right.name; + + if (node.left.argument.argument.name === variableName) { + return variableName; + } + }), + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer the use of Number.isInteger over the other various methods', + }, + fixable: 'code', + messages, + hasSuggestions: true, + }, +}; diff --git a/test/prefer-number-is-integer.mjs b/test/prefer-number-is-integer.mjs new file mode 100644 index 0000000000..7ad3c4d794 --- /dev/null +++ b/test/prefer-number-is-integer.mjs @@ -0,0 +1,88 @@ +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +const suggestionCase = ({code, suggestions}) => ({ + code, + errors: [ + { + messageId: 'preferNumberIsInteger', + suggestions, + }, + ], +}); + +test({ + valid: [ + 'Number.isInteger(13)', + 'Number.isInteger(13.0)', + 'Number.isInteger(value)', + '(value^0) === notValue', + '(value | 0) === notValue', + 'Math.round(value) === notValue', + 'parseInt(value, 10) === notValue', + '~~value === notValue', + ], + invalid: [ + suggestionCase({code: 'value % 1 === 0', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'value % 1 == 0', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '(value^0) === value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '(value^0) == value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '(value | 0) === value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '(value | 0) == value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'parseInt(value, 10) === value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'parseInt(value, 10) == value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '_.isInteger(value)', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'lodash.isInteger(value)', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'underscore.isInteger(value)', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'Math.round(value) === value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: 'Math.round(value) == value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '~~value === value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + suggestionCase({code: '~~value == value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(value)', + }]}), + ], +}); From d26d2dbadb6135b5a153ffdc7df56e14bda8420c Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 4 Sep 2021 11:32:56 +1000 Subject: [PATCH 2/8] prefer-number-is-integer: update readme.md --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 448f1da824..ccea5a4001 100644 --- a/readme.md +++ b/readme.md @@ -111,6 +111,7 @@ Configure it in `package.json`. "unicorn/prefer-negative-index": "error", "unicorn/prefer-node-protocol": "error", "unicorn/prefer-number-properties": "error", + "unicorn/prefer-number-is-integer": "error", "unicorn/prefer-object-from-entries": "error", "unicorn/prefer-optional-catch-binding": "error", "unicorn/prefer-prototype-methods": "error", From 1894d484e9099e1765fc0acf2b3434f036c91f4a Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 4 Sep 2021 12:20:24 +1000 Subject: [PATCH 3/8] prefer-number-is-integer: update documentation --- docs/rules/prefer-number-is-integer.md | 2 +- readme.md | 3 ++- rules/prefer-number-is-integer.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/rules/prefer-number-is-integer.md b/docs/rules/prefer-number-is-integer.md index 448d65195a..701b4a882c 100644 --- a/docs/rules/prefer-number-is-integer.md +++ b/docs/rules/prefer-number-is-integer.md @@ -1,4 +1,4 @@ -# Prefer the use of `Number.isInteger` over the other various methods +# Prefer `Number.isInteger` for integer checking Enforces the use of [Number.isInteger()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) for checking if a number is an integer. diff --git a/readme.md b/readme.md index ccea5a4001..95f5746d4b 100644 --- a/readme.md +++ b/readme.md @@ -110,8 +110,8 @@ Configure it in `package.json`. "unicorn/prefer-module": "error", "unicorn/prefer-negative-index": "error", "unicorn/prefer-node-protocol": "error", - "unicorn/prefer-number-properties": "error", "unicorn/prefer-number-is-integer": "error", + "unicorn/prefer-number-properties": "error", "unicorn/prefer-object-from-entries": "error", "unicorn/prefer-optional-catch-binding": "error", "unicorn/prefer-prototype-methods": "error", @@ -227,6 +227,7 @@ Each rule has emojis denoting: | [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ | 🔧 | 💡 | | [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-is-integer](docs/rules/prefer-number-is-integer.md) | Prefer `Number.isInteger()` for integer checking. | ✅ | | 💡 | | [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ | 🔧 | 💡 | | [prefer-object-from-entries](docs/rules/prefer-object-from-entries.md) | Prefer using `Object.fromEntries(…)` to transform a list of key-value pairs into an object. | ✅ | 🔧 | | | [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 | | diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js index 5e2fc2207e..d239abc337 100644 --- a/rules/prefer-number-is-integer.js +++ b/rules/prefer-number-is-integer.js @@ -142,7 +142,7 @@ module.exports = { meta: { type: 'suggestion', docs: { - description: 'Prefer the use of Number.isInteger over the other various methods', + description: 'Prefer Number.isInteger for integer checking', }, fixable: 'code', messages, From 431ad7d936b09fa2d7ad26934ac16cfbbe27b59a Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 4 Sep 2021 13:24:22 +1000 Subject: [PATCH 4/8] prefer-number-is-integer: align documentation with rule --- docs/rules/prefer-number-is-integer.md | 2 +- rules/prefer-number-is-integer.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/rules/prefer-number-is-integer.md b/docs/rules/prefer-number-is-integer.md index 701b4a882c..b58ff7a83f 100644 --- a/docs/rules/prefer-number-is-integer.md +++ b/docs/rules/prefer-number-is-integer.md @@ -1,4 +1,4 @@ -# Prefer `Number.isInteger` for integer checking +# Prefer `Number.isInteger()` for integer checking Enforces the use of [Number.isInteger()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) for checking if a number is an integer. diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js index d239abc337..266aa4e781 100644 --- a/rules/prefer-number-is-integer.js +++ b/rules/prefer-number-is-integer.js @@ -142,9 +142,8 @@ module.exports = { meta: { type: 'suggestion', docs: { - description: 'Prefer Number.isInteger for integer checking', + description: 'Prefer `Number.isInteger()` for integer checking.', }, - fixable: 'code', messages, hasSuggestions: true, }, From 43ef6dc9e4471ec2466540c21e73d36498d44c66 Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 11 Sep 2021 21:26:42 +1000 Subject: [PATCH 5/8] prefer-number-is-integer: handle any reference in integer check --- configs/recommended.js | 1 + docs/rules/prefer-number-is-integer.md | 14 ++- index.js | 120 +------------------------ rules/prefer-number-is-integer.js | 61 +++++++------ test/prefer-number-is-integer.mjs | 20 +++++ 5 files changed, 68 insertions(+), 148 deletions(-) diff --git a/configs/recommended.js b/configs/recommended.js index a7c34c424c..7f8e8705c6 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -78,6 +78,7 @@ module.exports = { 'unicorn/prefer-negative-index': 'error', 'unicorn/prefer-node-protocol': 'error', 'unicorn/prefer-number-properties': 'error', + 'unicorn/prefer-number-is-integer': 'error', 'unicorn/prefer-object-from-entries': 'error', 'unicorn/prefer-optional-catch-binding': 'error', 'unicorn/prefer-prototype-methods': 'error', diff --git a/docs/rules/prefer-number-is-integer.md b/docs/rules/prefer-number-is-integer.md index b58ff7a83f..5e25433e69 100644 --- a/docs/rules/prefer-number-is-integer.md +++ b/docs/rules/prefer-number-is-integer.md @@ -2,14 +2,22 @@ Enforces the use of [Number.isInteger()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) for checking if a number is an integer. -These different implementations have slightly different behaviours. +There are multiple ways to check if a variable is an integer, but these approaches tend to have slightly different behaviours. For example: ```js -let number = [['1']]; +// this is not an integer (or a number) +let notInteger = [['1']]; -number % 1 === 0; // true +notInteger % 1 === 0; // true - ?! an array is defintely not an integer +Number.isInteger(notInteger); // false - makes sense + +// this is an integer that is larger than Number.MAX_SAFE_INTEGER +let largeInteger = 1_000_000_000_000_000_000; + +largeInteger^0 === largeInteger; // false - its an integer, should be true +Number.isInteger(largeInteger); // true - makes sense ``` Due to the difference in behaviours across the different implementations, this rule is fixable via the suggestions API. diff --git a/index.js b/index.js index 28c3b49bb5..33b3f5dd65 100644 --- a/index.js +++ b/index.js @@ -29,123 +29,7 @@ module.exports = { ...deprecatedRules, }, configs: { - recommended: { - env: { - es6: true, - }, - parserOptions: { - ecmaVersion: 2021, - sourceType: 'module', - }, - plugins: [ - 'unicorn', - ], - rules: { - 'unicorn/better-regex': 'error', - 'unicorn/catch-error-name': 'error', - 'unicorn/consistent-destructuring': 'error', - 'unicorn/consistent-function-scoping': 'error', - 'unicorn/custom-error-definition': 'off', - 'unicorn/empty-brace-spaces': 'error', - 'unicorn/error-message': 'error', - 'unicorn/escape-case': 'error', - 'unicorn/expiring-todo-comments': 'error', - 'unicorn/explicit-length-check': 'error', - 'unicorn/filename-case': 'error', - 'unicorn/import-index': 'off', - 'unicorn/import-style': 'error', - 'unicorn/new-for-builtins': 'error', - 'unicorn/no-abusive-eslint-disable': 'error', - 'unicorn/no-array-callback-reference': 'error', - 'unicorn/no-array-for-each': 'error', - 'unicorn/no-array-method-this-argument': 'error', - 'unicorn/no-array-push-push': 'error', - 'unicorn/no-array-reduce': 'error', - 'unicorn/no-console-spaces': 'error', - 'unicorn/no-document-cookie': 'error', - 'unicorn/no-for-loop': 'error', - 'unicorn/no-hex-escape': 'error', - 'unicorn/no-instanceof-array': 'error', - 'unicorn/no-invalid-remove-event-listener': 'error', - 'unicorn/no-keyword-prefix': 'off', - 'unicorn/no-lonely-if': 'error', - 'no-nested-ternary': 'off', - 'unicorn/no-nested-ternary': 'error', - 'unicorn/no-new-array': 'error', - 'unicorn/no-new-buffer': 'error', - 'unicorn/no-null': 'error', - 'unicorn/no-object-as-default-parameter': 'error', - 'unicorn/no-process-exit': 'error', - 'unicorn/no-static-only-class': 'error', - 'unicorn/no-this-assignment': 'error', - 'unicorn/no-unreadable-array-destructuring': 'error', - 'unicorn/no-unsafe-regex': 'off', - 'unicorn/no-unused-properties': 'off', - 'unicorn/no-useless-fallback-in-spread': 'error', - 'unicorn/no-useless-length-check': 'error', - 'unicorn/no-useless-spread': 'error', - 'unicorn/no-useless-undefined': 'error', - 'unicorn/no-zero-fractions': 'error', - 'unicorn/number-literal-case': 'error', - 'unicorn/numeric-separators-style': 'error', - 'unicorn/prefer-add-event-listener': 'error', - 'unicorn/prefer-array-find': 'error', - 'unicorn/prefer-array-flat': 'error', - 'unicorn/prefer-array-flat-map': 'error', - 'unicorn/prefer-array-index-of': 'error', - 'unicorn/prefer-array-some': 'error', - // TODO: Enable this by default when targeting a Node.js version that supports `Array#at`. - 'unicorn/prefer-at': 'off', - 'unicorn/prefer-date-now': 'error', - 'unicorn/prefer-default-parameters': 'error', - 'unicorn/prefer-dom-node-append': 'error', - 'unicorn/prefer-dom-node-dataset': 'error', - 'unicorn/prefer-dom-node-remove': 'error', - 'unicorn/prefer-dom-node-text-content': 'error', - 'unicorn/prefer-includes': 'error', - 'unicorn/prefer-keyboard-event-key': 'error', - 'unicorn/prefer-math-trunc': 'error', - 'unicorn/prefer-modern-dom-apis': 'error', - 'unicorn/prefer-module': 'error', - 'unicorn/prefer-negative-index': 'error', - 'unicorn/prefer-node-protocol': 'error', - 'unicorn/prefer-number-is-integer': 'error', - 'unicorn/prefer-number-properties': 'error', - 'unicorn/prefer-object-from-entries': 'error', - // TODO: Enable this by default when targeting a Node.js version that supports `Object.hasOwn`. - 'unicorn/prefer-object-has-own': 'off', - 'unicorn/prefer-optional-catch-binding': 'error', - 'unicorn/prefer-prototype-methods': 'error', - 'unicorn/prefer-query-selector': 'error', - 'unicorn/prefer-reflect-apply': 'error', - 'unicorn/prefer-regexp-test': 'error', - 'unicorn/prefer-set-has': 'error', - 'unicorn/prefer-spread': 'error', - // TODO: Enable this by default when targeting Node.js 16. - 'unicorn/prefer-string-replace-all': 'off', - 'unicorn/prefer-string-slice': 'error', - 'unicorn/prefer-string-starts-ends-with': 'error', - 'unicorn/prefer-string-trim-start-end': 'error', - 'unicorn/prefer-switch': 'error', - 'unicorn/prefer-ternary': 'error', - // TODO: Enable this by default when targeting Node.js 14. - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/prefer-type-error': 'error', - 'unicorn/prevent-abbreviations': 'error', - 'unicorn/require-array-join-separator': 'error', - 'unicorn/require-number-to-fixed-digits-argument': 'error', - 'unicorn/require-post-message-target-origin': 'error', - 'unicorn/string-content': 'off', - 'unicorn/throw-new-error': 'error', - }, - overrides: [ - { - files: ['*.ts', '*.tsx'], - rules: { - 'unicorn/require-post-message-target-origin': 'off', - }, - }, - ], - }, + recommended: recommendedConfig, + all, }, }; diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js index 266aa4e781..efccf29758 100644 --- a/rules/prefer-number-is-integer.js +++ b/rules/prefer-number-is-integer.js @@ -1,5 +1,7 @@ 'use strict'; +const isSameReference = require('./utils/is-same-reference.js'); + const MESSAGE_ID = 'preferNumberIsInteger'; const MESSAGE_ID_SUGGEST = 'preferNumberIsIntegerSuggestion'; const messages = { @@ -13,7 +15,6 @@ const equalsSelector = ':matches([operator="==="],[operator="=="])'; const modulo1Selector = [ 'BinaryExpression', '[left.type="BinaryExpression"]', - '[left.left.type="Identifier"]', '[left.operator="%"]', '[left.right.value=1]', equalsSelector, @@ -24,11 +25,9 @@ const modulo1Selector = [ const mathOperatorSelector = [ 'BinaryExpression', '[left.type="BinaryExpression"]', - '[left.left.type="Identifier"]', `:matches(${['^', '|'].map(operator => `[left.operator="${operator}"]`).join(',')})`, '[left.right.value=0]', equalsSelector, - '[right.type="Identifier"]', ].join(''); // ParseInt(value,10) === value @@ -36,10 +35,8 @@ const parseIntSelector = [ 'BinaryExpression', '[left.type="CallExpression"]', '[left.callee.name="parseInt"]', - '[left.arguments.0.type="Identifier"]', '[left.arguments.1.value=10]', equalsSelector, - '[right.type="Identifier"]', ].join(''); // _.isInteger(value) @@ -56,9 +53,7 @@ const mathRoundSelector = [ '[left.callee.type="MemberExpression"]', '[left.callee.object.name="Math"]', '[left.callee.property.name="round"]', - '[left.arguments.0.type="Identifier"]', equalsSelector, - '[right.type="Identifier"]', ].join(''); // ~~value === value @@ -68,9 +63,7 @@ const bitwiseNotSelector = [ '[left.operator="~"]', '[left.argument.type="UnaryExpression"]', '[left.argument.operator="~"]', - '[left.argument.argument.type="Identifier"]', equalsSelector, - '[right.type="Identifier"]', ].join(''); function createNodeListener(sourceCode, variableGetter) { @@ -97,41 +90,55 @@ function createNodeListener(sourceCode, variableGetter) { }; } +function getNodeName(node) { + switch (node.type) { + case 'Identifier': { + return node.name; + } + + case 'ChainExpression': { + return getNodeName(node.expression); + } + + case 'MemberExpression': { + return `${getNodeName(node.object)}.${getNodeName(node.property)}`; + } + + default: { + return ''; + } + } +} + /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const sourceCode = context.getSourceCode(); return { - [modulo1Selector]: createNodeListener(sourceCode, node => node.left.left.name), + [modulo1Selector]: createNodeListener(sourceCode, node => getNodeName(node.left.left)), [mathOperatorSelector]: createNodeListener(sourceCode, node => { - const variableName = node.right.name; - - if (variableName === node.left.left.name) { - return variableName; + if (isSameReference(node.left.left, node.right)) { + return getNodeName(node.right); } }), [parseIntSelector]: createNodeListener(sourceCode, node => { - const variableName = node.right.name; - if ( - node.left.arguments[0].name === variableName + isSameReference(node.left.arguments[0], node.right) ) { - return variableName; + return getNodeName(node.right); } }), - [lodashIsIntegerSelector]: createNodeListener(sourceCode, node => node.arguments[0].name), + [lodashIsIntegerSelector]: createNodeListener(sourceCode, node => getNodeName(node.arguments[0])), [mathRoundSelector]: createNodeListener(sourceCode, node => { - const variableName = node.right.name; - - if (node.left.arguments[0].name === variableName) { - return variableName; + if ( + isSameReference(node.left.arguments[0], node.right) + ) { + return getNodeName(node.right); } }), [bitwiseNotSelector]: createNodeListener(sourceCode, node => { - const variableName = node.right.name; - - if (node.left.argument.argument.name === variableName) { - return variableName; + if (isSameReference(node.left.argument.argument, node.right)) { + return getNodeName(node.right); } }), }; diff --git a/test/prefer-number-is-integer.mjs b/test/prefer-number-is-integer.mjs index 7ad3c4d794..6a5d6dba57 100644 --- a/test/prefer-number-is-integer.mjs +++ b/test/prefer-number-is-integer.mjs @@ -84,5 +84,25 @@ test({ messageId: 'preferNumberIsIntegerSuggestion', output: 'Number.isInteger(value)', }]}), + suggestionCase({code: '~~object.value === object.value', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(object.value)', + }]}), + suggestionCase({code: '~~object.a.b.c === object.a.b.c', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(object.a.b.c)', + }]}), + suggestionCase({code: '~~object["a"] === object.a', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(object.a)', + }]}), + suggestionCase({code: '~~object?.a === object.a', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(object.a)', + }]}), + suggestionCase({code: '~~object?.a === object?.a', suggestions: [{ + messageId: 'preferNumberIsIntegerSuggestion', + output: 'Number.isInteger(object.a)', + }]}), ], }); From 6b4f72149cdfa0cb54fc6e22152acf65ce33ab41 Mon Sep 17 00:00:00 2001 From: thessell Date: Thu, 27 Jan 2022 23:10:38 +1100 Subject: [PATCH 6/8] prefer-number-is-integer: update rule implementation from review --- configs/recommended.js | 2 +- docs/rules/prefer-number-is-integer.md | 7 ++++++ rules/prefer-number-is-integer.js | 34 +++++++++++++------------- test/prefer-number-is-integer.mjs | 6 ++--- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/configs/recommended.js b/configs/recommended.js index 7f8e8705c6..99a5651634 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -77,8 +77,8 @@ module.exports = { 'unicorn/prefer-module': 'error', 'unicorn/prefer-negative-index': 'error', 'unicorn/prefer-node-protocol': 'error', - 'unicorn/prefer-number-properties': 'error', 'unicorn/prefer-number-is-integer': 'error', + 'unicorn/prefer-number-properties': 'error', 'unicorn/prefer-object-from-entries': 'error', 'unicorn/prefer-optional-catch-binding': 'error', 'unicorn/prefer-prototype-methods': 'error', diff --git a/docs/rules/prefer-number-is-integer.md b/docs/rules/prefer-number-is-integer.md index 5e25433e69..fdde7b50c7 100644 --- a/docs/rules/prefer-number-is-integer.md +++ b/docs/rules/prefer-number-is-integer.md @@ -1,5 +1,12 @@ # Prefer `Number.isInteger()` for integer checking + + +✅ *This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.* + +💡 *This rule provides [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).* + + Enforces the use of [Number.isInteger()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger) for checking if a number is an integer. There are multiple ways to check if a variable is an integer, but these approaches tend to have slightly different behaviours. diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js index efccf29758..31e29231fd 100644 --- a/rules/prefer-number-is-integer.js +++ b/rules/prefer-number-is-integer.js @@ -1,5 +1,6 @@ 'use strict'; +const {methodCallSelector} = require('./selectors/index.js'); const isSameReference = require('./utils/is-same-reference.js'); const MESSAGE_ID = 'preferNumberIsInteger'; @@ -11,7 +12,7 @@ const messages = { const equalsSelector = ':matches([operator="==="],[operator="=="])'; -// Value % 1 === 0 +// `value % 1 === 0` const modulo1Selector = [ 'BinaryExpression', '[left.type="BinaryExpression"]', @@ -21,7 +22,8 @@ const modulo1Selector = [ '[right.value="0"]', ].join(''); -// (value^0) === value OR (value | 0) === value +// `(value ^ 0) === value` +// `(value | 0) === value` const mathOperatorSelector = [ 'BinaryExpression', '[left.type="BinaryExpression"]', @@ -30,33 +32,30 @@ const mathOperatorSelector = [ equalsSelector, ].join(''); -// ParseInt(value,10) === value -const parseIntSelector = [ +// `Number.parseInt(value,10) === value` +const numberParseIntSelector = [ 'BinaryExpression', '[left.type="CallExpression"]', - '[left.callee.name="parseInt"]', + '[left.callee.type="MemberExpression"]', + '[left.callee.object.name="Number"]', + '[left.callee.property.name="parseInt"]', '[left.arguments.1.value=10]', equalsSelector, ].join(''); -// _.isInteger(value) +// `_.isInteger(value)` const lodashIsIntegerSelector = [ - 'CallExpression', - `:matches(${['_', 'lodash', 'underscore'].map(callee => `[callee.object.name="${callee}"]`).join(',')})`, - '[callee.property.name="isInteger"]', + methodCallSelector({method: 'isInteger', objects: ['_', 'lodash', 'underscore']}), ].join(''); -// Math.round(value) === value +// `Math.round(value) === value` const mathRoundSelector = [ 'BinaryExpression', - '[left.type="CallExpression"]', - '[left.callee.type="MemberExpression"]', - '[left.callee.object.name="Math"]', - '[left.callee.property.name="round"]', + methodCallSelector({method: 'round', object: 'Math', path: 'left'}), equalsSelector, ].join(''); -// ~~value === value +// `~~value === value` const bitwiseNotSelector = [ 'BinaryExpression', '[left.type="UnaryExpression"]', @@ -121,7 +120,7 @@ const create = context => { return getNodeName(node.right); } }), - [parseIntSelector]: createNodeListener(sourceCode, node => { + [numberParseIntSelector]: createNodeListener(sourceCode, node => { if ( isSameReference(node.left.arguments[0], node.right) ) { @@ -144,6 +143,7 @@ const create = context => { }; }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { create, meta: { @@ -151,7 +151,7 @@ module.exports = { docs: { description: 'Prefer `Number.isInteger()` for integer checking.', }, - messages, hasSuggestions: true, + messages, }, }; diff --git a/test/prefer-number-is-integer.mjs b/test/prefer-number-is-integer.mjs index 6a5d6dba57..9f0470f067 100644 --- a/test/prefer-number-is-integer.mjs +++ b/test/prefer-number-is-integer.mjs @@ -20,7 +20,7 @@ test({ '(value^0) === notValue', '(value | 0) === notValue', 'Math.round(value) === notValue', - 'parseInt(value, 10) === notValue', + 'Number.parseInt(value, 10) === notValue', '~~value === notValue', ], invalid: [ @@ -48,11 +48,11 @@ test({ messageId: 'preferNumberIsIntegerSuggestion', output: 'Number.isInteger(value)', }]}), - suggestionCase({code: 'parseInt(value, 10) === value', suggestions: [{ + suggestionCase({code: 'Number.parseInt(value, 10) === value', suggestions: [{ messageId: 'preferNumberIsIntegerSuggestion', output: 'Number.isInteger(value)', }]}), - suggestionCase({code: 'parseInt(value, 10) == value', suggestions: [{ + suggestionCase({code: 'Number.parseInt(value, 10) == value', suggestions: [{ messageId: 'preferNumberIsIntegerSuggestion', output: 'Number.isInteger(value)', }]}), From 42c9ba5e2e75a793fe95a47d56cfa09a106596b0 Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 29 Jan 2022 16:06:48 +1100 Subject: [PATCH 7/8] prefer-number-is-integer: add codecov ignore --- rules/prefer-number-is-integer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js index 31e29231fd..1e0386e555 100644 --- a/rules/prefer-number-is-integer.js +++ b/rules/prefer-number-is-integer.js @@ -102,7 +102,7 @@ function getNodeName(node) { case 'MemberExpression': { return `${getNodeName(node.object)}.${getNodeName(node.property)}`; } - + /* c8 ignore next 3 */ default: { return ''; } From 28fcabeb5c832cd896cb47407de70a75adae81f0 Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 29 Jan 2022 16:10:01 +1100 Subject: [PATCH 8/8] prefer-number-is-integer: fix lint failure --- rules/prefer-number-is-integer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/prefer-number-is-integer.js b/rules/prefer-number-is-integer.js index 1e0386e555..f8173e4271 100644 --- a/rules/prefer-number-is-integer.js +++ b/rules/prefer-number-is-integer.js @@ -102,6 +102,7 @@ function getNodeName(node) { case 'MemberExpression': { return `${getNodeName(node.object)}.${getNodeName(node.property)}`; } + /* c8 ignore next 3 */ default: { return '';