Skip to content

fix(rules): do not flag a lone control in a ButtonGroup - #30

Merged
TheOrcDev merged 1 commit into
mainfrom
fix/button-group-lone-input
Aug 4, 2026
Merged

fix(rules): do not flag a lone control in a ButtonGroup#30
TheOrcDev merged 1 commit into
mainfrom
fix/button-group-lone-input

Conversation

@TheOrcDev

@TheOrcDev TheOrcDev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

button-group-holds-only-buttons reported a text control inside any ButtonGroup, including one holding nothing else. That's a false positive.

ButtonGroup joins its children with [&>*:not(:first-child)]:rounded-l-none and border-l-0. With a single child those selectors never match — no squared corner, no dropped border, no seam, and therefore no ring to bisect. There is nothing to report.

// Reported before this fix. Nothing is joined; nothing is wrong.
<ButtonGroup>
  <Input placeholder="Email" />
</ButtonGroup>

The fix

Report only when the group has more than one meaningful direct child. A conditional child counts — {loading ? <Spinner/> : <Button/>} renders a sibling either way — so the common loading-state shape still reports.

How it was found, and what that says about the burn-in

Not by the burn-in. By the question "this rule is only when there are button and input right?" — which I could have answered from memory and got wrong, so I probed it instead and the probe came back advisory.

The original burn-in ran across nine projects and found no false positives, which I reported as a property of the rule. It was a property of that sample: none of the nine contained a lone-control ButtonGroup. Worth stating plainly, because the same caveat still applies to what's left — nine projects, one author.

Verification

  • 12 tests (2 new): the lone control now passes; a conditional sibling still reports.
  • Burn-in re-run after the fix — the same four true positives still report (orcdev, github-creature, star-history, youtubetoblog) and the same five stay silent. The fix narrowed the rule without blunting it.
  • 7 gates green including cli:smoke. Self-audit 100/100 A.

Release handling

Folded into 0.10.0 rather than shipped as a follow-up: 0.10.0 is prepared but not yet published to npm, so the rule has not reached anyone. The alternative — publishing a known false positive and fixing it in 0.10.1 — would be strictly worse. Both changelog entries updated to describe the lone-child exclusion as part of the rule's contract.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented false warnings for ButtonGroup components containing only one rendered child.
    • Conditional siblings are correctly recognized when determining whether controls are joined.
    • Text inputs now report only when joined to another control, avoiding warnings for standalone controls.
  • Documentation

    • Updated changelogs to document the corrected behavior.

The rule reported any text control inside a ButtonGroup, including a
group holding nothing else. That is a false positive: ButtonGroup joins
with [&>*:not(:first-child)], so a single child is joined to nothing —
no squared corner, no dropped border, no seam, and no ring to bisect.

Report only when the group has more than one meaningful direct child. A
conditional child counts, because {loading ? <Spinner/> : <Button/>}
renders a sibling either way.

Found by a question rather than by the burn-in, which happened to
contain no lone-control groups; the earlier "no false positives across
nine projects" was true of that sample but overstated as a property of
the rule. Re-ran the burn-in after the fix: the same four true positives
still report, the same five projects stay silent.

Folded into 0.10.0 rather than a follow-up, since 0.10.0 has not been
published and the rule has not reached anyone yet.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
shadscan Ready Ready Preview Aug 4, 2026 9:45pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The button-group-holds-only-buttons rule now ignores lone text controls in ButtonGroup instances. It still reports text controls when conditional or other joined siblings are present. Tests and changelogs document the behavior.

Changes

ButtonGroup rule refinement

