diff --git a/.eslintrc.json b/.eslintrc.json index e347c97897..51bb23282f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -398,7 +398,7 @@ "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-unsafe-argument": "error", - "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-enum-comparison": "off", "@typescript-eslint/no-unsafe-member-access": "off", diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index b770f311dd..3c7cf30b59 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -80,6 +80,7 @@ async function validateDictionaryBanks(mode, entries, schemasDetails) { export async function validateDictionary(mode, archiveData, schemas) { const entries = await getDictionaryArchiveEntries(archiveData); const indexFileName = getIndexFileName(); + /** @type {import('dictionary-data').Index} */ const index = await getDictionaryArchiveJson(entries, indexFileName); const version = index.format || index.version; diff --git a/ext/js/accessibility/google-docs-xray.js b/ext/js/accessibility/google-docs-xray.js index 15e1d50b02..6723a4689c 100644 --- a/ext/js/accessibility/google-docs-xray.js +++ b/ext/js/accessibility/google-docs-xray.js @@ -17,7 +17,7 @@ /** Entry point. */ function main() { - /** @type {Window} */ + /** @type {unknown} */ // @ts-expect-error - Firefox Xray vision const window2 = window.wrappedJSObject; if (!(typeof window2 === 'object' && window2 !== null)) { return; } diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 294c11db48..d042a25336 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -1831,16 +1831,7 @@ export class Backend { } try { - const tabWindow = await new Promise((resolve, reject) => { - chrome.windows.get(tab.windowId, {}, (value) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(value); - } - }); - }); + const tabWindow = await this._getWindow(tab.windowId); if (!tabWindow.focused) { await /** @type {Promise} */ (new Promise((resolve, reject) => { chrome.windows.update(tab.windowId, {focused: true}, () => { @@ -1858,6 +1849,23 @@ export class Backend { } } + /** + * @param {number} windowId + * @returns {Promise} + */ + _getWindow(windowId) { + return new Promise((resolve, reject) => { + chrome.windows.get(windowId, {}, (value) => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(value); + } + }); + }); + } + /** * @param {number} tabId * @param {number} frameId @@ -2208,6 +2216,7 @@ export class Backend { async _injectAnkiNoteDictionaryMedia(ankiConnect, timestamp, dictionaryMediaDetails) { const targets = []; const detailsList = []; + /** @type {Map} */ const detailsMap = new Map(); for (const {dictionary, path} of dictionaryMediaDetails) { const target = {dictionary, path}; diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index e65ec65e13..9e7b5b74a4 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -88,6 +88,7 @@ export class OffscreenProxy { if (!chrome.runtime.getContexts) { // Chrome version below 116 // Clients: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/clients // @ts-expect-error - Types not set up for service workers yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const matchedClients = await clients.matchAll(); // @ts-expect-error - Types not set up for service workers yet return await matchedClients.some((client) => client.url === offscreenUrl); diff --git a/ext/js/background/script-manager.js b/ext/js/background/script-manager.js index 84213452a5..ea1702e955 100644 --- a/ext/js/background/script-manager.js +++ b/ext/js/background/script-manager.js @@ -60,16 +60,7 @@ export function injectStylesheet(type, content, tabId, frameId, allFrames) { * @returns {Promise} `true` if a script is registered, `false` otherwise. */ export async function isContentScriptRegistered(id) { - const scripts = await new Promise((resolve, reject) => { - chrome.scripting.getRegisteredContentScripts({ids: [id]}, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - }); - }); + const scripts = await getRegisteredContentScripts([id]); for (const script of scripts) { if (script.id === id) { return true; @@ -155,3 +146,20 @@ function createContentScriptRegistrationOptions(details, id) { } return options; } + +/** + * @param {string[]} ids + * @returns {Promise} + */ +function getRegisteredContentScripts(ids) { + return new Promise((resolve, reject) => { + chrome.scripting.getRegisteredContentScripts({ids}, (result) => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(result); + } + }); + }); +} diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index 6a008f4078..23183e7937 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -18,6 +18,7 @@ import {ExtensionError} from '../core/extension-error.js'; import {parseJson} from '../core/json.js'; +import {isObjectNotArray} from '../core/object-utilities.js'; import {getRootDeckName} from '../data/anki-util.js'; /** @@ -606,15 +607,15 @@ export class AnkiConnect { if (typeof modelName !== 'string') { throw this._createError(`Unexpected result type at index ${i}, field modelName: expected string, received ${this._getTypeName(modelName)}`, result); } - if (typeof fields !== 'object' || fields === null) { - throw this._createError(`Unexpected result type at index ${i}, field fields: expected string, received ${this._getTypeName(fields)}`, result); + if (!isObjectNotArray(fields)) { + throw this._createError(`Unexpected result type at index ${i}, field fields: expected object, received ${this._getTypeName(fields)}`, result); } const tags2 = /** @type {string[]} */ (this._normalizeArray(tags, -1, 'string', ', field tags')); const cards2 = /** @type {number[]} */ (this._normalizeArray(cards, -1, 'number', ', field cards')); /** @type {{[key: string]: import('anki').NoteFieldInfo}} */ const fields2 = {}; for (const [key, fieldInfo] of Object.entries(fields)) { - if (typeof fieldInfo !== 'object' || fieldInfo === null) { continue; } + if (!isObjectNotArray(fieldInfo)) { continue; } const {value, order} = fieldInfo; if (typeof value !== 'string' || typeof order !== 'number') { continue; } fields2[key] = {value, order}; diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js index 3928873865..1915f12110 100644 --- a/ext/js/comm/frame-ancestry-handler.js +++ b/ext/js/comm/frame-ancestry-handler.js @@ -288,6 +288,7 @@ export class FrameAncestryHandler { } /** @type {?ShadowRoot|undefined} */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const shadowRoot = ( element.shadowRoot || // @ts-expect-error - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions diff --git a/ext/js/comm/frame-client.js b/ext/js/comm/frame-client.js index a30efa29c3..effd3e7c7b 100644 --- a/ext/js/comm/frame-client.js +++ b/ext/js/comm/frame-client.js @@ -83,6 +83,7 @@ export class FrameClient { */ _connectInternal(frame, targetOrigin, hostFrameId, setupFrame, timeout) { return new Promise((resolve, reject) => { + /** @type {Map} */ const tokenMap = new Map(); /** @type {?import('core').Timeout} */ let timer = null; diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 6a6a6177be..aec8cdd914 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -88,6 +88,7 @@ export class AnkiNoteBuilder { } const formattedFieldValues = await Promise.all(formattedFieldValuePromises); + /** @type {Map} */ const uniqueRequirements = new Map(); /** @type {import('anki').NoteFields} */ const noteFields = {}; diff --git a/ext/js/data/database.js b/ext/js/data/database.js index 7f37347bab..a53c8ddb45 100644 --- a/ext/js/data/database.js +++ b/ext/js/data/database.js @@ -194,10 +194,10 @@ export class Database { request.onsuccess = (e) => { const cursor = /** @type {IDBRequest} */ (e.target).result; if (cursor) { - /** @type {TResult} */ + /** @type {unknown} */ const value = cursor.value; - if (noPredicate || predicate(value, predicateArg)) { - resolve(value, data); + if (noPredicate || predicate(/** @type {TResult} */ (value), predicateArg)) { + resolve(/** @type {TResult} */ (value), data); } else { cursor.continue(); } @@ -424,9 +424,9 @@ export class Database { request.onsuccess = (e) => { const cursor = /** @type {IDBRequest} */ (e.target).result; if (cursor) { - /** @type {TResult} */ + /** @type {unknown} */ const value = cursor.value; - results.push(value); + results.push(/** @type {TResult} */ (value)); cursor.continue(); } else { onSuccess(results, data); diff --git a/ext/js/data/json-schema.js b/ext/js/data/json-schema.js index 9e1497e9e9..0a2b8d824e 100644 --- a/ext/js/data/json-schema.js +++ b/ext/js/data/json-schema.js @@ -1263,7 +1263,7 @@ class JsonSchemaProxyHandler { /** * @param {import('ext/json-schema').ValueObjectOrArray} target * @param {string|number|symbol} property - * @param {import('core').SafeAny} value + * @param {unknown} value * @returns {boolean} * @throws {Error} */ diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index b6fb668628..ba404bc2ac 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -27,6 +27,7 @@ import {JsonSchema} from './json-schema.js'; // of the options object to a newer format. SafeAny is used for much of this, since every single // legacy format does not contain type definitions. /* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export class OptionsUtil { constructor() { @@ -1295,4 +1296,5 @@ export class OptionsUtil { } } +/* eslint-enable @typescript-eslint/no-unsafe-assignment */ /* eslint-enable @typescript-eslint/no-unsafe-argument */ diff --git a/ext/js/dictionary/dictionary-data-util.js b/ext/js/dictionary/dictionary-data-util.js index a90668f46d..dfdd56013b 100644 --- a/ext/js/dictionary/dictionary-data-util.js +++ b/ext/js/dictionary/dictionary-data-util.js @@ -24,6 +24,7 @@ export function groupTermTags(dictionaryEntry) { const {headwords} = dictionaryEntry; const headwordCount = headwords.length; const uniqueCheck = (headwordCount > 1); + /** @type {Map} */ const resultsIndexMap = new Map(); const results = []; for (let i = 0; i < headwordCount; ++i) { diff --git a/ext/js/display/display-history.js b/ext/js/display/display-history.js index 183368d04f..0806e17bb2 100644 --- a/ext/js/display/display-history.js +++ b/ext/js/display/display-history.js @@ -37,6 +37,7 @@ export class DisplayHistory extends EventDispatcher { /** @type {Map} */ this._historyMap = new Map(); + /** @type {unknown} */ const historyState = history.state; const {id, state} = ( isObjectNotArray(historyState) ? @@ -188,6 +189,7 @@ export class DisplayHistory extends EventDispatcher { /** */ _updateStateFromHistory() { + /** @type {unknown} */ let state = history.state; let id = null; if (isObjectNotArray(state)) { @@ -208,7 +210,7 @@ export class DisplayHistory extends EventDispatcher { // Fallback this._current.id = (typeof id === 'string' ? id : this._generateId()); - this._current.state = state; + this._current.state = /** @type {import('display-history').EntryState} */ (state); this._current.content = null; this._clear(); } diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js index b495365d9b..f85735fcee 100644 --- a/ext/js/input/hotkey-help-controller.js +++ b/ext/js/input/hotkey-help-controller.js @@ -67,9 +67,10 @@ export class HotkeyHelpController { const hotkey = (global ? this._globalActionHotkeys : this._localActionHotkeys).get(action); for (let i = 0, ii = attributes.length; i < ii; ++i) { const attribute = attributes[i]; + /** @type {unknown} */ let value; if (typeof hotkey !== 'undefined') { - value = /** @type {unknown} */ (multipleValues ? values[i] : values); + value = multipleValues ? values[i] : values; if (typeof value === 'string') { value = value.replace(replacementPattern, hotkey); } @@ -158,7 +159,8 @@ export class HotkeyHelpController { if (typeof hotkey !== 'string') { return null; } const data = /** @type {unknown} */ (parseJson(hotkey)); if (!Array.isArray(data)) { return null; } - const [action, attributes, values] = /** @type {unknown[]} */ (data); + const dataArray = /** @type {unknown[]} */ (data); + const [action, attributes, values] = dataArray; if (typeof action !== 'string') { return null; } /** @type {string[]} */ const attributesArray = []; diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 845d53d56e..0d26b2f072 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -289,6 +289,7 @@ export class Translator { return false; } + /** @type {Map} */ const frequencyCounter = new Map(); for (const element of array1) { @@ -400,6 +401,7 @@ export class Translator { * @returns {Map} */ _groupDeinflectionsByTerm(deinflections) { + /** @type {Map} */ const result = new Map(); for (const deinflection of deinflections) { const {deinflectedText} = deinflection; @@ -455,7 +457,7 @@ export class Translator { /** @type {import('translation-internal').DatabaseDeinflection[]} */ const deinflections = []; const used = new Set(); - /** @type {Map} */ + /** @type {import('translation-internal').TextCache} */ const sourceCache = new Map(); // For reusing text processors' outputs for ( @@ -498,14 +500,15 @@ export class Translator { /** * @param {import('language').TextProcessorWithId[]} textProcessors - * @param {Map} processorVariant + * @param {import('translation-internal').TextProcessorVariant} processorVariant * @param {string} text - * @param {Map} textCache + * @param {import('translation-internal').TextCache} textCache * @returns {string} */ _applyTextProcessors(textProcessors, processorVariant, text, textCache) { for (const {id, textProcessor: {process}} of textProcessors) { const setting = processorVariant.get(id); + let level1 = textCache.get(text); if (!level1) { level1 = new Map(); @@ -522,7 +525,7 @@ export class Translator { text = process(text, setting); level2.set(setting, text); } else { - text = level2.get(setting); + text = level2.get(setting) || ''; } } @@ -681,6 +684,7 @@ export class Translator { /** @type {import('dictionary-database').TermExactRequest[]} */ const termList = []; const targetList = []; + /** @type {Map} */ const targetMap = new Map(); for (const group of groupedDictionaryEntries) { @@ -1362,10 +1366,10 @@ export class Translator { /** * @param {Map} arrayVariants - * @returns {Map[]} + * @returns {import('translation-internal').TextProcessorVariant[]} */ _getArrayVariants(arrayVariants) { - /** @type {Map[]} */ + /** @type {import('translation-internal').TextProcessorVariant[]} */ const results = []; const variantKeys = [...arrayVariants.keys()]; const entryVariantLengths = []; @@ -1376,7 +1380,7 @@ export class Translator { const totalVariants = entryVariantLengths.reduce((acc, length) => acc * length, 1); for (let variantIndex = 0; variantIndex < totalVariants; ++variantIndex) { - /** @type {Map} */ + /** @type {import('translation-internal').TextProcessorVariant}} */ const variant = new Map(); let remainingIndex = variantIndex; @@ -2076,6 +2080,7 @@ export class Translator { * @param {boolean} ascending */ _updateSortFrequencies(dictionaryEntries, dictionary, ascending) { + /** @type {Map} */ const frequencyMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { const {definitions, frequencies} = dictionaryEntry; diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 5c1688492d..f0876d3f9a 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -574,12 +574,16 @@ export class BackupController { * @returns {Promise} */ async _exportDatabase(databaseName) { - const db = await new Dexie(databaseName).open(); + const DexieConstructor = /** @type {import('dexie').DexieConstructor} */ (/** @type {unknown} */ (Dexie)); + const db = new DexieConstructor(databaseName); + await db.open(); + /** @type {unknown} */ + // @ts-expect-error - The export function is declared as an extension which has no type information. const blob = await db.export({ progressCallback: this._databaseExportProgressCallback.bind(this) }); - await db.close(); - return blob; + db.close(); + return /** @type {Blob} */ (blob); } /** */ diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index f63eb49e53..ecfadc1fd1 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -306,6 +306,7 @@ export class DictionaryImportController { * @param {Error[]} errors */ _showErrors(errors) { + /** @type {Map} */ const uniqueErrors = new Map(); for (const error of errors) { log.error(error); diff --git a/ext/js/templates/anki-template-renderer.js b/ext/js/templates/anki-template-renderer.js index ae3e7a369d..888be9b070 100644 --- a/ext/js/templates/anki-template-renderer.js +++ b/ext/js/templates/anki-template-renderer.js @@ -658,13 +658,20 @@ export class AnkiTemplateRenderer { return instance; } + /** + * @param {import('template-renderer').HelperOptions} options + * @returns {import('anki-templates').NoteData} + */ + _getNoteDataFromOptions(options) { + return options.data.root; + } + /** * @type {import('template-renderer').HelperFunction} */ _formatGlossary(args, _context, options) { const [dictionary, content] = /** @type {[dictionary: string, content: import('dictionary-data').TermGlossaryContent]} */ (args); - /** @type {import('anki-templates').NoteData} */ - const data = options.data.root; + const data = this._getNoteDataFromOptions(options); if (typeof content === 'string') { return this._safeString(this._stringToMultiLineHtml(content)); } if (!(typeof content === 'object' && content !== null)) { return ''; } switch (content.type) { @@ -703,8 +710,7 @@ export class AnkiTemplateRenderer { * @type {import('template-renderer').HelperFunction} */ _hasMedia(args, _context, options) { - /** @type {import('anki-templates').NoteData} */ - const data = options.data.root; + const data = this._getNoteDataFromOptions(options); return this._mediaProvider.hasMedia(data, args, options.hash); } @@ -712,8 +718,7 @@ export class AnkiTemplateRenderer { * @type {import('template-renderer').HelperFunction} */ _getMedia(args, _context, options) { - /** @type {import('anki-templates').NoteData} */ - const data = options.data.root; + const data = this._getNoteDataFromOptions(options); return this._mediaProvider.getMedia(data, args, options.hash); } diff --git a/ext/js/templates/template-renderer-media-provider.js b/ext/js/templates/template-renderer-media-provider.js index 2f238e205f..c4b07369ce 100644 --- a/ext/js/templates/template-renderer-media-provider.js +++ b/ext/js/templates/template-renderer-media-provider.js @@ -81,6 +81,8 @@ export class TemplateRendererMediaProvider { let {value} = data; const {escape = true} = namedArgs; if (escape) { + // Handlebars is a custom version of the library without type information, so it's assumed to be "any". + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment value = Handlebars.Utils.escapeExpression(value); } return value; diff --git a/ext/js/templates/template-renderer.js b/ext/js/templates/template-renderer.js index 7bb93aa251..c5b7cd6339 100644 --- a/ext/js/templates/template-renderer.js +++ b/ext/js/templates/template-renderer.js @@ -117,6 +117,8 @@ export class TemplateRenderer { let instance = cache.get(template); if (typeof instance === 'undefined') { this._updateCacheSize(this._cacheMaxSize - 1); + // Handlebars is a custom version of the library without type information, so it's assumed to be "any". + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment instance = /** @type {import('handlebars').TemplateDelegate} */ (Handlebars.compileAST(template)); cache.set(template, instance); } diff --git a/test/dom-text-scanner.test.js b/test/dom-text-scanner.test.js index b366caddbb..4ec3a44dd1 100644 --- a/test/dom-text-scanner.test.js +++ b/test/dom-text-scanner.test.js @@ -85,8 +85,13 @@ function createAbsoluteGetComputedStyle(window) { return (element, ...args) => { const style = getComputedStyleOld(element, ...args); return new Proxy(style, { + /** + * @param {CSSStyleDeclaration} target + * @param {string|symbol} property + * @returns {unknown} + */ get: (target, property) => { - let result = /** @type {import('core').SafeAny} */ (target)[property]; + let result = /** @type {Record} */ (/** @type {unknown} */ (target))[property]; if (typeof result === 'string') { /** * @param {string} g0 diff --git a/test/fixtures/dom-test.js b/test/fixtures/dom-test.js index 459383cc59..364612f605 100644 --- a/test/fixtures/dom-test.js +++ b/test/fixtures/dom-test.js @@ -28,7 +28,9 @@ function prepareWindow(window) { // Define innerText setter as an alias for textContent setter Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', { + /** @returns {string} */ get() { return this.textContent; }, + /** @param {string} value */ set(value) { this.textContent = value; } }); diff --git a/test/handlebars.test.js b/test/handlebars.test.js index d65e8c4297..8a566f4c85 100644 --- a/test/handlebars.test.js +++ b/test/handlebars.test.js @@ -18,6 +18,22 @@ import {describe, test} from 'vitest'; import {Handlebars} from '../ext/lib/handlebars.js'; +/** + * @param {string} template + * @returns {import('handlebars').TemplateDelegate} + */ +function compile(template) { + return Handlebars.compile(template); +} + +/** + * @param {string} template + * @returns {import('handlebars').TemplateDelegate} + */ +function compileAST(template) { + return Handlebars.compileAST(template); +} + describe('Handlebars', () => { test('compile vs compileAST 1', ({expect}) => { const template = '{{~test1~}}'; @@ -26,8 +42,8 @@ describe('Handlebars', () => { test1: '
Test
' }; - const instance1 = Handlebars.compile(template); - const instance2 = Handlebars.compileAST(template); + const instance1 = compile(template); + const instance2 = compileAST(template); const result1 = instance1(data); const result2 = instance2(data); @@ -44,8 +60,8 @@ describe('Handlebars', () => { } }; - const instance1 = Handlebars.compile(template); - const instance2 = Handlebars.compileAST(template); + const instance1 = compile(template); + const instance2 = compileAST(template); const result1 = instance1(data); const result2 = instance2(data); diff --git a/types/ext/translation-internal.d.ts b/types/ext/translation-internal.d.ts index 0005656259..26b043b425 100644 --- a/types/ext/translation-internal.d.ts +++ b/types/ext/translation-internal.d.ts @@ -60,3 +60,7 @@ export type TextProcessorMap = Map< postprocessorOptionsSpace: TextProcessorOptionsSpace; } >; + +export type TextProcessorVariant = Map; + +export type TextCache = Map>>;