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;