Layer / File(s) Summary
Count joined children and gate findings
packages/cli/src/rules/button-group-holds-only-buttons.ts
The rule counts meaningful direct JSX children, including conditional expressions and text nodes. It inspects text controls only when more than one child is joined.
Validate edge cases
packages/cli/test/button-group-holds-only-buttons.test.ts, CHANGELOG.md, changelog/0.10.0.md
Tests cover lone inputs and inputs with conditional siblings. Changelogs describe the single-child exception.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • TheOrcDev/shadscan#28: Directly refines the same button-group-holds-only-buttons rule and its test behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing reports for a lone control in a ButtonGroup.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/button-group-lone-input

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/rules/button-group-holds-only-buttons.ts`:
- Around line 164-179: Update countJoinedChildren to count rendered JSX children
only: ignore empty or comment-only JsxExpression containers, recursively flatten
JsxFragment children, and count their rendered descendants before the existing
>1 check. Add regression tests covering a comment alongside an element and a
fragment containing multiple elements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d0a9593-bcf4-4c83-b79c-f12c50ff9511

📥 Commits

Reviewing files that changed from the base of the PR and between 2348427 and 4f7234f.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • changelog/0.10.0.md
  • packages/cli/src/rules/button-group-holds-only-buttons.ts
  • packages/cli/test/button-group-holds-only-buttons.test.ts

Comment on lines +164 to +179
const countJoinedChildren = (node: Node): number => {
if (!isJsxElement(node)) {
return 0;
}

return node.children.filter((child) => {
if (isJsxText(child)) {
return child.text.trim() !== "";
}

return (
isJsxElement(child) ||
isJsxSelfClosingElement(child) ||
isJsxExpression(child)
);
}).length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)button-group-holds-only-buttons\.ts$|node_modules/next/dist/docs|tsconfig|package.json' | sed -n '1,120p'

echo "== file outline =="
ast-grep outline packages/cli/src/rules/button-group-holds-only-buttons.ts --view expanded || true

echo "== relevant file lines =="
cat -n packages/cli/src/rules/button-group-holds-only-buttons.ts | sed -n '1,260p'

echo "== search helper definitions/usages =="
rg -n "countJoinedChildren|isJsxFragment|isJsxText|JsxFragment|ButtonGroup" packages/cli/src -S || true

Repository: TheOrcDev/shadscan

Length of output: 14474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TypeScript version declarations =="
node -e "const p=require('./package.json'); console.log(JSON.stringify({root:p.dependencies?.typescript||p.devDependencies?.typescript, cli:p.packages?.find?.??.dependencies||p.packages?.cli?.dependencies}, null, 2))" 2>/dev/null || cat package.json | rg -n '"typescript"|"`@types/node`"|workspace|name' || true
cat package.json | sed -n '1,120p'
cat packages/cli/package.json | sed -n '1,120p'
cat packages/cli/tsconfig.json | sed -n '1,120p'
cat tsconfig.json | sed -n '1,120p'

echo "== local typescript availability and helpers =="
node - <<'JS'
try {
  const ts = require('./packages/cli/node_modules/typescript');
  console.log('has packages/cli/node_modules/typescript', !!ts, ts.version);
  console.log('isJsxFragment', typeof ts.isJsxFragment);
  console.log('isJsxText', typeof ts.isJsxText);
  console.log('isJsxExpression', typeof ts.isJsxExpression);
  console.log('has JsxEmptyExpression?', ts.SyntaxKind && ts.SyntaxKind.JsxEmptyExpression);
} catch (e) {
  console.log('no local packages/cli/node_modules/typescript', e && e.message);
}
try {
  const ts = require('typescript');
  console.log('has global typescript', !!ts, ts.version);
  console.log('isJsxFragment', typeof ts.isJsxFragment);
  console.log('isJsxText', typeof ts.isJsxText);
  console.log('isJsxExpression', typeof ts.isJsxExpression);
  console.log('has JsxEmptyExpression?', ts.SyntaxKind && ts.SyntaxKind.JsxEmptyExpression);
} catch (e) {
  console.log('no global typescript', e && e.message);
}
JS

echo "== relevant AST helper implementations =="
cat -n packages/cli/src/ast.ts | sed -n '1,80p'
cat -n packages/cli/src/ast.ts | sed -n '420,490p'
cat -n packages/cli/src/component-render-graph/client-surface-planning.ts | sed -n '150,210p'
cat -n packages/cli/src/component-render-graph/template-extraction.ts | sed -n '660,690p'

echo "== AST behavior probe if TypeScript is available globally =="
node - <<'JS'
const ts = (() => { try { return require('typescript'); } catch { return null } })();
if (!ts) {
  console.log('skipping behavioral TypeScript probe because global TypeScript is unavailable');
  process.exit(0);
}
const samples = {
  'comment expression': '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>',
  'fragment with two inputs': '<ButtonGroup><><Input /><Input /></></ButtonGroup>',
  'single input': '<ButtonGroup><Input /></ButtonGroup>',
  'fragment containing InputGroup': '<ButtonGroup><><InputGroup /><Button /></></ButtonGroup>',
};
for (const [name, src] of Object.entries(samples)) {
  const sf = ts.createSourceFile('/x.ts', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
  const childCounts = {
    directChildren: sf.getChildAt(0).children.length,
    directChildrenNames: sf.getChildAt(0).children.map(n => ts.SyntaxKind[n.kind]).join(', '),
  };
  const firstJsxElement = sf.getChildAt(0).children.find(n => ts.isJsxElement(n));
  console.log(JSON.stringify({
    sample: name,
    src,
    childCounts,
    buttonGroupChildren: firstJsxElement ? firstJsxElement.children.map(n => ({
      kind: ts.SyntaxKind[n.kind],
      expressionKind: ts.isJsxExpression(n) && n.expression && ts.SyntaxKind[n.expression.kind],
      fragmentChildCount: ts.isJsxFragment(n) && n.children.length,
      renderedCount: ts.isJsxExpression(n) ? (n.expression && n.expression.escapedText) : null,
    })) : null,
  }, null, 2));
}
JS

Repository: TheOrcDev/shadscan

Length of output: 16249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const ts = require('typescript');
const samples = {
  'comment expression': '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>',
  'fragment with two inputs': '<ButtonGroup><><Input /><Input /></></ButtonGroup>',
  'single input': '<ButtonGroup><Input /></ButtonGroup>',
  'fragment containing InputGroup and Button': '<ButtonGroup><><InputGroup /><Button /></></ButtonGroup>',
};

for (const [name, src] of Object.entries(samples)) {
  const sf = ts.createSourceFile('/x.tsx', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
  const topChildren = sf.statements[0].children;
  const buttonGroup = topChildren.find((n) => ts.isJsxElement(n) && ts.getFullyQualifiedName(n.name) === 'ButtonGroup');
  const countJoinedChildrenCurrent = (node) => {
    if (!ts.isJsxElement(node)) return 0;
    let count = 0;
    for (const child of node.children) {
      if (ts.isJsxText(child)) {
        if (child.text.trim() !== '') count++;
        continue;
      }
      if (
        ts.isJsxElement(child) ||
        ts.isJsxSelfClosingElement(child) ||
        ts.isJsxExpression(child)
      ) {
        count++;
      }
    }
    return count;
  };
  console.log(JSON.stringify({
    sample: name,
    src,
    topChildrenCount: topChildren.length,
    topChildrenKinds: topChildren.map(n => ts.SyntaxKind[n.kind]).join(', '),
    hasButtonGroup: !!buttonGroup,
    buttonGroupDirectChildrenCount: buttonGroup ? buttonGroup.children.length : 0,
    buttonGroupDirectChildMatchesCurrent: buttonGroup ? buttonGroup.children.map((child) => ({
      kind: ts.SyntaxKind[child.kind],
      includedByCurrent: !!child && (
        ts.isJsxElement(child) ||
        ts.isJsxSelfClosingElement(child) ||
        ts.isJsxExpression(child) ||
        (ts.isJsxText(child) && child.text.trim() !== '')
      ),
      expressionKind: ts.isJsxExpression(child) && child.expression ? ts.SyntaxKind[child.expression.kind] : undefined,
      fragmentChildCount: ts.isJsxFragment(child) && child.children.length,
    })) : null,
    buttonGroupCountWithCurrentImpl: buttonGroup ? countJoinedChildrenCurrent(buttonGroup) : 0,
  }, null, 2));
}
JS

Repository: TheOrcDev/shadscan

Length of output: 896


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const ts = require('typescript');
const samples = {
  'comment expression': '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>',
  'fragment with two inputs': '<ButtonGroup><><Input /><Input /></></ButtonGroup>',
  'single input': '<ButtonGroup><Input /></ButtonGroup>',
  'fragment containing InputGroup and Button': '<ButtonGroup><><InputGroup /><Button /></></ButtonGroup>',
};

for (const [name, src] of Object.entries(samples)) {
  const sf = ts.createSourceFile('/x.tsx', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
  const stmt = sf.statements[0];
  const topChildren = stmt && stmt.children;
  const buttonGroup = topChildren && topChildren.find((n) => ts.isJsxElement(n) && n.name && n.name.text === 'ButtonGroup');
  const countJoinedChildrenCurrent = (node) => {
    if (!ts.isJsxElement(node)) return 0;
    let count = 0;
    for (const child of node.children) {
      if (ts.isJsxText(child)) {
        if (child.text.trim() !== '') count++;
        continue;
      }
      if (
        ts.isJsxElement(child) ||
        ts.isJsxSelfClosingElement(child) ||
        ts.isJsxExpression(child)
      ) {
        count++;
      }
    }
    return count;
  };
  const filtered = [];
  for (const child of (buttonGroup ? buttonGroup.children : [])) {
    filtered.push({
      kind: ts.SyntaxKind[child.kind],
      includedByCurrent: !!child && (
        ts.isJsxElement(child) ||
        ts.isJsxSelfClosingElement(child) ||
        ts.isJsxExpression(child) ||
        (ts.isJsxText(child) && child.text.trim() !== '')
      ),
      expressionKind: ts.isJsxExpression(child) && child.expression ? ts.SyntaxKind[child.expression.kind] : undefined,
      fragmentChildCount: ts.isJsxFragment(child) && child.children.length,
    });
  }
  console.log(JSON.stringify({
    sample: name,
    src,
    topStatementIsJsxElement: ts.isJsxElement(stmt),
    topStatementChildrenCount: topChildren ? topChildren.length : 0,
    topChildrenKinds: topChildren ? topChildren.map((n) => ts.SyntaxKind[n.kind]).join(', ') : '',
    hasButtonGroup,
    buttonGroupDirectChildrenCount: buttonGroup ? buttonGroup.children.length : 0,
    buttonGroupChildMatchesCurrent: filtered,
    buttonGroupCountWithCurrentImpl: buttonGroup ? countJoinedChildrenCurrent(buttonGroup) : 0,
  }, null, 2));
}
JS

Repository: TheOrcDev/shadscan

Length of output: 746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const ts = require('typescript');
const samples = {
  'comment expression': '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>',
  'fragment with two inputs': '<ButtonGroup><><Input /><Input /></></ButtonGroup>',
  'single input': '<ButtonGroup><Input /></ButtonGroup>',
  'fragment containing InputGroup and Button': '<ButtonGroup><><InputGroup /><Button /></></ButtonGroup>',
};

for (const [name, src] of Object.entries(samples)) {
  const sf = ts.createSourceFile('/x.tsx', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
  const stmt = sf.statements[0];
  const topChildren = (stmt && stmt.children) || [];
  const buttonGroup = topChildren.find((n) => ts.isJsxElement(n) && n.name && n.name.text === 'ButtonGroup');
  const hasButtonGroup = !!buttonGroup;
  const countJoinedChildrenCurrent = (node) => {
    if (!ts.isJsxElement(node)) return 0;
    let count = 0;
    for (const child of node.children) {
      if (ts.isJsxText(child)) {
        if (child.text.trim() !== '') count++;
        continue;
      }
      if (
        ts.isJsxElement(child) ||
        ts.isJsxSelfClosingElement(child) ||
        ts.isJsxExpression(child)
      ) {
        count++;
      }
    }
    return count;
  };
  const filtered = [];
  for (const child of buttonGroup ? buttonGroup.children : []) {
    filtered.push({
      kind: ts.SyntaxKind[child.kind],
      includedByCurrent: ts.isJsxElement(child) ||
        ts.isJsxSelfClosingElement(child) ||
        ts.isJsxExpression(child) ||
        ((ts.isJsxText(child) && child.text.trim() !== '')),
      expressionKind: ts.isJsxExpression(child) && child.expression ? ts.SyntaxKind[child.expression.kind] : undefined,
      fragmentChildCount: ts.isJsxFragment(child) && child.children.length,
    });
  }
  console.log(JSON.stringify({
    sample: name,
    src,
    topStatementIsJsxElement: ts.isJsxElement(stmt),
    topChildrenCount: topChildren.length,
    topChildrenKinds: topChildren.map((n) => ts.SyntaxKind[n.kind]).join(', '),
    hasButtonGroup,
    buttonGroupDirectChildrenCount: buttonGroup ? buttonGroup.children.length : 0,
    buttonGroupChildMatchesCurrent: filtered,
    buttonGroupCountWithCurrentImpl: buttonGroup ? countJoinedChildrenCurrent(buttonGroup) : 0,
  }, null, 2));
}
JS

Repository: TheOrcDev/shadscan

Length of output: 1508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const ts = require('typescript');
const src = '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>';
const sf = ts.createSourceFile('/x.tsx', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
console.log(sf.transformFlags ? 'has transformFlags' : 'no transformFlags');
console.log('raw source length', src.length);
console.log('sf.text', JSON.stringify(sf.text));
for (const key of Object.keys(sf).sort()) {
  if (key[0] === '_' || key === 'statements' || key === 'end' || key === 'flags' || key === 'kind' || key === 'transformFlags') {
    const val = sf[key];
    if (Array.isArray(val)) {
      console.log(`${key}: [${val.length}]`, val.map((n) => n && { kind: ts.SyntaxKind[n.kind], text: (n.text || '') }).slice(0, 3));
    } else {
      console.log(`${key}:`, val);
    }
  }
}
const exprs = sf.statements[0].expression.expressions;
for (const e of exprs) {
  console.log('statement expression child', ts.SyntaxKind[e.kind], e.toString && e.toString(), e.children && e.children.length, e.text && JSON.stringify(e.text), e.expression && { kind: ts.SyntaxKind[e.expression.kind] });
}
JS

Repository: TheOrcDev/shadscan

Length of output: 960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const ts = require('typescript');
const src = '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>';
const sf = ts.createSourceFile('/x.tsx', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const stmt = sf.statements[0];
console.log('statement kind', ts.SyntaxKind[stmt.kind]);
console.log('statement expression kind', stmt.expression && ts.SyntaxKind[stmt.expression.kind]);
console.log('statement expression type kind', stmt.expression && stmt.expression.type && ts.SyntaxKind[stmt.expression.type.kind]);
console.log('statement expression expressions?', Array.isArray(stmt.expression.expressions));
for (const key of ['expression', 'childList', 'elements', 'properties', 'arguments', 'expressions']) {
  const val = stmt.expression && stmt.expression[key];
  if (val) console.log(`expression.${key}`, typeof val, Array.isArray(val) ? val.length : val);
}
console.log('has forEachChild', typeof ts.forEachChild);
if (typeof ts.forEachChild === 'function') {
  const expr = stmt.expression;
  const collected = [];
  ts.forEachChild(expr, (child) => {
    collected.push({ kind: ts.SyntaxKind[child.kind], expressionKind: child.expression && ts.SyntaxKind[child.expression && child.expression.kind] });
  });
  console.log('forEachChild collected', collected);
}
const jsxFragments = sf.statements[0].expression;
for (const prop of ['jsxFragments', 'jsxTypeArgumentElements', 'jsxAttributes']) {
  console.log('expression.', prop, Array.isArray(jsxFragments[prop]));
}
JS

Repository: TheOrcDev/shadscan

Length of output: 706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const ts = require('typescript');
const cases = [
  { label: 'comment expression', src: '<ButtonGroup>{/* comment */}<Input /></ButtonGroup>' },
  { label: 'fragment with two inputs', src: '<ButtonGroup><><Input /><Input /></></ButtonGroup>' },
  { label: 'single input', src: '<ButtonGroup><Input /></ButtonGroup>' },
  { label: 'fragment containing InputGroup and Button', src: '<ButtonGroup><><InputGroup /><Button /></></ButtonGroup>' },
];
for (const { label, src } of cases) {
  const sf = ts.createSourceFile('/x.tsx', src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
  const stmt = sf.statements[0];
  const buttonGroup = stmt && stmt.expression;
  const currentFn = (node) => {
    if (!ts.isJsxElement(node)) return 0;
    let count = 0;
    for (const child of node.children) {
      if (ts.isJsxText(child)) {
        if (child.text.trim() !== '') count++;
        continue;
      }
      if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxExpression(child)) {
        count++;
      }
    }
    return count;
  };
  console.log(JSON.stringify({
    label,
    src,
    countJoinedChildren: currentFn(buttonGroup),
    directChildren: buttonGroup.children.map((child) => {
      const included = ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxExpression(child) || ((ts.isJsxText(child) && child.text.trim() !== ''));
      return {
        kind: ts.SyntaxKind[child.kind],
        included,
        expressionKind: ts.isJsxExpression(child) && child.expression ? ts.SyntaxKind[child.expression.kind] : undefined,
        fragmentChildCount: ts.isJsxFragment(child) && child.children.length,
      };
    }),
  }, null, 2));
}
JS

Repository: TheOrcDev/shadscan

Length of output: 1300


Count rendered children, not every JSX child node.

countJoinedChildren treats {/* comment */} as a joined child, so <ButtonGroup>{/* comment */}<Input /></ButtonGroup> reports as joined even though the comment does not render. It also skips JsxFragment, so <ButtonGroup><><Input /><Button /></></ButtonGroup> is treated as a lone child while it joins two rendered elements. Ignore expression containers without an expression and flatten fragment children before applying the > 1 check. Add regression tests for both cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/rules/button-group-holds-only-buttons.ts` around lines 164 -
179, Update countJoinedChildren to count rendered JSX children only: ignore
empty or comment-only JsxExpression containers, recursively flatten JsxFragment
children, and count their rendered descendants before the existing >1 check. Add
regression tests covering a comment alongside an element and a fragment
containing multiple elements.

@TheOrcDev
TheOrcDev merged commit b3bf654 into main Aug 4, 2026
8 checks passed
@TheOrcDev
TheOrcDev deleted the fix/button-group-lone-input branch August 4, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant