From 2e39762e1d89b4395283185a6eb6446c10dd5846 Mon Sep 17 00:00:00 2001 From: thessell Date: Sat, 11 Sep 2021 21:26:42 +1000 Subject: [PATCH] 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 750f8df822..510af283a3 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -71,6 +71,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', // TODO: Enable this by default when targeting a Node.js version that supports `Object.hasOwn`. 'unicorn/prefer-object-has-own': 'off', 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 3ae8645a76..6cf8b95487 100644 --- a/index.js +++ b/index.js @@ -28,123 +28,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)', + }]}), ], });