From 4da4827bcbcdd1ef163f635d9b29416ff272b0bb Mon Sep 17 00:00:00 2001
From: toasted-nutbread <toasted-nutbread@users.noreply.github.com>
Date: Mon, 27 Nov 2023 12:48:14 -0500
Subject: Add JSDoc type annotations to project (rebased)

---
 ext/js/general/cache-map.js                | 45 ++++++++++++++++++++++-------
 ext/js/general/object-property-accessor.js | 29 ++++++++++---------
 ext/js/general/regex-util.js               |  8 ++++--
 ext/js/general/task-accumulator.js         | 46 ++++++++++++++++++++++++++----
 ext/js/general/text-source-map.js          | 34 ++++++++++++++++++++++
 5 files changed, 128 insertions(+), 34 deletions(-)

(limited to 'ext/js/general')

diff --git a/ext/js/general/cache-map.js b/ext/js/general/cache-map.js
index 1ee385bf..cc706380 100644
--- a/ext/js/general/cache-map.js
+++ b/ext/js/general/cache-map.js
@@ -18,6 +18,7 @@
 
 
 /**
+ * @template K,V
  * Class which caches a map of values, keeping the most recently accessed values.
  */
 export class CacheMap {
@@ -35,9 +36,13 @@ export class CacheMap {
             throw new Error('Invalid maxCount');
         }
 
+        /** @type {number} */
         this._maxSize = maxSize;
+        /** @type {Map<K, import('cache-map').Node<K, V>>} */
         this._map = new Map();
+        /** @type {import('cache-map').Node<K, V>} */
         this._listFirst = this._createNode(null, null);
+        /** @type {import('cache-map').Node<K, V>} */
         this._listLast = this._createNode(null, null);
         this._resetEndNodes();
     }
