一键导入
frontend-sonar
Use when fixing SonarQube issues in apps/frontend or writing Sonar-clean code. Contains the full enforced rule set with examples and quick fixes.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when fixing SonarQube issues in apps/frontend or writing Sonar-clean code. Contains the full enforced rule set with examples and quick fixes.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Use when fixing SonarQube issues in apps/frontend or writing Sonar-clean code. Contains the full enforced rule set with examples and quick fixes.
Use when working in apps/backend — new endpoints, controllers, services, models, queues/workers, or integrations. Covers the Router to Controller to Service to Model architecture, Zod validation, Prisma/PostgreSQL data access, Winston logging, BullMQ jobs, and FHIR/IDEXX/Merck integrations.
Use when reviewing code, auditing a PR, or doing an adversarial review of a change. Runs quality, security, accessibility, and convention checks specific to this codebase.
Use when fixing SonarCloud issues in apps/desktop or writing Sonar-clean Electron and renderer code. Covers the enforced rule categories, the accepted role=dialog deferral, CSP and page-asset rules, and links the live SonarCloud project.
Use when building or changing UI in apps/frontend. Covers the custom design system, component library (Button, Card, Badge, and more), design tokens, and styling conventions so UI stays consistent and on-brand.
Use when writing or fixing tests in apps/frontend. Covers Jest and React Testing Library conventions, targeted test runs, the coverage mandate, Zustand mocking, and common pitfalls.
| name | frontend-sonar |
| description | Use when fixing SonarQube issues in apps/frontend or writing Sonar-clean code. Contains the full enforced rule set with examples and quick fixes. |
Use this skill when fixing SonarQube issues in apps/frontend, or when writing new code that must pass Sonar checks. Contains the complete rule set enforced in this repo with examples and quick fixes.
TRIGGER: any mention of "sonar", "code quality", "lint issues", or when writing new frontend code.
Run all three in order before declaring any task done. Never skip.
# 1. Type check — from apps/frontend/
npx tsc --noemit
# 2. Lint — from repo root
pnpm --filter frontend run lint
# 3. Prefer targeted tests for files you modified; full Jest runs are allowed if the user explicitly asks or if you are validating shared test infrastructure
pnpm --filter frontend run test -- --testPathPattern="<ModifiedComponentName>"
Full suite (pnpm run test with no filter) is discouraged by default. Use it only when the user explicitly asks, when triaging repo-wide breakage, or when validating shared test infrastructure. Playwright and accessibility runs are allowed whenever relevant.
Resolved/open issues log: docs/guide/sonar-tracker.md
Raw Sonar dump: docs/guide/sonar.md
Update sonar-tracker.md after every fix.
as SomeType | string when SomeType already covers all values.void somePromise() — either await it or .then() it.as X type assertions where TypeScript can infer..closest<HTMLElement>(selector) generic overload instead of as HTMLElement | null.RefObject<T> not MutableRefObject<T> unless you need to mutate .current externally.const [value, setValue] = useState(...).const state = useState(...).const [, setter] = useState(...) — use useRef instead when value is never read:// Bad
const [, setBlurred] = useState(false);
// Good
const blurredRef = useRef(false);
const setBlurred = (v: boolean) => {
blurredRef.current = v;
};
set + PascalCase(valueName). E.g. [items, setItems].defaultProps / mock objects — otherwise TypeScript will error on excess properties.<button> in tests — use <span>.<button> inside <button>.onClick/onKeyDown on <dialog> — move to inner <div>.role="group" when native structure is already semantic.Sonar flags a raw text node immediately followed by a sibling element inside the same parent as "ambiguous spacing before next element". Fix by wrapping the text node in a JSX expression:
// Bad — raw text node adjacent to <span>
<span>
Finance
<span className="text-text-tertiary">{` (${count})`}</span>
</span>
// Good — text wrapped in expression, no ambiguity
<span>
{"Finance"}
<span className="text-text-tertiary">{` (${count})`}</span>
</span>
This applies anywhere a bare string literal sits next to a JSX child element inside the same container.
Prefer native elements over ARIA roles:
| Instead of | Use |
|---|---|
<div role="article"> | <article> |
<div role="region" aria-label="..."> | <section aria-label="..."> |
<div role="list"> / <div role="listitem"> | <ul> / <li> |
<div role="dialog"> | <dialog open> |
<div role="button"> | <button> |
<ul>, children must become <li> (including empty-state placeholders).<article>, drag handler type → React.DragEvent<HTMLElement>.<div>/<span> with onClick must also have role, tabIndex={0}, onKeyDown.const before return.value={a ? b ? 'X' : 'Y' : b ? 'Z' : 'W'}): extract to a named module-level helper function placed before the component, not an inline const. This keeps the component body clean and the logic reusable.// Bad — nested ternary inline in prop
value={gender === 'female' ? isNeutered ? 'Spayed' : 'Not spayed' : isNeutered ? 'Neutered' : 'Not neutered'}
// Good — module-level helper
const getNeuteredStatusLabel = (gender: string | undefined, isNeutered: boolean | undefined): string => {
if (gender === 'female') return isNeutered ? 'Spayed' : 'Not spayed';
return isNeutered ? 'Neutered' : 'Not neutered';
};
// then in JSX:
value={getNeuteredStatusLabel(companion.gender, companion.isneutered)}
// If array is only used for .includes() → convert to Set
const OPTIONS = new Set<Foo>(['A', 'B', 'C']);
if (OPTIONS.has(value)) { ... }
| Old | New |
|---|---|
window | globalThis.window |
typeof x === 'undefined' | x === undefined |
str.replace(/foo/g, ...) | str.replaceAll('foo', ...) |
/^foo/.test(str) | str.startsWith('foo') |
arr[arr.length - 1] | arr.at(-1) |
.filter(...).pop() | .findLast(...) |
str.match(regex) | RegExp.exec(str) |
arr.findIndex(x => x === val) >= 0 | arr.includes(val) |
.sort() when original mustn't change | .toSorted() |
{ ...{} } | remove empty spread |
arr.length > 0 && arr.every(...) | arr.every(...) |
Additional constraints:
replaceAll with a RegExp, the regex must be global (/g) or runtime errors occur./[,]/g -> ',').String.raw to reduce escaping bugs.else if instead of else { if (...) { ... } } to satisfy Sonar readability rules.All drag handlers shared across element types: use React.DragEvent<HTMLElement> not HTMLDivElement.
/* Bad — same selector twice */
.foo,
.bar {
border: 1px solid #eee;
}
.foo,
.bar {
padding: 18px;
}
/* Good — merged */
.foo,
.bar {
border: 1px solid #eee;
padding: 18px;
}
!== undefined ? value : fallback to === undefined ? fallback : value.if / else if branches that set the same value into a single combined condition.?? (nullish coalescing) rather than || when the left-hand side is an optional chain — a?.b ?? c not a?.b || c (avoids falsiness bugs with empty strings and 0).String(value ?? '') where value is typed unknown, guard the type first to avoid [object Object]: typeof value === 'string' || typeof value === 'number' ? String(value) : ''.!parsed.hostname?.includes('.') not !parsed.hostname || !parsed.hostname.includes('.').onClick on <img>, <div>, or <span> directly.<button> with type="button" and move the handler there. Set disabled instead of guarding inside the handler.role + tabIndex + onKeyDown approach when wrapping in a <button> would break layout (e.g. inside a <label>).<dialog> MigrationWhen converting <div role="dialog"> to <dialog open>:
open attribute — without it the browser hides the element.m-0 w-full h-full max-w-none border-0 to neutralise native <dialog> styles.aria-modal="true" — it is implied on <dialog> and can cause double-announcement.onClick/onKeyDown directly on <dialog> — keep interactive handlers on inner elements.fixed inset-0 overlay pattern still works on <dialog open> when combined with the overrides above.Extracting setState updaters (fixes nesting > 4 in onChange callbacks):
// Bad — 5 levels deep inline
onChange={(event) =>
setEditor((prev) =>
prev == null ? prev : { ...prev, service: { ...prev.service, name: event.target.value } }
)
}
// Good — named handler outside JSX
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEditor((prev) =>
prev == null ? prev : { ...prev, service: { ...prev.service, name: event.target.value } }
);
};
// then: onChange={handleNameChange}
Reducing cognitive complexity in guard functions — extract sub-conditions into named helpers:
// Bad — one large function with nested if/else
const resolveRedirect = (...) => {
if (owner) {
if (!verified) {
if (step < 3) { ... }
if (...) { ... }
...
}
}
};
// Good — extract unverified-owner branch
const resolveUnverifiedOwnerRedirect = (step, profileStep, pathname, orgId) => {
if (step < 3) return `/create-org?orgId=${orgId}`;
if (profileStep < 3 && pathname !== '/team-onboarding') return `/team-onboarding?orgId=${orgId}`;
if (isUnverifiedPathAllowed(pathname)) return '';
return '/dashboard';
};
Extracting inner .map() callbacks (fixes nesting inside setFormData updaters):
// Bad — nested .map() inside setState callback inside useEffect
setFormData((prev) => {
const next = prev.map((item) => {
return items.map((s) => ({ ...s, id: '', organisationId: orgId })); // 5 levels
});
});
// Good — module-level helper
const buildItem = (s: Template, orgId: string): Service => ({
...s,
id: '',
organisationId: orgId,
});
// then: .map((s) => buildItem(s, orgId))
eslint --fix will auto-fix some of these but NOT semantic HTML, complexity, or accessibility issues — fix those manually.npx tsc --noemit + pnpm --filter frontend run lint before marking resolved.h-[100px] → h-25, z-[5000] → z-5000, max-w-[220px] → max-w-55). Fix these when the IDE flags them.