From 9bbc948cd0900706ce74ff94c1d898ca7ba6d354 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Tue, 9 May 2023 13:32:19 +0800 Subject: [PATCH] `prefer-modern-math-api`: Check cases that `Math.hypot()` should be preferred (#2080) --- docs/rules/prefer-modern-math-apis.md | 21 ++ rules/prefer-modern-math-apis.js | 66 +++++- test/prefer-modern-math-apis.mjs | 30 +++ test/snapshots/prefer-modern-math-apis.mjs.md | 224 ++++++++++++++++++ .../prefer-modern-math-apis.mjs.snap | Bin 825 -> 1495 bytes 5 files changed, 340 insertions(+), 1 deletion(-) diff --git a/docs/rules/prefer-modern-math-apis.md b/docs/rules/prefer-modern-math-apis.md index fd0ac536ab..c25a7e0bd2 100644 --- a/docs/rules/prefer-modern-math-apis.md +++ b/docs/rules/prefer-modern-math-apis.md @@ -59,6 +59,27 @@ Math.LOG2E * Math.log(x) Math.log(x) / Math.LN2 ``` +## Prefer `Math.hypot(…)` over + +```js +Math.sqrt(a * a + b * b) +``` + +```js +Math.sqrt(a ** 2 + b ** 2) +``` + +```js +Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)) +``` + +*This case requires [`prefer-exponentiation-operator`](https://eslint.org/docs/latest/rules/prefer-exponentiation-operator) rule to fix it first.* + +```js +Math.sqrt(x ** 2) +// This case fix to `Math.abs(x)`, since it should be better than `Math.hypot(x)` +``` + ## Separate rule for `Math.trunc()` See [`unicorn/prefer-math-trunc`](./prefer-math-trunc.md) rule. diff --git a/rules/prefer-modern-math-apis.js b/rules/prefer-modern-math-apis.js index bc7d0ac275..69e7dbb9fe 100644 --- a/rules/prefer-modern-math-apis.js +++ b/rules/prefer-modern-math-apis.js @@ -1,5 +1,9 @@ 'use strict'; -const {getParenthesizedText} = require('./utils/parentheses.js'); +const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js'); +const {methodCallSelector} = require('./selectors/index.js'); +const isSameReference = require('./utils/is-same-reference.js'); +const {isLiteral} = require('./ast/index.js'); +const {replaceNodeOrTokenAndSpacesBefore, removeParentheses} = require('./fix/index.js'); const MESSAGE_ID = 'prefer-modern-math-apis'; const messages = { @@ -102,11 +106,71 @@ const checkFunctions = [ createLogCallDivideConstantCheck({constantName: 'LN2', replacementMethod: 'log2'}), ]; +const mathSqrtCallSelector = methodCallSelector({object: 'Math', method: 'sqrt', argumentsLength: 1}); + +const isPlusExpression = node => node.type === 'BinaryExpression' && node.operator === '+'; + +const isPow2Expression = node => + node.type === 'BinaryExpression' + && ( + // `x * x` + (node.operator === '*' && isSameReference(node.left, node.right)) + // `x ** 2` + || (node.operator === '**' && isLiteral(node.right, 2)) + ); + +const flatPlusExpression = node => + isPlusExpression(node) + ? [node.left, node.right].flatMap(child => flatPlusExpression(child)) + : [node]; + /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const nodes = []; return { + [mathSqrtCallSelector](callExpression) { + const expressions = flatPlusExpression(callExpression.arguments[0]); + if (expressions.some(expression => !isPow2Expression(expression))) { + return; + } + + const replacementMethod = expressions.length === 1 ? 'abs' : 'hypot'; + const plusExpressions = new Set(expressions.length === 1 ? [] : expressions.map(expression => expression.parent)); + + return { + node: callExpression.callee.property, + messageId: MESSAGE_ID, + data: { + replacement: `Math.${replacementMethod}(…)`, + description: 'Math.sqrt(…)', + }, + * fix(fixer) { + const {sourceCode} = context; + + // `Math.sqrt` -> `Math.{hypot,abs}` + yield fixer.replaceText(callExpression.callee.property, replacementMethod); + + // `a ** 2 + b ** 2` -> `a, b` + for (const expression of plusExpressions) { + const plusToken = sourceCode.getTokenAfter(expression.left, token => token.type === 'Punctuator' && token.value === '+'); + + yield * replaceNodeOrTokenAndSpacesBefore(plusToken, ',', fixer, sourceCode); + yield * removeParentheses(expression, fixer, sourceCode); + } + + // `x ** 2` => `x` + // `x * a` => `x` + for (const expression of expressions) { + yield fixer.removeRange([ + getParenthesizedRange(expression.left, sourceCode)[1], + expression.range[1], + ]); + } + }, + }; + }, + BinaryExpression(node) { nodes.push(node); }, diff --git a/test/prefer-modern-math-apis.mjs b/test/prefer-modern-math-apis.mjs index fdaa9e472a..9fd1407c33 100644 --- a/test/prefer-modern-math-apis.mjs +++ b/test/prefer-modern-math-apis.mjs @@ -65,3 +65,33 @@ test.snapshot({ `, ].flatMap(code => duplicateLog10Test(code)), }); + +// `Math.hypot` +test.snapshot({ + valid: [ + 'Math.notSqrt(a ** 2 + b ** 2)', + 'NotMath.sqrt(a ** 2 + b ** 2)', + 'Math.sqrt(a ** 2 - b ** 2)', + 'Math.sqrt(a ** 2 + 2 ** b)', + 'Math.sqrt(a * c + b * c)', + 'Math.sqrt((++a) * (++a))', + // Leave this to `prefer-exponentiation-operator` rule + 'Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))', + ], + invalid: [ + 'Math.sqrt(a * a + b * b)', + 'Math.sqrt(a ** 2 + b ** 2)', + 'Math.sqrt(a * a + b ** 2)', + 'Math.sqrt(a * a + b * b + c * c)', + 'Math.sqrt(a ** 2 + b ** 2 + c ** 2)', + 'Math.sqrt(a * a)', + 'Math.sqrt(a ** 2)', + 'Math.sqrt(a * a,)', + 'Math.sqrt(a ** 2,)', + 'Math.sqrt((a, b) ** 2)', + 'Math.sqrt((++a) ** 2)', + 'Math.sqrt(a * a + b * b,)', + 'Math.sqrt(a ** 2 + b ** 2,)', + 'Math.sqrt((( a ** 2 )) + (( b ** 2 + c ** 2 )) + (( d )) * (( d )) + (( e )) ** (( 2 )))', + ], +}); diff --git a/test/snapshots/prefer-modern-math-apis.mjs.md b/test/snapshots/prefer-modern-math-apis.mjs.md index cbdf2dc5d7..637f41a6f1 100644 --- a/test/snapshots/prefer-modern-math-apis.mjs.md +++ b/test/snapshots/prefer-modern-math-apis.mjs.md @@ -257,3 +257,227 @@ Generated by [AVA](https://avajs.dev). 5 | );␊ 6 | }␊ ` + +## Invalid #1 + 1 | Math.sqrt(a * a + b * b) + +> Output + + `␊ + 1 | Math.hypot(a, b)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a * a + b * b)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #2 + 1 | Math.sqrt(a ** 2 + b ** 2) + +> Output + + `␊ + 1 | Math.hypot(a, b)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a ** 2 + b ** 2)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #3 + 1 | Math.sqrt(a * a + b ** 2) + +> Output + + `␊ + 1 | Math.hypot(a, b)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a * a + b ** 2)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #4 + 1 | Math.sqrt(a * a + b * b + c * c) + +> Output + + `␊ + 1 | Math.hypot(a, b, c)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a * a + b * b + c * c)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #5 + 1 | Math.sqrt(a ** 2 + b ** 2 + c ** 2) + +> Output + + `␊ + 1 | Math.hypot(a, b, c)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a ** 2 + b ** 2 + c ** 2)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #6 + 1 | Math.sqrt(a * a) + +> Output + + `␊ + 1 | Math.abs(a)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a * a)␊ + | ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #7 + 1 | Math.sqrt(a ** 2) + +> Output + + `␊ + 1 | Math.abs(a)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a ** 2)␊ + | ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #8 + 1 | Math.sqrt(a * a,) + +> Output + + `␊ + 1 | Math.abs(a,)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a * a,)␊ + | ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #9 + 1 | Math.sqrt(a ** 2,) + +> Output + + `␊ + 1 | Math.abs(a,)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a ** 2,)␊ + | ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #10 + 1 | Math.sqrt((a, b) ** 2) + +> Output + + `␊ + 1 | Math.abs((a, b))␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt((a, b) ** 2)␊ + | ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #11 + 1 | Math.sqrt((++a) ** 2) + +> Output + + `␊ + 1 | Math.abs((++a))␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt((++a) ** 2)␊ + | ^^^^ Prefer \`Math.abs(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #12 + 1 | Math.sqrt(a * a + b * b,) + +> Output + + `␊ + 1 | Math.hypot(a, b,)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a * a + b * b,)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #13 + 1 | Math.sqrt(a ** 2 + b ** 2,) + +> Output + + `␊ + 1 | Math.hypot(a, b,)␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt(a ** 2 + b ** 2,)␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` + +## Invalid #14 + 1 | Math.sqrt((( a ** 2 )) + (( b ** 2 + c ** 2 )) + (( d )) * (( d )) + (( e )) ** (( 2 ))) + +> Output + + `␊ + 1 | Math.hypot((( a )), b, c , (( d )), (( e )))␊ + ` + +> Error 1/1 + + `␊ + > 1 | Math.sqrt((( a ** 2 )) + (( b ** 2 + c ** 2 )) + (( d )) * (( d )) + (( e )) ** (( 2 )))␊ + | ^^^^ Prefer \`Math.hypot(…)\` over \`Math.sqrt(…)\`.␊ + ` diff --git a/test/snapshots/prefer-modern-math-apis.mjs.snap b/test/snapshots/prefer-modern-math-apis.mjs.snap index 8a94f5ab7ae3bedff72cbd8bf93371d80c36be5c..3b0aafca3c0bfce8c12984db96127d829803198e 100644 GIT binary patch literal 1495 zcmV;|1t|JKRzVqvK4Y3^9X%iU?x?ySNC5LhSMoNy*264g*9! zLTjWF4HeVCUJhW2g*KJi_-Gu$5UnPY0j4P)S?8X6#J%?}yLa(sXXcx|`^~xk`QPWi zZ4LnV0dJ4ee#NxO&d!}zR-;qh*$^;m5yb$8-!B{eD?ZIq7|+OWCf59X6cTo+jD-rn zfSOLa-PoIXrgrgH@`Ja7*FnN2Q1oL2IKSJP*HLxTzghPD;__GKLBdXRV9|mx+qq)b ztl|A8x2O~6u3oMmf`s*aip51c-*K;DZ_)k1>X!YGu)8Q`KMg=i)ccKbZ7wZF_ct=)3v(hN zVWTNN#7OtNzInJbzJ6A8yT_d=D+3^5uTezF@s7Vw$CvNUyj=9xg|i#i6%9bb=1^S4 zFd)PC@5LD#_B8xcRh@Y8EF|m*MYaNfJjcN37md4K`bNGpb?O>d99xF1r0{kIV8d@d z-4THon|4%;3~%}=+y@dif}#$iY{%%-h^&w$e+->Z_IrE9aY)$H6pLH{C|`bWdRKV; zq{wA^P97>=Far`co8kwI!*dE|Wckc5Yj>(CY;r3PgoGWTNP7l=j~efvGS2jFT3u`U zy3x1m79{Le3O82(zU*slANkI3?02WFbL#`DFF?XZP&8nC+nA*GRwRb^xq41--B#BD z3EN4L;0C}Ye?2%ZyOD|(Jojc#Qe7w{>}HC|?f`VT+`VeZ>xcXok!7* z(d*h%o80hvOVaV^-mSXp8IZ8QQf%?SzxxI~t_&Vc{j7P@ir7EzcS6D*qzLo`V0hrb zzUI^uC7D6_AGkdDq!JP~hT>z4(S*wR9o<{54OK3?I60~f=Q_i7Q>^d;zyUasN|iYR z4_Sl)p)gd6mgr2G!I>NKa!pF566ugir9nuqLFq_?FtM5t_`!f5R0?wuupTHDK9XU4 zHJ^$I3PM`E8>yN1^z2_X7Rccl!6<0#N&WB)W*2_clH(9wrHHH)yik0;A;XZ5(#(|Z zDLbS}L%D@xG|_{w!5npEOR2>f1?$sIS-I;`MsBXMScNuXUW<8ozQI(GzaA;^|2fQm zIgyu>yCy6Y@55|3!>Z=vrwIJ?8rK^afN1#(dGcB5h;S*77T!lrl*>a{7stegh0={F z+{N__tFpi|%t;K1%*0_QvDUD5%@x=s^EXcvRx*xHsHRw$5LKLnh>42|3ys7v9y{FZ zJ&RTdmngf02U!ZgAI}c$xMu9+n88*2;VJk`ZKOym1V(7B#Kv4c--P%HXRua80-t{p z39Pl~GrK(WNS5v7^)e5-PH$A|xUrV46tpn5f7$3gj41J)$5+^EbsD7Ssgh<%72HnM zOb=ytOWF-5T-M{(1Fk_y%i2Oo;3N2UOtb7;e1Y-YyLOyNv;H-|qa^mU_{Ou%nxk!$ z&|@_nPDyc;_ZAe8hUeF$ar~;0w1l1=C(Jx$(rpFVmR9x#cM9J>O0`qvva86=& z{a=knXRXmlpa0&1{4eGyRKgn@w4BSl*w(ZlCe;N}Eoo}=wn((>IZ0z^r@L4W*)x_% zHjap_Nvr8bw*PnOX0c;gyz`Mx+J@yLykGfo++=-Pwwd=*PRHUphu&6ul{l96I#Eyz zB4!c^XAwJQstQq8{dk{yk7on-^Uq#~7h7<+XoV$HbfRB#f_uR3Z&Jx_8{3WEWX;vq x$3Th3KVQLkL%IS_&zFVs4cR9BfFpX^&$NE&u(Lk%*5mf8e*uIhei;)N0029k=-vPT literal 825 zcmV-91IGM8RzVlU$B-E_tk%%LEop zg5v)`tjN7^Z^4ni+p?P(&0X4KEt$ZgnoJOU2#D3WJEo}qnq9*0zTxoIiwk}-fkhRV zA@~ar>t{!*T=>H|cN){rNB#okKr=w1@+=U%2Z$}!L@LI~9(l8LdilH0{aa=;fkmG| zaV0AQLjkLrr(^LHkF%Uz@lxr0SxjKj*-)&^#=y|f`A5n$Q{VUN&!dqtb3)fKfkj=R z_$&}_TKR5kv5Lsrlu0G0S4!M{zyuck0L3-z3=HR1Mrez0g_*qP6O!B8GV=%%Sad!V zYjZF#T#!v-SjTuHUcp9p@}r2E225blFerWt#CLg~J(ypzQD~A{gZ7rBeG{0#q5_-{ zJPnAS@I9IoxwLUh#5&I>&5763n82b3pg59?f#Kz>&w`geua4ijxh~Z2`@0iNV9{zQ z{tLv+4D8_8WMpL!WDMj|P%u=eQAjJzOD@UG&r?Xt&)2BXRHz1u8v(^Ri&9HUi}DmS zfP7;RpVK$7BttJJKi$v(s17J<0ut4<2C__ntXeK)bYsZh|=)I<$}J_ewZ;c9*S-3<*~6|~?wk@aEq1tuWMUziqwmFS`R zLk7(sMmW9Eh2jlVhr?_E+G*rUq%SZ$VT31ckj+Q;LIBw**ocS(e3Tvu*a&NwkrYA* zvuK!jamG3@rBOBUVu^U5733%07JS|y5DgS1T$E@*al8nI<10`d%~*w2ZWy8D<_bc^ z1u^c#Fcn)FL840`jw7W=pjuHNikYy{omz*#FeVtP)GT~SbuLE85|QnwnB7PyHadvP zZ@B7h%KU&@kCPhU6<7kCXa`fxq2yM}82Opx#swWqjS_r8jlV1=zYGQdB{wSJ-wXf% DME89r