@@ -1684,9 +1683,9 @@ function DamageTable({
{t("analyzer:damage.header.type")}
- {showDistanceColumn && (
+ {showDistanceColumn ? (
{t("analyzer:damage.header.distance")}
- )}
+ ) : null}
{damageIsSubWeaponDamage(firstRow) ? (
{comparisonValues
@@ -1694,7 +1693,9 @@ function DamageTable({
: t("analyzer:damage.header.baseDamage")}
) : null}
- {showDamageColumn && {t("analyzer:damage.header.damage")} }
+ {showDamageColumn ? (
+ {t("analyzer:damage.header.damage")}
+ ) : null}
{showPopovers ? : null}
@@ -1731,27 +1732,27 @@ function DamageTable({
) : null}
- {showDistanceColumn && (
+ {showDistanceColumn ? (
{typeof val.distance === "number"
? val.distance
: val.distance?.join("-")}
- )}
- {damageIsSubWeaponDamage(val) &&
{val.baseValue} }
- {showDamageColumn && (
+ ) : null}
+ {damageIsSubWeaponDamage(val) ?
{val.baseValue} : null}
+ {showDamageColumn ? (
{damage(val)}
{comparisonVal ? `/${damage(comparisonVal)}` : null}{" "}
- {val.shotsToSplat && (
+ {val.shotsToSplat ? (
{t("analyzer:damage.toSplat", {
count: val.shotsToSplat,
})}
- )}
+ ) : null}
- )}
+ ) : null}
{showPopovers ? (
{renderPopover(val, (val as SubWeaponDamage).subWeaponId) ? (
@@ -1858,11 +1859,11 @@ function ConsumptionTable({
})}
- {subWeaponId === TORPEDO_ID && (
+ {subWeaponId === TORPEDO_ID ? (
{t("analyzer:torpedoExplanation")}
- )}
+ ) : null}
>
);
}
diff --git a/app/features/calendar/routes/calendar.$id.tsx b/app/features/calendar/routes/calendar.$id.tsx
index 813baa78c..4fc92c673 100644
--- a/app/features/calendar/routes/calendar.$id.tsx
+++ b/app/features/calendar/routes/calendar.$id.tsx
@@ -191,7 +191,7 @@ function Results() {
return (
- {data.event.participantCount && (
+ {data.event.participantCount ? (
{isTeamResults
? t("calendar:participatedCount", {
@@ -201,7 +201,7 @@ function Results() {
count: data.event.participantCount,
})}
- )}
+ ) : null}
@@ -276,9 +276,9 @@ function Description() {
{data.event.username}
- {data.event.description && (
+ {data.event.description ? (
{data.event.description}
- )}
+ ) : null}
);
diff --git a/app/features/lfg/components/LFGPost.tsx b/app/features/lfg/components/LFGPost.tsx
index 9cc8b8442..431f732ce 100644
--- a/app/features/lfg/components/LFGPost.tsx
+++ b/app/features/lfg/components/LFGPost.tsx
@@ -92,7 +92,9 @@ function TeamLFGPost({
- {isHydrated &&
}
+ {isHydrated ? (
+
+ ) : null}
{post.languages ? (
) : null}
@@ -273,12 +275,12 @@ function PostPills({
invisible: !isHydrated,
})}
>
- {typeof timezone === "string" && isHydrated && (
+ {typeof timezone === "string" && isHydrated ? (
- )}
- {!isHydrated &&
}
+ ) : null}
+ {!isHydrated ?
: null}
{languages ?
: null}
- {canEdit &&
}
+ {canEdit ?
: null}
);
}
diff --git a/app/features/map-list-generator/routes/maps.tsx b/app/features/map-list-generator/routes/maps.tsx
index 10dd21ff8..72708d26c 100644
--- a/app/features/map-list-generator/routes/maps.tsx
+++ b/app/features/map-list-generator/routes/maps.tsx
@@ -128,7 +128,7 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
{t("common:maps.createMapList")}
- {mapList && (
+ {mapList ? (
<>
{mapList.map(({ mode, stageId }, i) => (
@@ -163,7 +163,7 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
{t("common:actions.copyToClipboard")}
>
- )}
+ ) : null}
);
}
diff --git a/app/features/notifications/routes/notifications.tsx b/app/features/notifications/routes/notifications.tsx
index 05f25b4e1..3f0e893a8 100644
--- a/app/features/notifications/routes/notifications.tsx
+++ b/app/features/notifications/routes/notifications.tsx
@@ -61,9 +61,9 @@ export default function NotificationsPage() {
seen: Number(!unseenIds.has(notification.id)),
}}
/>
- {i !== data.notifications.length - 1 && (
+ {i !== data.notifications.length - 1 ? (
- )}
+ ) : null}
))}
diff --git a/app/features/object-damage-calculator/routes/object-damage-calculator.tsx b/app/features/object-damage-calculator/routes/object-damage-calculator.tsx
index d8ffe8653..99a08f620 100644
--- a/app/features/object-damage-calculator/routes/object-damage-calculator.tsx
+++ b/app/features/object-damage-calculator/routes/object-damage-calculator.tsx
@@ -321,7 +321,9 @@ function DamageReceiversGrid({
{t(`analyzer:damage.${damage.type}` as any)}
- {damage.objectShredder &&
}
+ {damage.objectShredder ? (
+
+ ) : null}
))}
@@ -332,8 +334,9 @@ function DamageReceiversGrid({
- {abilityPoints !== "0" &&
- damageReceiverAp[damageToReceiver.receiver]}
+ {abilityPoints !== "0"
+ ? damageReceiverAp[damageToReceiver.receiver]
+ : null}
{t("q:looking.columns.groups")}
- {isMobile && (
+ {isMobile ? (
- )}
- {isMobile && data.ownGroup && (
+ ) : null}
+ {isMobile && data.ownGroup ? (
{t("q:looking.columns.myGroup")}
- )}
+ ) : null}
diff --git a/app/features/tournament-bracket/components/Bracket/Swiss.tsx b/app/features/tournament-bracket/components/Bracket/Swiss.tsx
index 2933cc840..e6510030e 100644
--- a/app/features/tournament-bracket/components/Bracket/Swiss.tsx
+++ b/app/features/tournament-bracket/components/Bracket/Swiss.tsx
@@ -98,7 +98,7 @@ export function SwissBracket({
return (
- {groups.length > 1 && (
+ {groups.length > 1 ? (
{groups.map((g) => (
))}
- )}
+ ) : null}
{rounds.map((round, roundI) => {
const matches = bracket.data.match.filter(
diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.finalize.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.finalize.tsx
index 5a1164248..2b4a44d62 100644
--- a/app/features/tournament-bracket/routes/to.$id.brackets.finalize.tsx
+++ b/app/features/tournament-bracket/routes/to.$id.brackets.finalize.tsx
@@ -293,9 +293,9 @@ function NewBadgeReceiversSelector({
- {i !== standingToReceive?.members.length - 1 && (
+ {i !== standingToReceive?.members.length - 1 ? (
- )}
+ ) : null}
);
})}
diff --git a/app/features/tournament-organization/routes/org.$slug.tsx b/app/features/tournament-organization/routes/org.$slug.tsx
index f6acf32cf..f8014d7e3 100644
--- a/app/features/tournament-organization/routes/org.$slug.tsx
+++ b/app/features/tournament-organization/routes/org.$slug.tsx
@@ -424,12 +424,12 @@ function SeriesView({
- {hasLeaderboard && (
+ {hasLeaderboard ? (
- )}
+ ) : null}
diff --git a/app/features/user-page/components/UserResultsTable.tsx b/app/features/user-page/components/UserResultsTable.tsx
index 958f9c818..0027a3c96 100644
--- a/app/features/user-page/components/UserResultsTable.tsx
+++ b/app/features/user-page/components/UserResultsTable.tsx
@@ -37,7 +37,7 @@ export function UserResultsTable({
- {hasHighlightCheckboxes && }
+ {hasHighlightCheckboxes ? : null}
{t("results.tournament")}
{t("results.date")}
@@ -59,7 +59,7 @@ export function UserResultsTable({
return (
- {hasHighlightCheckboxes && (
+ {hasHighlightCheckboxes ? (
- )}
+ ) : null}
{" "}
diff --git a/app/root.tsx b/app/root.tsx
index ad6c5c0fa..4fd550ba3 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -259,7 +259,7 @@ function Document({
- {IS_E2E_TEST_RUN &&
}
+ {IS_E2E_TEST_RUN ?
: null}
diff --git a/biome-plugins/no-and-conditional-rendering.grit b/biome-plugins/no-and-conditional-rendering.grit
new file mode 100644
index 000000000..12fc1bbda
--- /dev/null
+++ b/biome-plugins/no-and-conditional-rendering.grit
@@ -0,0 +1,18 @@
+language js
+
+// `cond && ` renders the left value when it is falsy but not `null` /
+// `undefined` / `false`: a `0` count or an empty string ends up in the DOM as
+// text. The ternary form has no such edge, so it is the repo-wide convention
+// (AGENTS.md, "React"). Matches both `cond && ` anywhere and any `&&`
+// standing directly as a JSX child, which covers helpers like
+// `{icon && React.cloneElement(icon, {...})}`.
+`$cond && $rendered` as $expression where {
+ or {
+ $rendered <: or {
+ JsxTagExpression(),
+ JsParenthesizedExpression(expression=JsxTagExpression())
+ },
+ $expression <: within JsxExpressionChild(expression=$expression)
+ },
+ register_diagnostic(span=$expression, message="Do not use `&&` for conditional rendering: a falsy left side that is not `null`/`undefined`/`false` (a `0` count, an empty string, `NaN`) renders itself. Use a ternary instead: `cond ? : null`.", severity="error")
+}
diff --git a/biome.json b/biome.json
index 70759a085..f8c0e0c7b 100644
--- a/biome.json
+++ b/biome.json
@@ -221,6 +221,10 @@
{
"includes": ["**", "!app/utils/kysely.server.ts"],
"plugins": ["./biome-plugins/no-kysely-sqlite-helpers.grit"]
+ },
+ {
+ "includes": ["app/**"],
+ "plugins": ["./biome-plugins/no-and-conditional-rendering.grit"]
}
]
}