From 51d7e06b6f3fa803cdfacda7dc52981472934870 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sun, 29 May 2022 12:54:55 +0800 Subject: [PATCH] `prefer-number-properties`: Check any use of global functions (#1834) --- rules/prefer-number-properties.js | 87 +++++++----------- test/prefer-number-properties.mjs | 35 ++++--- .../snapshots/prefer-number-properties.mjs.md | 67 +++++++++++--- .../prefer-number-properties.mjs.snap | Bin 2387 -> 2531 bytes 4 files changed, 110 insertions(+), 79 deletions(-) diff --git a/rules/prefer-number-properties.js b/rules/prefer-number-properties.js index f1be2b34f8..6bf677ad4a 100644 --- a/rules/prefer-number-properties.js +++ b/rules/prefer-number-properties.js @@ -3,20 +3,21 @@ const {ReferenceTracker} = require('eslint-utils'); const {replaceReferenceIdentifier} = require('./fix/index.js'); const {fixSpaceAroundKeyword} = require('./fix/index.js'); -const METHOD_ERROR_MESSAGE_ID = 'method-error'; -const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion'; -const PROPERTY_ERROR_MESSAGE_ID = 'property-error'; +const MESSAGE_ID_ERROR = 'error'; +const MESSAGE_ID_SUGGESTION = 'suggestion'; const messages = { - [METHOD_ERROR_MESSAGE_ID]: 'Prefer `Number.{{name}}()` over `{{name}}()`.', - [METHOD_SUGGESTION_MESSAGE_ID]: 'Replace `{{name}}()` with `Number.{{name}}()`.', - [PROPERTY_ERROR_MESSAGE_ID]: 'Prefer `Number.{{property}}` over `{{identifier}}`.', + [MESSAGE_ID_ERROR]: 'Prefer `Number.{{property}}` over `{{description}}`.', + [MESSAGE_ID_SUGGESTION]: 'Replace `{{description}}` with `Number.{{property}}`.', }; -const methods = { - // Safe +const globalObjects = { + // Safe to replace with `Number` properties parseInt: true, parseFloat: true, - // Unsafe + NaN: true, + Infinity: true, + + // Unsafe to replace with `Number` properties isNaN: false, isFinite: false, }; @@ -26,47 +27,14 @@ const isNegative = node => { return parent && parent.type === 'UnaryExpression' && parent.operator === '-' && parent.argument === node; }; -function * checkMethods({sourceCode, tracker}) { - const traceMap = Object.fromEntries( - Object.keys(methods).map(name => [name, {[ReferenceTracker.CALL]: true}]), - ); - - for (const {node: callExpression, path: [name]} of tracker.iterateGlobalReferences(traceMap)) { - const node = callExpression.callee; - const isSafe = methods[name]; - - const problem = { - node, - messageId: METHOD_ERROR_MESSAGE_ID, - data: { - name, - }, - }; - - const fix = fixer => replaceReferenceIdentifier(node, `Number.${name}`, fixer, sourceCode); - - if (isSafe) { - problem.fix = fix; - } else { - problem.suggest = [ - { - messageId: METHOD_SUGGESTION_MESSAGE_ID, - data: { - name, - }, - fix, - }, - ]; - } - - yield problem; +function * checkProperties({sourceCode, tracker, checkInfinity}) { + let names = Object.keys(globalObjects); + if (!checkInfinity) { + names = names.filter(name => name !== 'Infinity'); } -} -function * checkProperties({sourceCode, tracker, checkInfinity}) { - const properties = checkInfinity ? ['NaN', 'Infinity'] : ['NaN']; const traceMap = Object.fromEntries( - properties.map(name => [name, {[ReferenceTracker.READ]: true}]), + names.map(name => [name, {[ReferenceTracker.READ]: true}]), ); for (const {node, path: [name]} of tracker.iterateGlobalReferences(traceMap)) { @@ -79,22 +47,38 @@ function * checkProperties({sourceCode, tracker, checkInfinity}) { const problem = { node, - messageId: PROPERTY_ERROR_MESSAGE_ID, + messageId: MESSAGE_ID_ERROR, data: { - identifier: name, + description: name, property, }, }; if (property === 'NEGATIVE_INFINITY') { problem.node = parent; - problem.data.identifier = '-Infinity'; + problem.data.description = '-Infinity'; problem.fix = function * (fixer) { yield fixer.replaceText(parent, 'Number.NEGATIVE_INFINITY'); yield * fixSpaceAroundKeyword(fixer, parent, sourceCode); }; + + yield problem; + continue; + } + + const fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); + const isSafeToFix = globalObjects[name]; + + if (isSafeToFix) { + problem.fix = fix; } else { - problem.fix = fixer => replaceReferenceIdentifier(node, `Number.${property}`, fixer, sourceCode); + problem.suggest = [ + { + messageId: MESSAGE_ID_SUGGESTION, + data: problem.data, + fix, + }, + ]; } yield problem; @@ -115,7 +99,6 @@ const create = context => { const sourceCode = context.getSourceCode(); const tracker = new ReferenceTracker(context.getScope()); - yield * checkMethods({sourceCode, tracker}); yield * checkProperties({sourceCode, tracker, checkInfinity}); }, }; diff --git a/test/prefer-number-properties.mjs b/test/prefer-number-properties.mjs index 61efa8ed8d..68384b1b71 100644 --- a/test/prefer-number-properties.mjs +++ b/test/prefer-number-properties.mjs @@ -3,9 +3,8 @@ import {getTester} from './utils/test.mjs'; const {test} = getTester(import.meta); -const METHOD_ERROR_MESSAGE_ID = 'method-error'; -const METHOD_SUGGESTION_MESSAGE_ID = 'method-suggestion'; -const PROPERTY_ERROR_MESSAGE_ID = 'property-error'; +const MESSAGE_ID_ERROR = 'error'; +const MESSAGE_ID_SUGGESTION = 'suggestion'; const methods = { parseInt: { @@ -30,17 +29,19 @@ const createError = (name, suggestionOutput) => { const {safe} = methods[name]; const error = { - messageId: METHOD_ERROR_MESSAGE_ID, + messageId: MESSAGE_ID_ERROR, data: { - name, + description: name, + property: name, }, }; const suggestions = safe ? undefined : [ { - messageId: METHOD_SUGGESTION_MESSAGE_ID, + messageId: MESSAGE_ID_SUGGESTION, data: { - name, + description: name, + property: name, }, output: suggestionOutput, }, @@ -75,12 +76,6 @@ test({ 'Number.isNaN(10);', 'Number.isFinite(10);', - // Not call - ...Object.keys(methods), - - // New - ...Object.values(methods).map(({code}) => `new ${code}`), - // Shadowed ...Object.entries(methods).map(([name, {code}]) => outdent` const ${name} = function() {}; @@ -161,9 +156,9 @@ test({ // `NaN` and `Infinity` const errorNaN = [ { - messageId: PROPERTY_ERROR_MESSAGE_ID, + messageId: MESSAGE_ID_ERROR, data: { - identifier: 'NaN', + description: 'NaN', property: 'NaN', }, }, @@ -401,5 +396,15 @@ test.snapshot({ 'self.parseFloat(foo);', 'globalThis.NaN', '-globalThis.Infinity', + + // Not a call + outdent` + const options = { + normalize: parseFloat, + parseInt, + }; + + run(foo, options); + `, ], }); diff --git a/test/snapshots/prefer-number-properties.mjs.md b/test/snapshots/prefer-number-properties.mjs.md index 1d1515616f..93c1778e7d 100644 --- a/test/snapshots/prefer-number-properties.mjs.md +++ b/test/snapshots/prefer-number-properties.mjs.md @@ -583,10 +583,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | globalThis.isNaN(foo);␊ - | ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -597,10 +597,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | global.isNaN(foo);␊ - | ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -611,10 +611,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | window.isNaN(foo);␊ - | ^^^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -625,10 +625,10 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | self.isNaN(foo);␊ - | ^^^^^^^^^^ Prefer \`Number.isNaN()\` over \`isNaN()\`.␊ + | ^^^^^^^^^^ Prefer \`Number.isNaN\` over \`isNaN\`.␊ ␊ --------------------------------------------------------------------------------␊ - Suggestion 1/1: Replace \`isNaN()\` with \`Number.isNaN()\`.␊ + Suggestion 1/1: Replace \`isNaN\` with \`Number.isNaN\`.␊ 1 | Number.isNaN(foo);␊ ` @@ -645,7 +645,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | globalThis.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #40 @@ -661,7 +661,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | global.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #41 @@ -677,7 +677,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | window.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #42 @@ -693,7 +693,7 @@ Generated by [AVA](https://avajs.dev). `␊ > 1 | self.parseFloat(foo);␊ - | ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat()\` over \`parseFloat()\`.␊ + | ^^^^^^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ ` ## Invalid #43 @@ -727,3 +727,46 @@ Generated by [AVA](https://avajs.dev). > 1 | -globalThis.Infinity␊ | ^^^^^^^^^^^^^^^^^^^^ Prefer \`Number.NEGATIVE_INFINITY\` over \`-Infinity\`.␊ ` + +## Invalid #45 + 1 | const options = { + 2 | normalize: parseFloat, + 3 | parseInt, + 4 | }; + 5 | + 6 | run(foo, options); + +> Output + + `␊ + 1 | const options = {␊ + 2 | normalize: Number.parseFloat,␊ + 3 | parseInt: Number.parseInt,␊ + 4 | };␊ + 5 |␊ + 6 | run(foo, options);␊ + ` + +> Error 1/2 + + `␊ + 1 | const options = {␊ + > 2 | normalize: parseFloat,␊ + | ^^^^^^^^^^ Prefer \`Number.parseFloat\` over \`parseFloat\`.␊ + 3 | parseInt,␊ + 4 | };␊ + 5 |␊ + 6 | run(foo, options);␊ + ` + +> Error 2/2 + + `␊ + 1 | const options = {␊ + 2 | normalize: parseFloat,␊ + > 3 | parseInt,␊ + | ^^^^^^^^ Prefer \`Number.parseInt\` over \`parseInt\`.␊ + 4 | };␊ + 5 |␊ + 6 | run(foo, options);␊ + ` diff --git a/test/snapshots/prefer-number-properties.mjs.snap b/test/snapshots/prefer-number-properties.mjs.snap index b87e45ab4459bb29a6d2ee77ec3568cf0f33254d..e38e01730af650874a30398f0b86be4ff2de0d0e 100644 GIT binary patch literal 2531 zcmV<92^{u8RzVpmk%%$sR##Uu(>>jDz?eU>{oc>#{H)`v zufC(cDxc{EfC7Ms+>d(i7&!98waT0A>5C^rz- z7a`cId~w4bKf|f$KBJqx8ZNqEHr9p3d4jASNG{oru8F!M!_Vq^^;)gpQ42ArX3Ed`y^}09*-SYQmDFPkr1<BZMxD+dyA@*gTY@}S0ABlZ=)L{x{+O((?6b-cKDz;8b~C}iX8=fT*!kUk zb=+0u`4Oj9Y)YgdW+xJCM3|F!usZIGuzQbAyKgVrFnArr?AHWpH?-n!V{+D<5A3h= zDc`cDs{R7R>{|q1Bha%iuBmnXd;72t`t{pTbLkw!tec!g2?D+0{c9DQGlG)cZY6w^ z_-u;|vn9O&$dL_P=v%R>Iq}`&gYH$_aeH|Ed2?dS6*p*uk0m+@?XyjwYx?Sn_%G-l%zz?|l|?@QWDNdG{e^8wpGP2Ldm~*~b^wbccNTpH0#M#i zbY-bqX+-F8)q;qV6Aweo#uC&ZtodqmO8H7UOC4QzZ(LH{Fo@ZI6KJ0U;N8Go9^t-j zKNc0{W@Z)cih!8iP2fKWt!v?PTaJL9|0y4{?bz6(+wRFQs~8Nx`b%GpIh2#~z1Hx^ zz3I(!UJ$cs1cwkd-nbh1PMBZhn9D`w7gP7|fSA2Qkm&)ywu#Nkw3Ww(#>;x{`n2_! z8e(=e!F7b1CAsZ0O5}n2V{2=!RBlIp!m#n4Ec}N6uqPEBIpC4{hN|Q7d+Qdi^>x8) z8G(E#iya92#$Ik3vO93g{PxCWR7hYG#B4o5wrmro6oAVL|`j-~PS?V%B#!i;obl7KJAbJ6@c+HuwY#dNpx^470nv0JwH$ zlHWwC<7B^2}+*_piO`F0n~{*55qhs9omw)2DUUaa`^ z`L?s3iHicRW5w@LDk{|g{yU-V<~F~XzOrB7;FG!GY4 zPbl-e)QI93!?qI4_XofQxZ;>E>j?(P!sHYcMwL)%oz`HavUNIY9Qv)G5(@J(X?^JQ z#98r4@yV0t#3xLNPl!)?JzCD|Zjg@`?H;eq)@U`xMbT(@3jMqAOD7*ST~BAzdMZO| zz6?rN#QZxqU`8lk0B<5c>0hAI8|W!{I+Za12@@!gmL}5XNVP&0HNSTB5LL|!s}(JZKr8^l*mdH0c5m<$#F5f(S$1y zGZaODf}2cKbd&HF(p${ONw1I+|FF8--sEgad>E4z@L-d9I5GqGvU_UB* z7v!nbG&d!Z-8feuQz&2W=F8%#JWwoBD^;fK$T*XwS*MFdX0{vGUMeO#Y(kM{ZkU*v zORJ4&Im}u@Br~{(BbJ&uqF@xeT+xZn7|u&0g5btieJK;rgr~h^ULN7%9Z^2lqnoiW z!|^$~zQralBvsM>n-}`=EJEmh=$A50&|@ZVe>y#nG_}|mhxdQ1SgFDpbd;py2~AZi zRt;D$0G#G#4y7Iy5!68EW!sr}x|4QR0G@tz1&mo4808>79Pw^iojzZcr&&x#SuP+1 zHF}Y0gsFI~k>3UpOpDQ|1y=DGss#P{68fnx)UqLuOeoL-JS;8Ncnh)k@-PZ=6WKBXi-*N-(t#G=TZe+tv<+PH2um}?fImuK@ zD@{#IigH**={$GS7^dqC8l$F&o=xi+_bF2471GqW@!Vg=jiW|UFT8-dj5MR{yQza0 z_?M4eoXpm3j6z>EqIN+S?1k;O<#5j)X+R0aWtUQ@2z4V)|K<`G8^)UTA6W&w*&kGTvNyO@% zq-9FU6iq3DOGD|*;;=d8b#K0I%q%?4CeS&mt{!J?l$-F9@HpG0$!xwZI`la)N?Ci} tw)G~TFXA`BcD-(Q+hMHTf>;JHv0!Tuy9IIJ5~M3j_#a!Zv`={}006E4?LYtk literal 2387 zcmV-Z39R-(RzV-r|wRDH0>>yY7# zsh`FG#CBt33kYKD6iV@a$Yb~T@z+DoFB8iEVh>?R;gF(&^lu*L)<4&FC(t)=Y@#1P zY#v58M6hq=nzq9M`o;*4SzW$u%`S+Ibupm@W7#Mum*9K11nnuGOIlag3f8kPA}ivsh3kr0b)}yZa~DJoZfUKd+lM--qsYd=}Z_vtdFY+A44Se zToUd0>Uev^tA_9k`Ezyw#NNj!a3hG<{~+x>w&gd;>TMny^)r{W0mSac5Isc@scrl2 z{g5^Prs7Iq<2%&}6hQ1N7_|^f6HXkM|Cy}!(FLzPMLQ;K0f_CyP`N|>{v%qo>56!q z)~~X5Q_c0O0I}s5oe<$@9DXtuH`a7tcTEqw;`wqVE1&E#M zNf0HER{VQhCv~)}Ms_0csCzs>Y&^zC5NrQ(;G^*6Veg%fKJi(!+wva(V*iDq9S2+V z)dsxTbtmc4yY>DVS96*HVrwxx#}hlgK}l)xu}x>v9Dkpg4lFpR!Zdt zDmyEpsW&pI$r~W{K1P8TL2MNt6wUN^|Gua&H#58NU?@QBVT`~D1hH+^GqtCP(f2E7 z?>;l<^zL2(Vnd%Hi0v&mW`C+m`BtNU$W}6`i<=C zlT7%z2uhj?P8}DezNzeg{N9#ToBdr7TY)iovI$2Zj?TH(G3B@7+7*4b){`^D9RRU^ z$B3Om5SK^Q1h466&2Upc5p{IMC;(zhG44WqKJA{U=4Q%^)ej5C{rb+gB>=IqsV1C+ zxLGta$-ALAb#w4pF#WZJxdOyCcoRhX#h8F7vj3cC_kyeaJ8Lci#Jc*JPyuoK*6yHP zA2wZFd-|9*b-@=w0I{tYDqn)=N)wI=El>7VRP4*Yc3}N9fY@q`2N2zTC;r%W#cO-= z%NO!5K54uK5F7O~6F!IdZpoicRhRqa-wj+{rhQ`<^cO~U{4^7`LiA*PtuL$lRsU;l zdq%Z>S-!^wv27R&NE7}H(Q{?e-R7!Kp6%KA;9S?z8PE!itn^tEJp7;UMur#jG&fT>Zlw_M`rNNmqBWajQ^(x%#bn#n13zgWSi zjI?l)Z>Cb#%1Ss^R;-}KfCCMPRBC;Z!`&gUff%kRBcpnW180Dx+~rmbe?+Za${}+w zp9WY$7jOt2EV1eqov3ZvjLo^cHPP+{2g+$*LVW zaalHkm90JD@}S1<*0;sEOCXu?%3=U+)0dbcR?;%HUP_P57p6>2BHq? z!(+fsb0uD^ZCA?Kk!f&dg7(?ihHHx*)8UZvqm^n+wsz%zD?b7c2KAi)ZH3Yt zDUAoq(!|rbytJts-JZ`sJ8Vv2gN|fEW-gUwfXgwiWd?6F=Qxz5gAOHY6uXY46A$K0 zMo3{fI;QGNjn0jo`6V-A3hn!_^64JK^n)3W&oT5ZHlIdZCH=pi#sG$8u#X}9rATA- zNM!8OzzM`nEjGwuJD9^&zR-pp&U*maCadA95UdvkPCIT6g(C_Js;BaD>h3(Tfk0rHpjmf}KW*peM{i@EQm-eY*=NM!h$PT^XKp{xkiBsuo8zvq+(>)# zTxU2KA9G=)V1^SJOVAi|Ri!XDF)PbqHKj9-o?5?9t5+M;Mbr{XXFN(p%Dh5~jC_gy zBQlbFfqedX*d=5TW#63*xXQeS8scQO>T-uZr^PU~V(;9_Eu2Gh{5SXf z&k$T!-GlD%ZM;M{(?t7-!TF)JX#CTS!(1hS#PPSKRn}4~d$)AhhugmT#@1sDSkLLY z%;VVo0OG{7msw1*Jp)&=&MYR2ZB_gKqhiwhM!++Uqq=tTg^o#{Or?(hm5-%&1IK!d z{RaZyh(BqEat0HtH&d3OC6hC>u&$2yBaFl5lsCNjhA}fct(8+MW@E@n?Wte3t F002ogwx|FA