fix: pr-bug-scan validated finding from #2437 (#2445)

* fix: address pr-bug-scan validated finding from #2437

Fixed jsxElementReports: spread className now overrides earlier explicit className (later wins); expressionHasStyledScrollbarLiteral skips conditional/short-circuit branches so a conditional scrollbar

* test: cover styled scrollbar checker regressions

---------

Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
buf0-bot[bot]
2026-05-31 01:41:23 -07:00
committed by GitHub
co-authored by orca-bug-scan-bot Neil
parent 54366688ee
commit 5d5cd06a79
2 changed files with 45 additions and 1 deletions
@@ -129,6 +129,22 @@ describe('check-styled-scrollbars', () => {
expect(reports).toHaveLength(0)
})
it('reports inline vertical overflow when the scrollbar class is conditional or short-circuited', () => {
for (const classNameExpression of [
"enabled && 'scrollbar-sleek'",
"enabled ? 'scrollbar-sleek' : undefined",
"enabled || 'scrollbar-sleek'",
"enabled ?? 'scrollbar-sleek'"
]) {
const reports = reportUnstyledScrollbars(
'Example.tsx',
`export function Example({ enabled }) { return <div className={${classNameExpression}} style={{ overflowY: 'auto' }} /> }`
)
expect(reports, classNameExpression).toHaveLength(1)
}
})
it('reports logical inline style spreads without an Orca scrollbar class', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
@@ -156,6 +172,15 @@ describe('check-styled-scrollbars', () => {
expect(reports).toHaveLength(0)
})
it('uses later spread className props over earlier explicit className props', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
"export function Example() { return <div className=\"scrollbar-sleek\" {...{ className: 'overflow-y-auto' }} /> }"
)
expect(reports).toHaveLength(1)
})
it('supports variant helper className config', () => {
const reports = reportUnstyledScrollbars(
'Example.tsx',
@@ -138,6 +138,20 @@ function expressionHasStyledScrollbarLiteral(node) {
hasStyledScrollbar = true
return
}
// Why: a scrollbar literal that only renders on some branches must not be
// treated as covering an unconditional inline overflow. Skip conditional
// and short-circuit expressions when proving unconditional coverage.
if (ts.isConditionalExpression(current)) {
return
}
if (
ts.isBinaryExpression(current) &&
(current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
current.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)
) {
return
}
ts.forEachChild(current, visit)
}
@@ -246,7 +260,12 @@ function jsxElementReports(node, filePath, sourceFile) {
for (const attribute of node.attributes.properties) {
if (ts.isJsxSpreadAttribute(attribute)) {
classExpression ??= spreadPropExpressions(attribute.expression, 'className').at(-1)
// Why: at runtime React applies attributes in source order, so a later
// spread that supplies className overrides an earlier explicit className.
const spreadClassExpression = spreadPropExpressions(attribute.expression, 'className').at(-1)
if (spreadClassExpression) {
classExpression = spreadClassExpression
}
styleExpressions.push(...spreadPropExpressions(attribute.expression, 'style'))
} else if (jsxAttributeName(attribute) === 'className') {
classExpression = jsxAttributeExpression(attribute)