From 08df1812beb4afda67a9a2e85ef1f19215d66686 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 17 Sep 2026 16:19:02 +0200 Subject: [PATCH] fix(sonar): spell out the order these five sorts already have SonarCloud's S2871 fires on a bare .sort() over strings and asks for localeCompare, and it had five of them on new code, which is what put new reliability at D and failed the gate. All five sort ISO dates or query parameter names, where the default lexicographic order is exactly the one wanted. Taking the rule's suggestion literally would have been the wrong fix twice over: a locale-aware comparison reorders dates differently per runtime, and on the Amap signature it would produce a canonical string Amap itself does not compute, so every signed request would come back rejected. So the comparator is written out rather than changed. Byte order, explicitly, with a line saying why it has to stay byte order. Behaviour is identical: the 43 Amap cases, the 40 track cases and the 30 client cases over these modules all pass unchanged. --- client/src/components/Dawarich/dawarichSuggestionModel.ts | 4 +++- client/src/components/Map/dawarichTrail.ts | 4 +++- client/src/hooks/useDawarichJournalTrail.ts | 4 +++- server/src/nest/integrations/dawarich.helpers.ts | 3 ++- server/src/nest/maps/providers/amap.provider.ts | 5 ++++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/client/src/components/Dawarich/dawarichSuggestionModel.ts b/client/src/components/Dawarich/dawarichSuggestionModel.ts index 7dbfe2179..0e7927e31 100644 --- a/client/src/components/Dawarich/dawarichSuggestionModel.ts +++ b/client/src/components/Dawarich/dawarichSuggestionModel.ts @@ -34,7 +34,9 @@ export function groupByDay(suggestions: DawarichSuggestion[]): SuggestionDay[] { else byDate.set(stay.localDate, [stay]) } - const oldestFirst = [...byDate.keys()].sort() + // ISO dates, so lexicographic order IS chronological order. Spelled out rather than + // left bare, and deliberately not locale-aware, which would vary by runtime. + const oldestFirst = [...byDate.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) return oldestFirst .map((date, index) => ({ date, diff --git a/client/src/components/Map/dawarichTrail.ts b/client/src/components/Map/dawarichTrail.ts index 97adec6af..5fc536d6a 100644 --- a/client/src/components/Map/dawarichTrail.ts +++ b/client/src/components/Map/dawarichTrail.ts @@ -130,7 +130,9 @@ export function collapsedDayDates( const foldedInPlan = !!expandedDayIds && !expandedDayIds.has(day.id) if (foldedInPlan || collapsedDayIds?.has(day.id)) dates.add(day.date) } - return [...dates].sort() + // Explicit rather than bare: these are ISO dates, where lexicographic IS chronological, + // and a locale-aware comparison would reorder them differently per runtime. + return [...dates].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) } /** The overlay as a GeoJSON FeatureCollection, for the GL renderer. */ diff --git a/client/src/hooks/useDawarichJournalTrail.ts b/client/src/hooks/useDawarichJournalTrail.ts index de9e7c380..a16068c24 100644 --- a/client/src/hooks/useDawarichJournalTrail.ts +++ b/client/src/hooks/useDawarichJournalTrail.ts @@ -37,7 +37,9 @@ export function useDawarichJournalTrail( // here rather than trusting insertion order. const window = useMemo(() => { if (dates.size === 0) return null - const sorted = [...dates].sort() + // ISO dates: lexicographic order is chronological, and staying locale-independent + // keeps the window identical on every runtime. + const sorted = [...dates].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) return { from: `${sorted[0]}T00:00:00Z`, to: `${sorted[sorted.length - 1]}T23:59:59Z` } }, [dates]) diff --git a/server/src/nest/integrations/dawarich.helpers.ts b/server/src/nest/integrations/dawarich.helpers.ts index 428666290..e82e28aaa 100644 --- a/server/src/nest/integrations/dawarich.helpers.ts +++ b/server/src/nest/integrations/dawarich.helpers.ts @@ -340,7 +340,8 @@ export function bucketPointsByDay( else byDay.set(date, [{ lat, lng, ts }]); } - const dates = [...byDay.keys()].sort(); + // ISO dates, so lexicographic order is chronological. Explicit and locale-independent. + const dates = [...byDay.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); const days: DawarichTrackDay[] = []; for (let d = 0; d < dates.length; d++) { diff --git a/server/src/nest/maps/providers/amap.provider.ts b/server/src/nest/maps/providers/amap.provider.ts index 83dca9f13..e273cf81c 100644 --- a/server/src/nest/maps/providers/amap.provider.ts +++ b/server/src/nest/maps/providers/amap.provider.ts @@ -203,7 +203,10 @@ export class AmapPlacesProvider implements PlacesProvider { if (!secret) return null; const signed = { ...params, key: this.credential.key, output: 'JSON' }; const canonical = Object.keys(signed) - .sort() + // Byte order, and it has to stay byte order: Amap computes the same signature over + // the same sorted names, so a locale-aware comparison would produce a signature the + // other side rejects. + .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) .map((k) => `${k}=${signed[k]}`) .join('&'); return createHash('md5').update(`${canonical}${secret}`).digest('hex');