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); + }); }); // ---------------------------------------------------------------------------