Enable @typescript eslint/prefer nullish coalescing eslint rule (#1420)

## Description:

Fixes #952 
Enabled @typescript-eslint/prefer-nullish-coalescing rule and worked
through every error, introducing ?? and ??= operators or disabling
errors with inline comments where appropriate, to the best of my
ability.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

g_santos_m
This commit is contained in:
g-santos-m
2025-07-15 05:00:06 +02:00
committed by GitHub
parent ac9a9ec253
commit 31381f67f4
28 changed files with 95 additions and 85 deletions
+18
View File
@@ -19,6 +19,23 @@ export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: [
"__mocks__/fileMock.js",
"eslint.config.js",
"jest.config.ts",
"postcss.config.js",
"tailwind.config.js",
"webpack.config.js",
],
},
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
// Disable rules that would fail. The failures should be fixed, and the entries here removed.
@@ -34,6 +51,7 @@ export default [
{
rules: {
// Enable rules
"@typescript-eslint/prefer-nullish-coalescing": "error",
eqeqeq: "error",
},
},
+2 -2
View File
@@ -99,7 +99,7 @@ export class LangSelector extends LitElement {
private async initializeLanguage() {
const browserLocale = navigator.language;
const savedLang = localStorage.getItem("lang");
const userLang = this.getClosestSupportedLang(savedLang || browserLocale);
const userLang = this.getClosestSupportedLang(savedLang ?? browserLocale);
this.defaultTranslations = this.loadLanguage("en");
this.translations = this.loadLanguage(userLang);
@@ -110,7 +110,7 @@ export class LangSelector extends LitElement {
}
private loadLanguage(lang: string): Record<string, string> {
const language = this.languageMap[lang] || {};
const language = this.languageMap[lang] ?? {};
const flat = flattenTranslations(language);
return flat;
}
+1 -1
View File
@@ -110,7 +110,7 @@ export class PublicLobby extends LitElement {
const teamCount =
lobby.gameConfig.gameMode === GameMode.Team
? lobby.gameConfig.playerTeams || 0
? (lobby.gameConfig.playerTeams ?? 0)
: null;
const mapImageSrc = this.mapImages.get(lobby.gameID);
+8 -10
View File
@@ -245,16 +245,14 @@ export class Transport {
}
private startPing() {
if (this.isLocal || this.pingInterval) return;
if (this.pingInterval === null) {
this.pingInterval = window.setInterval(() => {
if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) {
this.sendMsg({
type: "ping",
} satisfies ClientPingMessage);
}
}, 5 * 1000);
}
if (this.isLocal) return;
this.pingInterval ??= window.setInterval(() => {
if (this.socket !== null && this.socket.readyState === WebSocket.OPEN) {
this.sendMsg({
type: "ping",
} satisfies ClientPingMessage);
}
}, 5 * 1000);
}
private stopPing() {
+1 -1
View File
@@ -172,7 +172,7 @@ export function getAltKey(): string {
export function getGamesPlayed(): number {
try {
return parseInt(localStorage.getItem("gamesPlayed") || "0", 10) || 0;
return parseInt(localStorage.getItem("gamesPlayed") ?? "0", 10) || 0;
} catch (error) {
console.warn("Failed to read games played from localStorage:", error);
return 0;
+2 -2
View File
@@ -91,9 +91,9 @@ const getSpriteForUnit = (unit: UnitView): ImageBitmap | null => {
const unitType = unit.type();
if (unitType === UnitType.Train) {
const trainType = trainTypeToSpriteType(unit);
return spriteMap.get(trainType) || null;
return spriteMap.get(trainType) ?? null;
}
return spriteMap.get(unitType) || null;
return spriteMap.get(unitType) ?? null;
};
export const isSpriteReady = (unit: UnitView): boolean => {
+1 -1
View File
@@ -137,7 +137,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
private toggleEventFilter(filterName: MessageCategory) {
const currentState = this.eventsFilters.get(filterName) || false;
const currentState = this.eventsFilters.get(filterName) ?? false;
this.eventsFilters.set(filterName, !currentState);
this.requestUpdate();
}
+8 -8
View File
@@ -298,14 +298,14 @@ export class RadialMenu implements Layer {
const disabled = this.params === null || d.data.disabled(this.params);
const color = disabled
? this.config.disabledColor
: d.data.color || "#333333";
: (d.data.color ?? "#333333");
const opacity = disabled ? 0.5 : 0.7;
if (d.data.id === this.selectedItemId && this.currentLevel > level) {
return color;
}
return d3.color(color)?.copy({ opacity: opacity })?.toString() || color;
return d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color;
})
.attr("stroke", "#ffffff")
.attr("stroke-width", "2")
@@ -341,7 +341,7 @@ export class RadialMenu implements Layer {
const color =
this.params === null || d.data.disabled(this.params)
? this.config.disabledColor
: d.data.color || "#333333";
: (d.data.color ?? "#333333");
path.attr("fill", color);
}
});
@@ -405,11 +405,11 @@ export class RadialMenu implements Layer {
path.attr("stroke-width", "2");
const color = disabled
? this.config.disabledColor
: d.data.color || "#333333";
: (d.data.color ?? "#333333");
const opacity = disabled ? 0.5 : 0.7;
path.attr(
"fill",
d3.color(color)?.copy({ opacity: opacity })?.toString() || color,
d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color,
);
};
@@ -625,7 +625,7 @@ export class RadialMenu implements Layer {
this.selectedItemId = null;
}
this.currentMenuItems = previousItems || [];
this.currentMenuItems = previousItems ?? [];
if (this.currentLevel === 0) {
this.updateCenterButtonState("default");
@@ -645,11 +645,11 @@ export class RadialMenu implements Layer {
const disabled = this.params === null || item.disabled(this.params);
const color = disabled
? this.config.disabledColor
: item.color || "#333333";
: (item.color ?? "#333333");
const opacity = disabled ? 0.5 : 0.7;
selectedPath.attr(
"fill",
d3.color(color)?.copy({ opacity: opacity })?.toString() || color,
d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color,
);
}
}
@@ -352,9 +352,9 @@ export const buildMenuElement: MenuElement = {
: undefined,
icon: item.icon,
tooltipItems: [
{ text: translateText(item.key || ""), className: "title" },
{ text: translateText(item.key ?? ""), className: "title" },
{
text: translateText(item.description || ""),
text: translateText(item.description ?? ""),
className: "description",
},
{
@@ -401,7 +401,7 @@ export const boatMenuElement: MenuElement = {
params.playerActionHandler.handleBoatAttack(
params.myPlayer,
params.selected?.id() || null,
params.selected?.id() ?? null,
params.tile,
spawn !== false ? spawn : null,
);
+1 -1
View File
@@ -52,7 +52,7 @@ export class TeamStats extends LitElement implements Layer {
for (const player of players) {
const team = player.team();
if (team === null) continue;
if (!grouped[team]) grouped[team] = [];
grouped[team] ??= [];
grouped[team].push(player);
}
+7 -1
View File
@@ -317,10 +317,16 @@ export class UILayer implements Layer {
if (constructionType === undefined) {
return 1;
}
const constDuration =
this.game.unitInfo(constructionType).constructionDuration;
if (constDuration === undefined) {
throw new Error("unit does not have constructionTime");
}
return (
(this.game.ticks() - unit.createdAt()) /
(this.game.unitInfo(constructionType).constructionDuration || 1)
(constDuration === 0 ? 1 : constDuration)
);
case UnitType.MissileSilo:
case UnitType.SAMLauncher:
return unit.missileReadinesss();
+2 -3
View File
@@ -104,9 +104,8 @@ export type IsLoggedInResponse =
| false;
let __isLoggedIn: IsLoggedInResponse | undefined = undefined;
export function isLoggedIn(): IsLoggedInResponse {
if (__isLoggedIn === undefined) {
__isLoggedIn = _isLoggedIn();
}
__isLoggedIn ??= _isLoggedIn();
return __isLoggedIn;
}
function _isLoggedIn(): IsLoggedInResponse {
+1 -1
View File
@@ -119,7 +119,7 @@ export function getMode(list: Set<number>): number {
// Count occurrences
const counts = new Map<number, number>();
for (const item of list) {
counts.set(item, (counts.get(item) || 0) + 1);
counts.set(item, (counts.get(item) ?? 0) + 1);
}
// Find the item with the highest count
+3 -5
View File
@@ -97,11 +97,9 @@ export class AttackExecution implements Execution {
}
}
if (this.startTroops === null) {
this.startTroops = this.mg
.config()
.attackAmount(this._owner, this.target);
}
this.startTroops ??= this.mg
.config()
.attackAmount(this._owner, this.target);
if (this.removeTroops) {
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
this._owner.removeTroops(this.startTroops);
+1 -3
View File
@@ -19,9 +19,7 @@ export class DonateGoldExecution implements Execution {
}
this.recipient = mg.player(this.recipientID);
if (this.gold === null) {
this.gold = this.sender.gold() / 3n;
}
this.gold ??= this.sender.gold() / 3n;
}
tick(ticks: number): void {
+1 -3
View File
@@ -19,9 +19,7 @@ export class DonateTroopsExecution implements Execution {
}
this.recipient = mg.player(this.recipientID);
if (this.troops === null) {
this.troops = mg.config().defaultDonationAmount(this.sender);
}
this.troops ??= mg.config().defaultDonationAmount(this.sender);
const maxDonation =
mg.config().maxPopulation(this.recipient) - this.recipient.population();
this.troops = Math.min(this.troops, maxDonation);
+1 -3
View File
@@ -109,9 +109,7 @@ export class SAMLauncherExecution implements Execution {
this.player = this.sam.owner();
}
if (this.pseudoRandom === undefined) {
this.pseudoRandom = new PseudoRandom(this.sam.id());
}
this.pseudoRandom ??= new PseudoRandom(this.sam.id());
const mirvWarheadTargets = this.mg.nearbyUnits(
this.sam.tile(),
+5 -7
View File
@@ -31,13 +31,11 @@ export class SAMMissileExecution implements Execution {
}
tick(ticks: number): void {
if (this.SAMMissile === undefined) {
this.SAMMissile = this._owner.buildUnit(
UnitType.SAMMissile,
this.spawn,
{},
);
}
this.SAMMissile ??= this._owner.buildUnit(
UnitType.SAMMissile,
this.spawn,
{},
);
if (!this.SAMMissile.isActive()) {
this.active = false;
return;
+1 -3
View File
@@ -23,9 +23,7 @@ export class ShellExecution implements Execution {
}
tick(ticks: number): void {
if (this.shell === undefined) {
this.shell = this._owner.buildUnit(UnitType.Shell, this.spawn, {});
}
this.shell ??= this._owner.buildUnit(UnitType.Shell, this.spawn, {});
if (!this.shell.isActive()) {
this.active = false;
return;
+3 -5
View File
@@ -89,11 +89,9 @@ export class TransportShipExecution implements Execution {
this.target = mg.player(this.targetID);
}
if (this.troops === null) {
this.troops = this.mg
.config()
.boatAttackAmount(this.attacker, this.target);
}
this.troops ??= this.mg
.config()
.boatAttackAmount(this.attacker, this.target);
this.troops = Math.min(this.troops, this.attacker.troops());
+1 -3
View File
@@ -507,9 +507,7 @@ export class GameView implements GameMap {
}
myPlayer(): PlayerView | null {
if (this._myPlayer === null) {
this._myPlayer = this.playerByClientID(this._myClientID);
}
this._myPlayer ??= this.playerByClientID(this._myClientID);
return this._myPlayer;
}
+8 -8
View File
@@ -66,7 +66,7 @@ export class StatsImpl implements Stats {
private _addAttack(player: Player, index: number, value: BigIntLike) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.attacks === undefined) p.attacks = [0n];
p.attacks ??= [0n];
while (p.attacks.length <= index) p.attacks.push(0n);
p.attacks[index] += _bigint(value);
}
@@ -89,8 +89,8 @@ export class StatsImpl implements Stats {
) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.boats === undefined) p.boats = { [type]: [0n] };
if (p.boats[type] === undefined) p.boats[type] = [0n];
p.boats ??= { [type]: [0n] };
p.boats[type] ??= [0n];
while (p.boats[type].length <= index) p.boats[type].push(0n);
p.boats[type][index] += _bigint(value);
}
@@ -104,8 +104,8 @@ export class StatsImpl implements Stats {
const type = unitTypeToBombUnit[nukeType];
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.bombs === undefined) p.bombs = { [type]: [0n] };
if (p.bombs[type] === undefined) p.bombs[type] = [0n];
p.bombs ??= { [type]: [0n] };
p.bombs[type] ??= [0n];
while (p.bombs[type].length <= index) p.bombs[type].push(0n);
p.bombs[type][index] += _bigint(value);
}
@@ -113,7 +113,7 @@ export class StatsImpl implements Stats {
private _addGold(player: Player, index: number, value: BigIntLike) {
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.gold === undefined) p.gold = [0n];
p.gold ??= [0n];
while (p.gold.length <= index) p.gold.push(0n);
p.gold[index] += _bigint(value);
}
@@ -127,8 +127,8 @@ export class StatsImpl implements Stats {
const type = unitTypeToOtherUnit[otherUnitType];
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.units === undefined) p.units = { [type]: [0n] };
if (p.units[type] === undefined) p.units[type] = [0n];
p.units ??= { [type]: [0n] };
p.units[type] ??= [0n];
while (p.units[type].length <= index) p.units[type].push(0n);
p.units[type][index] += _bigint(value);
}
+1 -3
View File
@@ -26,9 +26,7 @@ class GameMapLoader {
private createLazyLoader<T>(importFn: () => Promise<T>): () => Promise<T> {
let cache: Promise<T> | null = null;
return () => {
if (!cache) {
cache = importFn();
}
cache ??= importFn();
return cache;
};
}
+4 -4
View File
@@ -37,7 +37,7 @@ export async function archive(gameRecord: GameRecord) {
}
} catch (error) {
log.error(`${gameRecord.info.gameID}: Final archive error: ${error}`, {
message: error?.message || error,
message: error?.message ?? error,
stack: error?.stack,
name: error?.name,
...(error && typeof error === "object" ? error : {}),
@@ -70,7 +70,7 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) {
log.info(`${info.gameID}: successfully wrote game analytics to R2`);
} catch (error) {
log.error(`${info.gameID}: Error writing game analytics to R2: ${error}`, {
message: error?.message || error,
message: error?.message ?? error,
stack: error?.stack,
name: error?.name,
...(error && typeof error === "object" ? error : {}),
@@ -119,7 +119,7 @@ export async function readGameRecord(
} catch (error) {
// Log the error for monitoring purposes
log.error(`${gameId}: Error reading game record from R2: ${error}`, {
message: error?.message || error,
message: error?.message ?? error,
stack: error?.stack,
name: error?.name,
...(error && typeof error === "object" ? error : {}),
@@ -142,7 +142,7 @@ export async function gameRecordExists(gameId: GameID): Promise<boolean> {
return false;
}
log.error(`${gameId}: Error checking archive existence: ${error}`, {
message: error?.message || error,
message: error?.message ?? error,
stack: error?.stack,
name: error?.name,
...(error && typeof error === "object" ? error : {}),
+2
View File
@@ -162,7 +162,9 @@ export class Cloudflare {
);
const credentials = {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
AccountTag: tokenData.a || this.accountId,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
TunnelID: tokenData.t || tunnelId,
TunnelName: tunnelName,
TunnelSecret: tokenData.s,
+1 -1
View File
@@ -694,7 +694,7 @@ export class GameServer {
for (const client of this.activeClients) {
if (client.hashes.has(turnNumber)) {
const clientHash = client.hashes.get(turnNumber)!;
counts.set(clientHash, (counts.get(clientHash) || 0) + 1);
counts.set(clientHash, (counts.get(clientHash) ?? 0) + 1);
}
}
+2 -1
View File
@@ -146,8 +146,9 @@ app.get(
"/api/env",
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
const envConfig = {
game_env: process.env.GAME_ENV || "prod",
game_env: process.env.GAME_ENV,
};
if (!envConfig.game_env) return res.sendStatus(500);
res.json(envConfig);
}),
);
+6 -2
View File
@@ -29,7 +29,7 @@ import { initWorkerMetrics } from "./WorkerMetrics";
const config = getServerConfigFromServer();
const workerId = parseInt(process.env.WORKER_ID || "0");
const workerId = parseInt(process.env.WORKER_ID ?? "0");
const log = logger.child({ comp: `w_${workerId}` });
// Worker setup
@@ -94,6 +94,7 @@ export function startWorker() {
log.warn(`cannot create game, id not found`);
return res.status(400).json({ error: "Game ID is required" });
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
const result = CreateGameInputSchema.safeParse(req.body);
if (!result.success) {
@@ -140,6 +141,7 @@ export function startWorker() {
return;
}
if (game.isPublic()) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
log.info(
`cannot start public game ${game.id}, game is public, ip: ${ipAnonymize(clientIP)}`,
@@ -171,6 +173,7 @@ export function startWorker() {
return res.status(400).json({ error: "Game not found" });
}
if (game.isPublic()) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
log.warn(
`cannot update public game ${game.id}, ip: ${ipAnonymize(clientIP)}`,
@@ -296,7 +299,8 @@ export function startWorker() {
const forwarded = req.headers["x-forwarded-for"];
const ip = Array.isArray(forwarded)
? forwarded[0]
: forwarded || req.socket.remoteAddress || "unknown";
: // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
forwarded || req.socket.remoteAddress || "unknown";
try {
// Parse and handle client messages