From 04ae0327c2d7ef6f669dd16300cd36ac144a1a9b Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Tue, 31 May 2022 17:38:40 +0200 Subject: [PATCH] Add the recommended eslint plugins for TypeScript (#121) This adds both the recommended TypeScript checks, plus the recommended TypeScript checks that require type checking. This latter addition means that eslint essentially has to compile all of the TypeScript in the project, causing it to be slower. This isn't much of a problem in VS Code because there's a lot of caching being done, but it's clearly slower when run on the commandline. All of the errors are either fixed or suppressed. Some errors are suppressed because fixing them would be too laborious for the little value gained. The eslint config is also slightly refactored to separate the strictly TypeScript checks from the JavaScript checks. --- .eslintrc.json | 181 +++++++++++++++++---------- js/apple2.ts | 8 +- js/apple2io.ts | 12 +- js/base64.ts | 2 +- js/cards/disk2.ts | 14 ++- js/cards/mouse.ts | 4 +- js/cards/nsc.ts | 1 + js/cards/parallel.ts | 8 +- js/cards/ramfactor.ts | 4 +- js/cards/smartport.ts | 33 ++--- js/cards/thunderclock.ts | 5 +- js/components/Apple2.tsx | 2 +- js/components/Drives.tsx | 4 +- js/components/FileChooser.tsx | 7 +- js/components/FileModal.tsx | 3 +- js/components/util/files.ts | 9 +- js/components/util/promises.ts | 12 ++ js/cpu6502.ts | 6 +- js/debugger.ts | 30 ++--- js/entry.tsx | 1 + js/formats/format_utils.ts | 10 +- js/formats/woz.ts | 2 +- js/gl.ts | 47 +++---- js/main2.ts | 1 + js/main2e.ts | 1 + js/mmu.ts | 14 +-- js/roms/rom.ts | 12 +- js/types.ts | 2 +- js/ui/apple2.ts | 32 ++--- js/ui/audio_worker.ts | 6 +- js/ui/drive_lights.ts | 21 ++-- js/ui/gamepad.ts | 2 +- js/ui/keyboard.ts | 2 +- js/ui/tape.ts | 6 +- js/util.ts | 2 +- test/components/FileChooser.spec.tsx | 8 +- test/js/__mocks__/apple2shader.js | 2 +- test/js/formats/create_disk.spec.ts | 2 +- test/js/formats/testdata/woz.ts | 9 +- test/js/formats/util.ts | 2 +- test/js/formats/woz.spec.ts | 6 +- test/util/bios.ts | 1 + test/util/image.ts | 1 + 43 files changed, 322 insertions(+), 215 deletions(-) create mode 100644 js/components/util/promises.ts diff --git a/.eslintrc.json b/.eslintrc.json index 37bea7a..a5e6161 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,10 @@ { + // Global + "root": true, "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint/eslint-plugin" + "extends": [ + "eslint:recommended", + "plugin:jest/recommended" ], "rules": { "indent": [ @@ -19,47 +22,15 @@ "error", "unix" ], - "eqeqeq": ["error", "smart"], + "eqeqeq": [ + "error", + "smart" + ], "prefer-const": [ "error" ], - "semi": "off", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/semi": [ - "error", - "always" - ], - "@typescript-eslint/member-delimiter-style": [ - "error", - { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "semi", - "requireLast": false - } - } - ], + "no-var": "error", "no-use-before-define": "off", - "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-use-before-define": [ - "error", - { - "functions": false, - "classes": false - } - ], - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_" - } - ], - "no-redeclare": "off", - "@typescript-eslint/no-redeclare": ["error"], "no-dupe-class-members": "off", "no-console": [ "error", @@ -71,39 +42,118 @@ ] } ], - "@typescript-eslint/require-await": ["error"], - "jest/expect-expect": ["error", { - "assertFunctionNames": [ - "expect*", - "checkImageData", - "testCode" - ] - }] + // Jest configuration + "jest/expect-expect": [ + "error", + { + "assertFunctionNames": [ + "expect*", + "checkImageData", + "testCode" + ] + } + ] }, "env": { "builtin": true, "browser": true, "es6": true }, - "parserOptions": { - "sourceType": "module", - "project": "./tsconfig.json" - }, - "extends": [ - "eslint:recommended", - "plugin:jest/recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended" - ], "overrides": [ + // All overrides matching a file are applied in-order, with the last + // taking precedence. + // + // TypeScript/TSX-specific configuration { "files": [ - "**/*.ts" + "*.ts", + "*.tsx" + ], + "plugins": [ + "@typescript-eslint/eslint-plugin" + ], + "extends": [ + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "rules": { - "no-var": "error" + // recommended is just "warn" + "@typescript-eslint/no-explicit-any": "error", + // enforce semicolons at ends of statements + "semi": "off", + "@typescript-eslint/semi": [ + "error", + "always" + ], + // enforce semicolons to separate members + "@typescript-eslint/member-delimiter-style": [ + "error", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ], + // definitions must come before uses for variables + "@typescript-eslint/no-use-before-define": [ + "error", + { + "functions": false, + "classes": false + } + ], + // no used variables + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + } + ], + // no redeclaration of classes, members or variables + "no-redeclare": "off", + "@typescript-eslint/no-redeclare": [ + "error" + ], + // allow empty interface definitions and empty extends + "@typescript-eslint/no-empty-interface": "off", + // allow explicit type declaration + "@typescript-eslint/no-inferrable-types": "off", + // allow some non-string types in templates + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + "allowNumber": true, + "allowBoolean": true + } + ], + // react rules + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error" + }, + "parserOptions": { + "sourceType": "module", + "project": "./tsconfig.json" } }, + // UI elements + { + "files": [ + "js/ui/**.ts" + ], + "rules": { + // allow non-null assertions since these classes reference the DOM + "@typescript-eslint/no-non-null-assertion": "off" + } + }, + // JS Node configuration { "files": [ "bin/*", @@ -119,6 +169,7 @@ "browser": false } }, + // Test configuration { "files": [ "test/**/*" @@ -132,6 +183,7 @@ "no-console": 0 } }, + // Entry point configuration { "files": [ "js/entry2.ts", @@ -142,6 +194,7 @@ "commonjs": true } }, + // Worker configuration { "files": [ "workers/*" @@ -151,11 +204,13 @@ } } ], - "ignorePatterns": ["coverage/**/*"], + "ignorePatterns": [ + "coverage/**/*" + ], "settings": { "react": { - "pragma": "h", - "version": "16" + "pragma": "h", + "version": "16" } } -} +} \ No newline at end of file diff --git a/js/apple2.ts b/js/apple2.ts index 33457a3..283992d 100644 --- a/js/apple2.ts +++ b/js/apple2.ts @@ -28,7 +28,7 @@ import RAM, { RAMState } from './ram'; import SYMBOLS from './symbols'; import Debugger, { DebuggerContainer } from './debugger'; -import { Restorable, rom } from './types'; +import { ReadonlyUint8Array, Restorable, rom } from './types'; import { processGamepad } from './ui/gamepad'; export interface Apple2Options { @@ -47,7 +47,7 @@ export interface Stats { renderedFrames: number; } -interface State { +export interface State { cpu: CpuState; vm: VideoModesState; io: Apple2IOState; @@ -91,8 +91,8 @@ export class Apple2 implements Restorable, DebuggerContainer { } async init(options: Apple2Options) { - const romImportPromise = import(`./roms/system/${options.rom}`); - const characterRomImportPromise = import(`./roms/character/${options.characterRom}`); + const romImportPromise = import(`./roms/system/${options.rom}`) as Promise<{ default: new () => ROM }>; + const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>; const LoresPage = options.gl ? LoresPageGL : LoresPage2D; const HiresPage = options.gl ? HiresPageGL : HiresPage2D; diff --git a/js/apple2io.ts b/js/apple2io.ts index 07e8a09..a50d332 100644 --- a/js/apple2io.ts +++ b/js/apple2io.ts @@ -52,7 +52,7 @@ const LOC = { }; export default class Apple2IO implements MemoryPages, Restorable { - private _slot: Array = new Array(7).fill(null); + private _slot: Array = new Array(7).fill(null); private _auxRom: Memory | null = null; private _khz = 1023; @@ -81,7 +81,7 @@ export default class Apple2IO implements MemoryPages, Restorable private _annunciators: Annunciators = [false, false, false, false]; private _tape: TapeData = []; - private _tapeOffset = 0; + private _tapeOffset: number = 0; private _tapeNext: number = 0; private _tapeCurrent = false; @@ -106,7 +106,7 @@ export default class Apple2IO implements MemoryPages, Restorable if (this._audioListener) { this._audioListener(this._sample); } - this._sample = new Array(this._sample_size); + this._sample = new Array(this._sample_size); this._sampleIdx = 0; } } @@ -226,7 +226,7 @@ export default class Apple2IO implements MemoryPages, Restorable this._tapeCurrent = this._tape[this._tapeOffset][1]; while (now >= this._tapeNext) { if ((this._tapeOffset % 1000) === 0) { - debug('Read ' + (this._tapeOffset / 1000)); + debug(`Read ${this._tapeOffset / 1000}`); } this._tapeCurrent = this._tape[this._tapeOffset][1]; this._tapeNext += this._tape[this._tapeOffset++][0]; @@ -451,7 +451,7 @@ export default class Apple2IO implements MemoryPages, Restorable } setTape(tape: TapeData) { - debug('Tape length: ' + tape.length); + debug(`Tape length: ${tape.length}`); this._tape = tape; this._tapeOffset = -1; } @@ -459,7 +459,7 @@ export default class Apple2IO implements MemoryPages, Restorable sampleRate(rate: number, sample_size: number) { this._rate = rate; this._sample_size = sample_size; - this._sample = new Array(this._sample_size); + this._sample = new Array(this._sample_size); this._sampleIdx = 0; this._calcSampleRate(); } diff --git a/js/base64.ts b/js/base64.ts index 74feabe..dbdc70e 100644 --- a/js/base64.ts +++ b/js/base64.ts @@ -124,7 +124,7 @@ export function base64_decode(data: string | null | undefined): memory | undefin const DATA_URL_PREFIX = 'data:application/octet-stream;base64,'; -export function base64_json_parse(json: string) { +export function base64_json_parse(json: string): unknown { const reviver = (_key: string, value: unknown) => { if (typeof value ==='string' && value.startsWith(DATA_URL_PREFIX)) { return base64_decode(value.slice(DATA_URL_PREFIX.length)); diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 2664477..84534c6 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -308,7 +308,7 @@ function setDriveState(state: DriveState) { /** * Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller. */ -export default class DiskII implements Card { +export default class DiskII implements Card { private drives: Drive[] = [ { // Drive 1 @@ -488,7 +488,7 @@ export default class DiskII implements Card { return; } if (this.on && (this.skip || this.writeMode)) { - const track = this.cur.tracks![this.cur.track >> 2]; + const track = this.cur.tracks[this.cur.track >> 2]; if (track && track.length) { if (this.cur.head >= track.length) { this.cur.head = 0; @@ -520,7 +520,7 @@ export default class DiskII implements Card { * tracks by activating two neighboring coils at once. */ private setPhase(phase: Phase, on: boolean) { - this.debug('phase ' + phase + (on ? ' on' : ' off')); + this.debug(`phase ${phase}${on ? ' on' : ' off'}`); if (on) { this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2; this.cur.phase = phase; @@ -682,7 +682,7 @@ export default class DiskII implements Card { } else { // It's not explicitly stated, but writes to any address set the // data register. - this.bus = val!; + this.bus = val; } return result; @@ -703,7 +703,9 @@ export default class DiskII implements Card { return this.bootstrapRom[off]; } - write() { } + write() { + // not writable + } reset() { if (this.on) { @@ -722,7 +724,7 @@ export default class DiskII implements Card { this.moveHead(); } - getState() { + getState(): State { const result = { drives: [] as DriveState[], skip: this.skip, diff --git a/js/cards/mouse.ts b/js/cards/mouse.ts index 6f9daaf..2c8a5b6 100644 --- a/js/cards/mouse.ts +++ b/js/cards/mouse.ts @@ -217,7 +217,9 @@ export default class Mouse implements Card, Restorable { return rom[off]; } - write() {} + write() { + // not writable + } /** * Triggers interrupts based on activity since the last tick diff --git a/js/cards/nsc.ts b/js/cards/nsc.ts index fc1cb73..94b3081 100644 --- a/js/cards/nsc.ts +++ b/js/cards/nsc.ts @@ -110,6 +110,7 @@ export default class NoSlotClock { } setState(_: unknown) { + // Setting the state makes no sense. } } diff --git a/js/cards/parallel.ts b/js/cards/parallel.ts index 4eaaf96..045f354 100644 --- a/js/cards/parallel.ts +++ b/js/cards/parallel.ts @@ -37,11 +37,15 @@ export default class Parallel implements Card, Restorable { return rom[off]; } - write() {} + write() { + // not writable + } getState() { return {}; } - setState(_state: ParallelState) {} + setState(_state: ParallelState) { + // can't set the state + } } diff --git a/js/cards/ramfactor.ts b/js/cards/ramfactor.ts index 994ae2b..0376fad 100644 --- a/js/cards/ramfactor.ts +++ b/js/cards/ramfactor.ts @@ -126,7 +126,9 @@ export default class RAMFactor implements Card, Restorable { return rom[this.firmware << 12 | (page - 0xC0) << 8 | off]; } - write() {} + write() { + // not writable + } reset() { this.firmware = 0; diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index b450540..8e3c4aa 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -197,9 +197,9 @@ export default class SmartPort implements Card, MassStorage, Restorable { if (screen.current) { const options = { canvas: screen.current, - tick: () => {}, + tick: () => { /* do nothing */ }, ...props, }; const apple2 = new Apple2Impl(options); diff --git a/js/components/Drives.tsx b/js/components/Drives.tsx index 98604c5..1952c15 100644 --- a/js/components/Drives.tsx +++ b/js/components/Drives.tsx @@ -46,7 +46,9 @@ export const Drives = ({ io, sectors }: DrivesProps) => { side, })); }, - dirty: () => {} + dirty: () => { + // do nothing + } }; if (io) { diff --git a/js/components/FileChooser.tsx b/js/components/FileChooser.tsx index 81683eb..0b3e2ad 100644 --- a/js/components/FileChooser.tsx +++ b/js/components/FileChooser.tsx @@ -1,5 +1,6 @@ import { h, Fragment } from 'preact'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { noAwait } from './util/promises'; export interface FilePickerAcceptType { description?: string | undefined; @@ -41,7 +42,7 @@ interface ExtraProps { const InputFileChooser = ({ disabled = false, - onChange = () => { }, + onChange = () => { /* do nothing */ }, accept = [], }: InputFileChooserProps) => { const inputRef = useRef(null); @@ -104,7 +105,7 @@ interface FilePickerChooserProps { const FilePickerChooser = ({ disabled = false, - onChange = () => { }, + onChange = () => { /* do nothing */ }, accept = [ACCEPT_EVERYTHING_TYPE] }: FilePickerChooserProps) => { const [busy, setBusy] = useState(false); @@ -142,7 +143,7 @@ const FilePickerChooser = ({ return ( <> -   diff --git a/js/components/FileModal.tsx b/js/components/FileModal.tsx index dd95c07..82fef1b 100644 --- a/js/components/FileModal.tsx +++ b/js/components/FileModal.tsx @@ -7,6 +7,7 @@ import DiskII from '../cards/disk2'; import index from 'json/disks/index.json'; import { FileChooser, FilePickerAcceptType, FileSystemFileHandleLike } from './FileChooser'; +import { noAwait } from './util/promises'; const DISK_TYPES: FilePickerAcceptType[] = [ { @@ -125,7 +126,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => - + ); diff --git a/js/components/util/files.ts b/js/components/util/files.ts index 8d7198c..405c6a7 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -82,7 +82,7 @@ export const loadJSON = async (disk2: DiskII, number: DriveNumber, url: string) if (!response.ok) { throw new Error(`Error loading: ${response.statusText}`); } - const data: JSONDisk = await response.json(); + const data = await response.json() as JSONDisk; if (!includes(NIBBLE_FORMATS, data.type)) { throw new Error(`Type ${data.type} not recognized.`); } @@ -112,7 +112,10 @@ export const loadHttpFile = async ( if (!response.ok) { throw new Error(`Error loading: ${response.statusText}`); } - const reader = response.body!.getReader(); + if (!response.body) { + throw new Error('Error loading: no body'); + } + const reader = response.body.getReader(); let received = 0; const chunks: Uint8Array[] = []; @@ -131,7 +134,7 @@ export const loadHttpFile = async ( } const urlParts = url.split('/'); - const file = urlParts.pop()!; + const file = urlParts.pop() || url; const fileParts = file.split('.'); const ext = fileParts.pop()?.toLowerCase() || '[none]'; const name = decodeURIComponent(fileParts.join('.')); diff --git a/js/components/util/promises.ts b/js/components/util/promises.ts new file mode 100644 index 0000000..d60a46a --- /dev/null +++ b/js/components/util/promises.ts @@ -0,0 +1,12 @@ +/** + * Converts a function type returning a `Promise` to a function type returning `void`. + */ +export type NoAwait Promise> = + (...args: Parameters) => void; + +/** + * Signals that the argument returns a `Promise` that is intentionally not being awaited. + */ +export function noAwait Promise>(f: F): NoAwait { + return f as NoAwait; +} diff --git a/js/cpu6502.ts b/js/cpu6502.ts index 21b383e..0421492 100644 --- a/js/cpu6502.ts +++ b/js/cpu6502.ts @@ -135,7 +135,7 @@ function isResettablePageHandler(pageHandler: MemoryPages | ResettablePageHandle const BLANK_PAGE: Memory = { read: function () { return 0; }, - write: function () { } + write: function () { /* not writable */ } }; interface Opts { @@ -196,7 +196,7 @@ export default class CPU6502 { private addr: word = 0; /** Filled array of memory handlers by address page */ - private memPages: Memory[] = new Array(0x100); + private memPages: Memory[] = new Array(0x100); /** Callbacks invoked on reset signal */ private resetHandlers: ResettablePageHandler[] = []; /** Elapsed cycles */ @@ -246,7 +246,7 @@ export default class CPU6502 { } // Certain browsers benefit from using arrays over maps - this.opary = new Array(0x100); + this.opary = new Array(0x100); for (let idx = 0; idx < 0x100; idx++) { this.opary[idx] = ops[idx] || this.unknown(idx); diff --git a/js/debugger.ts b/js/debugger.ts index 54bb6d3..d2021f7 100644 --- a/js/debugger.ts +++ b/js/debugger.ts @@ -128,7 +128,7 @@ export default class Debugger { result += this.padWithSymbol(pc); - const cmd = new Array(size); + const cmd = new Array(size); for (let idx = 0, jdx = pc; idx < size; idx++, jdx++) { cmd[idx] = this.cpu.read(jdx); } @@ -238,13 +238,13 @@ export default class Debugger { case 'implied': break; case 'immediate': - result += '#' + toHexOrSymbol(lsb); + result += `#${toHexOrSymbol(lsb)}`; break; case 'absolute': - result += '' + toHexOrSymbol(addr, 4); + result += `${toHexOrSymbol(addr, 4)}`; break; case 'zeroPage': - result += '' + toHexOrSymbol(lsb); + result += `${toHexOrSymbol(lsb)}`; break; case 'relative': { @@ -253,38 +253,38 @@ export default class Debugger { off -= 256; } pc += off + 2; - result += '' + toHexOrSymbol(pc, 4) + ' (' + off + ')'; + result += `${toHexOrSymbol(pc, 4)} (${off})`; } break; case 'absoluteX': - result += '' + toHexOrSymbol(addr, 4)+ ',X'; + result += `${toHexOrSymbol(addr, 4)},X`; break; case 'absoluteY': - result += '' + toHexOrSymbol(addr, 4) + ',Y'; + result += `${toHexOrSymbol(addr, 4)},Y`; break; case 'zeroPageX': - result += '' + toHexOrSymbol(lsb) + ',X'; + result += `${toHexOrSymbol(lsb)},X`; break; case 'zeroPageY': - result += '' + toHexOrSymbol(lsb) + ',Y'; + result += `${toHexOrSymbol(lsb)},Y`; break; case 'absoluteIndirect': - result += '(' + toHexOrSymbol(addr, 4) + ')'; + result += `(${toHexOrSymbol(addr, 4)})`; break; case 'zeroPageXIndirect': - result += '(' + toHexOrSymbol(lsb) + ',X)'; + result += `(${toHexOrSymbol(lsb)},X)`; break; case 'zeroPageIndirectY': - result += '(' + toHexOrSymbol(lsb) + '),Y'; + result += `(${toHexOrSymbol(lsb)},),Y`; break; case 'accumulator': result += 'A'; break; case 'zeroPageIndirect': - result += '(' + toHexOrSymbol(lsb) + ')'; + result += `(${toHexOrSymbol(lsb)})`; break; case 'absoluteXIndirect': - result += '(' + toHexOrSymbol(addr, 4) + ',X)'; + result += `(${toHexOrSymbol(addr, 4)},X)`; break; case 'zeroPage_relative': val = lsb; @@ -293,7 +293,7 @@ export default class Debugger { off -= 256; } pc += off + 2; - result += '' + toHexOrSymbol(val) + ',' + toHexOrSymbol(pc, 4) + ' (' + off + ')'; + result += `${toHexOrSymbol(val)},${toHexOrSymbol(pc, 4)} (${off})`; break; default: break; diff --git a/js/entry.tsx b/js/entry.tsx index 27943fc..ad12e39 100644 --- a/js/entry.tsx +++ b/js/entry.tsx @@ -1,4 +1,5 @@ import { h, render } from 'preact'; import { App } from './components/App'; +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion render(, document.getElementById('app')!); diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 7f66534..5f42aec 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -1,7 +1,7 @@ import { bit, byte, memory } from '../types'; import { base64_decode, base64_encode } from '../base64'; import { bytify, debug, toHex } from '../util'; -import { NibbleDisk, ENCODING_NIBBLE } from './types'; +import { NibbleDisk, ENCODING_NIBBLE, JSONDisk } from './types'; /** * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector). @@ -465,14 +465,14 @@ export function jsonEncode(disk: NibbleDisk, pretty: boolean): string { export function jsonDecode(data: string): NibbleDisk { const tracks: memory[] = []; - const json = JSON.parse(data); - const v = json.volume; - const readOnly = json.readOnly; + const json = JSON.parse(data) as JSONDisk; + const v = json.volume || 254; + const readOnly = json.readOnly || false; for (let t = 0; t < json.data.length; t++) { let track: byte[] = []; for (let s = 0; s < json.data[t].length; s++) { const _s = json.type === 'po' ? PO[s] : DO[s]; - const sector: string = json.data[t][_s]; + const sector: string = json.data[t][_s] as string; const d = base64_decode(sector); track = track.concat(explodeSector16(v, t, s, d)); } diff --git a/js/formats/woz.ts b/js/formats/woz.ts index 17057f4..cd90f2f 100644 --- a/js/formats/woz.ts +++ b/js/formats/woz.ts @@ -22,7 +22,7 @@ function stringFromBytes(data: DataView, start: number, end: number): string { return String.fromCharCode.apply( null, new Uint8Array(data.buffer.slice(data.byteOffset + start, data.byteOffset + end)) - ); + ) as string; } export class InfoChunk { diff --git a/js/gl.ts b/js/gl.ts index aea2638..ac86b12 100644 --- a/js/gl.ts +++ b/js/gl.ts @@ -352,6 +352,22 @@ export class LoresPageGL implements LoresPage { * ***************************************************************************/ +const _drawPixel = (data: Uint8ClampedArray, off: number, color: Color) => { + const c0 = color[0], c1 = color[1], c2 = color[2]; + + data[off + 0] = data[off + 4] = c0; + data[off + 1] = data[off + 5] = c1; + data[off + 2] = data[off + 6] = c2; +}; + +const _drawHalfPixel = (data: Uint8ClampedArray, off: number, color: Color) => { + const c0 = color[0], c1 = color[1], c2 = color[2]; + + data[off + 0] = c0; + data[off + 1] = c1; + data[off + 2] = c2; +}; + export class HiresPageGL implements HiresPage { public imageData: ImageData; dirty: Region = {...notDirty}; @@ -371,22 +387,6 @@ export class HiresPageGL implements HiresPage { this.vm.setHiresPage(page, this); } - private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) { - const c0 = color[0], c1 = color[1], c2 = color[2]; - - data[off + 0] = data[off + 4] = c0; - data[off + 1] = data[off + 5] = c1; - data[off + 2] = data[off + 6] = c2; - } - - private _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) { - const c0 = color[0], c1 = color[1], c2 = color[2]; - - data[off + 0] = c0; - data[off + 1] = c1; - data[off + 2] = c2; - } - bank0(): MemoryPages { return { start: () => this._start(), @@ -453,9 +453,9 @@ export class HiresPageGL implements HiresPage { let bits = val; for (let jdx = 0; jdx < 7; jdx++, offset += 4) { if (bits & 0x01) { - this._drawHalfPixel(data, offset, whiteCol); + _drawHalfPixel(data, offset, whiteCol); } else { - this._drawHalfPixel(data, offset, blackCol); + _drawHalfPixel(data, offset, blackCol); } bits >>= 1; } @@ -468,17 +468,17 @@ export class HiresPageGL implements HiresPage { if (hbs) { const val0 = this._buffer[bank][base - 1] || 0; if (val0 & 0x40) { - this._drawHalfPixel(data, offset, whiteCol); + _drawHalfPixel(data, offset, whiteCol); } else { - this._drawHalfPixel(data, offset, blackCol); + _drawHalfPixel(data, offset, blackCol); } offset += 4; } let bits = val; for (let idx = 0; idx < 7; idx++, offset += 8) { const drawPixel = cropLastPixel && idx === 6 - ? this._drawHalfPixel - : this._drawPixel; + ? _drawHalfPixel + : _drawPixel; if (bits & 0x01) { drawPixel(data, offset, whiteCol); } else { @@ -587,6 +587,9 @@ export class VideoModesGL implements VideoModes { } async init() { + // There is a typing bug in https://github.com/whscullin/apple2shader/blob/master/index.d.ts + // that declares initOpenGL as returning void when it actually returns Promise. + // eslint-disable-next-line @typescript-eslint/await-thenable await this._sv.initOpenGL(); this._displayConfig = this.defaultMonitor(); diff --git a/js/main2.ts b/js/main2.ts index 0adddb3..5aeddd6 100644 --- a/js/main2.ts +++ b/js/main2.ts @@ -51,6 +51,7 @@ switch (romVersion) { } const options = { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion canvas: document.querySelector('#screen')!, gl: prefs.readPref('gl_canvas', 'true') === 'true', rom, diff --git a/js/main2e.ts b/js/main2e.ts index 55864e6..5fd4b93 100644 --- a/js/main2e.ts +++ b/js/main2e.ts @@ -42,6 +42,7 @@ switch (romVersion) { const options = { gl: prefs.readPref('gl_canvas', 'true') === 'true', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion canvas: document.querySelector('#screen')!, rom, characterRom, diff --git a/js/mmu.ts b/js/mmu.ts index fb47ed8..75cc3ee 100644 --- a/js/mmu.ts +++ b/js/mmu.ts @@ -525,19 +525,19 @@ export default class MMU implements Memory, Restorable { switch(off) { case LOC.BSRBANK2: - this._debug('Bank 2 Read ' + !this._bank1); + this._debug(`Bank 2 Read ${!this._bank1 ? 'true' : 'false'}`); result = !this._bank1 ? 0x80 : 0x00; break; case LOC.BSRREADRAM: - this._debug('Bank SW RAM Read ' + this._readbsr); + this._debug(`Bank SW RAM Read ${this._readbsr ? 'true' : 'false'}`); result = this._readbsr ? 0x80 : 0x00; break; case LOC.RAMRD: // 0xC013 - this._debug('Aux RAM Read ' + this._auxRamRead); + this._debug(`Aux RAM Read ${this._auxRamRead ? 'true' : 'false'}`); result = this._auxRamRead ? 0x80 : 0x0; break; case LOC.RAMWRT: // 0xC014 - this._debug('Aux RAM Write ' + this._auxRamWrite); + this._debug(`Aux RAM Write ${this._auxRamWrite ? 'true' : 'false'}`); result = this._auxRamWrite ? 0x80 : 0x0; break; case LOC.INTCXROM: // 0xC015 @@ -545,15 +545,15 @@ export default class MMU implements Memory, Restorable { result = this._intcxrom ? 0x80 : 0x00; break; case LOC.ALTZP: // 0xC016 - this._debug('Alt ZP ' + this._altzp); + this._debug(`Alt ZP ${this._altzp ? 'true' : 'false'}`); result = this._altzp ? 0x80 : 0x0; break; case LOC.SLOTC3ROM: // 0xC017 - this._debug('Slot C3 ROM ' + this._slot3rom); + this._debug(`Slot C3 ROM ${this._slot3rom ? 'true' : 'false'}`); result = this._slot3rom ? 0x80 : 0x00; break; case LOC._80STORE: // 0xC018 - this._debug('80 Store ' + this._80store); + this._debug(`80 Store ${this._80store ? 'true' : 'false'}`); result = this._80store ? 0x80 : 0x00; break; case LOC.VERTBLANK: // 0xC019 diff --git a/js/roms/rom.ts b/js/roms/rom.ts index 36fc2a5..86cd024 100644 --- a/js/roms/rom.ts +++ b/js/roms/rom.ts @@ -5,10 +5,10 @@ export type ROMState = null; export default class ROM implements MemoryPages, Restorable { constructor( - private readonly startPage: byte, - private readonly endPage: byte, - private readonly rom: rom) { - const expectedLength = (endPage-startPage+1) * 256; + private readonly startPage: byte, + private readonly endPage: byte, + private readonly rom: rom) { + const expectedLength = (endPage - startPage + 1) * 256; if (rom.length !== expectedLength) { throw Error(`rom does not have the correct length: expected ${expectedLength} was ${rom.length}`); } @@ -24,10 +24,12 @@ export default class ROM implements MemoryPages, Restorable { return this.rom[(page - this.startPage) << 8 | off]; } write() { + // not writable } getState() { return null; } - setState(_state: null) { + setState(_state: ROMState) { + // not restorable } } diff --git a/js/types.ts b/js/types.ts index 6df4a96..03c27d7 100644 --- a/js/types.ts +++ b/js/types.ts @@ -86,7 +86,7 @@ export interface MemoryPages extends Memory { } /* An interface card */ -export interface Card extends Memory, Restorable { +export interface Card extends Memory, Restorable { /* Reset the card */ reset?(): void; diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 4812664..e47572a 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -23,7 +23,7 @@ import ApplesoftDump from '../applesoft/decompiler'; import ApplesoftCompiler from '../applesoft/compiler'; import { debug } from '../util'; -import { Apple2, Stats } from '../apple2'; +import { Apple2, Stats, State as Apple2State } from '../apple2'; import DiskII from '../cards/disk2'; import CPU6502 from '../cpu6502'; import { VideoModes } from '../videomodes'; @@ -123,7 +123,7 @@ export function openSave(driveString: string, event: MouseEvent) { const a = document.querySelector('#local_save_link')!; if (!data) { - alert('No data from drive ' + drive); + alert(`No data from drive ${drive}`); return; } @@ -211,7 +211,7 @@ function loadingProgress(current: number, total: number) { const meter = document.querySelector('#loading-modal .meter')!; const progress = document.querySelector('#loading-modal .progress')!; meter.style.display = 'block'; - progress.style.width = current / total * meter.clientWidth + 'px'; + progress.style.width = `${current / total * meter.clientWidth}px`; } } @@ -236,13 +236,13 @@ export function loadAjax(drive: DriveNumber, url: string) { } }).then(function (data: JSONDisk | JSONBinaryImage) { if (data.type === 'binary') { - loadBinary(data as JSONBinaryImage); + loadBinary(data ); } else if (includes(DISK_FORMATS, data.type)) { loadDisk(drive, data); } initGamepad(data.gamepad); loadingStop(); - }).catch(function (error) { + }).catch(function (error: Error) { loadingStop(); openAlert(error.message); console.error(error); @@ -414,7 +414,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { if (url) { fetch(url).then(function (response) { if (response.ok) { - const reader = response!.body!.getReader(); + const reader = response.body!.getReader(); let received = 0; const chunks: Uint8Array[] = []; const contentLength = parseInt(response.headers.get('content-length')!, 10); @@ -468,7 +468,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { throw new Error(`Extension ${ext} not recognized.`); } loadingStop(); - }).catch(function (error) { + }).catch((error: Error) => { loadingStop(); openAlert(error.message); console.error(error); @@ -499,19 +499,19 @@ export function updateKHz() { case 0: { delta = cycles - lastCycles; khz = Math.trunc(delta / ms); - kHzElement.innerText = khz + ' kHz'; + kHzElement.innerText = `${khz} kHz`; break; } case 1: { delta = stats.renderedFrames - lastRenderedFrames; fps = Math.trunc(delta / (ms / 1000)); - kHzElement.innerText = fps + ' rps'; + kHzElement.innerText = `${fps} rps`; break; } default: { delta = stats.frames - lastFrames; fps = Math.trunc(delta / (ms / 1000)); - kHzElement.innerText = fps + ' fps'; + kHzElement.innerText = `${fps} fps`; } } @@ -579,7 +579,7 @@ export function selectCategory() { const file = cat[idx]; let name = file.name; if (file.disk) { - name += ' - ' + file.disk; + name += ` - ${file.disk}`; } const option = document.createElement('option'); option.value = file.filename; @@ -622,7 +622,7 @@ function loadDisk(drive: DriveNumber, disk: JSONDisk) { */ function updateLocalStorage() { - const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}'); + const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; const names = Object.keys(diskIndex); const cat: DiskDescriptor[] = disk_categories['Local Saves'] = []; @@ -654,7 +654,7 @@ type LocalDiskIndex = { }; function saveLocalStorage(drive: DriveNumber, name: string) { - const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; const json = _disk2.getJSON(drive); diskIndex[name] = json; @@ -667,7 +667,7 @@ function saveLocalStorage(drive: DriveNumber, name: string) { } function deleteLocalStorage(name: string) { - const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; if (diskIndex[name]) { delete diskIndex[name]; openAlert('Deleted'); @@ -677,7 +677,7 @@ function deleteLocalStorage(name: string) { } function loadLocalStorage(drive: DriveNumber, name: string) { - const diskIndex = JSON.parse(window.localStorage.diskIndex || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; if (diskIndex[name]) { _disk2.setJSON(drive, diskIndex[name]); driveLights.label(drive, name); @@ -891,7 +891,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, print }); keyboard.setFunction('F9', () => { if (window.localStorage.state) { - _apple2.setState(base64_json_parse(window.localStorage.state)); + _apple2.setState(base64_json_parse(window.localStorage.state as string) as Apple2State); } }); diff --git a/js/ui/audio_worker.ts b/js/ui/audio_worker.ts index 4ed7389..2a7d110 100644 --- a/js/ui/audio_worker.ts +++ b/js/ui/audio_worker.ts @@ -12,13 +12,17 @@ declare global { function registerProcessor(name: string, ctor :{ new(): AudioWorkletProcessor }): void; } +export interface AppleAudioMessageEvent extends MessageEvent { + data: Float32Array; +} + export class AppleAudioProcessor extends AudioWorkletProcessor { private samples: Float32Array[] = []; constructor() { super(); console.info('AppleAudioProcessor constructor'); - this.port.onmessage = (ev: MessageEvent) => { + this.port.onmessage = (ev: AppleAudioMessageEvent) => { this.samples.push(ev.data); if (this.samples.length > 256) { this.samples.shift(); diff --git a/js/ui/drive_lights.ts b/js/ui/drive_lights.ts index f189dcb..a33f2cc 100644 --- a/js/ui/drive_lights.ts +++ b/js/ui/drive_lights.ts @@ -3,11 +3,12 @@ import type { DriveNumber } from '../formats/types'; export default class DriveLights implements Callbacks { public driveLight(drive: DriveNumber, on: boolean) { - const disk = - document.querySelector('#disk' + drive)! as HTMLElement; - disk.style.backgroundImage = - on ? 'url(css/red-on-16.png)' : - 'url(css/red-off-16.png)'; + const disk = document.querySelector(`#disk${drive}`); + if (disk) { + disk.style.backgroundImage = + on ? 'url(css/red-on-16.png)' : + 'url(css/red-off-16.png)'; + } } public dirty(_drive: DriveNumber, _dirty: boolean) { @@ -15,11 +16,11 @@ export default class DriveLights implements Callbacks { } public label(drive: DriveNumber, label?: string, side?: string) { - const labelElement = - document.querySelector('#disk-label' + drive)! as HTMLElement; - if (label) { - labelElement.innerText = label + (side ? ` - ${side}` : ''); + const labelElement = document.querySelector(`#disk-label${drive}`); + const labelText = `${label || ''} ${(side ? `- ${side}` : '')}`; + if (label && labelElement) { + labelElement.innerText = labelText; } - return labelElement.innerText; + return labelText; } } diff --git a/js/ui/gamepad.ts b/js/ui/gamepad.ts index ba4e426..3ef3d3b 100644 --- a/js/ui/gamepad.ts +++ b/js/ui/gamepad.ts @@ -58,7 +58,7 @@ export function processGamepad(io: Apple2IO) { if (val <= 0) { io.buttonDown(-val as 0 | 1 | 2); } else { - io.keyDown(gamepadMap[idx]!); + io.keyDown(val); } } else if (!pressed && old) { if (val <= 0) { diff --git a/js/ui/keyboard.ts b/js/ui/keyboard.ts index 016d865..efd1010 100644 --- a/js/ui/keyboard.ts +++ b/js/ui/keyboard.ts @@ -362,7 +362,7 @@ export default class KeyBoard { for (y = 0; y < 5; y++) { row = document.createElement('div'); row.classList.add('row'); - row.classList.add('row' + y); + row.classList.add(`row${y}`); this.kb.append(row); for (x = 0; x < this.keys[0][y].length; x++) { const key1 = this.keys[0][y][x]; diff --git a/js/ui/tape.ts b/js/ui/tape.ts index 8e63886..c3ff4aa 100644 --- a/js/ui/tape.ts +++ b/js/ui/tape.ts @@ -32,8 +32,8 @@ export default class Tape { let old = (datum > 0.0), current; let last = 0; let delta: number; - debug('Sample Count: ' + data.length); - debug('Sample rate: ' + buffer.sampleRate); + debug(`Sample Count: ${data.length}`); + debug(`Sample rate: ${buffer.sampleRate}`); for (let idx = 1; idx < data.length; idx++) { datum = data[idx]; if ((datum > 0.1) || (datum < -0.1)) { @@ -65,7 +65,7 @@ export default class Tape { if (done) { done(); } - }, function (error) { + }, (error: Error) => { window.alert(error.message); }); }; diff --git a/js/util.ts b/js/util.ts index 51372af..3302ca3 100644 --- a/js/util.ts +++ b/js/util.ts @@ -46,7 +46,7 @@ export function bytify(ary: number[]): memory { /** Writes to the console. */ export function debug(...args: unknown[]): void { - console.log.apply(console, args); + console.log(...args); } /** diff --git a/test/components/FileChooser.spec.tsx b/test/components/FileChooser.spec.tsx index 03b178f..7ffa95a 100644 --- a/test/components/FileChooser.spec.tsx +++ b/test/components/FileChooser.spec.tsx @@ -22,6 +22,8 @@ const FAKE_FILE_HANDLE = { isDirectory: false, } as const; +const NOP = () => { /* do nothing */ }; + // eslint-disable-next-line no-undef const EMPTY_FILE_LIST = backdoors.newFileList(); @@ -30,13 +32,13 @@ const FAKE_FILE = new File([], 'fake'); describe('FileChooser', () => { describe('input-based chooser', () => { it('should be instantiable', () => { - const { container } = render( { }} />); + const { container } = render(); expect(container).not.toBeNull(); }); it('should use the file input element', async () => { - render( { }} />); + render(); const inputElement = await screen.findByRole('button') as HTMLInputElement; expect(inputElement.type).toBe('file'); @@ -90,7 +92,7 @@ describe('FileChooser', () => { }); it('should be instantiable', () => { - const { container } = render( { }} />); + const { container } = render(); expect(container).not.toBeNull(); }); diff --git a/test/js/__mocks__/apple2shader.js b/test/js/__mocks__/apple2shader.js index 5cbe040..77cfb9a 100644 --- a/test/js/__mocks__/apple2shader.js +++ b/test/js/__mocks__/apple2shader.js @@ -11,7 +11,7 @@ export const screenEmu = (function () { DisplayConfiguration: class {}, Point: class {}, ScreenView: class { - initOpenGL() {} + initOpenGL() { return Promise.resolve(); } }, Size: class{}, }; diff --git a/test/js/formats/create_disk.spec.ts b/test/js/formats/create_disk.spec.ts index 191973a..32f64ac 100644 --- a/test/js/formats/create_disk.spec.ts +++ b/test/js/formats/create_disk.spec.ts @@ -11,7 +11,7 @@ describe('createDiskFromJsonDisk', () => { readOnly: undefined, side: 'Front', volume: 254, - tracks: expect.any(Array) + tracks: expect.any(Array) as number[][] }); }); }); diff --git a/test/js/formats/testdata/woz.ts b/test/js/formats/testdata/woz.ts index b822608..6ab595d 100644 --- a/test/js/formats/testdata/woz.ts +++ b/test/js/formats/testdata/woz.ts @@ -1,3 +1,4 @@ +import { byte } from 'js/types'; import { numberToBytes, stringToBytes, @@ -49,7 +50,7 @@ const mockInfo2 = [ * Track map all pointing to track 0 */ -export const mockTMAP = new Array(160); +export const mockTMAP = new Array(160); mockTMAP.fill(0); /** @@ -58,7 +59,7 @@ mockTMAP.fill(0); // 24 bits of track data, padded -const mockTrackData = new Array(6646); +const mockTrackData = new Array(6646); mockTrackData.fill(0); mockTrackData[0] = 0xd5; mockTrackData[1] = 0xaa; @@ -82,7 +83,7 @@ const mockTRKS = [ * Version 2 TRKS structure */ -const mockTrackMap = new Array(160 * 8); +const mockTrackMap = new Array(160 * 8); mockTrackMap.fill(0); mockTrackMap[0x00] = 0x03; mockTrackMap[0x01] = 0x00; @@ -93,7 +94,7 @@ mockTrackMap[0x07] = 0x00; mockTrackMap[0x08] = 0x00; mockTrackMap[0x09] = 0x00; -const mockTrackData2 = new Array(512); +const mockTrackData2 = new Array(512); mockTrackData2.fill(0); mockTrackData2[0] = 0xd5; mockTrackData2[1] = 0xaa; diff --git a/test/js/formats/util.ts b/test/js/formats/util.ts index 8b4d368..a96e6c0 100644 --- a/test/js/formats/util.ts +++ b/test/js/formats/util.ts @@ -24,7 +24,7 @@ export function compareSequences(track: memory, bytes: number[], pos: number): b export function expectSequence(track: memory, pos: number, bytes: number[]): number { if (!compareSequences(track, bytes, pos)) { const track_slice = track.slice(pos, Math.min(track.length, pos + bytes.length)); - throw new Error(`expected ${bytes} got ${track_slice}`); + throw new Error(`expected ${bytes.toString()} got ${track_slice.toString()}`); } return pos + bytes.length; } diff --git a/test/js/formats/woz.spec.ts b/test/js/formats/woz.spec.ts index 45df19d..c40fd09 100644 --- a/test/js/formats/woz.spec.ts +++ b/test/js/formats/woz.spec.ts @@ -1,4 +1,4 @@ -import { WozDisk, ENCODING_BITSTREAM } from 'js/formats/types'; +import { ENCODING_BITSTREAM } from 'js/formats/types'; import createDiskFromWoz from 'js/formats/woz'; import { mockWoz1, @@ -19,7 +19,7 @@ describe('woz', () => { rawData: mockWoz1 }; - const disk = createDiskFromWoz(options) as WozDisk; + const disk = createDiskFromWoz(options) ; expect(disk).toEqual({ name: 'Mock Woz 1', readOnly: true, @@ -58,7 +58,7 @@ describe('woz', () => { rawData: mockWoz2 }; - const disk = createDiskFromWoz(options) as WozDisk; + const disk = createDiskFromWoz(options) ; expect(disk).toEqual({ name: 'Mock Woz 2', side: 'B', diff --git a/test/util/bios.ts b/test/util/bios.ts index de3ad3b..786017a 100644 --- a/test/util/bios.ts +++ b/test/util/bios.ts @@ -23,6 +23,7 @@ export class Program implements MemoryPages { } write(_page: byte, _off: byte, _val: byte) { + // do nothing } } diff --git a/test/util/image.ts b/test/util/image.ts index 761e6c2..b727941 100644 --- a/test/util/image.ts +++ b/test/util/image.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ export const createImageFromImageData = (data: ImageData) => { const canvas = document.createElement('canvas'); canvas.width = data.width;