diff --git a/src/utils/highlights.js b/src/utils/highlights.js index 65a3eeffd..a403ef1bd 100644 --- a/src/utils/highlights.js +++ b/src/utils/highlights.js @@ -74,6 +74,21 @@ const toNumber = (value) => { return undefined; }; +const extractNumericToken = (value) => { + if (typeof value !== "string") return undefined; + const match = value.match(/[-+]?\d[\d\s.,]*/); + if (!match) return undefined; + + const token = match[0].trim(); + if (!token) return undefined; + + const prefix = value.slice(0, match.index).trim(); + const suffix = value.slice((match.index ?? 0) + match[0].length).trim(); + if (/\d/.test(prefix) || /\d/.test(suffix)) return undefined; + + return token; +}; + const parseNumericValue = (value) => { if (value === null || value === undefined) return undefined; if (typeof value === "number" && Number.isFinite(value)) return value; @@ -85,7 +100,9 @@ const parseNumericValue = (value) => { const direct = Number(trimmed); if (!Number.isNaN(direct)) return direct; - const compact = trimmed.replace(/\s+/g, ""); + const candidate = extractNumericToken(trimmed); + const numericString = candidate ?? trimmed; + const compact = numericString.replace(/\s+/g, ""); if (!compact || !/^[-+]?[0-9.,]+$/.test(compact)) return undefined; const commaCount = (compact.match(/,/g) || []).length; diff --git a/src/utils/highlights.test.js b/src/utils/highlights.test.js index 9be6bd078..dca18a8e5 100644 --- a/src/utils/highlights.test.js +++ b/src/utils/highlights.test.js @@ -136,6 +136,9 @@ describe("utils/highlights", () => { const cfg = buildHighlightConfig(null, { // string numeric rule values go through toNumber() gt: { numeric: { when: "gt", value: "5", level: "warn" } }, + withUnitSuffix: { numeric: { when: "gt", value: 5, level: "warn" } }, + withUnitPrefix: { numeric: { when: "gt", value: 5, level: "warn" } }, + localizedUnitSuffix: { numeric: { when: "gt", value: 0.5, level: "warn" } }, commaGrouped: { numeric: { when: "eq", value: 1234, level: "good" } }, commaDecimal: { numeric: { when: "eq", value: 12.34, level: "good" } }, dotDecimal: { numeric: { when: "eq", value: 12.34, level: "good" } }, @@ -143,6 +146,12 @@ describe("utils/highlights", () => { }); expect(evaluateHighlight("gt", "6", cfg)).toMatchObject({ level: "warn", source: "numeric" }); + expect(evaluateHighlight("withUnitSuffix", "5.2 ms", cfg)).toMatchObject({ level: "warn", source: "numeric" }); + expect(evaluateHighlight("withUnitPrefix", "ms 5.2", cfg)).toMatchObject({ level: "warn", source: "numeric" }); + expect(evaluateHighlight("localizedUnitSuffix", "0,71\u202Fms", cfg)).toMatchObject({ + level: "warn", + source: "numeric", + }); expect(evaluateHighlight("commaGrouped", "1,234", cfg)).toMatchObject({ level: "good", source: "numeric" }); expect(evaluateHighlight("commaDecimal", "12,34", cfg)).toMatchObject({ level: "good", source: "numeric" }); // Include a space so Number(trimmed) fails and we exercise the dot parsing branch. @@ -161,6 +170,9 @@ describe("utils/highlights", () => { // "1.2.3" is not a valid grouped or decimal number for our parser. expect(evaluateHighlight("num", "1.2.3", cfg)).toBeNull(); + // Multiple numbers in one string should not be treated as a single numeric value. + expect(evaluateHighlight("num", "5/10 ms", cfg)).toBeNull(); + // JSX-ish values should not be treated as numeric. expect(evaluateHighlight("num", { props: { children: "x" } }, cfg)).toBeNull(); });