From b416aab0bb16f4f5c640a946e1f1a168068ddd25 Mon Sep 17 00:00:00 2001 From: yjin Date: Tue, 17 Dec 2024 16:24:50 +0900 Subject: [PATCH] refactor: [GSW-2032] SonarQube issue when using Array.sort() Provide a compare function to avoid sorting elements alphabetically. "Array.prototype.sort()" and "Array.prototype.toSorted()" should use a compare function typescript:S2871 Software qualities impacted: Reliability --- packages/web/src/constants/common.constant.ts | 3 +- .../data/pool/use-pool-add-serach-param.ts | 3 +- packages/web/src/hooks/data/pool/use-pool.tsx | 3 +- .../src/hooks/data/pool/use-select-pool.tsx | 3 +- .../SelectPriceRangeCustom.tsx | 3 +- .../SelectPriceRangeCustomReposition.tsx | 3 +- .../EarnAddLiquidityContainer.tsx | 5 +-- .../PoolAddLiquidityContainer.tsx | 3 +- .../web/src/repositories/pool/pool.message.ts | 3 +- packages/web/src/utils/pool-utils.ts | 5 +-- packages/web/src/utils/sort-utils.spec.ts | 34 +++++++++++++++++++ packages/web/src/utils/sort-utils.ts | 30 ++++++++++++++++ 12 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 packages/web/src/utils/sort-utils.spec.ts create mode 100644 packages/web/src/utils/sort-utils.ts diff --git a/packages/web/src/constants/common.constant.ts b/packages/web/src/constants/common.constant.ts index df1c44f3f..577f94b67 100644 --- a/packages/web/src/constants/common.constant.ts +++ b/packages/web/src/constants/common.constant.ts @@ -1,4 +1,5 @@ import { GNS_TOKEN_PATH, WRAPPED_GNOT_PATH } from "./environment.constant"; +import { sortTokenPaths } from "@utils/sort-utils"; export const DEFAULT_NETWORK_ID = "portal-loop"; @@ -309,7 +310,7 @@ export const DEFAULT_POOL_ADD_URI = `/earn/add?tokenA=gnot&tokenB=${GNS_TOKEN_PA export const DEFAULT_TOKEN_PAIR = [WRAPPED_GNOT_PATH, GNS_TOKEN_PATH]; -export const DEFAULT_POOL_PATH = [...DEFAULT_TOKEN_PAIR.sort(), "3000"].join(":"); +export const DEFAULT_POOL_PATH = [...DEFAULT_TOKEN_PAIR.sort(sortTokenPaths), "3000"].join(":"); export const LANGUAGE_CODE_MAP: Record = { en: "en-US", diff --git a/packages/web/src/hooks/data/pool/use-pool-add-serach-param.ts b/packages/web/src/hooks/data/pool/use-pool-add-serach-param.ts index ef84d67fa..ab8661401 100644 --- a/packages/web/src/hooks/data/pool/use-pool-add-serach-param.ts +++ b/packages/web/src/hooks/data/pool/use-pool-add-serach-param.ts @@ -2,6 +2,7 @@ import { useMemo } from "react"; import useCustomRouter from "@hooks/data/common/router/use-custom-router"; import { checkGnotPath } from "@utils/common"; +import { sortTokenPaths } from "@utils/sort-utils"; export const usePoolAddSearchParams = () => { const router = useCustomRouter(); @@ -16,7 +17,7 @@ export const usePoolAddSearchParams = () => { const poolPathSplit = poolPathParam?.split(":"); return [poolPathSplit[0], poolPathSplit[1]]; } - return [checkGnotPath(tokenAPath), checkGnotPath(tokenBPath)].sort(); + return [checkGnotPath(tokenAPath), checkGnotPath(tokenBPath)].sort(sortTokenPaths); }, [poolPathParam, tokenAPath, tokenBPath]); const poolPath = useMemo(() => { diff --git a/packages/web/src/hooks/data/pool/use-pool.tsx b/packages/web/src/hooks/data/pool/use-pool.tsx index 48c4cfe59..8d00f68a8 100644 --- a/packages/web/src/hooks/data/pool/use-pool.tsx +++ b/packages/web/src/hooks/data/pool/use-pool.tsx @@ -8,6 +8,7 @@ import { PoolModel } from "@models/pool/pool-model"; import { isNativeToken, TokenModel } from "@models/token/token-model"; import { useGetPoolCreationFee, useGetRPCPoolsBy } from "@query/pools"; import { checkGnotPath } from "@utils/common"; +import { sortTokenPaths } from "@utils/sort-utils"; interface Props { compareToken: TokenModel | null; @@ -29,7 +30,7 @@ export const usePool = ({ compareToken, tokenA, tokenB, isReverted = false }: Pr const tokenATokenPath = checkGnotPath(tokenA.path) ? tokenA.wrappedPath : tokenA.path; const tokenBTokenPath = checkGnotPath(tokenB.path) ? tokenB.wrappedPath : tokenB.path; - const tokenPair = [tokenATokenPath, tokenBTokenPath].sort(); + const tokenPair = [tokenATokenPath, tokenBTokenPath].sort(sortTokenPaths); return [ SwapFeeTierInfoMap.FEE_100, diff --git a/packages/web/src/hooks/data/pool/use-select-pool.tsx b/packages/web/src/hooks/data/pool/use-select-pool.tsx index b8da0ff04..5f4461eb7 100644 --- a/packages/web/src/hooks/data/pool/use-select-pool.tsx +++ b/packages/web/src/hooks/data/pool/use-select-pool.tsx @@ -25,6 +25,7 @@ import { tickToPrice, } from "@utils/swap-utils"; import { makeDisplayTokenAmount } from "@utils/token-utils"; +import { sortTokenPaths } from "@utils/sort-utils"; type RenderState = "NONE" | "CREATE" | "LOADING" | "DONE"; @@ -118,7 +119,7 @@ export const useSelectPool = ({ return null; } - return [checkGnotPath(tokenA.path), checkGnotPath(tokenB.path)].sort(); + return [checkGnotPath(tokenA.path), checkGnotPath(tokenB.path)].sort(sortTokenPaths); }, [tokenA, tokenB]); const isReverse = useMemo(() => { diff --git a/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustom.tsx b/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustom.tsx index 720251853..8b566c2e5 100644 --- a/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustom.tsx +++ b/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustom.tsx @@ -24,6 +24,7 @@ import { TokenModel } from "@models/token/token-model"; import { checkGnotPath } from "@utils/common"; import { formatTokenExchangeRate } from "@utils/stake-position-utils"; import { priceToTick, tickToPrice } from "@utils/swap-utils"; +import { sortTokenPaths } from "@utils/sort-utils"; import PriceSteps from "./price-steps/PriceSteps"; import StartingPrice from "./starting-price/StartingPrice"; @@ -91,7 +92,7 @@ const SelectPriceRangeCustom = forwardRef { - const compareTokenPaths = [checkGnotPath(tokenA.path), checkGnotPath(tokenB.path)].sort(); + const compareTokenPaths = [checkGnotPath(tokenA.path), checkGnotPath(tokenB.path)].sort(sortTokenPaths); return compareTokenPaths[0] !== checkGnotPath(selectPool.compareToken?.path || ""); }, [selectPool.compareToken, tokenA.path, tokenB.path]); diff --git a/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustomReposition.tsx b/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustomReposition.tsx index e3ffdd019..408fc3a35 100644 --- a/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustomReposition.tsx +++ b/packages/web/src/layouts/pool/common/components/select-price-range/select-price-range-custom/SelectPriceRangeCustomReposition.tsx @@ -20,6 +20,7 @@ import { TokenModel } from "@models/token/token-model"; import { checkGnotPath } from "@utils/common"; import { formatTokenExchangeRate } from "@utils/stake-position-utils"; import { priceToTick, tickToPrice } from "@utils/swap-utils"; +import { sortTokenPaths } from "@utils/sort-utils"; import SelectPriceRangeCutomController from "./price-steps/PriceSteps"; @@ -78,7 +79,7 @@ const SelectPriceRangeCustomReposition: React.FC { useEffect(() => { if (pools.length > 0 && tokenA && tokenB && selectPool.compareToken) { - const tokenPair = [tokenA.wrappedPath, tokenB.wrappedPath].sort(); + const tokenPair = [tokenA.wrappedPath, tokenB.wrappedPath].sort(sortTokenPaths); const compareToken = selectPool.compareToken; const reverse = tokenPair.findIndex(path => { @@ -575,7 +576,7 @@ const EarnAddLiquidityContainer: React.FC = () => { useEffect(() => { const pair = [tokenA?.path, tokenB?.path] .filter(item => item !== undefined) - .sort() + .sort(sortTokenPaths) .join(":"); const isDifferentPair = pair !== lastPoolPathRef.current; diff --git a/packages/web/src/layouts/pool/pool-add/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx b/packages/web/src/layouts/pool/pool-add/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx index fec93c155..43bb41d98 100644 --- a/packages/web/src/layouts/pool/pool-add/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx +++ b/packages/web/src/layouts/pool/pool-add/containers/pool-add-liquidity-container/PoolAddLiquidityContainer.tsx @@ -35,6 +35,7 @@ import { tickToPrice, } from "@utils/swap-utils"; import { makeDisplayTokenAmount, makeRawTokenAmount } from "@utils/token-utils"; +import { sortTokenPaths } from "@utils/sort-utils"; import PoolAddLiquidity, { PriceRangeSummary } from "../../components/pool-add-liquidity/PoolAddLiquidity"; import { usePool } from "../../../../../hooks/data/pool/use-pool"; @@ -470,7 +471,7 @@ const PoolAddLiquidityContainer: React.FC = () => { useEffect(() => { if (pools.length > 0 && tokenA && tokenB && selectPool.compareToken) { - const tokenPair = [tokenA.wrappedPath, tokenB.wrappedPath].sort(); + const tokenPair = [tokenA.wrappedPath, tokenB.wrappedPath].sort(sortTokenPaths); const compareToken = selectPool.compareToken; const reverse = tokenPair.findIndex(path => { diff --git a/packages/web/src/repositories/pool/pool.message.ts b/packages/web/src/repositories/pool/pool.message.ts index 29c6e9eae..e36462c9a 100644 --- a/packages/web/src/repositories/pool/pool.message.ts +++ b/packages/web/src/repositories/pool/pool.message.ts @@ -25,6 +25,7 @@ import { MAX_INT64, tickToSqrtPriceX96 } from "@utils/math.utils"; import { isOrderedTokenPaths } from "@utils/pool-utils"; import { priceToTick } from "@utils/swap-utils"; import { makeRawTokenAmount } from "@utils/token-utils"; +import { sortTokenPaths } from "@utils/sort-utils"; enum PoolTransactionMessageFunctionType { CreatePool = "CreatePool", @@ -73,7 +74,7 @@ export function makeCreatePoolMessageWithApproves( */ const isOrdered = isOrderedTokenPaths(tokenAPath, tokenBPath); - const [orderedPoolAPath, orderedPoolBPath] = [tokenAPath, tokenBPath].sort(); + const [orderedPoolAPath, orderedPoolBPath] = [tokenAPath, tokenBPath].sort(sortTokenPaths); const orderedStartPriceNum = isOrdered || startPriceNum === 0 ? startPriceNum : 1 / startPriceNum; const startPriceSqrt = tickToSqrtPriceX96(priceToTick(orderedStartPriceNum)); diff --git a/packages/web/src/utils/pool-utils.ts b/packages/web/src/utils/pool-utils.ts index d977e416a..a4babadb6 100644 --- a/packages/web/src/utils/pool-utils.ts +++ b/packages/web/src/utils/pool-utils.ts @@ -6,6 +6,7 @@ import { } from "@constants/option.constant"; import { TokenModel } from "@models/token/token-model"; import { tickToPriceStr } from "./swap-utils"; +import { sortTokenPaths } from "./sort-utils"; const maxTicks = Object.values(SwapFeeTierMaxPriceRangeMap).map(range => range.maxTick); const minTicks = Object.values(SwapFeeTierMaxPriceRangeMap).map(range => range.maxTick); @@ -20,7 +21,7 @@ export function makePoolPath( } const tokenAPath = tokenA.wrappedPath || tokenA.path || ""; const tokenBPath = tokenB.wrappedPath || tokenB.path || ""; - return [tokenAPath, tokenBPath].sort().join(":") + ":" + SwapFeeTierInfoMap[swapFeeTier].fee; + return [tokenAPath, tokenBPath].sort(sortTokenPaths).join(":") + ":" + SwapFeeTierInfoMap[swapFeeTier].fee; } export function isMaxTick(tick: number) { @@ -50,5 +51,5 @@ export function checkPoolStakingRewards(type?: INCENTIVE_TYPE) { } export function isOrderedTokenPaths(tokenAPath: string, tokenBPath: string): boolean { - return [tokenAPath, tokenBPath].sort()?.[0] === tokenAPath; + return [tokenAPath, tokenBPath].sort(sortTokenPaths)?.[0] === tokenAPath; } diff --git a/packages/web/src/utils/sort-utils.spec.ts b/packages/web/src/utils/sort-utils.spec.ts new file mode 100644 index 000000000..3771eab66 --- /dev/null +++ b/packages/web/src/utils/sort-utils.spec.ts @@ -0,0 +1,34 @@ +import { sortTokenPaths } from "./sort-utils"; +import { GNS_TOKEN_PATH, WRAPPED_GNOT_PATH } from "@constants/environment.constant"; + +describe("sortTokenPaths utility function test", () => { + test("Same result as default sort - plain string", () => { + const tokens = ["gns", "GNS", "Gns", "FOO", "BAR", "BAZ"]; + + expect([...tokens].sort()).toEqual([...tokens].sort(sortTokenPaths)); + }); + + test("Same result as default sort - includes undefined", () => { + const tokens = ["gns", "GNS", undefined, "Gns", "FOO", "BAR", "BAZ"]; + + expect([...tokens].sort()).toEqual([...tokens].sort(sortTokenPaths)); + }); + + test("Same result as default sort - includes number", () => { + const tokens = ["GNOT1", "GNOT2", "GNOT10", "GNS1", "GNS10", "GNS2", "wrapped.GNOT1", "wrapped.GNS1", undefined]; + + expect([...tokens].sort()).toEqual([...tokens].sort(sortTokenPaths)); + }); + + test("Same result as default sort - includes special characters", () => { + const tokens = ["gns-1", "gns_1", "gns.1", "gns/1", undefined, "GNS"]; + + expect([...tokens].sort()).toEqual([...tokens].sort(sortTokenPaths)); + }); + + test("Testing real-world use cases", () => { + const tokens = [WRAPPED_GNOT_PATH, GNS_TOKEN_PATH]; + + expect([...tokens].sort()).toEqual([...tokens].sort(sortTokenPaths)); + }); +}); diff --git a/packages/web/src/utils/sort-utils.ts b/packages/web/src/utils/sort-utils.ts new file mode 100644 index 000000000..82b87208b --- /dev/null +++ b/packages/web/src/utils/sort-utils.ts @@ -0,0 +1,30 @@ +/** + * A comparison function for token path strings + * + * to comply with SonarQube's "Provide a compare function when using 'Array.prototype.sort()'" rule. + * Used as a comparison function for Array.prototype.sort() to determine the order of token paths. + * + * Performs lexicographical sorting based on UTF-16 code unit values, identical to default .sort() + * Handles undefined values by treating them as empty strings + * + * @param tokenA - First token path (possibly undefined) + * @param tokenB - Second token path (possibly undefined) + * @returns + * -1 if tokenA < tokenB + * 1 if tokenA > tokenB + * 0 if tokenA === tokenB + * + * @example + * Same result as default .sort() + * ['gns', undefined, 'GNS'].sort() // ['GNS', 'gns', undefined] + * ['gns', undefined, 'GNS'].sort(compareTokenPaths) // ['GNS', 'gns', undefined] + * + * Actual usage examples + * const tokenPair = [tokenAPath, tokenBPath].sort(compareTokenPaths); + * const poolPath = [...[WRAPPED_GNOT_PATH, GNS_TOKEN_PATH].sort(compareTokenPaths), "3000"].join(":"); + */ +export const sortTokenPaths = (tokenA: string | undefined, tokenB: string | undefined): number => { + const tokenAString = tokenA ?? ""; + const tokenBString = tokenB ?? ""; + return tokenAString === tokenBString ? 0 : tokenAString > tokenBString ? 1 : -1; +};