From d23fa1c882319f1ae97448a7e6da4dc016ba6cca Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Mon, 10 Aug 2026 14:00:55 +0500 Subject: [PATCH] Fix: layout-transition false positives on JSX quoted transition values (#548) The value-capture regex stopped only at ;{}, so in single-line JSX style objects it ran past the closing quote and swallowed later properties, flagging layout props that were never transitioned. The capture now stops at the matching closing quote when the value is a quoted string, falling back to the old bounds for real CSS. Prepared with AI assistance under maintainer direction. Co-authored-by: Cursor --- cli/engine/engines/regex/detect-text.mjs | 19 +++++++++++-------- tests/detect-antipatterns.test.js | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cli/engine/engines/regex/detect-text.mjs b/cli/engine/engines/regex/detect-text.mjs index 0b88cdb63..8bce32525 100644 --- a/cli/engine/engines/regex/detect-text.mjs +++ b/cli/engine/engines/regex/detect-text.mjs @@ -425,25 +425,28 @@ const REGEX_MATCHERS = [ }, fmt: (m) => `cubic-bezier(${m[1]}, ${m[2]}, ${m[3]}, ${m[4]})` }, // --- Layout property transition --- - { id: 'layout-transition', regex: /transition\s*:\s*([^;{}]+)/gi, + // JSX inline style objects use comma-delimited quoted values, not semicolons (issue #548). + { id: 'layout-transition', regex: /transition\s*:\s*(?:(['"])((?:(?!\1)[^\\]|\\.)*)\1|([^;{}]+))/gi, test: (m) => { - const val = m[1].toLowerCase(); + const val = (m[2] ?? m[3] ?? '').toLowerCase(); if (/\ball\b/.test(val)) return false; return /\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding\b|\bmargin\b/.test(val); }, fmt: (m) => { - const found = m[1].match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi); - return `transition: ${found ? found.join(', ') : m[1].trim()}`; + const raw = m[2] ?? m[3] ?? ''; + const found = raw.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi); + return `transition: ${found ? found.join(', ') : raw.trim()}`; } }, - { id: 'layout-transition', regex: /transition-property\s*:\s*([^;{}]+)/gi, + { id: 'layout-transition', regex: /transition-property\s*:\s*(?:(['"])((?:(?!\1)[^\\]|\\.)*)\1|([^;{}]+))/gi, test: (m) => { - const val = m[1].toLowerCase(); + const val = (m[2] ?? m[3] ?? '').toLowerCase(); if (/\ball\b/.test(val)) return false; return /\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding\b|\bmargin\b/.test(val); }, fmt: (m) => { - const found = m[1].match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi); - return `transition-property: ${found ? found.join(', ') : m[1].trim()}`; + const raw = m[2] ?? m[3] ?? ''; + const found = raw.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi); + return `transition-property: ${found ? found.join(', ') : raw.trim()}`; } }, // --- Broken image: src="" or src="#" or src=" " --- { id: 'broken-image', regex: /]*?\bsrc\s*=\s*(?:""|''|"\s+"|'\s+'|"#"|'#')/gi, diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js index f5afc97b6..7e5c8bdb8 100644 --- a/tests/detect-antipatterns.test.js +++ b/tests/detect-antipatterns.test.js @@ -853,6 +853,26 @@ describe('detectText — motion', () => { const f = detectText('.btn { transition: opacity 0.2s ease; }', 'test.css'); expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); }); + + test('passes JSX quoted paint-only transition with later layout prop', () => { + const f = detectText("
", 'test.jsx'); + expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); + }); + + test('passes JSX grid-template-rows transition with later padding and width', () => { + const f = detectText("
", 'test.jsx'); + expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); + }); + + test('detects JSX quoted width transition', () => { + const f = detectText("
", 'test.jsx'); + expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true); + }); + + test('skips JSX quoted transition: all', () => { + const f = detectText("
", 'test.jsx'); + expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0); + }); }); // ---------------------------------------------------------------------------