@@ -60,7 +65,7 @@ export class CacheMap {
 
     /**
      * Returns whether or not an element exists at the given key.
-     * @param {*} key The key of the element.
+     * @param {K} key The key of the element.
      * @returns {boolean} `true` if an element with the specified key exists, `false` otherwise.
      */
     has(key) {
@@ -69,20 +74,20 @@ export class CacheMap {
 
     /**
      * Gets an element at the given key, if it exists. Otherwise, returns undefined.
-     * @param {*} key The key of the element.
-     * @returns {*} The existing value at the key, if any; `undefined` otherwise.
+     * @param {K} key The key of the element.
+     * @returns {V|undefined} The existing value at the key, if any; `undefined` otherwise.
      */
     get(key) {
         const node = this._map.get(key);
         if (typeof node === 'undefined') { return void 0; }
         this._updateRecency(node);
-        return node.value;
+        return /** @type {V} */ (node.value);
     }
 
     /**
      * Sets a value at a given key.
-     * @param {*} key The key of the element.
-     * @param {*} value The value to store in the cache.
+     * @param {K} key The key of the element.
+     * @param {V} value The value to store in the cache.
      */
     set(key, value) {
         let node = this._map.get(key);
@@ -98,9 +103,9 @@ export class CacheMap {
 
             // Remove
             for (let removeCount = this._map.size - this._maxSize; removeCount > 0; --removeCount) {
-                node = this._listLast.previous;
+                node = /** @type {import('cache-map').Node<K, V>} */ (this._listLast.previous);
                 this._removeNode(node);
-                this._map.delete(node.key);
+                this._map.delete(/** @type {K} */ (node.key));
             }
         }
     }
@@ -115,28 +120,46 @@ export class CacheMap {
 
     // Private
 
+    /**
+     * @param {import('cache-map').Node<K, V>} node
+     */
     _updateRecency(node) {
         this._removeNode(node);
         this._addNode(node, this._listFirst);
     }
 
+    /**
+     * @param {?K} key
+     * @param {?V} value
+     * @returns {import('cache-map').Node<K, V>}
+     */
     _createNode(key, value) {
         return {key, value, previous: null, next: null};
     }
 
+    /**
+     * @param {import('cache-map').Node<K, V>} node
+     * @param {import('cache-map').Node<K, V>} previous
+     */
     _addNode(node, previous) {
         const next = previous.next;
         node.next = next;
         node.previous = previous;
         previous.next = node;
-        next.previous = node;
+        /** @type {import('cache-map').Node<K, V>} */ (next).previous = node;
     }
 
+    /**
+     * @param {import('cache-map').Node<K, V>} node
+     */
     _removeNode(node) {
-        node.next.previous = node.previous;
-        node.previous.next = node.next;
+        /** @type {import('cache-map').Node<K, V>} */ (node.next).previous = node.previous;
+        /** @type {import('cache-map').Node<K, V>} */ (node.previous).next = node.next;
     }
 
+    /**
+     * @returns {void}
+     */
     _resetEndNodes() {
         this._listFirst.next = this._listLast;
         this._listLast.previous = this._listFirst;
diff --git a/ext/js/general/object-property-accessor.js b/ext/js/general/object-property-accessor.js
index b8309ed2..d818c9d1 100644
--- a/ext/js/general/object-property-accessor.js
+++ b/ext/js/general/object-property-accessor.js
@@ -22,9 +22,10 @@
 export class ObjectPropertyAccessor {
     /**
      * Create a new accessor for a specific object.
-     * @param {object} target The object which the getter and mutation methods are applied to.
+     * @param {unknown} target The object which the getter and mutation methods are applied to.
      */
     constructor(target) {
+        /** @type {unknown} */
         this._target = target;
     }
 
@@ -33,7 +34,7 @@ export class ObjectPropertyAccessor {
      * @param {(string|number)[]} pathArray The path to the property on the target object.
      * @param {number} [pathLength] How many parts of the pathArray to use.
      *   This parameter is optional and defaults to the length of pathArray.
-     * @returns {*} The value found at the path.
+     * @returns {unknown} The value found at the path.
      * @throws {Error} An error is thrown if pathArray is not valid for the target object.
      */
     get(pathArray, pathLength) {
@@ -44,7 +45,7 @@ export class ObjectPropertyAccessor {
             if (!ObjectPropertyAccessor.hasProperty(target, key)) {
                 throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray.slice(0, i + 1))}`);
             }
-            target = target[key];
+            target = /** @type {import('core').SerializableObject} */ (target)[key];
         }
         return target;
     }
@@ -52,7 +53,7 @@ export class ObjectPropertyAccessor {
     /**
      * Sets the value at the specified path.
      * @param {(string|number)[]} pathArray The path to the property on the target object.
-     * @param {*} value The value to assign to the property.
+     * @param {unknown} value The value to assign to the property.
      * @throws {Error} An error is thrown if pathArray is not valid for the target object.
      */
     set(pathArray, value) {
@@ -65,7 +66,7 @@ export class ObjectPropertyAccessor {
             throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
         }
 
-        target[key] = value;
+        /** @type {import('core').SerializableObject} */ (target)[key] = value;
     }
 
     /**
@@ -87,7 +88,7 @@ export class ObjectPropertyAccessor {
             throw new Error('Invalid type');
         }
 
-        delete target[key];
+        delete /** @type {import('core').SerializableObject} */ (target)[key];
     }
 
     /**
@@ -110,16 +111,16 @@ export class ObjectPropertyAccessor {
         const key2 = pathArray2[ii2];
         if (!ObjectPropertyAccessor.isValidPropertyType(target2, key2)) { throw new Error(`Invalid path 2: ${ObjectPropertyAccessor.getPathString(pathArray2)}`); }
 
-        const value1 = target1[key1];
-        const value2 = target2[key2];
+        const value1 = /** @type {import('core').SerializableObject} */ (target1)[key1];
+        const value2 = /** @type {import('core').SerializableObject} */ (target2)[key2];
 
-        target1[key1] = value2;
+        /** @type {import('core').SerializableObject} */ (target1)[key1] = value2;
         try {
-            target2[key2] = value1;
+            /** @type {import('core').SerializableObject} */ (target2)[key2] = value1;
         } catch (e) {
             // Revert
             try {
-                target1[key1] = value1;
+                /** @type {import('core').SerializableObject} */ (target1)[key1] = value1;
             } catch (e2) {
                 // NOP
             }
@@ -178,7 +179,7 @@ export class ObjectPropertyAccessor {
         let value = '';
         let escaped = false;
         for (const c of pathString) {
-            const v = c.codePointAt(0);
+            const v = /** @type {number} */ (c.codePointAt(0));
             switch (state) {
                 case 'empty': // Empty
                 case 'id-start': // Expecting identifier start
@@ -288,7 +289,7 @@ export class ObjectPropertyAccessor {
 
     /**
      * Checks whether an object or array has the specified property.
-     * @param {*} object The object to test.
+     * @param {unknown} object The object to test.
      * @param {string|number} property The property to check for existence.
      *   This value should be a string if the object is a non-array object.
      *   For arrays, it should be an integer.
@@ -317,7 +318,7 @@ export class ObjectPropertyAccessor {
 
     /**
      * Checks whether a property is valid for the given object
-     * @param {object} object The object to test.
+     * @param {unknown} object The object to test.
      * @param {string|number} property The property to check for existence.
      * @returns {boolean} `true` if the property is correct for the given object type, otherwise `false`.
      *   For arrays, this means that the property should be a positive integer.
diff --git a/ext/js/general/regex-util.js b/ext/js/general/regex-util.js
index 298189d4..726ce9f2 100644
--- a/ext/js/general/regex-util.js
+++ b/ext/js/general/regex-util.js
@@ -61,7 +61,7 @@ export class RegexUtil {
      * Applies the replacement string for a given regular expression match.
      * @param {string} replacement The replacement string that follows the format of the standard
      *   JavaScript regular expression replacement string.
-     * @param {object} match A match object returned from RegExp.match.
+     * @param {RegExpMatchArray} match A match object returned from RegExp.match.
      * @returns {string} A new string with the pattern replacement applied.
      */
     static applyMatchReplacement(replacement, match) {
@@ -79,11 +79,13 @@ export class RegexUtil {
                     return groups[g2];
                 }
             } else {
+                let {index} = match;
+                if (typeof index !== 'number') { index = 0; }
                 switch (g0) {
                     case '$': return '$';
                     case '&': return match[0];
-                    case '`': return replacement.substring(0, match.index);
-                    case '\'': return replacement.substring(match.index + g0.length);
+                    case '`': return replacement.substring(0, index);
+                    case '\'': return replacement.substring(index + g0.length);
                 }
             }
             return g0;
diff --git a/ext/js/general/task-accumulator.js b/ext/js/general/task-accumulator.js
index cae58b94..cb136908 100644
--- a/ext/js/general/task-accumulator.js
+++ b/ext/js/general/task-accumulator.js
@@ -18,25 +18,46 @@
 
 import {log} from '../core.js';
 
+/**
+ * @template K,V
+ */
 export class TaskAccumulator {
+    /**
+     * @param {(tasks: [key: ?K, task: import('task-accumulator').Task<V>][]) => Promise<void>} runTasks
+     */
     constructor(runTasks) {
+        /** @type {?Promise<void>} */
         this._deferPromise = null;
+        /** @type {?Promise<void>} */
         this._activePromise = null;
+        /** @type {import('task-accumulator').Task<V>[]} */
         this._tasks = [];
+        /** @type {import('task-accumulator').Task<V>[]} */
         this._tasksActive = [];
+        /** @type {Map<K, import('task-accumulator').Task<V>>} */
         this._uniqueTasks = new Map();
+        /** @type {Map<K, import('task-accumulator').Task<V>>} */
         this._uniqueTasksActive = new Map();
+        /** @type {() => Promise<void>} */
         this._runTasksBind = this._runTasks.bind(this);
+        /** @type {() => void} */
         this._tasksCompleteBind = this._tasksComplete.bind(this);
-        this._runTasks = runTasks;
+        /** @type {(tasks: [key: ?K, task: import('task-accumulator').Task<V>][]) => Promise<void>} */
+        this._runTasksCallback = runTasks;
     }
 
+    /**
+     * @param {?K} key
+     * @param {V} data
+     * @returns {Promise<void>}
+     */
     enqueue(key, data) {
         if (this._deferPromise === null) {
             const promise = this._activePromise !== null ? this._activePromise : Promise.resolve();
             this._deferPromise = promise.then(this._runTasksBind);
         }
 
+        /** @type {import('task-accumulator').Task<V>} */
         const task = {data, stale: false};
         if (key !== null) {
             const activeTaskInfo = this._uniqueTasksActive.get(key);
@@ -52,6 +73,9 @@ export class TaskAccumulator {
         return this._deferPromise;
     }
 
+    /**
+     * @returns {Promise<void>}
+     */
     _runTasks() {
         this._deferPromise = null;
 
@@ -64,18 +88,28 @@ export class TaskAccumulator {
         return this._activePromise;
     }
 
+    /**
+     * @returns {Promise<void>}
+     */
     async _runTasksAsync() {
         try {
-            const allTasks = [
-                ...this._tasksActive.map((taskInfo) => [null, taskInfo]),
-                ...this._uniqueTasksActive.entries()
-            ];
-            await this._runTasks(allTasks);
+            /** @type {[key: ?K, task: import('task-accumulator').Task<V>][]} */
+            const allTasks = [];
+            for (const taskInfo of this._tasksActive) {
+                allTasks.push([null, taskInfo]);
+            }
+            for (const [key, taskInfo] of this._uniqueTasksActive) {
+                allTasks.push([key, taskInfo]);
+            }
+            await this._runTasksCallback(allTasks);
         } catch (e) {
             log.error(e);
         }
     }
 
+    /**
+     * @returns {void}
+     */
     _tasksComplete() {
         this._tasksActive.length = 0;
         this._uniqueTasksActive.clear();
diff --git a/ext/js/general/text-source-map.js b/ext/js/general/text-source-map.js
index 6a136451..b03f6eb2 100644
--- a/ext/js/general/text-source-map.js
+++ b/ext/js/general/text-source-map.js
@@ -17,15 +17,26 @@
  */
 
 export class TextSourceMap {
+    /**
+     * @param {string} source
+     * @param {number[]|null} [mapping=null]
+     */
     constructor(source, mapping=null) {
+        /** @type {string} */
         this._source = source;
+        /** @type {?number[]} */
         this._mapping = (mapping !== null ? TextSourceMap.normalizeMapping(mapping) : null);
     }
 
+    /** @type {string} */
     get source() {
         return this._source;
     }
 
+    /**
+     * @param {unknown} other
+     * @returns {boolean}
+     */
     equals(other) {
         if (this === other) {
             return true;
@@ -61,6 +72,10 @@ export class TextSourceMap {
         return true;
     }
 
+    /**
+     * @param {number} finalLength
+     * @returns {number}
+     */
     getSourceLength(finalLength) {
         const mapping = this._mapping;
         if (mapping === null) {
@@ -74,6 +89,10 @@ export class TextSourceMap {
         return sourceLength;
     }
 
+    /**
+     * @param {number} index
+     * @param {number} count
+     */
     combine(index, count) {
         if (count <= 0) { return; }
 
@@ -89,6 +108,10 @@ export class TextSourceMap {
         this._mapping[index] = sum;
     }
 
+    /**
+     * @param {number} index
+     * @param {number[]} items
+     */
     insert(index, ...items) {
         if (this._mapping === null) {
             this._mapping = TextSourceMap.createMapping(this._source);
@@ -97,14 +120,25 @@ export class TextSourceMap {
         this._mapping.splice(index, 0, ...items);
     }
 
+    /**
+     * @returns {?number[]}
+     */
     getMappingCopy() {
         return this._mapping !== null ? [...this._mapping] : null;
     }
 
+    /**
+     * @param {string} text
+     * @returns {number[]}
+     */
     static createMapping(text) {
         return new Array(text.length).fill(1);
     }
 
+    /**
+     * @param {number[]} mapping
+     * @returns {number[]}
+     */
     static normalizeMapping(mapping) {
         const result = [];
         for (const value of mapping) {
-- 
cgit v1.2.3


From 7aed9a371b0d74c0d75179a08068e8935b76d780 Mon Sep 17 00:00:00 2001
From: toasted-nutbread <toasted-nutbread@users.noreply.github.com>
Date: Mon, 27 Nov 2023 14:55:27 -0500
Subject: Update types

---
 ext/js/background/backend.js                       | 22 ++++-----
 ext/js/background/offscreen-proxy.js               | 27 +++++++++--
 ext/js/background/offscreen.js                     |  9 +++-
 ext/js/background/request-builder.js               | 13 ++----
 ext/js/comm/api.js                                 |  6 +--
 ext/js/comm/clipboard-reader.js                    |  4 +-
 ext/js/display/search-action-popup-controller.js   |  4 +-
 ext/js/dom/sandbox/css-style-applier.js            |  2 +-
 ext/js/dom/text-source-element.js                  |  2 +-
 ext/js/dom/text-source-range.js                    |  2 +-
 ext/js/general/regex-util.js                       |  2 +-
 .../__mocks__/dictionary-importer-media-loader.js  |  1 +
 ext/js/language/dictionary-importer.js             |  2 +-
 ext/js/language/dictionary-worker.js               |  2 +
 ext/js/language/sandbox/japanese-util.js           |  8 ++--
 ext/js/language/text-scanner.js                    |  1 +
 ext/js/language/translator.js                      |  4 +-
 ext/js/media/audio-downloader.js                   |  6 +--
 ext/js/pages/settings/backup-controller.js         | 54 +++++++++-------------
 .../settings/recommended-permissions-controller.js | 36 +++++++++++++--
 types/ext/api.d.ts                                 | 12 +++++
 types/ext/request-builder.d.ts                     |  2 +
 22 files changed, 139 insertions(+), 82 deletions(-)

(limited to 'ext/js/general')

diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index 14877cf1..be68ecf4 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -96,7 +96,7 @@ export class Backend {
         });
         /** @type {?import('settings').Options} */
         this._options = null;
-        /** @type {JsonSchema[]} */
+        /** @type {import('../data/json-schema.js').JsonSchema[]} */
         this._profileConditionsSchemaCache = [];
         /** @type {ProfileConditionsUtil} */
         this._profileConditionsUtil = new ProfileConditionsUtil();
@@ -665,7 +665,7 @@ export class Backend {
     async _onApiInjectStylesheet({type, value}, sender) {
         const {frameId, tab} = sender;
         if (typeof tab !== 'object' || tab === null || typeof tab.id !== 'number') { throw new Error('Invalid tab'); }
-        return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false, true, 'document_start');
+        return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false);
     }
 
     /** @type {import('api').Handler<import('api').GetStylesheetContentDetails, import('api').GetStylesheetContentResult>} */
@@ -895,13 +895,7 @@ export class Backend {
         }
     }
 
-    /**
-     *
-     * @param root0
-     * @param root0.targetTabId
-     * @param root0.targetFrameId
-     * @param sender
-     */
+    /** @type {import('api').Handler<import('api').OpenCrossFramePortDetails, import('api').OpenCrossFramePortResult, true>} */
     _onApiOpenCrossFramePort({targetTabId, targetFrameId}, sender) {
         const sourceTabId = (sender && sender.tab ? sender.tab.id : null);
         if (typeof sourceTabId !== 'number') {
@@ -922,7 +916,9 @@ export class Backend {
             otherTabId: sourceTabId,
             otherFrameId: sourceFrameId
         };
+        /** @type {?chrome.runtime.Port} */
         let sourcePort = chrome.tabs.connect(sourceTabId, {frameId: sourceFrameId, name: JSON.stringify(sourceDetails)});
+        /** @type {?chrome.runtime.Port} */
         let targetPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(targetDetails)});
 
         const cleanup = () => {
@@ -937,8 +933,12 @@ export class Backend {
             }
         };
 
-        sourcePort.onMessage.addListener((message) => { targetPort.postMessage(message); });
-        targetPort.onMessage.addListener((message) => { sourcePort.postMessage(message); });
+        sourcePort.onMessage.addListener((message) => {
+            if (targetPort !== null) { targetPort.postMessage(message); }
+        });
+        targetPort.onMessage.addListener((message) => {
+            if (sourcePort !== null) { sourcePort.postMessage(message); }
+        });
         sourcePort.onDisconnect.addListener(cleanup);
         targetPort.onDisconnect.addListener(cleanup);
 
diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js
index c01f523d..0fb2f269 100644
--- a/ext/js/background/offscreen-proxy.js
+++ b/ext/js/background/offscreen-proxy.js
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-import {deserializeError, isObject} from '../core.js';
+import {isObject} from '../core.js';
 import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';
 
 export class OffscreenProxy {
@@ -158,15 +158,36 @@ export class TranslatorProxy {
 }
 
 export class ClipboardReaderProxy {
+    /**
+     * @param {OffscreenProxy} offscreen
+     */
     constructor(offscreen) {
+        /** @type {?import('environment').Browser} */
+        this._browser = null;
+        /** @type {OffscreenProxy} */
         this._offscreen = offscreen;
     }
 
+    /** @type {?import('environment').Browser} */
+    get browser() { return this._browser; }
+    set browser(value) {
+        if (this._browser === value) { return; }
+        this._browser = value;
+        this._offscreen.sendMessagePromise({action: 'clipboardSetBrowserOffsecreen', params: {value}});
+    }
+
+    /**
+     * @param {boolean} useRichText
+     * @returns {Promise<string>}
+     */
     async getText(useRichText) {
-        return this._offscreen.sendMessagePromise({action: 'clipboardGetTextOffscreen', params: {useRichText}});
+        return await this._offscreen.sendMessagePromise({action: 'clipboardGetTextOffscreen', params: {useRichText}});
     }
 
+    /**
+     * @returns {Promise<?string>}
+     */
     async getImage() {
-        return this._offscreen.sendMessagePromise({action: 'clipboardGetImageOffscreen'});
+        return await this._offscreen.sendMessagePromise({action: 'clipboardGetImageOffscreen'});
     }
 }
diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js
index 27cee8c4..6302aa84 100644
--- a/ext/js/background/offscreen.js
+++ b/ext/js/background/offscreen.js
@@ -50,6 +50,7 @@ export class Offscreen {
         this._messageHandlers = new Map([
             ['clipboardGetTextOffscreen',    {async: true,  contentScript: true,  handler: this._getTextHandler.bind(this)}],
             ['clipboardGetImageOffscreen',   {async: true,  contentScript: true,  handler: this._getImageHandler.bind(this)}],
+            ['clipboardSetBrowserOffsecreen', {async: false, contentScript: true, handler: this._setClipboardBrowser.bind(this)}],
             ['databasePrepareOffscreen',     {async: true,  contentScript: true,  handler: this._prepareDatabaseHandler.bind(this)}],
             ['getDictionaryInfoOffscreen',   {async: true,  contentScript: true,  handler: this._getDictionaryInfoHandler.bind(this)}],
             ['databasePurgeOffscreen',       {async: true,  contentScript: true,  handler: this._purgeDatabaseHandler.bind(this)}],
@@ -59,7 +60,6 @@ export class Offscreen {
             ['findTermsOffscreen',           {async: true,  contentScript: true,  handler: this._findTermsHandler.bind(this)}],
             ['getTermFrequenciesOffscreen',  {async: true,  contentScript: true,  handler: this._getTermFrequenciesHandler.bind(this)}],
             ['clearDatabaseCachesOffscreen', {async: false,  contentScript: true,  handler: this._clearDatabaseCachesHandler.bind(this)}]
-
         ]);
 
         const onMessage = this._onMessage.bind(this);
@@ -76,6 +76,13 @@ export class Offscreen {
         return this._clipboardReader.getImage();
     }
 
+    /**
+     * @param {{value: import('environment').Browser}} details
+     */
+    _setClipboardBrowser({value}) {
+        this._clipboardReader.browser = value;
+    }
+
     _prepareDatabaseHandler() {
         if (this._prepareDatabasePromise !== null) {
             return this._prepareDatabasePromise;
diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js
index 48fe2dd9..5ae7fbf5 100644
--- a/ext/js/background/request-builder.js
+++ b/ext/js/background/request-builder.js
@@ -21,12 +21,6 @@
  * with additional controls over anonymity and error handling.
  */
 export class RequestBuilder {
-    /**
-     * A progress callback for a fetch read.
-     * @callback ProgressCallback
-     * @param {boolean} complete Whether or not the data has been completely read.
-     */
-
     /**
      * Creates a new instance.
      */
@@ -109,14 +103,17 @@ export class RequestBuilder {
     /**
      * Reads the array buffer body of a fetch response, with an optional `onProgress` callback.
      * @param {Response} response The response of a `fetch` call.
-     * @param {ProgressCallback} onProgress The progress callback
+     * @param {?import('request-builder.js').ProgressCallback} onProgress The progress callback
      * @returns {Promise<Uint8Array>} The resulting binary data.
      */
     static async readFetchResponseArrayBuffer(response, onProgress) {
         let reader;
         try {
             if (typeof onProgress === 'function') {
-                reader = response.body.getReader();
+                const {body} = response;
+                if (body !== null) {
+                    reader = body.getReader();
+                }
             }
         } catch (e) {
             // Not supported
diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js
index 62dc98b1..0cfdba59 100644
--- a/ext/js/comm/api.js
+++ b/ext/js/comm/api.js
@@ -415,9 +415,9 @@ export class API {
     }
 
     /**
-     *
-     * @param targetTabId
-     * @param targetFrameId
+     * @param {import('api').OpenCrossFramePortDetails['targetTabId']} targetTabId
+     * @param {import('api').OpenCrossFramePortDetails['targetFrameId']} targetFrameId
+     * @returns {Promise<import('api').OpenCrossFramePortResult>}
      */
     openCrossFramePort(targetTabId, targetFrameId) {
         return this._invoke('openCrossFramePort', {targetTabId, targetFrameId});
diff --git a/ext/js/comm/clipboard-reader.js b/ext/js/comm/clipboard-reader.js
index c7b45a7c..364e31a3 100644
--- a/ext/js/comm/clipboard-reader.js
+++ b/ext/js/comm/clipboard-reader.js
@@ -29,7 +29,7 @@ export class ClipboardReader {
     constructor({document=null, pasteTargetSelector=null, richContentPasteTargetSelector=null}) {
         /** @type {?Document} */
         this._document = document;
-        /** @type {?string} */
+        /** @type {?import('environment').Browser} */
         this._browser = null;
         /** @type {?HTMLTextAreaElement} */
         this._pasteTarget = null;
@@ -43,7 +43,7 @@ export class ClipboardReader {
 
     /**
      * Gets the browser being used.
-     * @type {?string}
+     * @type {?import('environment').Browser}
      */
     get browser() {
         return this._browser;
diff --git a/ext/js/display/search-action-popup-controller.js b/ext/js/display/search-action-popup-controller.js
index 733fd70a..3a2057a1 100644
--- a/ext/js/display/search-action-popup-controller.js
+++ b/ext/js/display/search-action-popup-controller.js
@@ -18,10 +18,10 @@
 
 export class SearchActionPopupController {
     /**
-     * @param {SearchPersistentStateController} searchPersistentStateController
+     * @param {import('./search-persistent-state-controller.js').SearchPersistentStateController} searchPersistentStateController
      */
     constructor(searchPersistentStateController) {
-        /** @type {SearchPersistentStateController} */
+        /** @type {import('./search-persistent-state-controller.js').SearchPersistentStateController} */
         this._searchPersistentStateController = searchPersistentStateController;
     }
 
diff --git a/ext/js/dom/sandbox/css-style-applier.js b/ext/js/dom/sandbox/css-style-applier.js
index 332ca4f2..ea36a02d 100644
--- a/ext/js/dom/sandbox/css-style-applier.js
+++ b/ext/js/dom/sandbox/css-style-applier.js
@@ -24,7 +24,7 @@ export class CssStyleApplier {
     /**
      * Creates a new instance of the class.
      * @param {string} styleDataUrl The local URL to the JSON file continaing the style rules.
-     *   The style rules should follow the format of {@link CssStyleApplierRawStyleData}.
+     *   The style rules should follow the format of `CssStyleApplierRawStyleData`.
      */
     constructor(styleDataUrl) {
         /** @type {string} */
diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js
index 47c18e30..40ff5cc9 100644
--- a/ext/js/dom/text-source-element.js
+++ b/ext/js/dom/text-source-element.js
@@ -173,7 +173,7 @@ export class TextSourceElement {
 
     /**
      * Checks whether another text source has the same starting point.
-     * @param {TextSourceElement|TextSourceRange} other The other source to test.
+     * @param {import('text-source').TextSource} other The other source to test.
      * @returns {boolean} `true` if the starting points are equivalent, `false` otherwise.
      */
     hasSameStart(other) {
diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js
index 5dbbd636..fd09fdda 100644
--- a/ext/js/dom/text-source-range.js
+++ b/ext/js/dom/text-source-range.js
@@ -206,7 +206,7 @@ export class TextSourceRange {
 
     /**
      * Checks whether another text source has the same starting point.
-     * @param {TextSourceElement|TextSourceRange} other The other source to test.
+     * @param {import('text-source').TextSource} other The other source to test.
      * @returns {boolean} `true` if the starting points are equivalent, `false` otherwise.
      * @throws {Error} An exception can be thrown if `Range.compareBoundaryPoints` fails,
      *   which shouldn't happen, but the handler is kept in case of unexpected errors.
diff --git a/ext/js/general/regex-util.js b/ext/js/general/regex-util.js
index 726ce9f2..62248968 100644
--- a/ext/js/general/regex-util.js
+++ b/ext/js/general/regex-util.js
@@ -25,7 +25,7 @@ export class RegexUtil {
      * Applies string.replace using a regular expression and replacement string as arguments.
      * A source map of the changes is also maintained.
      * @param {string} text A string of the text to replace.
-     * @param {TextSourceMap} sourceMap An instance of `TextSourceMap` which corresponds to `text`.
+     * @param {import('./text-source-map.js').TextSourceMap} sourceMap An instance of `TextSourceMap` which corresponds to `text`.
      * @param {RegExp} pattern A regular expression to use as the replacement.
      * @param {string} replacement A replacement string that follows the format of the standard
      *   JavaScript regular expression replacement string.
diff --git a/ext/js/language/__mocks__/dictionary-importer-media-loader.js b/ext/js/language/__mocks__/dictionary-importer-media-loader.js
index 96f0f6dd..ffda29b3 100644
--- a/ext/js/language/__mocks__/dictionary-importer-media-loader.js
+++ b/ext/js/language/__mocks__/dictionary-importer-media-loader.js
@@ -17,6 +17,7 @@
  */
 
 export class DictionaryImporterMediaLoader {
+    /** @type {import('dictionary-importer-media-loader').GetImageDetailsFunction} */
     async getImageDetails(content) {
         // Placeholder values
         return {content, width: 100, height: 100};
diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js
index aa6d7ae6..2a2f4063 100644
--- a/ext/js/language/dictionary-importer.js
+++ b/ext/js/language/dictionary-importer.js
@@ -36,7 +36,7 @@ export class DictionaryImporter {
     }
 
     /**
-     * @param {DictionaryDatabase} dictionaryDatabase
+     * @param {import('./dictionary-database.js').DictionaryDatabase} dictionaryDatabase
      * @param {ArrayBuffer} archiveContent
      * @param {import('dictionary-importer').ImportDetails} details
      * @returns {Promise<import('dictionary-importer').ImportResult>}
diff --git a/ext/js/language/dictionary-worker.js b/ext/js/language/dictionary-worker.js
index 3e78a6ff..3119dd7b 100644
--- a/ext/js/language/dictionary-worker.js
+++ b/ext/js/language/dictionary-worker.js
@@ -157,6 +157,8 @@ export class DictionaryWorker {
                 resolve(result2);
             } else {
                 // If formatResult is not provided, the response is assumed to be the same type
+                // For some reason, eslint thinks the TResponse type is undefined
+                // eslint-disable-next-line jsdoc/no-undefined-types
                 resolve(/** @type {TResponse} */ (/** @type {unknown} */ (result)));
             }
         }
diff --git a/ext/js/language/sandbox/japanese-util.js b/ext/js/language/sandbox/japanese-util.js
index f7f20b3b..4c9c46bd 100644
--- a/ext/js/language/sandbox/japanese-util.js
+++ b/ext/js/language/sandbox/japanese-util.js
@@ -466,7 +466,7 @@ export class JapaneseUtil {
 
     /**
      * @param {string} text
-     * @param {?TextSourceMap} [sourceMap]
+     * @param {?import('../../general/text-source-map.js').TextSourceMap} [sourceMap]
      * @returns {string}
      */
     convertHalfWidthKanaToFullWidth(text, sourceMap=null) {
@@ -513,7 +513,7 @@ export class JapaneseUtil {
 
     /**
      * @param {string} text
-     * @param {?TextSourceMap} sourceMap
+     * @param {?import('../../general/text-source-map.js').TextSourceMap} sourceMap
      * @returns {string}
      */
     convertAlphabeticToKana(text, sourceMap=null) {
@@ -676,7 +676,7 @@ export class JapaneseUtil {
     /**
      * @param {string} text
      * @param {boolean} fullCollapse
-     * @param {?TextSourceMap} [sourceMap]
+     * @param {?import('../../general/text-source-map.js').TextSourceMap} [sourceMap]
      * @returns {string}
      */
     collapseEmphaticSequences(text, fullCollapse, sourceMap=null) {
@@ -816,7 +816,7 @@ export class JapaneseUtil {
 
     /**
      * @param {string} text
-     * @param {?TextSourceMap} sourceMap
+     * @param {?import('../../general/text-source-map.js').TextSourceMap} sourceMap
      * @param {number} sourceMapStart
      * @returns {string}
      */
diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js
index b4d9a642..f6bcde8d 100644
--- a/ext/js/language/text-scanner.js
+++ b/ext/js/language/text-scanner.js
@@ -18,6 +18,7 @@
 
 import {EventDispatcher, EventListenerCollection, clone, log} from '../core.js';
 import {DocumentUtil} from '../dom/document-util.js';
+import {TextSourceElement} from '../dom/text-source-element.js';
 import {yomitan} from '../yomitan.js';
 
 /**
diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js
index 67cc53c6..c21b16b1 100644
--- a/ext/js/language/translator.js
+++ b/ext/js/language/translator.js
@@ -29,9 +29,9 @@ export class Translator {
      * @param {import('translator').ConstructorDetails} details The details for the class.
      */
     constructor({japaneseUtil, database}) {
-        /** @type {JapaneseUtil} */
+        /** @type {import('./sandbox/japanese-util.js').JapaneseUtil} */
         this._japaneseUtil = japaneseUtil;
-        /** @type {DictionaryDatabase} */
+        /** @type {import('./dictionary-database.js').DictionaryDatabase} */
         this._database = database;
         /** @type {?Deinflector} */
         this._deinflector = null;
diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js
index 7b236790..0847d479 100644
--- a/ext/js/media/audio-downloader.js
+++ b/ext/js/media/audio-downloader.js
@@ -25,10 +25,10 @@ import {SimpleDOMParser} from '../dom/simple-dom-parser.js';
 
 export class AudioDownloader {
     /**
-     * @param {{japaneseUtil: JapaneseUtil, requestBuilder: RequestBuilder}} details
+     * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil, requestBuilder: RequestBuilder}} details
      */
     constructor({japaneseUtil, requestBuilder}) {
-        /** @type {JapaneseUtil} */
+        /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */
         this._japaneseUtil = japaneseUtil;
         /** @type {RequestBuilder} */
         this._requestBuilder = requestBuilder;
@@ -314,7 +314,7 @@ export class AudioDownloader {
      */
     async _downloadAudioFromUrl(url, sourceType, idleTimeout) {
         let signal;
-        /** @type {?(done: boolean) => void} */
+        /** @type {?import('request-builder.js').ProgressCallback} */
         let onProgress = null;
         /** @type {?number} */
         let idleTimer = null;
diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js
index 50a50b1a..52c5f418 100644
--- a/ext/js/pages/settings/backup-controller.js
+++ b/ext/js/pages/settings/backup-controller.js
@@ -534,12 +534,11 @@ export class BackupController {
     // Exporting Dictionaries Database
 
     /**
-     *
-     * @param message
-     * @param isWarning
+     * @param {string} message
+     * @param {boolean} [isWarning]
      */
     _databaseExportImportErrorMessage(message, isWarning=false) {
-        const errorMessageContainer = document.querySelector('#db-ops-error-report');
+        const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report'));
         errorMessageContainer.style.display = 'block';
         errorMessageContainer.textContent = message;
 
@@ -553,15 +552,11 @@ export class BackupController {
     }
 
     /**
-     *
-     * @param root0
-     * @param root0.totalRows
-     * @param root0.completedRows
-     * @param root0.done
+     * @param {{totalRows: number, completedRows: number, done: boolean}} details
      */
     _databaseExportProgressCallback({totalRows, completedRows, done}) {
         console.log(`Progress: ${completedRows} of ${totalRows} rows completed`);
-        const messageContainer = document.querySelector('#db-ops-progress-report');
+        const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report'));
         messageContainer.style.display = 'block';
         messageContainer.textContent = `Export Progress: ${completedRows} of ${totalRows} rows completed`;
 
@@ -572,8 +567,8 @@ export class BackupController {
     }
 
     /**
-     *
-     * @param databaseName
+     * @param {string} databaseName
+     * @returns {Promise<Blob>}
      */
     async _exportDatabase(databaseName) {
         const db = await new Dexie(databaseName).open();
@@ -592,7 +587,7 @@ export class BackupController {
             return;
         }
 
-        const errorMessageContainer = document.querySelector('#db-ops-error-report');
+        const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report'));
         errorMessageContainer.style.display = 'none';
 
         const date = new Date(Date.now());
@@ -616,15 +611,11 @@ export class BackupController {
     // Importing Dictionaries Database
 
     /**
-     *
-     * @param root0
-     * @param root0.totalRows
-     * @param root0.completedRows
-     * @param root0.done
+     * @param {{totalRows: number, completedRows: number, done: boolean}} details
      */
     _databaseImportProgressCallback({totalRows, completedRows, done}) {
         console.log(`Progress: ${completedRows} of ${totalRows} rows completed`);
-        const messageContainer = document.querySelector('#db-ops-progress-report');
+        const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report'));
         messageContainer.style.display = 'block';
         messageContainer.style.color = '#4169e1';
         messageContainer.textContent = `Import Progress: ${completedRows} of ${totalRows} rows completed`;
@@ -637,9 +628,8 @@ export class BackupController {
     }
 
     /**
-     *
-     * @param databaseName
-     * @param file
+     * @param {string} databaseName
+     * @param {File} file
      */
     async _importDatabase(databaseName, file) {
         await yomitan.api.purgeDatabase();
@@ -648,16 +638,13 @@ export class BackupController {
         yomitan.trigger('storageChanged');
     }
 
-    /**
-     *
-     */
+    /** */
     _onSettingsImportDatabaseClick() {
-        document.querySelector('#settings-import-db').click();
+        /** @type {HTMLElement} */ (document.querySelector('#settings-import-db')).click();
     }
 
     /**
-     *
-     * @param e
+     * @param {Event} e
      */
     async _onSettingsImportDatabaseChange(e) {
         if (this._settingsExportDatabaseToken !== null) {
@@ -666,22 +653,23 @@ export class BackupController {
             return;
         }
 
-        const errorMessageContainer = document.querySelector('#db-ops-error-report');
+        const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report'));
         errorMessageContainer.style.display = 'none';
 
-        const files = e.target.files;
-        if (files.length === 0) { return; }
+        const element = /** @type {HTMLInputElement} */ (e.currentTarget);
+        const files = element.files;
+        if (files === null || files.length === 0) { return; }
 
         const pageExitPrevention = this._settingsController.preventPageExit();
         const file = files[0];
-        e.target.value = null;
+        element.value = '';
         try {
             const token = {};
             this._settingsExportDatabaseToken = token;
             await this._importDatabase(this._dictionariesDatabaseName, file);
         } catch (error) {
             console.log(error);
-            const messageContainer = document.querySelector('#db-ops-progress-report');
+            const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report'));
             messageContainer.style.color = 'red';
             this._databaseExportImportErrorMessage('Encountered errors when importing. Please restart the browser and try again. If it continues to fail, reinstall Yomitan and import dictionaries one-by-one.');
         } finally {
diff --git a/ext/js/pages/settings/recommended-permissions-controller.js b/ext/js/pages/settings/recommended-permissions-controller.js
index e04dbdf7..b19311aa 100644
--- a/ext/js/pages/settings/recommended-permissions-controller.js
+++ b/ext/js/pages/settings/recommended-permissions-controller.js
@@ -19,13 +19,21 @@
 import {EventListenerCollection} from '../../core.js';
 
 export class RecommendedPermissionsController {
+    /**
+     * @param {import('./settings-controller.js').SettingsController} settingsController
+     */
     constructor(settingsController) {
+        /** @type {import('./settings-controller.js').SettingsController} */
         this._settingsController = settingsController;
+        /** @type {?NodeListOf<HTMLInputElement>} */
         this._originToggleNodes = null;
+        /** @type {EventListenerCollection} */
         this._eventListeners = new EventListenerCollection();
+        /** @type {?HTMLElement} */
         this._errorContainer = null;
     }
 
+    /** */
     async prepare() {
         this._originToggleNodes = document.querySelectorAll('.recommended-permissions-toggle');
         this._errorContainer = document.querySelector('#recommended-permissions-error');
@@ -39,35 +47,53 @@ export class RecommendedPermissionsController {
 
     // Private
 
+    /**
+     * @param {import('settings-controller').PermissionsChangedEvent} details
+     */
     _onPermissionsChanged({permissions}) {
         this._eventListeners.removeAllEventListeners();
         const originsSet = new Set(permissions.origins);
-        for (const node of this._originToggleNodes) {
-            node.checked = originsSet.has(node.dataset.origin);
+        if (this._originToggleNodes !== null) {
+            for (const node of this._originToggleNodes) {
+                const {origin} = node.dataset;
+                node.checked = typeof origin === 'string' && originsSet.has(origin);
+            }
         }
     }
 
+    /**
+     * @param {Event} e
+     */
     _onOriginToggleChange(e) {
-        const node = e.currentTarget;
+        const node = /** @type {HTMLInputElement} */ (e.currentTarget);
         const value = node.checked;
         node.checked = !value;
 
         const {origin} = node.dataset;
+        if (typeof origin !== 'string') { return; }
         this._setOriginPermissionEnabled(origin, value);
     }
 
+    /** */
     async _updatePermissions() {
         const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
         this._onPermissionsChanged({permissions});
     }
 
+    /**
+     * @param {string} origin
+     * @param {boolean} enabled
+     * @returns {Promise<boolean>}
+     */
     async _setOriginPermissionEnabled(origin, enabled) {
         let added = false;
         try {
             added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled);
         } catch (e) {
-            this._errorContainer.hidden = false;
-            this._errorContainer.textContent = e.message;
+            if (this._errorContainer !== null) {
+                this._errorContainer.hidden = false;
+                this._errorContainer.textContent = e instanceof Error ? e.message : `${e}`;
+            }
         }
         if (!added) { return false; }
         await this._updatePermissions();
diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts
index 19a62c1c..6b7b4b19 100644
--- a/types/ext/api.d.ts
+++ b/types/ext/api.d.ts
@@ -444,6 +444,18 @@ export type LoadExtensionScriptsDetails = {
 
 export type LoadExtensionScriptsResult = void;
 
+// openCrossFramePort
+
+export type OpenCrossFramePortDetails = {
+    targetTabId: number;
+    targetFrameId: number;
+};
+
+export type OpenCrossFramePortResult = {
+    targetTabId: number;
+    targetFrameId: number;
+};
+
 // requestBackendReadySignal
 
 export type RequestBackendReadySignalDetails = Record<string, never>;
diff --git a/types/ext/request-builder.d.ts b/types/ext/request-builder.d.ts
index 0acf5ede..8f375754 100644
--- a/types/ext/request-builder.d.ts
+++ b/types/ext/request-builder.d.ts
@@ -19,3 +19,5 @@ export type FetchEventListeners = {
     onBeforeSendHeaders: ((details: chrome.webRequest.WebRequestHeadersDetails) => (chrome.webRequest.BlockingResponse | void)) | null;
     onErrorOccurred: ((details: chrome.webRequest.WebResponseErrorDetails) => void) | null;
 };
+
+export type ProgressCallback = (complete: boolean) => void;
-- 
cgit v1.2.3