Repository navigation
fix(lint): unknown family parts, React Aria composition hints, and a stopped ESLint loader - #265
Merged
Merged
Conversation
…stopped ESLint loader - An unknown part of a family (`DialogContent` where Dialog is the content, as on React Aria) gets the family's component and its parts, not the sibling spelled closest (`DialogFooter`). A misspelled part (`DialogTitel`) is still taken for that part, with the rename. - `asChild` on a trigger without asChild or render says it takes the element that opens it as a child. On React Aria's function `render`, asChild, as, component and href no longer get Base UI's render element advice. - The ESLint loader worker answers every waiting call from its exit handler, so a worker that stops (process.exit in a config, an uncaught error) fails the lint at once instead of after 120 s. Roadmap item OS2-59.
This was referenced Oct 30, 2026
…nu parts, Img, href hints - The bridge and the loader worker share a "stopped" flag: the worker's exit handler sets it before it answers, and the next file starts another worker instead of posting to the stopped one, whose exit event arrives too late. A 3-file lint with process.exit(1), an uncaught error or an unhandled rejection in onsystem.config.mjs now fails each file at once, with the reason. - A sibling part that is this part with a word in front (DropdownMenuSubContent for DropdownMenuContent) is not taken for it; a short part two edits away is (AvatarImg is AvatarImage, with the rename in scope). - React Aria's render is recognised when spelled out ((props: …) => …) on a component whose props come from React Aria; a render function of the design system's own keeps the old advice. - The trigger hint names a button for dialog, menu, popover and tooltip triggers, a menu item for a submenu's, and no element for a context menu's area; "take no "as" prop", and pass href to a component that takes it. - docs/rules.md describes the family guess and the React Aria hints.
…for a trigger that renders a button
- src/eslint/worker.ts: the exit handler also takes the calls still on the worker's port, posted
while it was busy, and answers them with why it stopped. A config whose timer blocked for 1.5 s
and then called process.exit(3) left the next file waiting 120 s; it now fails in under a
second with "(exit code 3)".
- src/lint/rules/components.ts: the trigger hint ("It takes the element that opens it as a
child") is only for a trigger that renders no element of its own, as React Aria's do. A
DialogTrigger that renders a button gets main's message again, rather than advice that nests
one button in another. A part is a typo of a sibling within a third of its length (at least
two edits), not half: CardToggle is no longer taken for CardTitle, nor CarouselProvider for
CarouselPrevious.
- Tests for each, docs/rules.md and the changeset updated.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the OS2-59 polish bundle: the message and ESLint area. The CLI and Action area was #264 and the package and contributor area #266, both merged. None of them share a file.
What was wrong
import { DialogContent } from "@/components/ui/dialog"(there is none: Dialog is the content) gotDid you mean <DialogFooter>?, the part spelled closest, which means something else.DropdownMenuContentandContextMenuContentgot the submenu'sDropdownMenuSubContentandContextMenuSubContent, renamed when in scope.<DialogTrigger asChild>on a React Aria trigger got<DialogTrigger> has no prop "asChild".with no hint, while Base UI and Radix get one.renderprop is a function (DOMRenderFunction, or that type spelled out,((props: …, renderProps: …) => …), as on shadcn/ui'sSidebarMenuButton),asChild,as,componentandhrefgot Base UI's advice,compose with render instead: <Button render={<Link href="…" />}>, which React Aria'srenderdoes not take: it must return an element of the same kind.process.exit(1)in anonsystem.config.mjs, an uncaught error or an unhandled rejection), a file waited the bridge's whole 120-second timeout and then said the design system "did not load within 120 seconds". The worker'sexitevent reaches the bridge only after ESLint has moved on, so later files of the run were posted to the stopped worker too.What changed
src/lint/rules/components.tsguessComponent(): when an unknown name starts with a component's name and another word (Dialog+Content), and the closest spelling is another part of that family that is not a typo of this part, or there is no close spelling, it names the family's component and lists its parts:Did you mean <Dialog>, or one of its parts: DialogTrigger, DialogHeader, DialogFooter, DialogTitle?, with no rename. A typo of the part is within a third of its length or two edits of it (DialogTitel,DialogHeadr,AvatarImgforAvatarImage), and keeps the rename when it is in scope; half its length took unlike parts for typos, as main does (CardToggleforCardTitle,CarouselProviderforCarouselPrevious,SelectOptionforSelectSection); a part that is this one with a word in front (SubContentforContent) is another part. A component without parts is not offered for a name that only starts with it (ButtonGroupnext toButton).compositionHint(): a component named*Triggerthat has neitherasChildnorrenderand renders no element of its own, as React Aria'sDialogTrigger,MenuTriggerandSubmenuTriggerdo, says, forasChild,as,componentorrender, that it takes the element that opens it as a child:<DialogTrigger><Button>…</Button>…</DialogTrigger>for dialog, sheet, drawer, menu, popover and tooltip triggers,<SubmenuTrigger><MenuItem>…</MenuItem>…</SubmenuTrigger>for a submenu's, the area that opens the menu on a right-click forContextMenuTrigger, and no element for others. A trigger that renders its own<button>gets main's message,<DialogTrigger> has no prop "asChild"., since a button inside it would nest one button in another. A component whoserenderis React Aria's says, forasChild,asandcomponent,React Aria components take no "asChild" prop., thenFor a link, pass href: <SidebarMenuButton href="…">…</SidebarMenuButton>.when it takeshref(suggestionhref), or that a link is a link component; forhref, that a link is a link component.rendercounts as React Aria's when its type isDOMRenderFunction<…>, or a function ofpropson a component whose props come from React Aria (@react-types/*,react-aria-components); a render function of the design system's own ((props) => ReactNode, shadcn/ui'sAttachmentTrigger) keeps the advice it had.src/eslint/worker.ts: the worker keeps the calls it has not answered. If it stops, itsexithandler (which runs forprocess.exit(), an uncaught exception and an unhandled rejection in a worker thread) first sets astoppedflag it shares with the bridge, then takes the calls still on its port, posted while it was busy and never read (receiveMessageOnPort), and answers those and the ones it was working on withthe design system's loader stopped before it answered, plus the error's message (fromuncaughtExceptionMonitor, which does not change how the worker ends) or the exit code. The background refresh from fix(eslint): non-blocking refresh, config edits, rule options and suppression warnings #255 (setImmediate,looking()) is unchanged and its tests pass.src/eslint/bridge.ts: thestoppedflag (anInt32Arrayon aSharedArrayBuffer, passed inworkerData) makes the next file start another worker; a stopped worker'serrorandexitevents forget it only while it is still the current one.docs/rules.md: theno-unknown-componentandno-unknown-proprows describe the family guess and the React Aria hints.How it was tested
test/component-guesses.test.ts: the family case as a missing export and as an unresolved tag without a rename;DropdownMenuContentandContextMenuContentnext to their*SubContentparts, imported, without a rename;AvatarImgtaken forAvatarImagewith the rename; misspelled parts that keep their rename;ButtonGroupnext toButton; ontest/fixtures/rac-ui(now withmenu.tsxandsidebar.tsx), the trigger hints forDialogTrigger,MenuTrigger,SubmenuTriggerandContextMenuTrigger, theasChild/hrefhints on React Aria'sButton, andhrefforSidebarMenuButton, whoserendertype starts with((props:; and the old advice for anAttachmentTriggerwith a render function of its own. 6 of its 11 tests failed on 3cc1fe0. Added since: aDialogTriggerthat renders a button gets no trigger hint, andCardToggleandCarouselProviderget their family, whileCardContntkeeps its rename; both tests fail on 09874ea.test/eslint-worker-exit.test.ts: ESLint with the plugin, built as it ships, lints three files of a project whoseonsystem.config.mjscallsprocess.exit(1), throws fromsetImmediate, or leaves a rejected promise. Each file fails withonsystem could not check this file: the design system's loader stopped before it answered, plus(exit code 1),: boom in configor: unhandled in config. The three tests took 3.15 s together; theprocess.exitone on 3cc1fe0 took 120,505 ms and failed its "under 30 s" check. A fourth test lints one file, then has the config block for 3 seconds and callprocess.exit(3), and lints a second file while it is blocked: that file fails with(exit code 3). On 09874ea it took 120,015 ms and failed the same check; here the four tests took 710, 665, 679 and 3,444 ms.process.exit(1), 709 ms for the uncaught error and 708 ms for the unhandled rejection, every file with the reason; a worker that exits between lints (setTimeout(() => process.exit(0), 1500), 3 s apart) is replaced, 260 ms for the next file. A config that blocks for 1.5 s from 400 ms after it loads and then callsprocess.exit(3), with one file linted, then another 800 ms later: the first is checked in about 500 ms, the second fails with(exit code 3)in 870 to 876 ms (three runs, Node 24.18.0).check appon shadcn/ui's aria, base and radix registries and React Aria's starter, against this PR's previous commit: on aria, 76 of 455 findings changed (the quoted prop,hreffor components that take it, the trigger examples, the*SubContentandAvatarImgguesses, and the old advice back forBadgeandAttachmentTrigger); on base and radix, onlyAvatarImg(3 findings each); on the starter, 29 (the quoted prop,href,SubmenuTrigger). On material-kit-react, no finding changed. Then against 09874ea: the third-of-length typo test changed 6 of 455 findings on aria, 6 of 426 on base and 6 of 429 on radix (CardSubtitleandEmptyStateTitlegetCardandEmptywith their parts, notCardTitleandEmptyTitle), 2 of 149 on the starter (SelectOptiongetsSelectand its parts, notSelectSection), and none of 107 on material-kit-react. The trigger change changed no finding on any of them.Gate:
pnpm install --frozen-lockfile --prefer-offline,pnpm format:check,pnpm lint,pnpm typecheck,pnpm test(114 files passed, 1 skipped; 1476 tests passed, 3 skipped),pnpm buildandpnpm smokeall pass; for the docs,pnpm --filter site generateand the site'stest(295 passed), and the generated rules data has the new row (the site'stypecheck,lint,buildand the built/rules/no-unknown-componentand/rules/no-unknown-proppages were checked on 09874ea).pnpm corpuson all 25 runs: "Counts match corpus/snapshot.json", and all 53,872 findings are the same as on 09874ea.Left for later
Badge,AttachmentTrigger) keeps main's advice,Base UI components compose with render instead: render={<Link href="…" />}, though itsrendertakes a function; namingrender={(props) => <Link {...props} href="…" />}there would fit it.src/lint/rules/hardcoded.ts: drop a redundantdark:when the light class maps to the same token (bg-red-500 dark:bg-red-400→dark:bg-destructive); keep the variant prefix in the colorsuggestion(hover:bg-gray-100givesbg-muted, while the fix sayshover:bg-muted); say "off the scale" for an inlineborderRadius: 4; treatcolor: "white"in a style object astext-whiteis treated.src/tokens/): on a tie, prefer the name without atintorcontainerqualifier (bg-[#855300]matchedcolors.surface-tintbeforecolors.primary).docs/eslint.md: the parser snippet covers**/*.{ts,tsx}only, so a.jsxfile gets a parsing error.color.accent.$rootin messages follows DTCG 2025.10 and docs/tokens.md, as the finding's verifier noted.Roadmap item: OS2-59 (os-adopt-clients/message-polish (2, 3), os-bugs-edges/os-eslint-worker-death-hang).