diff --git a/.eslintrc b/.eslintrc index 7fb18090d..7039d0caf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -81,8 +81,19 @@ // ***** Operands ***** "@typescript-eslint/prefer-nullish-coalescing": "error", + // ***** Conditionals ***** + // Don't allow unnecessary conditional checks, such as when a value is always true, which can also help catch cases + // such as accidentally checking `if([]){}` vs. `if([].length){}` + "@typescript-eslint/strict-boolean-expressions": ["error", { + "allowAny": true, + "allowNullableBoolean": true, + "allowNullableString": true + }], + // ***** Arrays ***** + // Try to enforce early terminations of loops, rather than statements such as `.find(x=>x)[0]` "unicorn/prefer-array-find": "error", + // TypeScript doesn't do a good job of reporting indexed values as potentially undefined, such as `[1,2,3][999]` "unicorn/prefer-at": "error", // ***** Numbers ***** diff --git a/src/modules/argumentsParser.ts b/src/modules/argumentsParser.ts index 83bbfd2aa..6c86ac714 100644 --- a/src/modules/argumentsParser.ts +++ b/src/modules/argumentsParser.ts @@ -712,6 +712,7 @@ Example use cases: type: 'boolean', }) .fail((msg, err, _yargs) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (err) { throw err; } diff --git a/src/modules/candidateGenerator.ts b/src/modules/candidateGenerator.ts index 7f73bfae8..2841a974e 100644 --- a/src/modules/candidateGenerator.ts +++ b/src/modules/candidateGenerator.ts @@ -231,7 +231,7 @@ export default class CandidateGenerator extends Module { .filter(([, roms]) => roms.length === game.getRoms().length) .map(([archive]) => archive); - const archiveWithEveryRom = archivesWithEveryRom[0]; + const archiveWithEveryRom = archivesWithEveryRom.at(0); if (archiveWithEveryRom) { // An Archive was found, use that as the only possible input file // For each of this Game's ROMs, find the matching ArchiveEntry from this Archive diff --git a/src/modules/datScanner.ts b/src/modules/datScanner.ts index ad80730bd..f1dc63620 100644 --- a/src/modules/datScanner.ts +++ b/src/modules/datScanner.ts @@ -193,7 +193,7 @@ export default class DATScanner extends Scanner { }); proc.on('exit', (code) => { - if (code) { + if (code !== null && code > 0) { reject(new Error(`exit code ${code}`)); return; } diff --git a/src/polyfill/arrayPoly.ts b/src/polyfill/arrayPoly.ts index cd11f1c9a..779fd6579 100644 --- a/src/polyfill/arrayPoly.ts +++ b/src/polyfill/arrayPoly.ts @@ -28,7 +28,7 @@ export default class ArrayPoly { public static filterUniqueMapped( mapper: (arg: T) => V, ): (value: T, idx: number, values: T[]) => boolean { - let mappedValues: V[]; + let mappedValues: V[] | undefined; return (value: T, idx: number, values: T[]): boolean => { if (!mappedValues) { mappedValues = values.map((val) => mapper(val)); diff --git a/src/types/datStatus.ts b/src/types/datStatus.ts index 6aaa39a52..6140bce51 100644 --- a/src/types/datStatus.ts +++ b/src/types/datStatus.ts @@ -245,7 +245,7 @@ export default class DATStatus { const foundReleaseCandidate = foundReleaseCandidates .find((rc) => rc && rc.getGame().equals(game)); - if (foundReleaseCandidate ?? !game.getRoms().length) { + if (foundReleaseCandidate !== undefined || !game.getRoms().length) { status = GameStatus.FOUND; } diff --git a/src/types/dats/game.ts b/src/types/dats/game.ts index 4bd89ba1c..5c534d265 100644 --- a/src/types/dats/game.ts +++ b/src/types/dats/game.ts @@ -112,12 +112,12 @@ export default class Game implements GameProps { @Expose() @Type(() => Release) @Transform(({ value }) => value || []) - readonly release: Release | Release[]; + readonly release?: Release | Release[]; @Expose() @Type(() => ROM) @Transform(({ value }) => value || []) - readonly rom: ROM | ROM[]; + readonly rom?: ROM | ROM[]; constructor(props?: GameProps) { this.name = props?.name ?? ''; diff --git a/src/types/dats/logiqx/logiqxDat.ts b/src/types/dats/logiqx/logiqxDat.ts index 44b52761f..c1ff88e73 100644 --- a/src/types/dats/logiqx/logiqxDat.ts +++ b/src/types/dats/logiqx/logiqxDat.ts @@ -22,13 +22,13 @@ export default class LogiqxDAT extends DAT { @Expose() @Type(() => Game) @Transform(({ value }) => value || []) - private readonly game: Game | Game[]; + private readonly game?: Game | Game[]; // NOTE(cemmer): this is not Logiqx DTD-compliant, but it's what pleasuredome Datfiles use @Expose() @Type(() => Machine) @Transform(({ value }) => value || []) - private readonly machine: Machine | Machine[]; + private readonly machine?: Machine | Machine[]; constructor(header: Header, games: Game | Game[]) { super(); @@ -89,7 +89,7 @@ export default class LogiqxDAT extends DAT { } if (Array.isArray(this.machine)) { - if (this.machine) { + if (this.machine.length) { return this.machine; } } else if (this.machine) { diff --git a/src/types/dats/mame/machine.ts b/src/types/dats/mame/machine.ts index ac6def58d..49c63f0ce 100644 --- a/src/types/dats/mame/machine.ts +++ b/src/types/dats/mame/machine.ts @@ -14,7 +14,7 @@ export default class Machine extends Game implements MachineProps { @Expose({ name: 'device_ref' }) @Type(() => DeviceRef) @Transform(({ value }) => value || []) - readonly deviceRef: DeviceRef | DeviceRef[]; + readonly deviceRef?: DeviceRef | DeviceRef[]; constructor(props?: MachineProps) { super(props); diff --git a/src/types/dats/mame/mameDat.ts b/src/types/dats/mame/mameDat.ts index 426edcbac..5fd8a1b47 100644 --- a/src/types/dats/mame/mameDat.ts +++ b/src/types/dats/mame/mameDat.ts @@ -23,7 +23,7 @@ export default class MameDAT extends DAT { @Expose() @Type(() => Machine) @Transform(({ value }) => value || []) - private readonly machine: Machine | Machine[]; + private readonly machine?: Machine | Machine[]; constructor(machine: Machine | Machine[]) { super(); @@ -50,7 +50,7 @@ export default class MameDAT extends DAT { getGames(): Game[] { if (Array.isArray(this.machine)) { - if (this.machine) { + if (this.machine.length) { return this.machine; } } else if (this.machine) { diff --git a/src/types/files/archives/tar.ts b/src/types/files/archives/tar.ts index bd113f05e..86d789177 100644 --- a/src/types/files/archives/tar.ts +++ b/src/types/files/archives/tar.ts @@ -27,7 +27,7 @@ export default class Tar extends Archive { // WARN(cemmer): entries in tar archives don't have headers, the entire file has to be read to // calculate the CRCs - let errorMessage; + let errorMessage: string | undefined; const writeStream = new tar.Parse({ onwarn: (code, message): void => { errorMessage = `${code}: ${message}`; diff --git a/src/types/options.ts b/src/types/options.ts index 9390e6ada..482a62f6f 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -634,7 +634,7 @@ export default class Options implements OptionsProps { if (await fsPoly.isDirectory(inputPath)) { const dirPaths = (await fsPoly.walk(inputPathNormalized, walkCallback)) .map((filePath) => path.normalize(filePath)); - if (!dirPaths || !dirPaths.length) { + if (!dirPaths.length) { if (!requireFiles) { return []; } @@ -652,7 +652,7 @@ export default class Options implements OptionsProps { // Otherwise, process it as a glob pattern const paths = (await fg(inputPathNormalized, { onlyFiles: true })) .map((filePath) => path.normalize(filePath)); - if (!paths || !paths.length) { + if (!paths.length) { if (URLPoly.canParse(inputPath)) { // Allow URLs, let the scanner modules deal with them walkCallback(1); diff --git a/src/types/outputFactory.ts b/src/types/outputFactory.ts index 3969f4afd..1b1ac0259 100644 --- a/src/types/outputFactory.ts +++ b/src/types/outputFactory.ts @@ -381,12 +381,11 @@ export default class OutputFactory { output = path.join(dir, output); } - if (game && ( - (options.getDirGameSubdir() === GameSubdirMode.MULTIPLE + if ((options.getDirGameSubdir() === GameSubdirMode.MULTIPLE && game.getRoms().length > 1 && !FileFactory.isArchive(ext)) || options.getDirGameSubdir() === GameSubdirMode.ALWAYS - )) { + ) { output = path.join(game.getName(), output); } diff --git a/test/modules/candidatePreferer.test.ts b/test/modules/candidatePreferer.test.ts index b5d1f999a..348b1f9fb 100644 --- a/test/modules/candidatePreferer.test.ts +++ b/test/modules/candidatePreferer.test.ts @@ -60,7 +60,7 @@ async function expectPreferredCandidates( } function arrayCoerce(val: T | T[] | undefined): T[] { - if (!val) { + if (val === undefined) { return []; } return Array.isArray(val) ? val : [val]; diff --git a/test/modules/datFilter.test.ts b/test/modules/datFilter.test.ts index db5466f0e..0fa413ed0 100644 --- a/test/modules/datFilter.test.ts +++ b/test/modules/datFilter.test.ts @@ -22,7 +22,7 @@ async function expectFilteredDAT( } function arrayCoerce(val: T | T[] | undefined): T[] { - if (!val) { + if (val === undefined) { return []; } return Array.isArray(val) ? val : [val];