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.
This commit is contained in:
Ian Flanigan 2022-05-31 17:38:40 +02:00 committed by GitHub
parent d4db26574d
commit 04ae0327c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 322 additions and 215 deletions

View File

@ -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"
}
}
}
}

View File

@ -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<State>, 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;

View File

@ -52,7 +52,7 @@ const LOC = {
};
export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
private _slot: Array<Card | null> = new Array(7).fill(null);
private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
private _auxRom: Memory | null = null;
private _khz = 1023;
@ -81,7 +81,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
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<Apple2IOState>
if (this._audioListener) {
this._audioListener(this._sample);
}
this._sample = new Array(this._sample_size);
this._sample = new Array<number>(this._sample_size);
this._sampleIdx = 0;
}
}
@ -226,7 +226,7 @@ export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState>
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<Apple2IOState>
}
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<Apple2IOState>
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<number>(this._sample_size);
this._sampleIdx = 0;
this._calcSampleRate();
}

View File

@ -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));

View File

@ -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<State> {
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,

View File

@ -217,7 +217,9 @@ export default class Mouse implements Card, Restorable<MouseState> {
return rom[off];
}
write() {}
write() {
// not writable
}
/**
* Triggers interrupts based on activity since the last tick

View File

@ -110,6 +110,7 @@ export default class NoSlotClock {
}
setState(_: unknown) {
// Setting the state makes no sense.
}
}

View File

@ -37,11 +37,15 @@ export default class Parallel implements Card, Restorable<ParallelState> {
return rom[off];
}
write() {}
write() {
// not writable
}
getState() {
return {};
}
setState(_state: ParallelState) {}
setState(_state: ParallelState) {
// can't set the state
}
}

View File

@ -126,7 +126,9 @@ export default class RAMFactor implements Card, Restorable<RAMFactorState> {
return rom[this.firmware << 12 | (page - 0xC0) << 8 | off];
}
write() {}
write() {
// not writable
}
reset() {
this.firmware = 0;

View File

@ -197,9 +197,9 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
*/
readBlock(state: CpuState, drive: number, block: number, buffer: Address) {
this.debug('read drive=' + drive);
this.debug('read buffer=' + buffer);
this.debug('read block=$' + toHex(block));
this.debug(`read drive=${drive}`);
this.debug(`read buffer=${buffer.toString()}`);
this.debug(`read block=$${toHex(block)}`);
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
@ -224,9 +224,9 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
*/
writeBlock(state: CpuState, drive: number, block: number, buffer: Address) {
this.debug('write drive=' + drive);
this.debug('write buffer=' + buffer);
this.debug('write block=$' + toHex(block));
this.debug(`write drive=${drive}`);
this.debug(`write buffer=${buffer.toString()}`);
this.debug(`write block=$${toHex(block)}`);
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
@ -332,11 +332,11 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
buffer = bufferAddr.readAddress();
block = blockAddr.readWord();
this.debug('cmd=' + cmd);
this.debug(`cmd=${cmd}`);
this.debug('unit=$' + toHex(unit));
this.debug('slot=' + driveSlot + ' drive=' + drive);
this.debug('buffer=' + buffer + ' block=$' + toHex(block));
this.debug(`slot=${driveSlot} drive=${drive}`);
this.debug(`buffer=${buffer.toString()} block=$${toHex(block)}`);
switch (cmd) {
case 0: // INFO
@ -362,14 +362,14 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
const retVal = stackAddr.readAddress();
this.debug('return=' + retVal);
this.debug(`return=${retVal.toString()}`);
const cmdBlockAddr = retVal.inc(1);
cmd = cmdBlockAddr.readByte();
const cmdListAddr = cmdBlockAddr.inc(1).readAddress();
this.debug('cmd=' + cmd);
this.debug('cmdListAddr=' + cmdListAddr);
this.debug(`cmd=${cmd}`);
this.debug(`cmdListAddr=${cmdListAddr.toString()}`);
stackAddr.writeAddress(retVal.inc(3));
@ -378,13 +378,13 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
buffer = cmdListAddr.inc(2).readAddress();
let status;
this.debug('parameterCount=' + parameterCount);
this.debug(`parameterCount=${parameterCount}`);
switch (cmd) {
case 0x00: // INFO
status = cmdListAddr.inc(4).readByte();
this.debug('info unit=' + unit);
this.debug('info buffer=' + buffer);
this.debug('info status=' + status);
this.debug(`info unit=${unit}`);
this.debug(`info buffer=${buffer.toString()}`);
this.debug(`info status=${status}`);
switch (unit) {
case 0:
switch (status) {
@ -482,6 +482,7 @@ export default class SmartPort implements Card, MassStorage, Restorable<SmartPor
}
write() {
// not writable
}
getState() {

View File

@ -138,6 +138,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
}
write() {
// not writable
}
ioSwitch(off: byte, val?: byte) {
@ -148,5 +149,7 @@ export default class Thunderclock implements Card, Restorable<ThunderclockState>
return {};
}
setState() {}
setState(_state: ThunderclockState) {
// can't set the state
}
}

View File

@ -45,7 +45,7 @@ export const Apple2 = (props: Apple2Props) => {
if (screen.current) {
const options = {
canvas: screen.current,
tick: () => {},
tick: () => { /* do nothing */ },
...props,
};
const apple2 = new Apple2Impl(options);

View File

@ -46,7 +46,9 @@ export const Drives = ({ io, sectors }: DrivesProps) => {
side,
}));
},
dirty: () => {}
dirty: () => {
// do nothing
}
};
if (io) {

View File

@ -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<HTMLInputElement>(null);
@ -104,7 +105,7 @@ interface FilePickerChooserProps {
const FilePickerChooser = ({
disabled = false,
onChange = () => { },
onChange = () => { /* do nothing */ },
accept = [ACCEPT_EVERYTHING_TYPE]
}: FilePickerChooserProps) => {
const [busy, setBusy] = useState<boolean>(false);
@ -142,7 +143,7 @@ const FilePickerChooser = ({
return (
<>
<button onClick={onClickInternal} disabled={disabled || busy}>
<button onClick={noAwait(onClickInternal)} disabled={disabled || busy}>
Choose File
</button>
&nbsp;

View File

@ -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) =>
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={doOpen} disabled={busy || empty}>Open</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
</ModalFooter>
</Modal>
);

View File

@ -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('.'));

View File

@ -0,0 +1,12 @@
/**
* Converts a function type returning a `Promise` to a function type returning `void`.
*/
export type NoAwait<F extends (...args: unknown[]) => Promise<unknown>> =
(...args: Parameters<F>) => void;
/**
* Signals that the argument returns a `Promise` that is intentionally not being awaited.
*/
export function noAwait<F extends (...args: unknown[]) => Promise<unknown>>(f: F): NoAwait<F> {
return f as NoAwait<F>;
}

View File

@ -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<Memory>(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<Instruction>(0x100);
for (let idx = 0; idx < 0x100; idx++) {
this.opary[idx] = ops[idx] || this.unknown(idx);

View File

@ -128,7 +128,7 @@ export default class Debugger {
result += this.padWithSymbol(pc);
const cmd = new Array(size);
const cmd = new Array<number>(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;

View File

@ -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(<App />, document.getElementById('app')!);

View File

@ -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));
}

View File

@ -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 {

View File

@ -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<void>.
// eslint-disable-next-line @typescript-eslint/await-thenable
await this._sv.initOpenGL();
this._displayConfig = this.defaultMonitor();

View File

@ -51,6 +51,7 @@ switch (romVersion) {
}
const options = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
canvas: document.querySelector<HTMLCanvasElement>('#screen')!,
gl: prefs.readPref('gl_canvas', 'true') === 'true',
rom,

View File

@ -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<HTMLCanvasElement>('#screen')!,
rom,
characterRom,

View File

@ -525,19 +525,19 @@ export default class MMU implements Memory, Restorable<MMUState> {
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<MMUState> {
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

View File

@ -5,10 +5,10 @@ export type ROMState = null;
export default class ROM implements MemoryPages, Restorable<ROMState> {
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<ROMState> {
return this.rom[(page - this.startPage) << 8 | off];
}
write() {
// not writable
}
getState() {
return null;
}
setState(_state: null) {
setState(_state: ROMState) {
// not restorable
}
}

View File

@ -86,7 +86,7 @@ export interface MemoryPages extends Memory {
}
/* An interface card */
export interface Card extends Memory, Restorable {
export interface Card<StateT = unknown> extends Memory, Restorable<StateT> {
/* Reset the card */
reset?(): void;

View File

@ -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<HTMLAnchorElement>('#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<HTMLDivElement>('#loading-modal .meter')!;
const progress = document.querySelector<HTMLDivElement>('#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);
}
});

View File

@ -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();

View File

@ -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<HTMLElement>(`#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<HTMLElement>(`#disk-label${drive}`);
const labelText = `${label || ''} ${(side ? `- ${side}` : '')}`;
if (label && labelElement) {
labelElement.innerText = labelText;
}
return labelElement.innerText;
return labelText;
}
}

View File

@ -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) {

View File

@ -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];

View File

@ -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);
});
};

View File

@ -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);
}
/**

View File

@ -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(<FileChooser control='input' onChange={() => { }} />);
const { container } = render(<FileChooser control='input' onChange={NOP} />);
expect(container).not.toBeNull();
});
it('should use the file input element', async () => {
render(<FileChooser control='input' onChange={() => { }} />);
render(<FileChooser control='input' onChange={NOP} />);
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(<FileChooser control='picker' onChange={() => { }} />);
const { container } = render(<FileChooser control='picker' onChange={NOP} />);
expect(container).not.toBeNull();
});

View File

@ -11,7 +11,7 @@ export const screenEmu = (function () {
DisplayConfiguration: class {},
Point: class {},
ScreenView: class {
initOpenGL() {}
initOpenGL() { return Promise.resolve(); }
},
Size: class{},
};

View File

@ -11,7 +11,7 @@ describe('createDiskFromJsonDisk', () => {
readOnly: undefined,
side: 'Front',
volume: 254,
tracks: expect.any(Array)
tracks: expect.any(Array) as number[][]
});
});
});

View File

@ -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<byte>(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<byte>(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<byte>(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<byte>(512);
mockTrackData2.fill(0);
mockTrackData2[0] = 0xd5;
mockTrackData2[1] = 0xaa;

View File

@ -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;
}

View File

@ -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',

View File

@ -23,6 +23,7 @@ export class Program implements MemoryPages {
}
write(_page: byte, _off: byte, _val: byte) {
// do nothing
}
}

View File

@ -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;