Readiness: Implemented
The toolchain
Formatting and linting
Both read the same tree the compiler does. The formatter is a Rust printer over Meta's Flow parser; the linter runs uf's own rules alongside Flow's built-in ones, in one pass, with one report.
What you will be able to do: format a project with familiar Prettier layout, lint it with uf's rules and Flow's in one pass, apply the fixes, and put both in CI.
What you need first: a project — Your first project.
uf fmt
uf fmtFormats every Flow, JavaScript, JSON, CSS and TypeScript file in the project —
Markdown is not among them. --check exits non-zero instead of writing, which
is what CI should run.
Flow files go through the official Flow parser and a printer written for it, so
component, hook, renders, match and enums are formatted as first-class
syntax rather than survived. The output follows Prettier's conventions — double quotes, trailing commas,
the same line-breaking decisions — because the point of a formatter is that
nobody argues about it, and Prettier already won that argument. The default
width is 100 rather than Prettier's 80; fmt.lineWidth changes it. Related
match arrows, component hook bindings, expanded object keys, multiline
function parameters, type aliases and object type fields are aligned in
columns, following Scalafmt's alignment model.
Only adjacent, single-line entries are aligned; comments, complex multiline
entries and the line width end a group.
Set fmt.align: false in uf.config.js to keep single spaces instead.
The underlying layout is checked against Prettier: uf_fmt's golden fixtures
disable column alignment and compare byte for byte with Prettier's output.
Separate tests check alignment and the opt out.
GraphQL in a tagged template
uf fmt formats the GraphQL inside a template tagged graphql, gql or
graphql.experimental, a template passed to graphql(…), and one written
behind a /* GraphQL */ comment — and re-indents it to the code around it,
which is the part you notice:
const fragment = graphql`
fragment UserFragment on User {
username
}
`;
becomes
const fragment = graphql`
fragment UserFragment on User {
username
}
`;
A template with ${…} in it works when each run of text between the holes is
a complete document on its own — a trailing ${Fragment} is the usual case —
and is left alone when it is not. That is Prettier's rule, not uf's.
A template uf will not reprint, it does not touch. It comes back exactly
as you wrote it rather than approximately right, and there are four reasons
for that to happen: the document holds a # comment, it holds a string with
a control character in it, it is not GraphQL, or it nests more than 128 deep.
The comment case is the one you are likely to meet — uf does not yet
reproduce where Prettier puts a GraphQL comment, and moving a comment to the
wrong line is worse than leaving the query alone.
GraphQL is the only embedded language uf reads. css, html, sql and
markdown templates go out exactly as written, where Prettier would format
them.
Files uf has no opinion about — JSON, CSS, TypeScript — are handed to Biome,
which is also Rust and also fast. uf runs the binary your project has rather
than shipping one, so Biome is a formatter you install; fmt.nonFlow.formatter
names a different one, or "none".
When it is not installed, uf fmt says so and names the files it left alone,
and that is a warning rather than a failure: uf picked Biome, you did not, and
the exit code answers for the files uf can format itself. Write
fmt.nonFlow.formatter in uf.config.js and the answer changes — a formatter
you asked for by name and do not have fails the run, which is what a CI job
that checks the JSON as well wants.
Measured on a 133 kB Flow and JSX file:
| Time | Throughput | |
|---|---|---|
| Parse alone | 10.4 ms | 12.9 MiB/s |
| Parse and format | 17.9 ms | 7.4 MiB/s |
--check, already formatted | 18.0 ms | 7.5 MiB/s |
The parser is 58% of that. The target is 10 MB/s end to end, and the printer is not what is standing between here and there.
When format is the slow check
Start with the files, not the formatter. uf fmt walks the same project set as
uf lint, uf check, uf test and uf doc: .gitignore, .ignore,
.uf/.git, then the top-level ignore list in uf.config.js. A generated
directory in none of those lists is source, so it will be printed.
uf fmt --check npm/ui
uf fmt --check dialog.js
The path arguments are substring filters. They do not use a glob language, and they do not silently pass when nothing matched; a filter that reads zero files is an error. That makes a slow full-project run easy to cut down without creating a second configuration file.
The next thing to check is who is formatting the file. Flow files are always
printed by uf. JSON, CSS and TypeScript are delegated to
fmt.nonFlow.formatter, Biome by default:
uf explain fmt --json
If a slow run is all Flow, the cost is parse plus print. If it is all JSON or CSS, the cost belongs to the provider uf started. If it is Markdown, it should not be in the run at all — uf does not format Markdown.
uf lint
uf lintRuns uf's rules, Flow's own lints and the official React Compiler's
diagnostics together and prints one list. --json emits the report
machine-readably.
Flow's built-in lints — sketchy-null, unclear-type, deprecated-type,
unnecessary-optional-chain and the rest — are configured in uf.config.js
alongside uf's rules rather than in a separate .flowconfig section, because a
project should have one place where "this is an error here" is written down.
Which rules there are
uf lint --rulesLists every rule instead of linting: the level it runs at in this project, the
command that fixes its findings, and what it checks. The level is read from
uf.config.js over the defaults, so the list answers "is this rule on for us"
rather than "is it on by default", and --json prints the same list for a
script. Read it rather than a list on this page — a rule name or a count written
into a guide goes stale the day a rule is added, and the command cannot.
The lint rule reference shows each rule with an example it
reports and an example it accepts. Both are run by uf's test suite, so the
findings printed under an example are the ones uf lint gives for it.
React rules come from the React Compiler
The react-compiler/* rules are the official React Compiler's own diagnostics,
not checks uf wrote. uf lint runs the compiler uf build runs —
react_compiler, published from
facebook/react — in the compiler's lint mode, with the options
eslint-plugin-react-hooks gives it, and files each diagnostic under the rule
named for its category. The message is the compiler's; uf adds the code frame
and uf-lint-disable.
| Rule | Compiler category | Level | What the compiler reports |
|---|---|---|---|
react-compiler/capitalized-calls | CapitalizedCalls | off | a capitalized function called instead of rendered with JSX |
react-compiler/error-boundaries | ErrorBoundaries | error | JSX built inside try/catch instead of under an error boundary |
react-compiler/exhaustive-effect-dependencies | EffectExhaustiveDependencies | off | an effect's dependency array missing or adding a value |
react-compiler/fbt | FBT | off | Meta's fbt internationalization library |
react-compiler/globals | Globals | error | a variable from outside a component or hook changed during render |
react-compiler/hooks | Hooks | error | the Rules of Hooks |
react-compiler/immutability | Immutability | error | props, state or another value React treats as immutable, mutated |
react-compiler/incompatible-library | IncompatibleLibrary | warn | an API known to break memoization |
react-compiler/invariant | Invariant | off | an internal assumption of the compiler that did not hold |
react-compiler/memo-dependencies | MemoDependencies | warn | a useMemo or useCallback dependency array missing or adding a value |
react-compiler/no-deriving-state-in-effects | EffectDerivationsOfState | off | a value derived from props or state, computed in an effect |
react-compiler/preserve-manual-memoization | PreserveManualMemo | error | a useMemo or useCallback the compiler cannot preserve |
react-compiler/purity | Purity | error | a known-impure function called during render |
react-compiler/refs | Refs | error | a ref read or written during render |
react-compiler/set-state-in-effect | EffectSetState | error | setState called synchronously in an effect |
react-compiler/set-state-in-render | RenderSetState | error | setState called during render |
react-compiler/static-components | StaticComponents | error | a component created during render |
react-compiler/syntax | Syntax | off | code the compiler rejects as invalid syntax |
react-compiler/todo | Todo | off | code the compiler does not compile yet |
react-compiler/unsupported-syntax | UnsupportedSyntax | warn | syntax the compiler does not support |
react-compiler/use-memo | UseMemo | error | a useMemo callback that is async, a generator, or takes parameters |
react-compiler/void-use-memo | VoidUseMemo | error | a useMemo callback that returns nothing |
Every level is the one eslint-plugin-react-hooks' recommended-latest preset
gives that category, so a project arriving from the plugin keeps the findings it
had. Two differ: hooks and memo-dependencies are on although the preset
leaves those compiler rules off, because the plugin answers both questions a
second time with its classic rules-of-hooks (error) and exhaustive-deps
(warn) rules, and uf has only the compiler's answer.
exhaustive-effect-dependencies is off because the plugin ships that
validation switched off — turning the rule on turns the validation on.
They run whether or not app.builtins.reactCompiler.enabled is on, because they
report the rules of React rather than what the build memoized.
Four of the compiler's categories have no rule, because no module can
produce one under the options uf lint gives the compiler: EffectDependencies
(no validation in this compiler reports it), Gating (only for a malformed
use memo if(…) directive, which the compiler reads only under
dynamicGating), Config (only for a module type configuration that
contradicts itself, which needs moduleTypeProvider) and Suppression (the
compiler looks for ESLint's suppression comments only while hook usage or
exhaustive memoization dependencies go unvalidated, and both are validated, and
for Flow's only under flowSuppressions, which is false). A category a newer
compiler adds does not compile until somebody decides where it goes.
Which functions are checked. The compiler checks the functions it would
compile in infer mode, the mode eslint-plugin-react-hooks uses: every
component and hook declaration, a function named like a component (Card)
or a hook (useCard) that calls a hook or returns JSX, and a function passed to
memo or forwardRef. A hook called anywhere else — at the top of a module, or
in function helper() — is not reported, because that function is not a
component or a hook.
Which files are checked. The files eslint-plugin-react-hooks hands to the
compiler: ones with a top-level component or hook declaration, a top-level
function or const named like a component or a hook, or an export default of
a function. A file whose only component is export const Card = memo(() => …)
is not compiled by the plugin, and not by uf lint.
Flow suppressions. A $FlowFixMe[react-rule-hook] or
$FlowFixMe[react-rule-unsafe-ref] comment on the line above a finding removes
it, as it does in ESLint: Flow has already reported the same thing.
Migrating from uf's own React rules
Those rules were checks uf wrote itself, and they are gone. Their ids still
work, in uf.config.js and in uf-lint-disable comments, as deprecated
spellings of the compiler's rules:
| Old id | Now means |
|---|---|
react/hooks-rules | react-compiler/hooks |
react/no-derived-state-effect | react-compiler/no-deriving-state-in-effects |
react/no-render-side-effects | react-compiler/purity |
Rename them when you next touch the line. What changed underneath:
- A hook called outside a component or a hook is no longer reported, for the reason above.
- A method named like a hook on a value that can change is reported.
api.useThing()on a prop may be a different function next render, and the compiler requires a hook to be the same function every time. Call the hook directly, or rename a method that is not one. - Reassigning a module variable during render is
react-compiler/globals. A level set onreact/no-render-side-effectsdoes not reach it; set it by name. - Reading
ref.current, callingconsole.*, or reaching forwindowordocumentduring render is not reported by these rules. - The messages are the compiler's, so a CI log or a script that matched the old wording needs the new one.
react/no-derived-state-effectnow names a rule the preset leavesoff. It asked whether state was derived from props or state in an effect, which is the compiler'sEffectDerivationsOfStatequestion and so itsreact-compiler/no-deriving-state-in-effectsrule. Writing state in an effect at all is a different question,react-compiler/set-state-in-effect, and that rule iserror. A level set on the old id reaches neither of the other two: setreact-compiler/set-state-in-effectby name to change what it reports.
What a lint run tells you
uf lint --json is the report to feed an editor, CI annotation step, or a
small script:
{
"command": "uf lint",
"filesChecked": 1,
"errors": 0,
"warnings": 0,
"diagnostics": [],
"unavailableRules": []
}
diagnostics is the list that should become annotations. errors and
warnings are the totals a CI step should branch on. filesChecked is a guard
against a path filter that was too narrow, and unavailableRules is part of the
contract: a rule that needs inference and could not run is named instead of
quietly disappearing.
uf explain lint --json answers a different question:
{
"command": "uf lint",
"stages": [
{ "name": "uf rules", "provider": "Rust", "detail": "96 rules configured" },
{ "name": "Flow's own lints", "provider": "OfficialFlowRust" },
{ "name": "React Compiler", "provider": "OfficialRust", "detail": "15 `react-compiler/*` rules, in the compiler's lint mode" }
]
}
Use the first report when you need findings. Use the second when you need to know whose work is in the run before chasing performance. The important part is that both are command output rather than documentation promises: if a rule is added, disabled or made unavailable, the machine-readable answer changes with the command.
Fixing what it finds
uf lint --fixApplies every fix uf is willing to make without asking, writes the files, and reports how many it applied and what it left. It is the same catalogue the language server offers in an editor, reached from a terminal, a git hook or CI rather than only from a lightbulb menu.
Two tiers, and the difference is real. --fix applies only the fixes where the
program afterwards means what it meant before — bool becomes boolean, and
nothing has been decided on your behalf. --fix-unsafe also applies the ones
whose correctness rests on something the rule could not check: export let count = 0 becomes export const count = 0, which still parses and still
formats, and throws at run time if anything reassigns the binding. Read that
diff before you commit it.
Most rules have no fix at all, which is deliberate — uf lint --rules names the
few that do, and the flag that applies each. flow/unclear-type
could turn any into mixed, an opaque type, or a generated router type
depending on what you meant; three answers is none, and guessing at one is
worse than leaving the finding where you can see it.
What is written parses, and running it twice is running it once. Every file
that passed uf fmt --check before the fix passes it after: a file the
formatter was content with is reprinted around the edit, because four bytes is
enough to push a line past the print width. A file that was already failing
that check keeps failing it — the edit is written as the edit left it, and
laying out a file nobody asked to have laid out is uf fmt's job rather than
this one's. The CLI reference spells out each of those and
what the exit status means.
uf fmt, uf lint and uf check all take paths, the way uf test does:
uf lint npm/uiAnything whose path contains one of the arguments is read and nothing else, so
uf fmt dialog.js is a way to format the file in front of you. A pattern that
matches nothing is an error rather than a clean run.
Suppressing a finding
A finding you have decided to keep is silenced by a comment naming its rule, with the reason beside it:
// The payload is JSON the server serialized, and React escapes a text child.
// uf-lint-disable-next-line security/no-dangerously-set-inner-html
<script type="application/json" dangerouslySetInnerHTML={html} />
uf-lint-disable-next-line covers the line below it. uf-lint-disable covers
everything after it until a uf-lint-enable naming the same rule, or the end of
the file. Both take one rule or several, separated by commas or spaces.
Two rules keep these comments honest. uniflowed/unknown-lint-suppression
(error) reports a comment naming a rule that does not exist, since a typo
silences nothing. uniflowed/unused-lint-suppression (warn) reports a comment
whose rule found nothing on the lines it covers. That is usually a finding that
went away: the code changed, or the rule stopped reporting a false positive. Left
in place, the comment would silence the next real finding on its line without
anyone reading it. Delete the comment.
The second rule only judges a comment whose rule ran with everything it needs.
It skips rules that are off, rules that wait on type inference, and, when an
editor lints one file on its own, the import/* rules that read the rest of
the project.
Project rules
A rule your team writes is a JavaScript module listed in plugins and switched
on in lint.rules. uf lint runs it beside uf's own rules, into one report:
// uf.config.js
export default {
plugins: ["./rules/acme.js"],
lint: { rules: { "acme/no-foo": "error" } },
};
// rules/acme.js
export default {
name: "acme",
rules: {
"no-foo": {
meta: { messages: { reserved: "`{{ name }}` is reserved for the build" } },
create(context) {
return {
Identifier(node) {
if (node.name === "foo") {
context.report({ node, messageId: "reserved", data: { name: node.name } });
}
},
};
},
},
},
};
The shape is ESLint's, so a rule you already have mostly moves as it is. A
plugin is a module whose default export has a name and rules, and a rule's
id is the two joined: acme/no-foo. Vite ignores a key it does not know, so
the same object can also be one of the project's Vite plugins. What a rule
gets:
- Listeners keyed by ESTree node type, and by the type with
:exiton the way out. The tree is the one uf's own Flow parser produced, so type annotations and component syntax are nodes like any other, and every node has aparent. There are no selectors beyond a node type. An identifier with a type annotation spans the annotation too, as in any Flow ESTree, so a fix that renames one replacesnode.name.lengthcharacters fromnode.range[0]rather than the whole node. context.reportwith anodeor aloc(lines from 1, columns from 0), and amessage, or amessageIdfrommeta.messagesfilled fromdata.context.sourceCode—text,ast,lines,getText(node),getLocFromIndexandgetIndexFromLoc— withcontext.filenameandcontext.cwd.fix, reported only by a rule that setsmeta.fixable, with ESLint's fixer:replaceText,replaceTextRange,insertTextBefore,insertTextAfter, theirRangeforms,removeandremoveRange.
What is not there fails out loud instead of answering nothing. There is no
scope analysis, no token list, and no context.options, because lint.rules
takes a level and nothing else. A rule that calls getScope throws, and a rule
that throws is named in the report.
The rules run in one worker on the project's Capability JS Host — Node.js or
Bun, through the Flow loader uf test uses, so a rule module can be written in
Flow — and the worker is started only when a project rule is on, from
@uniflowed/host, which has to be installed. Its cost is bounded and reported.
It has 30 seconds to load the plugins and 5 seconds for each file; a worker past
either is stopped and the file named, and after three workers the remaining
files are named rather than tried. uf lint --json carries a projectRules
object with the time spent inside each rule, the wall time of the whole pass
and every problem — a plugin that did not import, a rule no plugin defines, a
file that ran out of time — and any problem fails the run.
A rule's fixes go through the same two tiers as uf's own, and the rule picks
the tier with the word ESLint already gives it. meta.fixable: "whitespace"
promises edits that only move layout, and --fix applies those; "code" is
applied by --fix-unsafe alone, because uf cannot read an edit to find out
whether the program still means what it meant. Either way the result has to
parse and stay formatted, exactly as with uf's own fixes.
The language server runs project rules too, with one worker for the whole
editor session. Their findings appear beside uf's, a finding with a fix has it
in the lightbulb menu, "Fix all" on save applies the "whitespace" ones, and
anything that keeps a rule from answering is written to the editor's log for
the server.
When lint is too expensive
The slowest lint is usually not "the linter"; it is one rule repeatedly asking for a shape that should have been computed once. That is the class of bug behind #668: one rule spent most of the allocation budget building a Babel AST for a file that uf had already parsed.
The public workflow is:
- Narrow the path until the cost follows a directory or a single large file.
- Run
uf lint --json <path>and confirmfilesCheckedis the number you meant. - Run
uf explain lint --jsonto record which lint stages were in the run. - File the slow case with the file size, the rule name if one dominates the findings, and the command output.
There is no uf profile subcommand. The repository has profiler support for
its own benches and allocation reports, but a project run should not depend on
an internal profiling interface. Until a public profiler exists, the stable
answers are the narrowed command, the JSON report and the explain output.
Coming from ESLint
Where each rule of the four ESLint plugins React projects most often run lands
in uf. A rule id on the right is the uf rule that answers the same question, and
uf lint --rules shows its level in your project and whether it has a fix. The
other answers:
- Flow —
uf check's type inference reports it, so no lint rule has to guess. uf fmt— layout, which is the formatter's to decide.- React Compiler — uf compiles components with it, and it memoizes what the rule would have you hoist or wrap.
- Not needed — what the rule guards against does not exist in a uf project; the row says why.
- Not planned — rules for class components, their lifecycle methods and the legacy APIs around them. uf's React rules are written for component syntax.
- Project rule — a team's convention rather than a defect. uf does not ship conventions as defaults; a project's own rules are how a team keeps them (#974).
- Not yet — a defect uf does not catch today, tracked in the linked issue.
"Needs inference" marks a uf rule that needs Flow type inference. uf check
runs it. uf lint does not: a lint run names it under unavailableRules
instead of running it.
eslint-plugin-react
| ESLint | uf |
|---|---|
no-danger, no-danger-with-children | security/no-dangerously-set-inner-html |
no-unstable-nested-components | flow/nested-component |
jsx-key | react/jsx-key, which also reports a <> fragment in a list, and leaves React.Children.map alone because React keys what it returns |
no-array-index-key | react/no-array-index-key, which leaves alone a key that also holds the item itself, and an empty element with no state for a key to carry |
jsx-no-duplicate-props | react/jsx-no-duplicate-props |
no-children-prop | react/no-children-prop |
void-dom-elements-no-children | react/void-dom-elements-no-children |
jsx-no-comment-textnodes | react/jsx-no-comment-textnodes, which leaves text inside <code> and <pre> alone |
display-name, function-component-definition | react/component-syntax — a component declaration always has a name |
jsx-no-leaked-render | flow/sketchy-number (needs inference) |
prop-types, default-props-match-prop-types, require-default-props, prefer-exact-props, prefer-read-only-props, jsx-no-undef | Flow — component syntax types, defaults, exactness and read-only props, and inference reports an undefined name |
forbid-prop-types, forbid-foreign-prop-types, sort-prop-types, no-typos | Not needed — they check PropTypes, which Flow types replace |
react-in-jsx-scope, jsx-uses-react | Not needed — the automatic JSX runtime needs no React in scope |
jsx-uses-vars | Not needed — it exists to feed ESLint's no-unused-vars |
jsx-filename-extension | Not needed — Flow's JSX lives in .js files |
no-find-dom-node, no-render-return-value | Not needed — findDOMNode and ReactDOM.render are gone from React 19 |
jsx-no-bind, jsx-no-constructed-context-values, no-object-type-as-default-prop | React Compiler — see react/no-redundant-memo |
jsx-child-element-spacing, jsx-closing-bracket-location, jsx-closing-tag-location, jsx-curly-newline, jsx-curly-spacing, jsx-equals-spacing, jsx-first-prop-new-line, jsx-indent, jsx-indent-props, jsx-max-props-per-line, jsx-newline, jsx-one-expression-per-line, jsx-props-no-multi-spaces, jsx-space-before-closing, jsx-tag-spacing, jsx-wrap-multilines | uf fmt |
forward-ref-uses-ref, no-access-state-in-setstate, no-arrow-function-lifecycle, no-deprecated, no-did-mount-set-state, no-did-update-set-state, no-direct-mutation-state, no-is-mounted, no-redundant-should-component-update, no-set-state, no-string-refs, no-unsafe, no-unused-class-component-methods, no-unused-state, no-will-update-set-state, prefer-es6-class, prefer-stateless-function, require-optimization, require-render-return, sort-comp, state-in-constructor, static-property-placement | Not planned |
boolean-prop-naming, destructuring-assignment, forbid-component-props, forbid-dom-props, forbid-elements, hook-use-state, jsx-boolean-value, jsx-curly-brace-presence, jsx-fragments, jsx-handler-names, jsx-max-depth, jsx-no-literals, jsx-no-useless-fragment, jsx-pascal-case, jsx-props-no-spread-multi, jsx-props-no-spreading, jsx-sort-default-props, jsx-sort-props, no-adjacent-inline-elements, no-multi-comp, self-closing-comp, sort-default-props | Project rule |
jsx-no-script-url | security/no-script-url, for every prop the browser navigates to except href on an <a> — that pair is a11y/anchor-is-valid, which already reports it |
jsx-no-target-blank | security/no-target-blank, which asks for rel="noreferrer" alone: target="_blank" already gives the opened page a null window.opener, so noopener adds nothing and the Referer header is what is left to stop |
iframe-missing-sandbox | security/iframe-has-sandbox, which asks only whether the frame is sandboxed — allow-scripts allow-same-origin defeats a sandbox only when the framed document is same-origin, which the markup does not say |
no-unknown-property | react/no-unknown-property, which reports an attribute written with its HTML spelling and names the prop React spells instead — uf keeps no table of every DOM property, so an attribute it has not heard of is left alone rather than reported as unknown |
no-invalid-html-attribute | markup/no-invalid-rel — the plugin implements rel and so does uf, reporting a keyword that belongs to another element and the spellings the HTML standard calls non-conforming; an unrecognised keyword is left alone, because the registries rel draws on are open |
style-prop-object | react/style-prop-object |
no-namespace | react/no-namespace |
no-unescaped-entities | react/no-unescaped-entities, which reports > and } — the two that are usually the wreckage of a mistyped tag or a stray brace — and leaves ' and " alone, because they render exactly as written and don't is the false positive this rule is known for. Text inside <code> and <pre> is meant as written, so it is not read |
no-this-in-sfc | react/no-this-in-sfc, answered for the component and hook declarations uf's syntax gives it, where this provably names nothing. It follows an arrow function, which keeps the this around it, and leaves a nested function, a method and a class alone, because each of those has a this of its own |
no-unused-prop-types | react/no-unused-prop-types, which reports a prop a component declares and its body never reads, resolved through the binding the parameter introduces rather than the name it is spelled with — so a prop renamed with as is followed to its local name. A prop destructured into parts is left alone, and so is a name the body reads anywhere, because a shadowed read is still a read. Off by default, as in the plugin's own preset: a prop nothing reads is dead weight rather than a defect, and dropping one from a component other code calls is an API decision rather than a lint fix |
button-has-type | react/button-has-type |
checked-requires-onchange-or-readonly | react/checked-requires-onchange-or-readonly, which stays silent when the companion is written in its HTML spelling (readonly, onchange) — that is react/no-unknown-property's finding, so one line draws one diagnostic |
eslint-plugin-react-hooks
The plugin's compiler rules and uf's react-compiler/* rules are the same
compiler's categories under the same names; see React rules come from the
React Compiler.
| ESLint | uf |
|---|---|
rules-of-hooks | react-compiler/hooks, with flow/nested-hook — the compiler's Rules of Hooks, which report inside components and hooks only; a hook called in a plain function is not reported |
hooks | react-compiler/hooks |
purity | react-compiler/purity |
globals | react-compiler/globals |
component-hook-factories | flow/nested-component and flow/nested-hook |
static-components | react-compiler/static-components, with flow/nested-component |
set-state-in-effect | react-compiler/set-state-in-effect, with react-compiler/no-deriving-state-in-effects for the narrower question of state derived from props or state — off, as the preset leaves it |
exhaustive-deps | react-compiler/memo-dependencies for useMemo and useCallback; react-compiler/exhaustive-effect-dependencies for effects — off, as the preset leaves it |
set-state-in-render | react-compiler/set-state-in-render |
refs | react-compiler/refs |
immutability | react-compiler/immutability |
error-boundaries | react-compiler/error-boundaries |
incompatible-library | react-compiler/incompatible-library |
preserve-manual-memoization | react-compiler/preserve-manual-memoization |
unsupported-syntax | react-compiler/unsupported-syntax, with security/no-eval for eval |
use-memo | react-compiler/use-memo, and react-compiler/void-use-memo for a callback that returns nothing |
config, gating | Not needed — they check a Babel setup of the React Compiler, and uf runs the compiler itself |
eslint-plugin-jsx-a11y
a11y/heading-order and markup/no-invalid-nesting have no counterpart in the
plugin.
| ESLint | uf |
|---|---|
alt-text | a11y/alt-text |
anchor-ambiguous-text | a11y/anchor-ambiguous-text |
anchor-has-content | a11y/anchor-has-content |
anchor-is-valid | a11y/anchor-is-valid |
aria-activedescendant-has-tabindex | a11y/aria-activedescendant-has-tabindex |
aria-props | a11y/aria-props |
aria-proptypes | a11y/aria-proptypes |
aria-role | a11y/aria-role |
aria-unsupported-elements | a11y/aria-unsupported-elements |
autocomplete-valid | a11y/autocomplete-valid (the token a browser would not know; the order of the optional section-*, grouping and recipient parts is not policed) |
click-events-have-key-events | a11y/click-events-have-key-events (the element that has a role; the element with none is a11y/no-static-element-interactions) |
control-has-associated-label | a11y/control-has-associated-label (warn; only where no id, no <label> around it and no name of its own, since the label is usually a sibling) |
heading-has-content | a11y/heading-has-content |
html-has-lang | a11y/html-has-lang |
iframe-has-title | a11y/iframe-has-title |
img-redundant-alt | a11y/img-redundant-alt |
interactive-supports-focus | a11y/interactive-supports-focus |
label-has-associated-control, label-has-for | a11y/label-has-associated-control |
lang | a11y/lang (the <html lang> that is not a language tag; the missing or empty one is a11y/html-has-lang. A shape check rather than a registry lookup, so en_US is reported and a well-formed but unregistered subtag is left alone) |
media-has-caption | a11y/media-has-caption |
mouse-events-have-key-events | a11y/mouse-events-have-key-events |
no-access-key | a11y/no-access-key |
no-aria-hidden-on-focusable | a11y/no-aria-hidden-on-focusable |
no-autofocus | a11y/no-autofocus |
no-distracting-elements | a11y/no-distracting-elements |
no-interactive-element-to-noninteractive-role | a11y/no-interactive-element-to-noninteractive-role |
no-noninteractive-element-interactions | a11y/no-noninteractive-element-interactions (the non-interactive role that has a key handler; see the two rules it divides the question with) |
no-noninteractive-element-to-interactive-role | a11y/no-noninteractive-element-to-interactive-role |
no-noninteractive-tabindex | a11y/no-noninteractive-tabindex (warn; a scrollable region is given tabIndex={0} on purpose) |
no-redundant-roles | a11y/no-redundant-roles |
no-static-element-interactions | a11y/no-static-element-interactions |
prefer-tag-over-role | a11y/prefer-tag-over-role (warn; the roles a tag really replaces) |
role-has-required-aria-props | a11y/role-has-required-aria-props |
role-supports-aria-props | a11y/role-supports-aria-props |
scope | a11y/scope |
tabindex-no-positive | a11y/tabindex-no-positive |
accessible-emoji, no-onchange | Not needed — deprecated in the plugin itself |
Where an a11y/* rule answers differently from the plugin, it is on purpose
and always in the same direction: a rule that cannot see what it is asking
about says nothing. An element that spreads props (<iframe {...props} />,
<video {...props} />) may be passing the attribute through, and a component
or an expression renders something the file does not show, so neither is
reported. The rules that ask whether an element has a name look inside host
elements instead of counting any child as content, so an icon link with no
title is reported:
// a11y/anchor-has-content: nothing inside this <a> can be announced
<a href="/"><svg viewBox="0 0 24 24"><path d="M3 12l9-9 9 9" /></svg></a>
// named, so not reported
<a href="/" aria-label="Home"><svg viewBox="0 0 24 24"><path d="M3 12l9-9 9 9" /></svg></a>
The rules that read roles and aria-* attributes — a11y/aria-props,
a11y/aria-proptypes, a11y/aria-role, a11y/role-has-required-aria-props
and a11y/role-supports-aria-props — share one table, generated from
aria-query, which is the encoding
of WAI-ARIA 1.2 the ESLint plugin itself
reads. Where axe-core's table is more permissive than the specification, uf
follows the specification: axe allows aria-required on radio, ARIA 1.2 does
not give radio that attribute, and the plugin's own documentation lists it as
a failure.
One difference is uf's own. ARIA 1.2 makes generic — the role of a plain
<div> or <span> — name-prohibited, so aria-label on one is discarded and
the element ends up with no name at all. The plugin does not report this,
because its table of implicit roles has no entry for div;
a11y/role-supports-aria-props does:
// a11y/role-supports-aria-props: this name is discarded, not announced
<div aria-label="Close" />
// named, because the role takes a name
<div role="group" aria-label="Filters" />
<section aria-label="Timeline posts" />
eslint-plugin-import
| ESLint | uf |
|---|---|
named, default, namespace, export, no-unresolved | Flow — uf check resolves every import and types what it names |
no-mutable-exports | flow/non-const-var-export |
no-named-as-default-member | flow/default-import-access (needs inference) |
no-import-module-exports | flow/mixed-import-and-require |
no-default-export | react/no-default-export-component for modules that declare components; a project rule elsewhere |
no-cycle | import/no-cycle |
no-duplicates | import/no-duplicates |
no-absolute-path | import/no-absolute-path |
no-extraneous-dependencies | import/no-extraneous-dependencies |
no-named-as-default | import/no-named-as-default for relative value imports |
no-self-import | import/no-self-import |
no-relative-packages | import/no-relative-packages |
no-useless-path-segments | import/no-useless-path-segments |
dynamic-import-chunkname, no-webpack-loader-syntax | Not needed — webpack syntax, which Vite does not read |
amd, commonjs, consistent-type-specifier-style, enforce-node-protocol-usage, exports-last, extensions, first, group-exports, imports-first, max-dependencies, newline-after-import, no-amd, no-anonymous-default-export, no-commonjs, no-dynamic-require, no-empty-named-blocks, no-internal-modules, no-named-default, no-named-export, no-namespace, no-nodejs-modules, no-relative-parent-imports, no-restricted-paths, no-unassigned-import, order, prefer-default-export, unambiguous | Project rule |
no-deprecated | import/no-deprecated for relative value imports |
no-unused-modules | import/no-unused-modules, off by default, for unused modules and value exports in the relative source graph |
uf check
uf check runs this page's linter and then Flow's type inference, so every lint
finding here fails a check as well. The inference half — what an import resolves
to, the library definitions it reads, and a JSON report with two lists of
diagnostics — has a page of its own: Type checking.
In CI
uf fmt --check && uf lint && uf check && uf testFour commands, no configuration, and each one exits non-zero when it should. Run them in that order when you want the cheapest failure first:
| Step | Why it goes there |
|---|---|
uf fmt --check | cheapest and writes nothing |
uf lint | catches syntax, framework and policy errors before inference |
uf check | reruns lint, then adds Flow inference |
uf test | runs code, so it pays host start-up and fixture cost |
If CI is split into jobs, keep fmt and lint separate even though check
includes lint. A contributor wants the fast, precise failure when a file only
needs formatting; the release gate still wants check as the command that says
the project is typed.
Better still, name them once. uf.config.js takes tasks, uf run ci runs
them, and the pipeline calls the same thing:
export default defineConfig({
tasks: {
"fmt:check": "uf fmt --check",
lint: "uf lint",
check: "uf check",
test: "uf test",
ci: { command: "echo ok", dependsOn: ["fmt:check", "lint", "check", "test"] },
},
});
A check that is in the pipeline and not in uf.config.js is a check a
contributor cannot run before pushing. uf's own repository is set up this way —
its CI builds uf once and every job runs uf run <task>.
Where to go next
uf in CI is next: the checks on this page and the last three, run on every push.
Edit this pagedocs/app/guide/format/$page.mdx