diff --git a/.eslintrc.js b/.eslintrc.js index 9d2726f398..ff5e381c5e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,7 +45,6 @@ module.exports = { }, ], '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-non-null-assertion': 'error', 'no-redeclare': 'off', '@typescript-eslint/no-redeclare': 'error', 'no-shadow': 'off', @@ -55,6 +54,7 @@ module.exports = { 'error', { functions: false, allowNamedExports: true, typedefs: false, ignoreTypeReferences: true }, ], + '@typescript-eslint/no-unnecessary-type-assertion': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', @@ -70,6 +70,7 @@ module.exports = { // Should use our logger anytime you want logs that persist. Otherwise use console only in testing 'no-console': 'warn', 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], + 'no-type-assertion/no-type-assertion': 'error', 'prettier/prettier': ['warn', { tabWidth: 2, trailingComma: 'all' }], 'react/jsx-indent-props': ['warn', 2], 'react/jsx-props-no-spreading': ['error', { custom: 'ignore' }], @@ -87,7 +88,7 @@ module.exports = { tsconfigRootDir: __dirname, createDefaultProgram: true, }, - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', 'no-type-assertion'], settings: { 'import/resolver': { // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below diff --git a/.storybook/main.ts b/.storybook/main.ts index 23725c1d59..276c1cb856 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -52,8 +52,8 @@ const config: StorybookConfig = { return mergeWithCustomize({ customizeArray(wpModule: object[], rModule: object[], moduleKey: string) { if (moduleKey === 'rules') { - const wpRules = wpModule as RuleSetRule[]; - const rRules = rModule as RuleSetRule[]; + const wpRules: RuleSetRule[] = wpModule; + const rRules: RuleSetRule[] = rModule; return [ ...wpRules.filter( (rule) => diff --git a/extensions/src/hello-someone/hello-someone.ts b/extensions/src/hello-someone/hello-someone.ts index c2d7f66030..97e57a5df0 100644 --- a/extensions/src/hello-someone/hello-someone.ts +++ b/extensions/src/hello-someone/hello-someone.ts @@ -72,11 +72,14 @@ const peopleDataProviderEngine: IDataProviderEngine & */ getPerson( name: string, + // Assert the conditional type. + // eslint-disable-next-line no-type-assertion/no-type-assertion createIfDoesNotExist: T = true as T, ): T extends true ? Person : Person | undefined { const nameLower = name.toLowerCase(); if (createIfDoesNotExist && !this.people[nameLower]) this.people[nameLower] = {}; // Type assert because we know this person exists + // eslint-disable-next-line no-type-assertion/no-type-assertion return this.people[nameLower] as T extends true ? Person : Person | undefined; }, @@ -238,6 +241,8 @@ const peopleWebViewProvider: IWebViewProvider = { return { ...savedWebView, title: 'People', + // Can't use the enum value from a definition file so assert the type from the string literal. + // eslint-disable-next-line no-type-assertion/no-type-assertion contentType: 'html' as WebViewContentType.HTML, content: helloSomeoneHtmlWebView, }; diff --git a/extensions/src/hello-world/hello-world.ts b/extensions/src/hello-world/hello-world.ts index 514d292b88..04ad3b2743 100644 --- a/extensions/src/hello-world/hello-world.ts +++ b/extensions/src/hello-world/hello-world.ts @@ -34,6 +34,8 @@ const htmlWebViewProvider: IWebViewProviderWithType = { return { ...savedWebView, title: 'Hello World HTML', + // Can't use the enum value from a definition file so assert the type from the string literal. + // eslint-disable-next-line no-type-assertion/no-type-assertion contentType: 'html' as WebViewContentType.HTML, content: helloWorldHtmlWebView, }; diff --git a/extensions/src/hello-world/web-views/hello-world.web-view.tsx b/extensions/src/hello-world/web-views/hello-world.web-view.tsx index ab1959a394..9f574bff28 100644 --- a/extensions/src/hello-world/web-views/hello-world.web-view.tsx +++ b/extensions/src/hello-world/web-views/hello-world.web-view.tsx @@ -97,6 +97,8 @@ globalThis.webViewComponent = function HelloWorld() { iconUrl: 'papi-extension://hello-world/assets/offline.svg', title: 'Select Hello World Project', }).current, + // Assert as string type rather than string literal type. + // eslint-disable-next-line no-type-assertion/no-type-assertion 'None' as DialogTypes['platform.selectProject']['responseType'], ); @@ -191,10 +193,7 @@ globalThis.webViewComponent = function HelloWorld() { {/* no label available */} {/* no label available */} - setScrRef(newScrRef)} - /> + setScrRef(newScrRef)} /> columns={[ { diff --git a/extensions/webpack/webpack.util.ts b/extensions/webpack/webpack.util.ts index 73a4906c76..89f09d1e8c 100644 --- a/extensions/webpack/webpack.util.ts +++ b/extensions/webpack/webpack.util.ts @@ -70,13 +70,13 @@ export function getWebViewTempPath( */ export async function getWebViewEntries(): Promise { const tsxWebViews = await getWebViewTsxPaths(); - const webViewEntries = Object.fromEntries( + const webViewEntries: webpack.EntryObject = Object.fromEntries( tsxWebViews.map((webViewPath) => [ webViewPath, { import: webViewPath, filename: getWebViewTempPath(webViewPath), - } as webpack.EntryObject[string], + }, ]), ); return webViewEntries; @@ -191,7 +191,7 @@ export function getMainEntries(extensions: ExtensionInfo[]): webpack.EntryObject library: { type: LIBRARY_TYPE, }, - } as webpack.EntryObject[string], + }, ]), ); return mainEntries; @@ -257,9 +257,9 @@ export async function getExtensions(): Promise { path.join(sourceFolder, extensionFolderName, 'manifest.json'), 'utf8', ); - const extensionManifest = Object.freeze({ + const extensionManifest: Readonly = Object.freeze({ // Note that this does not transform the main file .ts into .js unlike extension.service - ...(JSON.parse(extensionManifestJson) as ExtensionManifest), + ...JSON.parse(extensionManifestJson), }); // Get main file path from the manifest and return extension info diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 8fc7865447..424dfda3c2 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1057,8 +1057,10 @@ declare module 'shared/services/network.service' { * Register a local request handler to run on requests. * @param requestType the type of request on which to register the handler * @param handler function to register to run on requests - * @param handlerType type of handler function - indicates what type of parameters and what return type the handler has - * @returns promise that resolves if the request successfully registered and unsubscriber function to run to stop the passed-in function from handling requests + * @param handlerType type of handler function - indicates what type of parameters and what return + * type the handler has + * @returns promise that resolves if the request successfully registered and unsubscriber function + * to run to stop the passed-in function from handling requests */ export function registerRequestHandler( requestType: SerializedRequestType, @@ -1096,7 +1098,8 @@ declare module 'shared/services/network.service' { * Creates a function that is a request function with a baked requestType. * This is also nice because you get TypeScript type support using this function. * @param requestType requestType for request function - * @returns function to call with arguments of request that performs the request and resolves with the response contents + * @returns function to call with arguments of request that performs the request and resolves with + * the response contents */ export const createRequestFunction: ( requestType: SerializedRequestType, @@ -2131,7 +2134,12 @@ declare module 'shared/services/web-view.service' { * * Not exposed on the papi */ - export const addTab: (savedTabInfo: SavedTabInfo, layout: Layout) => Promise; + export const addTab: ( + savedTabInfo: SavedTabInfo & { + data?: TData | undefined; + }, + layout: Layout, + ) => Promise; /** * Creates a new web view or gets an existing one depending on if you request an existing one and * if the web view provider decides to give that existing one to you (it is up to the provider). @@ -2442,6 +2450,7 @@ declare module 'shared/services/data-provider.service' { export default dataProviderService; } declare module 'shared/models/project-metadata.model' { + import { ProjectTypes } from 'papi-shared-types'; /** * Low-level information describing a project that Platform.Bible directly manages and uses to load project data */ @@ -2461,7 +2470,7 @@ declare module 'shared/models/project-metadata.model' { /** * Indicates what sort of project this is which implies its data shape (e.g., what data streams should be available) */ - projectType: string; + projectType: ProjectTypes; }; } declare module 'shared/services/project-lookup.service-model' { @@ -2801,7 +2810,8 @@ declare module 'shared/services/settings.service' { /** * Retrieves the value of the specified setting * @param key The string id of the setting for which the value is being retrieved - * @returns The value of the specified setting, parsed to an object. Returns `null` if setting is not present or no value is available + * @returns The value of the specified setting, parsed to an object. Returns `null` if setting is + * not present or no value is available */ const getSetting: ( key: SettingName, @@ -2809,14 +2819,16 @@ declare module 'shared/services/settings.service' { /** * Sets the value of the specified setting * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `null` is the equivalent of deleting the setting + * @param newSetting The value that is to be stored. Setting the new value to `null` is the + * equivalent of deleting the setting */ const setSetting: ( key: SettingName, newSetting: Nullable, ) => void; /** - * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the callback function is executed. + * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the + * callback function is executed. * @param key The string id of the setting for which the value is being subscribed to * @param callback The function that will be called whenever the specified setting is updated * @returns Unsubscriber that should be called whenever the subscription should be deleted diff --git a/package-lock.json b/package-lock.json index 768b949d78..2fa5b01d9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "eslint-plugin-import": "^2.28.1", "eslint-plugin-jest": "^27.2.3", "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-no-type-assertion": "^1.3.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", @@ -204,15 +205,81 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", @@ -262,12 +329,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -428,20 +495,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -595,8 +664,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -638,11 +708,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -651,7 +722,8 @@ }, "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { "color-convert": "^1.9.0" }, @@ -661,7 +733,8 @@ }, "node_modules/@babel/highlight/node_modules/chalk": { "version": "2.4.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -673,32 +746,37 @@ }, "node_modules/@babel/highlight/node_modules/color-convert": { "version": "1.9.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "engines": { "node": ">=0.8.0" } }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { "node": ">=4" } }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { "has-flag": "^3.0.0" }, @@ -707,9 +785,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -2319,32 +2397,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2353,12 +2432,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -13826,6 +13905,19 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-no-type-assertion": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-type-assertion/-/eslint-plugin-no-type-assertion-1.3.0.tgz", + "integrity": "sha512-04wuuIP5ptNzp969tTt0gf/Jsw4G0T5md2/nbgv3dRL/HySSNU7H4vIKNjkuno9T+6h2daj1T9Aki6bDgmXDEw==", + "dev": true, + "engines": { + "node": ">=12.0.0", + "yarn": "^1.13.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, "node_modules/eslint-plugin-prettier": { "version": "4.2.1", "dev": true, @@ -26231,9 +26323,63 @@ } }, "@babel/code-frame": { - "version": "7.22.5", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { @@ -26274,12 +26420,12 @@ } }, "@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -26408,15 +26554,19 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -26516,7 +26666,9 @@ "version": "7.22.5" }, "@babel/helper-validator-identifier": { - "version": "7.22.5" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.22.5", @@ -26545,21 +26697,27 @@ } }, "@babel/highlight": { - "version": "7.22.5", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { "ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -26568,21 +26726,31 @@ }, "color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "requires": { "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.3" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "escape-string-regexp": { - "version": "1.0.5" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "has-flag": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "requires": { "has-flag": "^3.0.0" } @@ -26590,9 +26758,9 @@ } }, "@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -27642,39 +27810,41 @@ } }, "@babel/template": { - "version": "7.22.5", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -35585,6 +35755,13 @@ } } }, + "eslint-plugin-no-type-assertion": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-type-assertion/-/eslint-plugin-no-type-assertion-1.3.0.tgz", + "integrity": "sha512-04wuuIP5ptNzp969tTt0gf/Jsw4G0T5md2/nbgv3dRL/HySSNU7H4vIKNjkuno9T+6h2daj1T9Aki6bDgmXDEw==", + "dev": true, + "requires": {} + }, "eslint-plugin-prettier": { "version": "4.2.1", "dev": true, diff --git a/package.json b/package.json index 452a412286..5b05115ea0 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "eslint-plugin-import": "^2.28.1", "eslint-plugin-jest": "^27.2.3", "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-no-type-assertion": "^1.3.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", diff --git a/src/client/services/client-network-connector.service.ts b/src/client/services/client-network-connector.service.ts index 413965ac32..36a83e1537 100644 --- a/src/client/services/client-network-connector.service.ts +++ b/src/client/services/client-network-connector.service.ts @@ -175,11 +175,10 @@ export default class ClientNetworkConnector implements INetworkConnector { this.webSocket.addEventListener('close', this.disconnect); // Remove event listeners and try connecting again - const retry = (e: Event) => { - const err = (e as Event & { error?: Error }).error; + const retry = (e: Event & { error?: Error }) => { logger.warn( `ClientNetworkConnector WebSocket did not connect on attempt ${attempts}. Trying again. Error: ${getErrorMessage( - err, + e.error, )}`, ); if (this.webSocket) { @@ -315,6 +314,8 @@ export default class ClientNetworkConnector implements INetworkConnector { ) { // This message is from us and for us. Handle the message as if we just received it this.onMessage( + // Fake enough of the message event to handle it. + /* eslint-disable no-type-assertion/no-type-assertion */ { data: message as unknown as string, } as MessageEvent, @@ -332,9 +333,9 @@ export default class ClientNetworkConnector implements INetworkConnector { * @param fromSelf whether this message is from this connector instead of from someone else */ private onMessage = (event: MessageEvent, fromSelf = false) => { - const data = fromSelf - ? (event.data as unknown as Message) - : (JSON.parse(event.data as string) as Message); + // Assert our specific message type. + // eslint-disable-next-line no-type-assertion/no-type-assertion + const data: Message = fromSelf ? (event.data as unknown as Message) : JSON.parse(event.data); const emitter = this.messageEmitters.get(data.type); emitter?.emit(data); diff --git a/src/client/services/web-socket.factory.ts b/src/client/services/web-socket.factory.ts index 6227cd82fb..eb8a643d53 100644 --- a/src/client/services/web-socket.factory.ts +++ b/src/client/services/web-socket.factory.ts @@ -1,5 +1,6 @@ /** - * Creates a WebSocket from the node ws library or from the browser WebSocket depending on if we're in node or browser + * Creates a WebSocket from the node ws library or from the browser WebSocket depending on if we're + * in node or browser. */ import { isRenderer } from '@shared/utils/internal-util'; @@ -13,8 +14,11 @@ import { IWebSocket } from './web-socket.interface'; export const createWebSocket = async (url: string): Promise => { if (isRenderer()) { const Ws = (await import('@renderer/services/renderer-web-socket.model')).default; - return new Ws(url) as IWebSocket; + return new Ws(url); } const Ws = (await import('@extension-host/services/extension-host-web-socket.model')).default; + // Assert the return type. Note: this web socket is missing the `dispatchEvent` property that the + // renderer type has. + // eslint-disable-next-line no-type-assertion/no-type-assertion return new Ws(url) as unknown as IWebSocket; }; diff --git a/src/extension-host/extension-host.ts b/src/extension-host/extension-host.ts index c07bb1fa03..15c70eac37 100644 --- a/src/extension-host/extension-host.ts +++ b/src/extension-host/extension-host.ts @@ -47,6 +47,8 @@ networkService // Set up network commands await Promise.all( Object.entries(commandHandlers).map(async ([commandName, handler]) => { + // Re-assert type after passing through `map`. + // eslint-disable-next-line no-type-assertion/no-type-assertion await papi.commands.registerCommand(commandName as CommandNames, handler); }), ); diff --git a/src/extension-host/global-this.model.ts b/src/extension-host/global-this.model.ts index 25c2b95f60..f27d361bf8 100644 --- a/src/extension-host/global-this.model.ts +++ b/src/extension-host/global-this.model.ts @@ -18,6 +18,8 @@ import { ProcessType } from '@shared/global-this.model'; const isPackaged = getCommandLineSwitch(ARG_PACKAGED); const resourcesPath = getCommandLineArgument(ARG_RESOURCES_PATH) ?? 'resources://'; const logLevel = + // Assert the extracted type. + // eslint-disable-next-line no-type-assertion/no-type-assertion (getCommandLineArgument(ARG_LOG_LEVEL) as LogLevel) ?? (isPackaged ? 'error' : 'info'); // #endregion diff --git a/src/extension-host/services/extension.service.ts b/src/extension-host/services/extension.service.ts index 927124d41f..d84b944644 100644 --- a/src/extension-host/services/extension.service.ts +++ b/src/extension-host/services/extension.service.ts @@ -106,7 +106,7 @@ let availableExtensions: ExtensionInfo[]; /** Parse string extension manifest into an object and perform any transformations needed */ function parseManifest(extensionManifestJson: string): ExtensionManifest { - const extensionManifest = JSON.parse(extensionManifestJson) as ExtensionManifest; + const extensionManifest: ExtensionManifest = JSON.parse(extensionManifestJson); if (extensionManifest.name.includes('..')) throw new Error('Extension name must not include `..`!'); // Replace ts with js so people can list their source code ts name but run the transpiled js @@ -242,37 +242,45 @@ async function unzipCompressedExtensionFile(zipUri: Uri): Promise { async function getExtensions(): Promise { const extensionUris = await extensionUrisToLoad; return ( - await Promise.allSettled( - extensionUris.map(async (extensionUri) => { - try { - const extensionManifestJson = await nodeFS.readFileText( - joinUriPaths(extensionUri, MANIFEST_FILE_NAME), + ( + await Promise.allSettled( + extensionUris.map(async (extensionUri) => { + try { + const extensionManifestJson = await nodeFS.readFileText( + joinUriPaths(extensionUri, MANIFEST_FILE_NAME), + ); + // Assert the return type after freeze. + // eslint-disable-next-line no-type-assertion/no-type-assertion + return Object.freeze({ + ...parseManifest(extensionManifestJson), + dirUri: extensionUri, + }) as ExtensionInfo; + } catch (e) { + const error = new Error( + `Extension folder ${extensionUri} failed to load. Reason: ${e}`, + ); + logger.warn(error); + throw error; + } + }), + ) + ) + .filter((settled) => { + // Ignore failed to load manifest issues - already logged those issues + if (settled.status !== 'fulfilled') return false; + if (settled.value.main === undefined) { + logger.warn( + `Extension ${settled.value.name} failed to load. Must provide property \`main\` in \`manifest.json\`. If you do not have JavaScript code to run, provide \`"main": null\``, ); - return Object.freeze({ - ...parseManifest(extensionManifestJson), - dirUri: extensionUri, - }) as ExtensionInfo; - } catch (e) { - const error = new Error(`Extension folder ${extensionUri} failed to load. Reason: ${e}`); - logger.warn(error); - throw error; + return false; } - }), - ) - ) - .filter((settled) => { - // Ignore failed to load manifest issues - already logged those issues - if (settled.status !== 'fulfilled') return false; - if ((settled.value.main as Partial) === undefined) { - logger.warn( - `Extension ${settled.value.name} failed to load. Must provide property \`main\` in \`manifest.json\`. If you do not have JavaScript code to run, provide \`"main": null\``, - ); - return false; - } - // If main is null, having no JavaScript is intentional. Do not load this extension - return settled.value.main !== null; - }) - .map((fulfilled) => (fulfilled as PromiseFulfilledResult).value); + // If main is null, having no JavaScript is intentional. Do not load this extension + return settled.value.main !== null; + }) + // Assert the fulfilled type since the unfulfilled ones have been filtered out. + // eslint-disable-next-line no-type-assertion/no-type-assertion + .map((fulfilled) => (fulfilled as PromiseFulfilledResult).value) + ); } /** @@ -306,8 +314,9 @@ function watchForExtensionChanges(extensions: ExtensionInfo[]): void { */ async function activateExtension(extension: ExtensionInfo): Promise { // Import the extension file. Tell webpack to ignore it because extension files are not in the - // bundle and should not be looked up in the bundle + // bundle and should not be looked up in the bundle. Assert a more ambiguous type. // DO NOT REMOVE THE webpackIgnore COMMENT. It is a webpack "Magic Comment" https://webpack.js.org/api/module-methods/#magic-comments + // eslint-disable-next-line no-type-assertion/no-type-assertion const extensionModuleAmbiguous = systemRequire( /* webpackIgnore: true */ getPathFromUri(extension.dirUri), ) as AmbiguousExtensionModule; @@ -364,6 +373,8 @@ async function activateExtensions(extensions: ExtensionInfo[]): Promise { // Allow the extension to import papi and some other things if (moduleName === 'papi-backend') return papi; @@ -410,6 +421,8 @@ async function activateExtensions(extensions: ExtensionInfo[]): Promise diff --git a/src/extension-host/services/papi-backend.service.ts b/src/extension-host/services/papi-backend.service.ts index 2479a58fdf..0fd992d4a1 100644 --- a/src/extension-host/services/papi-backend.service.ts +++ b/src/extension-host/services/papi-backend.service.ts @@ -32,6 +32,7 @@ import { DialogService } from '@shared/services/dialog.service-model'; // 1) When adding new services here, consider whether they also belong in papi-frontend.service.ts. // 2) We need to provide type assertions for all members so they carry the JSDoc comments on the // papi.d.ts file so extension developers see the comments. Please add to all properties you add. +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion, no-type-assertion/no-type-assertion */ // 3) The "JSDOC DESTINATION" comments are there to provide anchors for JSDocs to be copied in. // Please add to all properties you add. const papi = { diff --git a/src/main/global-this.model.ts b/src/main/global-this.model.ts index 26226ab9d8..ce74463284 100644 --- a/src/main/global-this.model.ts +++ b/src/main/global-this.model.ts @@ -20,6 +20,8 @@ globalThis.processType = ProcessType.Main; globalThis.isPackaged = app.isPackaged; globalThis.resourcesPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '../../'); globalThis.logLevel = + // Assert the extracted type. + // eslint-disable-next-line no-type-assertion/no-type-assertion (getCommandLineArgument(ARG_LOG_LEVEL) as LogLevel) ?? (globalThis.isPackaged ? 'error' : 'info'); // #endregion diff --git a/src/main/main.ts b/src/main/main.ts index 41bf013df1..110d0d893b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -220,7 +220,7 @@ async function main() { /** Map from ipc channel to handler function. Use with ipcRenderer.invoke */ const ipcHandlers: { - [ipcChannel: string]: ( + [ipcChannel: SerializedRequestType]: ( event: IpcMainInvokeEvent, // We don't know the exact parameter types since ipc handlers can be anything // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -252,7 +252,11 @@ async function main() { Object.entries(ipcHandlers).forEach(([ipcHandle, handler]) => { networkService.registerRequestHandler( + // Re-assert type after passing through `forEach`. + // eslint-disable-next-line no-type-assertion/no-type-assertion ipcHandle as SerializedRequestType, + // Handle with an empty event. + // eslint-disable-next-line no-type-assertion/no-type-assertion async (...args: unknown[]) => handler({} as IpcMainInvokeEvent, ...args), ); }); @@ -262,6 +266,8 @@ async function main() { // #region Register test command handlers Object.entries(commandHandlers).forEach(([commandName, handler]) => { + // Re-assert type after passing through `forEach`. + // eslint-disable-next-line no-type-assertion/no-type-assertion commandService.registerCommand(commandName as CommandNames, handler); }); diff --git a/src/main/services/extension-asset-protocol.service.ts b/src/main/services/extension-asset-protocol.service.ts index 347b1f7500..24a344037c 100644 --- a/src/main/services/extension-asset-protocol.service.ts +++ b/src/main/services/extension-asset-protocol.service.ts @@ -32,6 +32,8 @@ function getMimeTypeForFileName(fileName: string): string { const dotIndex = fileName.lastIndexOf('.'); if (dotIndex > 0) { const fileType: string = fileName.substring(dotIndex); + // Assert key type confirmed in check. + // eslint-disable-next-line no-type-assertion/no-type-assertion if (fileType in knownMimeTypes) return knownMimeTypes[fileType as keyof typeof knownMimeTypes]; } diff --git a/src/main/services/server-network-connector.service.ts b/src/main/services/server-network-connector.service.ts index 6857549073..2d467bf6cf 100644 --- a/src/main/services/server-network-connector.service.ts +++ b/src/main/services/server-network-connector.service.ts @@ -298,9 +298,12 @@ export default class ServerNetworkConnector implements INetworkConnector { if (this.connectorInfo.clientId === recipientId) { // This message is from us and for us. Handle the message as if we just received it this.onMessage( + // Fake enough of the message event to handle it. + /* eslint-disable no-type-assertion/no-type-assertion */ { data: message as unknown as string, } as MessageEvent, + /* eslint-enable */ true, ); } else { @@ -315,9 +318,11 @@ export default class ServerNetworkConnector implements INetworkConnector { * @param fromSelf whether this message is from this connector instead of from someone else */ private onMessage = (event: MessageEvent, fromSelf = false) => { - const data = fromSelf - ? (event.data as unknown as Message) - : (JSON.parse(event.data as string) as Message); + const data: Message = fromSelf + ? // Assert our specific message type. + // eslint-disable-next-line no-type-assertion/no-type-assertion + (event.data as unknown as Message) + : JSON.parse(event.data.toString()); // Make sure the client isn't impersonating another client // TODO: consider speeding up validation by passing in webSocket client id? diff --git a/src/node/services/node-file-system.service.ts b/src/node/services/node-file-system.service.ts index e63e778edd..2f84afbe1d 100644 --- a/src/node/services/node-file-system.service.ts +++ b/src/node/services/node-file-system.service.ts @@ -95,7 +95,10 @@ export async function readDir( entryFilter?: (entryName: string) => boolean, ): Promise { const stats = await getStats(uri); - if (!stats || !stats.isDirectory()) return {}; + // Assert return type. + // TODO: this is covering up a potential bug. EITHER make DirectoryEntries properties optional, remove this assert, and fix everything affected. OR it might be better return empty arrays for each property like it does in L120. + // eslint-disable-next-line no-type-assertion/no-type-assertion + if (!stats || !stats.isDirectory()) return {} as DirectoryEntries; const unfilteredDirEntries = await fs.promises.readdir(getPathFromUri(uri), { withFileTypes: true, }); @@ -119,6 +122,8 @@ export async function readDir( if (!entryMap.has(entryType)) entryMap.set(entryType, []); }); + // Assert return type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return Object.freeze(Object.fromEntries(entryMap)) as DirectoryEntries; } diff --git a/src/renderer/components/dialogs/dialog-base.data.ts b/src/renderer/components/dialogs/dialog-base.data.ts index cb9c63ed3b..7648ffea9b 100644 --- a/src/renderer/components/dialogs/dialog-base.data.ts +++ b/src/renderer/components/dialogs/dialog-base.data.ts @@ -147,6 +147,8 @@ export function hookUpDialogService({ const DIALOG_BASE: DialogDefinitionBase = { initialSize: DIALOG_DEFAULT_SIZE, loadDialog(savedTabInfo) { + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion const maybeTabData = savedTabInfo.data as DialogData | undefined; if (!maybeTabData || !maybeTabData.isDialog) logger.error( @@ -156,6 +158,8 @@ const DIALOG_BASE: DialogDefinitionBase = { savedTabInfo, )}`, ); + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion const tabData = maybeTabData as DialogData; return { ...savedTabInfo, @@ -166,7 +170,7 @@ const DIALOG_BASE: DialogDefinitionBase = { minHeight: this.minHeight, // dialogs must define their own Component. It will then be used in this default // implementation of `loadDialog` - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line no-type-assertion/no-type-assertion content: createElement(this.Component!, { ...tabData, submitDialog: (data) => resolveDialogRequest(savedTabInfo.id, data), diff --git a/src/renderer/components/dialogs/index.ts b/src/renderer/components/dialogs/index.ts index 7a703d01b0..2970cf2351 100644 --- a/src/renderer/components/dialogs/index.ts +++ b/src/renderer/components/dialogs/index.ts @@ -11,6 +11,8 @@ const DIALOGS: { [DialogTabType in DialogTabTypes]: DialogDefinition { content: , minWidth: 150, minHeight: 150, + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion data: { errorMessage, } as ErrorTabData, diff --git a/src/renderer/components/docking/platform-dock-layout.component.test.ts b/src/renderer/components/docking/platform-dock-layout.component.test.ts index e98bacd1f3..731eac3f53 100644 --- a/src/renderer/components/docking/platform-dock-layout.component.test.ts +++ b/src/renderer/components/docking/platform-dock-layout.component.test.ts @@ -82,6 +82,7 @@ describe('Dock Layout Component', () => { describe('loadTab()', () => { it('should throw when no id', () => { + // eslint-disable-next-line no-type-assertion/no-type-assertion const savedTabInfoNoId = {} as SavedTabInfo; expect(() => loadTab(savedTabInfoNoId)).toThrow(); @@ -94,6 +95,7 @@ describe('Dock Layout Component', () => { const layout: Layout = { type: 'tab' }; expect(() => + // eslint-disable-next-line no-type-assertion/no-type-assertion addTabToDock('this is wrong' as unknown as SavedTabInfo, layout, dockLayout), ).toThrow(); }); @@ -103,6 +105,7 @@ describe('Dock Layout Component', () => { describe('addWebViewToDock()', () => { it('should throw when no id', () => { const dockLayout = instance(mockDockLayout); + // eslint-disable-next-line no-type-assertion/no-type-assertion const webView = {} as WebViewProps; const layout: Layout = { type: 'tab' }; @@ -114,6 +117,7 @@ describe('Dock Layout Component', () => { when(mockDockLayout.find(anything())).thenReturn(undefined); const dockLayout = instance(mockDockLayout); const webView: WebViewProps = { id: 'myId', webViewType: 'test', content: '' }; + // eslint-disable-next-line no-type-assertion/no-type-assertion const layout = { type: 'wacked' } as unknown as FloatLayout; expect(() => addWebViewToDock(webView, layout, dockLayout)).toThrow(); diff --git a/src/renderer/components/docking/platform-dock-layout.component.tsx b/src/renderer/components/docking/platform-dock-layout.component.tsx index 56708c26af..872c89618f 100644 --- a/src/renderer/components/docking/platform-dock-layout.component.tsx +++ b/src/renderer/components/docking/platform-dock-layout.component.tsx @@ -211,6 +211,8 @@ function saveTab(dockTabInfo: RCDockTabInfo): SavedTabInfo | undefined { * @returns `true` if its a tab or `false` otherwise. */ function isTab(tab: PanelData | TabData | BoxData | undefined): tab is TabData { + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion if (!tab || (tab as TabData).title == null) return false; return true; } @@ -239,7 +241,7 @@ export function getFloatPosition( layoutSize: LayoutSize, ): FloatPosition { // Defaults are added in `web-view.service.ts`. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line no-type-assertion/no-type-assertion const { width, height } = layout.floatSize!; let { left, top } = previousPosition; @@ -267,7 +269,8 @@ function findPreviousTab(dockLayout: DockLayout) { if (previousTab) return previousTab; } // We don't have a previous tab or we didn't find the one we thought we had. Just find the first - // available tab + // available tab. Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return dockLayout.find((tabData) => isTab(tabData)) as TabData; } @@ -337,7 +340,9 @@ export function addTabToDock( targetTab = findPreviousTab(dockLayout); if (targetTab) { if (previousTabId === undefined) - // The target tab is the first found tab, so just add this as a new panel on top + // The target tab is the first found tab, so just add this as a new panel on top. + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion dockLayout.dockMove(tab, targetTab.parent as PanelData, 'top'); // The target tab is a previously added tab, so add this as a tab next to it else dockLayout.dockMove(tab, targetTab, 'after-tab'); @@ -381,13 +386,14 @@ export function addTabToDock( dockLayout.dockMove( tab, - // Add to the parent of the found tab if we found a tab + // Add to the parent of the found tab if we found a tab. Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion (targetTab?.parent as PanelData) ?? // Otherwise find the first thing (the dock box) and add the tab to it dockLayout.find(() => true) ?? null, // Defaults are added in `web-view.service.ts`. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line no-type-assertion/no-type-assertion updatedLayout.direction!, ); break; @@ -395,6 +401,7 @@ export function addTabToDock( default: // Type assert here because TypeScript thinks this layout is `never` because the switch has // covered all its options (if JS were statically typed, this `default` would never hit) + // eslint-disable-next-line no-type-assertion/no-type-assertion throw new LogError(`Unknown layoutType: '${(updatedLayout as Layout).type}'`); } @@ -404,6 +411,8 @@ export function addTabToDock( // happen on startup if (tab.tabType === TAB_TYPE_ERROR) throw new LogError( + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion `Dock Layout created an error tab: ${(tab.data as ErrorTabData)?.errorMessage}`, ); @@ -437,7 +446,7 @@ export function addWebViewToDock( export default function PlatformDockLayout() { // This ref will always be defined - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line no-type-assertion/no-type-assertion const dockLayoutRef = useRef(null!); /** @@ -481,13 +490,17 @@ export default function PlatformDockLayout() { // Type assert `saveTab` as not returning `undefined` because rc-dock's types are wrong // Here, if `saveTab` returns `undefined` the tab is not saved // https://github.com/ticlo/rc-dock/blob/8b6481dca4b4dd07f89107d6f48b1831bbdf0470/src/Serializer.ts#L68 + // eslint-disable-next-line no-type-assertion/no-type-assertion saveTab={saveTab as (dockTabInfo: RCDockTabInfo) => SavedTabInfo} onLayoutChange={(...args) => { const [, currentTabId, direction] = args; // If a dialog was closed, tell the dialog service if (currentTabId && direction === 'remove') { + // Assert the more specific type. + /* eslint-disable no-type-assertion/no-type-assertion */ const removedTab = dockLayoutRef.current.find(currentTabId) as RCDockTabInfo; if ((removedTab.data as DialogData)?.isDialog && hasDialogRequest(currentTabId)) + /* eslint-enable */ resolveDialogRequest(currentTabId, null, false); } diff --git a/src/renderer/components/platform-bible-menu.commands.tsx b/src/renderer/components/platform-bible-menu.commands.tsx index a049f43ff5..b5f4d79070 100644 --- a/src/renderer/components/platform-bible-menu.commands.tsx +++ b/src/renderer/components/platform-bible-menu.commands.tsx @@ -31,6 +31,8 @@ export function handleMenuCommand(command: Command) { logger.info(`TODO: display about'`); break; default: + // Assert the more specific type. + // eslint-disable-next-line no-type-assertion/no-type-assertion commandService.sendCommand(command.command as CommandNames); } } diff --git a/src/renderer/components/platform-bible-toolbar.tsx b/src/renderer/components/platform-bible-toolbar.tsx index e11d950dfc..73bdecd4f8 100644 --- a/src/renderer/components/platform-bible-toolbar.tsx +++ b/src/renderer/components/platform-bible-toolbar.tsx @@ -18,7 +18,7 @@ export default function PlatformBibleToolbar() { return ( - + ); } diff --git a/src/renderer/components/projects/project-list.component.tsx b/src/renderer/components/projects/project-list.component.tsx index daf85048bd..add65314b8 100644 --- a/src/renderer/components/projects/project-list.component.tsx +++ b/src/renderer/components/projects/project-list.component.tsx @@ -1,5 +1,6 @@ import { List, ListItem, ListItemButton, ListItemText, ListSubheader } from '@mui/material'; import { ProjectMetadata } from '@shared/models/project-metadata.model'; +import { ProjectTypes } from 'papi-shared-types'; import { PropsWithChildren, useCallback } from 'react'; export type Project = ProjectMetadata & { @@ -22,6 +23,7 @@ export type Project = ProjectMetadata & { * @returns downloadable (and downloaded) project information */ export function fetchProjects(): Project[] { + /* eslint-disable no-type-assertion/no-type-assertion */ return [ { id: 'project-1', @@ -30,7 +32,7 @@ export function fetchProjects(): Project[] { isDownloadable: true, isDownloaded: false, storageType: 'test', - projectType: 'test', + projectType: 'test' as ProjectTypes, }, { id: 'project-2', @@ -39,7 +41,7 @@ export function fetchProjects(): Project[] { isDownloadable: false, isDownloaded: true, storageType: 'test', - projectType: 'test', + projectType: 'test' as ProjectTypes, }, { id: 'project-3', @@ -48,7 +50,7 @@ export function fetchProjects(): Project[] { isDownloadable: true, isDownloaded: false, storageType: 'test', - projectType: 'test', + projectType: 'test' as ProjectTypes, }, { id: 'project-4', @@ -57,7 +59,7 @@ export function fetchProjects(): Project[] { isDownloadable: false, isDownloaded: false, storageType: 'test', - projectType: 'test', + projectType: 'test' as ProjectTypes, }, { id: 'project-5', @@ -66,9 +68,10 @@ export function fetchProjects(): Project[] { isDownloadable: false, isDownloaded: true, storageType: 'test', - projectType: 'test', + projectType: 'test' as ProjectTypes, }, ]; + /* eslint-enable */ } export type ProjectListProps = PropsWithChildren<{ diff --git a/src/renderer/components/run-basic-checks-dialog/basic-checks.component.tsx b/src/renderer/components/run-basic-checks-dialog/basic-checks.component.tsx index ee9386d9b0..08603fe1bc 100644 --- a/src/renderer/components/run-basic-checks-dialog/basic-checks.component.tsx +++ b/src/renderer/components/run-basic-checks-dialog/basic-checks.component.tsx @@ -5,62 +5,62 @@ export type BasicCheck = { name: string; }; -export function fetchChecks() { +export function fetchChecks(): BasicCheck[] { return [ { name: 'Chapter/Verse Numbers', - } as BasicCheck, + }, { name: 'Markers', - } as BasicCheck, + }, { name: 'Characters (Combinations)', - } as BasicCheck, + }, { name: 'Punctuation (Sequences)', - } as BasicCheck, + }, { name: 'References', - } as BasicCheck, + }, { name: 'Footnote Quotes', - } as BasicCheck, + }, { name: 'Capitalization', - } as BasicCheck, + }, { name: 'Repeated Words', - } as BasicCheck, + }, { name: 'Unmatched Pairs of Punctuation', - } as BasicCheck, + }, { name: 'Quotations', - } as BasicCheck, + }, { name: 'Quotation types', - } as BasicCheck, + }, { name: 'Numbers', - } as BasicCheck, + }, { name: 'Another Example 1', - } as BasicCheck, + }, { name: 'Another Example 2', - } as BasicCheck, + }, { name: 'Another Example 3', - } as BasicCheck, + }, { name: 'Another Example 4', - } as BasicCheck, + }, { name: 'Another Example 5', - } as BasicCheck, + }, { name: 'Another Example 6', - } as BasicCheck, + }, ]; } diff --git a/src/renderer/components/settings-dialog/settings-tab.component.tsx b/src/renderer/components/settings-dialog/settings-tab.component.tsx index 0895c76832..ea785d35d5 100644 --- a/src/renderer/components/settings-dialog/settings-tab.component.tsx +++ b/src/renderer/components/settings-dialog/settings-tab.component.tsx @@ -188,6 +188,7 @@ function InterfaceLanguageSetting({ setting, setSetting }: SettingProps) value={setting} // Type asserting because combobox props aren't precise enough yet // Issue https://github.com/paranext/paranext-core/issues/560 + // eslint-disable-next-line no-type-assertion/no-type-assertion onChange={(_e, v) => setSetting(v as string)} width={200} /> @@ -223,6 +224,7 @@ function ProxySettings({ placeholder="0" value={setting.Port} // This is a mock component- But BUG here because this TextField can take strings but we are forcing it as a number + // eslint-disable-next-line no-type-assertion/no-type-assertion onChange={(e) => setSetting({ ...setting, Port: e.target.value as unknown as number })} /> ) { // Example component of a platform setting function ComboboxSetting({ setting, setSetting }: SettingProps) { + // Type asserting because combobox props aren't precise enough yet + // Issue https://github.com/paranext/paranext-core/issues/560 + // eslint-disable-next-line no-type-assertion/no-type-assertion return setSetting(v as string)} width={200} />; } diff --git a/src/renderer/components/web-view.component.tsx b/src/renderer/components/web-view.component.tsx index 8a39436b1b..3fe0a7d7d7 100644 --- a/src/renderer/components/web-view.component.tsx +++ b/src/renderer/components/web-view.component.tsx @@ -23,7 +23,7 @@ export function getTitle({ webViewType, title, contentType }: Partial(null!); // TODO: We may be catching iframe exceptions moving forward by posting messages from the child @@ -69,6 +69,7 @@ export function loadWebViewTab(savedTabInfo: SavedTabInfo): TabInfo { let data: WebViewProps; if (savedTabInfo.data) { // We need to make sure that the data is of the correct type + // eslint-disable-next-line no-type-assertion/no-type-assertion data = savedTabInfo.data as WebViewProps; if (savedTabInfo.id !== data.id) throw new Error('"id" does not match webView id.'); @@ -99,6 +100,8 @@ export function loadWebViewTab(savedTabInfo: SavedTabInfo): TabInfo { export function saveWebViewTab(tabInfo: TabInfo): SavedTabInfo { return { ...saveTabInfoBase(tabInfo), + // Assert what the unknown `data` type is. + // eslint-disable-next-line no-type-assertion/no-type-assertion data: convertWebViewDefinitionToSaved(tabInfo.data as WebViewDefinition), }; } diff --git a/src/renderer/context/papi-context/test.context.ts b/src/renderer/context/papi-context/test.context.ts index 9165478155..0cf8a0bc56 100644 --- a/src/renderer/context/papi-context/test.context.ts +++ b/src/renderer/context/papi-context/test.context.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; // This should always be defined, so non-null the value -// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +// eslint-disable-next-line no-type-assertion/no-type-assertion const TestContext = createContext(undefined!); export default TestContext; diff --git a/src/renderer/hooks/hook-generators/create-use-data-hook.util.ts b/src/renderer/hooks/hook-generators/create-use-data-hook.util.ts index bf3a948973..de1e82e14f 100644 --- a/src/renderer/hooks/hook-generators/create-use-data-hook.util.ts +++ b/src/renderer/hooks/hook-generators/create-use-data-hook.util.ts @@ -70,6 +70,7 @@ function createUseDataHook( const dataProvider = useDataProviderHook( // Type assertion needed because useDataProviderHook will have different generic types // based on which hook we are generating, but they will all be returning an IDataProvider + // eslint-disable-next-line no-type-assertion/no-type-assertion dataProviderSource as string | IDataProvider | undefined, ); @@ -83,14 +84,15 @@ function createUseDataHook( dataProvider ? async (eventCallback: PapiEventHandler) => { const unsub = - await // We need any here because for some reason IDataProvider loses its ability to index subscribe - ( - ( - dataProvider as /* eslint-disable @typescript-eslint/no-explicit-any */ any - ) /* eslint-enable */[ + // We need any here because for some reason IDataProvider loses its ability to + // index subscribe. Assert to specified generic type. + /* eslint-disable @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion */ + await ( + (dataProvider as any)[ `subscribe${dataType as DataTypeNames}` ] as DataProviderSubscriber )( + /* eslint-enable */ selector, (subscriptionData: TDataTypes[TDataType]['getData']) => { eventCallback(subscriptionData); @@ -119,12 +121,18 @@ function createUseDataHook( () => dataProvider ? async (newData: TDataTypes[TDataType]['setData']) => - // We need any here because for some reason IDataProvider loses its ability to index subscribe + // We need any here because for some reason IDataProvider loses its ability to index + // subscribe. Assert to specified generic type. + /* eslint-disable @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion */ ( - (dataProvider as /* eslint-disable @typescript-eslint/no-explicit-any */ any)[ - /* eslint-enable */ `set${dataType as DataTypeNames}` + (dataProvider as any)[ + `set${dataType as DataTypeNames}` ] as DataProviderSetter - )(selector, newData) + )( + /* eslint-enable */ + selector, + newData, + ) : undefined, [dataProvider, selector], ); @@ -138,7 +146,8 @@ function createUseDataHook( const useData: UseDataHook = new Proxy(useDataCachedHooks, { get(obj, prop) { - // Pass promises through + // Pass promises through. Assert type of `prop` to index `obj`. + // eslint-disable-next-line no-type-assertion/no-type-assertion if (prop === 'then') return obj[prop as keyof typeof obj]; // Special react prop to tell if it's a component @@ -146,6 +155,8 @@ function createUseDataHook( // If we have already generated the hook, return the cached version if (prop in useDataCachedHooks) + // Assert type of `prop` to index `useDataCachedHooks`. + // eslint-disable-next-line no-type-assertion/no-type-assertion return useDataCachedHooks[prop as keyof typeof useDataCachedHooks]; // Build a new useData hook @@ -154,7 +165,7 @@ function createUseDataHook( const newHook = createUseDataHookForDataTypeInternal(prop); // Save the hook in the cache to be used later - useDataCachedHooks[prop as keyof typeof useDataCachedHooks] = newHook; + useDataCachedHooks[prop] = newHook; return newHook; }, diff --git a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts index 77d2d01c88..c47dfaf490 100644 --- a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts @@ -13,8 +13,10 @@ import createUseNetworkObjectHook from '@renderer/hooks/hook-generators/create-u * @type `T` - the type of data provider to return. Use `IDataProvider`, * specifying your own types, or provide a custom data provider type */ + +// We don't know what type the data provider serves +// eslint-disable-next-line no-type-assertion/no-type-assertion const useDataProvider = createUseNetworkObjectHook(dataProviderService.get) as < - // We don't know what type the data provider serves // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends IDataProvider, >( diff --git a/src/renderer/hooks/papi-hooks/use-dialog-callback.hook.ts b/src/renderer/hooks/papi-hooks/use-dialog-callback.hook.ts index aec6141105..64e6942faa 100644 --- a/src/renderer/hooks/papi-hooks/use-dialog-callback.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-dialog-callback.hook.ts @@ -52,6 +52,7 @@ function useDialogCallback< // Since `defaultResponse` could be unspecified which is equivalent to `null`, we need to // type assert to tell TS that `null` will be part of `TResponse` if `defaultResponse` is not // specified but is not otherwise + // eslint-disable-next-line no-type-assertion/no-type-assertion defaultResponse: TResponse = null as TResponse, ): [TResponse, () => Promise, string | undefined, boolean] { // Keep track of whether we're mounted so we don't run stuff after unmount @@ -73,6 +74,7 @@ function useDialogCallback< try { // Looks like we need to type assert here because it can't tell this is a TResponse. It can // just tell that it is the dialog response type, which does not include undefined + // eslint-disable-next-line no-type-assertion/no-type-assertion const dialogResponse = (await dialogService.showDialog( dialogType, options, diff --git a/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts index d320e1552d..33a59c6e30 100644 --- a/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts @@ -14,6 +14,9 @@ import IDataProvider from '@shared/models/data-provider.interface'; * @ProjectType `ProjectType` - the project type for the project to use. The returned project data * provider will have the project data provider type associated with this project type. */ + +// Assert to specific data type for this hook. +// eslint-disable-next-line no-type-assertion/no-type-assertion const useProjectDataProvider = createUseNetworkObjectHook( papiFrontendProjectDataProviderService.getProjectDataProvider, ) as ( diff --git a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts index 73ad94e970..ddf0eab638 100644 --- a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts @@ -121,10 +121,13 @@ type UseProjectDataHook = { * @type `TDataType` - the specific data type on this project you want to use. Must match the data * type specified in `useProjectData.` */ +// Assert the more general and more specific types. +/* eslint-disable no-type-assertion/no-type-assertion */ const useProjectData: UseProjectDataHook = createUseDataHook( useProjectDataProvider as ( dataProviderSource: string | IDataProvider | undefined, ) => IDataProvider | undefined, ) as UseProjectDataHook; +/* eslint-enable */ export default useProjectData; diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 34ba796543..4234cf047e 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -54,7 +54,11 @@ logger.info('Starting renderer'); })(); const container = document.getElementById('root'); -const root = createRoot(container as HTMLElement); +if (!container) { + throw new Error('Document root element not found!'); +} + +const root = createRoot(container); root.render(); // This doesn't run if the renderer has an uncaught exception (which is a good thing) diff --git a/src/renderer/services/dialog.service-host.ts b/src/renderer/services/dialog.service-host.ts index ef94f5752e..23ec410ec0 100644 --- a/src/renderer/services/dialog.service-host.ts +++ b/src/renderer/services/dialog.service-host.ts @@ -169,11 +169,11 @@ async function showDialog( try { // Open dialog - await webViewService.addTab( + await webViewService.addTab( { id: dialogId, tabType: dialogType, - data: { ...options, isDialog: true } as DialogData, + data: { ...options, isDialog: true }, }, { type: 'float', diff --git a/src/renderer/services/papi-frontend.service.ts b/src/renderer/services/papi-frontend.service.ts index 06255fe897..5bad65ba8c 100644 --- a/src/renderer/services/papi-frontend.service.ts +++ b/src/renderer/services/papi-frontend.service.ts @@ -28,6 +28,7 @@ import { DialogService } from '@shared/services/dialog.service-model'; // 1) When adding new services here, consider whether they also belong in papi-backend.service.ts. // 2) We need to provide type assertions for all members so they carry the JSDoc comments on the // papi.d.ts file so extension developers see the comments. Please add to all properties you add. +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion, no-type-assertion/no-type-assertion */ // 3) The "JSDOC DESTINATION" comments are there to provide anchors for JSDocs to be copied in. // Please add to all properties you add. const papi = { diff --git a/src/renderer/services/web-view-state.service.ts b/src/renderer/services/web-view-state.service.ts index 1e9c167f40..3d9b697789 100644 --- a/src/renderer/services/web-view-state.service.ts +++ b/src/renderer/services/web-view-state.service.ts @@ -9,7 +9,7 @@ function loadIfNeeded(): void { const serializedState = localStorage.getItem(WEBVIEW_STATE_KEY); if (!serializedState) return; - const entries = JSON.parse(serializedState) as [[string, Record]]; + const entries: [[string, Record]] = JSON.parse(serializedState); entries.forEach(([key, value]) => { if (key && value) stateMap.set(key, value); }); @@ -70,7 +70,7 @@ export function getWebViewStateById(id: string, stateKey: string): T | undefi if (!id || !stateKey) throw new Error('id and stateKey must be provided to get webview state'); const state: Record = getRecord(id); const stateValue: string | undefined = state[stateKey]; - return stateValue ? (JSON.parse(stateValue) as T) : undefined; + return stateValue ? JSON.parse(stateValue) : undefined; } /** diff --git a/src/renderer/testing/test-layout.data.ts b/src/renderer/testing/test-layout.data.ts index 37bd7c02f9..17bdc69306 100644 --- a/src/renderer/testing/test-layout.data.ts +++ b/src/renderer/testing/test-layout.data.ts @@ -13,6 +13,8 @@ import { TAB_TYPE_BASIC_LIST } from '@renderer/components/basic-list/basic-list. export const FIRST_TAB_ID = 'About'; +// Using `as` here simplifies type changes. +/* eslint-disable no-type-assertion/no-type-assertion */ const testLayout: LayoutBase = { dockbox: { mode: 'horizontal', @@ -108,4 +110,5 @@ const testLayout: LayoutBase = { ], }, }; +/* eslint-enable */ export default testLayout; diff --git a/src/shared/models/data-provider.model.ts b/src/shared/models/data-provider.model.ts index 3300b083a5..3fe187bbd6 100644 --- a/src/shared/models/data-provider.model.ts +++ b/src/shared/models/data-provider.model.ts @@ -215,6 +215,8 @@ export function getDataProviderDataTypeFromFunctionName< const fnPrefix = dataProviderFunctionPrefixes.find((prefix) => fnName.startsWith(prefix)); if (!fnPrefix) throw new Error(`${fnName} is not a data provider data type function`); + // Assert the expected return type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return fnName.substring(fnPrefix.length) as DataTypeNames; } diff --git a/src/shared/models/papi-network-event-emitter.model.ts b/src/shared/models/papi-network-event-emitter.model.ts index cbb17aa579..5d9f0c73a1 100644 --- a/src/shared/models/papi-network-event-emitter.model.ts +++ b/src/shared/models/papi-network-event-emitter.model.ts @@ -48,6 +48,7 @@ export default class PapiNetworkEventEmitter extends PapiEventEmitter { override dispose = () => { const retVal = super.disposeFn(); // TODO: Do we need to set networkSubscriber to undefined? Had to remove readonly from it to do this + // eslint-disable-next-line no-type-assertion/no-type-assertion this.networkSubscriber = undefined as unknown as PapiEventHandler; this.networkDisposer(); return retVal; diff --git a/src/shared/models/project-metadata.model.ts b/src/shared/models/project-metadata.model.ts index b54eb993da..bd52065b76 100644 --- a/src/shared/models/project-metadata.model.ts +++ b/src/shared/models/project-metadata.model.ts @@ -1,3 +1,5 @@ +import { ProjectTypes } from 'papi-shared-types'; + /** * Low-level information describing a project that Platform.Bible directly manages and uses to load project data */ @@ -17,5 +19,5 @@ export type ProjectMetadata = { /** * Indicates what sort of project this is which implies its data shape (e.g., what data streams should be available) */ - projectType: string; + projectType: ProjectTypes; }; diff --git a/src/shared/services/command.service.ts b/src/shared/services/command.service.ts index 7e03ed6998..f1b9887cb6 100644 --- a/src/shared/services/command.service.ts +++ b/src/shared/services/command.service.ts @@ -52,6 +52,8 @@ const sendCommandUnsafe = async ( commandName: CommandName, ...args: Parameters ): Promise>> => { + // This type assertion is needed when the return type is unknown or when it's not Awaited<...>. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, no-type-assertion/no-type-assertion return networkService.request( serializeRequestType(CATEGORY_COMMAND, commandName), ...args, @@ -94,6 +96,8 @@ export const initialize = () => { if (isRenderer()) { // TODO: make a registerRequestHandlers function that we use here and in NetworkService.initialize? const unsubPromises = Object.entries(rendererCommandFunctions).map(([commandName, handler]) => + // Re-assert type of `commandName` even though the type is actually a string ultimately. + // eslint-disable-next-line no-type-assertion/no-type-assertion registerCommandUnsafe(commandName as CommandNames, handler), ); diff --git a/src/shared/services/connection.service.ts b/src/shared/services/connection.service.ts index cab45d9421..0b77f4b770 100644 --- a/src/shared/services/connection.service.ts +++ b/src/shared/services/connection.service.ts @@ -125,13 +125,14 @@ export const disconnect = () => { const handleInternalRequest: InternalRequestHandler = async ( requestType: string, incomingRequest: InternalRequest, -) => { +): Promise> => { if (!requestHandler) throw new Error('Handling request without a requestHandler!'); // Not sure if it's really responsible to put the whole incomingRequest in. Might want to destructure and just pass ComplexRequest members const response = await requestHandler( // We don't require requestType to be SerializedRequestType in this service, but they should all // be SerializedRequestType if they are all registered as such. Doesn't matter much to us here + // eslint-disable-next-line no-type-assertion/no-type-assertion requestType as SerializedRequestType, incomingRequest, ); @@ -140,7 +141,7 @@ const handleInternalRequest: InternalRequestHandler = async ( senderId: clientId, requesterId: incomingRequest.senderId, requestId: incomingRequest.requestId, - } as InternalResponse; + }; }; /** diff --git a/src/shared/services/data-provider.service.ts b/src/shared/services/data-provider.service.ts index 7beb641f46..73b27d4d73 100644 --- a/src/shared/services/data-provider.service.ts +++ b/src/shared/services/data-provider.service.ts @@ -142,11 +142,13 @@ function createDataProviderSubscriber( // TypeScript seems to be unable to figure out these `get${dataType}` types when we wrap // DataProviderInternal in NetworkObject to make IDataProvider, so we have to do all this work // to specify the specific types + /* eslint-disable no-type-assertion/no-type-assertion */ const data = await ( (dataProvider as unknown as DataProviderInternal)[ `get${dataType}` ] as DataProviderGetter )(selector); + /* eslint-enable */ // Take note that we have received an update so we don't run the callback with the old data below in the `retrieveDataImmediately` code receivedUpdate = true; @@ -169,11 +171,13 @@ function createDataProviderSubscriber( // TypeScript seems to be unable to figure out these `get${dataType}` types when we wrap // DataProviderInternal in NetworkObject to make IDataProvider, so we have to do all this work // to specify the specific types + /* eslint-disable no-type-assertion/no-type-assertion */ const data = await ( (dataProvider as unknown as DataProviderInternal)[ `get${dataType}` ] as DataProviderGetter )(selector); + /* eslint-enable */ // Only run the callback with this updated data if we have not already received an update so we don't accidentally overwrite the newly updated data with old data if (!receivedUpdate) { receivedUpdate = true; @@ -205,12 +209,17 @@ function createDataProviderProxy( // TODO: update network objects so remote objects know when methods do not exist, then make IDataProvider.set optional const dataProviderInternal: Partial> = {}; - // Create a proxy that runs the data provider method if it exists or runs the engine method otherwise + // Create a proxy that runs the data provider method if it exists or runs the engine method + // otherwise. + // Type assert the data provider engine proxy because it is a DataProviderInternal. + // eslint-disable-next-line no-type-assertion/no-type-assertion const dataProvider = new Proxy( + // eslint-disable-next-line no-type-assertion/no-type-assertion dataProviderEngine ?? (dataProviderInternal as IDataProviderEngine), { get(obj, prop) { - // Pass promises through + // Pass promises through. Assert type of `prop` to index `obj`. + // eslint-disable-next-line no-type-assertion/no-type-assertion if (prop === 'then') return obj[prop as keyof typeof obj]; // Do not let anyone but the data provider engine send updates @@ -219,6 +228,8 @@ function createDataProviderProxy( // If the data provider already has the method, run it if (prop in dataProviderInternal) + // Assert type of `prop` to index `dataProviderInternal`. + // eslint-disable-next-line no-type-assertion/no-type-assertion return dataProviderInternal[prop as keyof typeof dataProviderInternal]; /** Figure out the method that will go on the data provider to run */ @@ -243,7 +254,7 @@ function createDataProviderProxy( // There isn't indexing on IDataProviderEngine so normal objects could be used, // but now members can't be accessed by indexing in DataProviderService // TODO: fix it so it is indexable but can have specific members - newDataProviderMethod = // eslint-disable-next-line @typescript-eslint/no-explicit-any + newDataProviderMethod = // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion (obj[prop as keyof typeof obj] as IDataProviderEngine[any])?.bind( dataProviderEngine, ); @@ -254,7 +265,7 @@ function createDataProviderProxy( // There isn't indexing on IDataProviderEngine so normal objects could be used, // but now members can't be accessed by indexing in DataProviderService // TODO: fix it so it is indexable but can have specific members - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion (dataProviderInternal as any)[prop] = newDataProviderMethod; } return newDataProviderMethod; @@ -270,7 +281,7 @@ function createDataProviderProxy( return false; // If we cached a property previously, purge the cache for that property since it is changing. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion if ((dataProviderInternal as any)[prop]) delete (dataProviderInternal as any)[prop]; // Actually set the provided property @@ -284,7 +295,6 @@ function createDataProviderProxy( return prop in obj; }, }, - // Type assert the data provider engine proxy because it is a DataProviderInternal ) as DataProviderInternal; return dataProvider; @@ -306,7 +316,7 @@ function mapUpdateInstructionsToUpdateEvent]; + if (isString(updateInstructions)) return [updateInstructions]; if (Array.isArray(updateInstructions)) { // If the update instructions are a non-empty array, send it if (updateInstructions.length > 0) return updateInstructions; @@ -366,7 +376,7 @@ function ignore( if (typeof target === 'function') target.isIgnored = true; else { // We don't care what type the decorated object is. Just want to set some function metadata - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion (target[member as keyof T] as any).isIgnored = true; } } @@ -411,7 +421,7 @@ function buildDataProvider( (fnName) => { // If the function was decorated with @ignore, do not consider it a special function // We don't care about types. We just want to check the decorator - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion if ((dataProviderEngine as any)[fnName].isIgnored) return 'other'; if (fnName.startsWith('get')) return 'get'; @@ -420,6 +430,7 @@ function buildDataProvider( }, (fnName, fnType) => { // If it's not a get or a set, just return an empty string. We aren't planning to use this + // eslint-disable-next-line no-type-assertion/no-type-assertion if (fnType === 'other') return '' as DataTypeNames; // Grab the data type out of the function names @@ -456,12 +467,14 @@ function buildDataProvider( // if they return true dataTypes.get('set')?.forEach((dataType) => { if (dataProviderEngine[`set${dataType}`]) { + /* eslint-disable no-type-assertion/no-type-assertion */ /** Saved bound version of the data provider engine's set so we can call it from here */ const dpeSet = ( dataProviderEngine[`set${dataType}`] as DataProviderSetter ).bind(dataProviderEngine); /** Layered set that emits an update event after running the engine's set */ (dataProviderEngine[`set${dataType}`] as DataProviderSetter) = + /* eslint-enable */ async (...args) => { const dpeSetResult = await dpeSet(...args); const updateEventResult = mapUpdateInstructionsToUpdateEvent( @@ -532,8 +545,9 @@ async function registerEngine( const disposableDataProvider: IDisposableDataProvider = await networkObjectService.set(dataProviderObjectId, dataProviderInternal); - // Get the local network object proxy for the data provider so the provider can't be disposed outside - // the service that registered the provider engine + // Get the local network object proxy for the data provider so the provider can't be disposed + // outside the service that registered the provider engine. Assert type without NetworkObject. + // eslint-disable-next-line no-type-assertion/no-type-assertion const dataProvider = (await networkObjectService.get>( dataProviderObjectId, )) as IDataProvider; @@ -562,6 +576,8 @@ function createLocalDataProviderToProxy( const onDidUpdate = networkService.getNetworkEvent( serializeRequestType(dataProviderObjectId, ON_DID_UPDATE), ); + // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return createDataProviderProxy(undefined, dataProviderPromise, onDidUpdate) as Partial; } @@ -578,11 +594,13 @@ async function get>(providerName: string): Promise< // Get the object id for this data provider name const dataProviderObjectId = getDataProviderObjectId(providerName); - // Get the network object for this data provider + // Get the network object for this data provider. Assert to specified generic type. + /* eslint-disable no-type-assertion/no-type-assertion */ const dataProvider = (await networkObjectService.get( dataProviderObjectId, createLocalDataProviderToProxy as LocalObjectToProxyCreator, )) as T | undefined; + /* eslint-enable */ if (!dataProvider) { logger.info(`No data provider found with name = ${providerName}`); diff --git a/src/shared/services/graphql.service.ts b/src/shared/services/graphql.service.ts index c4732f7f03..9c4399e15e 100644 --- a/src/shared/services/graphql.service.ts +++ b/src/shared/services/graphql.service.ts @@ -29,18 +29,26 @@ const usfmSchema = buildSchema(` } `); -// There are probably frameworks that can be used to automatically convert between GraphQL and TS/JS types -// We should figure out how we want to use GraphQL in more detail before deciding which ones (if any) to use -function extractVerseRef(verseRef: object): VerseRef { +type MaybeVerseRef = { + book?: string; + chapter?: string; + verse?: string; + versification?: string; +}; + +// There are probably frameworks that can be used to automatically convert between GraphQL and TS/JS +// types. We should figure out how we want to use GraphQL in more detail before deciding which ones +// (if any) to use. +function extractVerseRef(maybeVerseRef: MaybeVerseRef): VerseRef { let versification: ScrVers | undefined; - if ('versification' in verseRef) versification = new ScrVers(verseRef.versification as string); - if ('book' in verseRef) { - const book = verseRef.book as string; - const chapter = 'chapter' in verseRef ? (verseRef.chapter as string) : '1'; - const verse = 'verse' in verseRef ? (verseRef.verse as string) : '1'; + if (maybeVerseRef.versification) versification = new ScrVers(maybeVerseRef.versification); + if (maybeVerseRef.book) { + const { book } = maybeVerseRef; + const chapter = maybeVerseRef.chapter ?? '1'; + const verse = maybeVerseRef.verse ?? '1'; return new VerseRef(book, chapter, verse, versification); } - throw new Error(`Invalid verseRef: ${verseRef}`); + throw new Error(`Invalid verseRef: ${maybeVerseRef}`); } // Caching some objects so we don't have to keep making network calls for every GraphQL query @@ -107,7 +115,11 @@ async function runQuery(query: string): Promise { if (key === 'dispose') return undefined; + // Assert type of `key` to index `target`. + // eslint-disable-next-line no-type-assertion/no-type-assertion if (key === 'then' || key in target) return target[key as keyof typeof target]; // If onDidDispose wasn't found in the target already, don't create a remote proxy for it if (key === 'onDidDispose') return undefined; @@ -198,13 +200,13 @@ const createRemoteProxy = ( // Took the indexing off of NetworkableObject so normal objects could be used, // but now members can't be accessed by indexing in NetworkObjectService // TODO: fix it so it is indexable but can have specific members - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion (target as any)[key] = requestFunction; return requestFunction; }, set(obj, prop, value) { // If we cached a property previously, purge the cache for that property since it is changing. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion if ((obj as any)[prop]) delete (obj as any)[prop]; // Actually set the provided property @@ -294,6 +296,8 @@ const get = async ( // If we already have this network object, return it const networkObjectRegistration = networkObjectRegistrations.get(id); if (networkObjectRegistration) + // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return networkObjectRegistration.networkObject as NetworkObject; // We don't already have this network object. See if it exists somewhere else. @@ -305,6 +309,8 @@ const get = async ( // this process called `set` on the object we were looking for. const networkObjectRegistrationSecondChance = networkObjectRegistrations.get(id); if (networkObjectRegistrationSecondChance) + // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return networkObjectRegistrationSecondChance.networkObject as NetworkObject; // At this point, the object exists remotely but does not yet exist locally. @@ -320,7 +326,9 @@ const get = async ( // If a property exists on the base object, we use it and won't look for it on the remote object. // If a property does not exist on the base object, it is assumed to exist on the remote object. const baseObject: Partial = createLocalObjectToProxy - ? (createLocalObjectToProxy(id, networkObjectVariable.promise) as Partial) + ? // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion + (createLocalObjectToProxy(id, networkObjectVariable.promise) as Partial) : {}; // Create a proxy with functions that will send requests to the remote object @@ -330,7 +338,9 @@ const get = async ( const eventEmitter = new PapiEventEmitter(); overrideOnDidDispose(id, remoteProxy.proxy, eventEmitter.event); - // The network object is finished! Rename it so we know it is finished + // The network object is finished! Rename it so we know it is finished. + // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion const networkObject = remoteProxy.proxy as NetworkObject; // Save the network object for future lookups @@ -390,7 +400,7 @@ const set = async ( networkService.registerRequestHandler( getNetworkObjectRequestType(id, NetworkObjectRequestSubtype.Function), (functionName: string, ...args: unknown[]) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion Promise.resolve((objectToShare as any)[functionName](...args)), ), ]; @@ -450,12 +460,16 @@ const set = async ( networkObjectRegistrations.set(id, { registrationType: NetworkObjectRegistrationType.Local, onDidDisposeEmitter: onDidDisposeLocalEmitter, + // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion networkObject: localProxy.proxy as NetworkObject, revokeProxy: localProxy.revoke, }); // Override objectToShare's type's force-undefined onDidDispose to DisposableNetworkObject's // onDidDispose type because it had an onDidDispose added in overrideOnDidDispose. + // Assert to specified generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return objectToShare as Omit, 'dispose'> as DisposableNetworkObject; }); }; diff --git a/src/shared/services/network.service.ts b/src/shared/services/network.service.ts index 029309301e..b320e5c43a 100644 --- a/src/shared/services/network.service.ts +++ b/src/shared/services/network.service.ts @@ -43,14 +43,18 @@ let isInitialized = false; /** Promise that resolves when this service is finished initializing */ let initializePromise: Promise | undefined; -/** Map of requestType to registered handler for that request or (on server) information about which connection to send the request */ +/** + * Map of requestType to registered handler for that request or (on server) information about which + * connection to send the request + */ const requestRegistrations = new Map(); /** - * Map from event type to the emitter for that type as well as if that emitter is "registered" aka one - * reference to that emitter has been provided somewhere such that that event can be emitted from that one place. - * NetworkEventEmitter types should not occur multiple times so extensions cannot emit events they - * shouldn't, so we have a quick and easy no sharing in process rule in createNetworkEventEmitter. + * Map from event type to the emitter for that type as well as if that emitter is "registered" aka + * one reference to that emitter has been provided somewhere such that that event can be emitted + * from that one place. NetworkEventEmitter types should not occur multiple times so extensions + * cannot emit events they shouldn't, so we have a quick and easy no sharing in process rule in + * createNetworkEventEmitter. * TODO: sync these between processes */ const networkEventEmitters = new Map< @@ -66,7 +70,10 @@ type LocalRequestRegistration = { handler: RoutedRequestHandler | RoutedRequestHandler; }; -/** Request handler that is not on this network service and must be requested on the network. Server-only as clients will all just send to the server */ +/** + * Request handler that is not on this network service and must be requested on the network. + * Server-only as clients will all just send to the server + */ type RemoteRequestRegistration = { registrationType: 'remote'; requestType: SerializedRequestType; @@ -175,8 +182,8 @@ const requestRawUnsafe = async ( ); // https://github.com/paranext/paranext-core/issues/51 - // If the request type doesn't have a registered handler yet, retry a few times to help with race conditions - // This approach is hacky but works well enough for now + // If the request type doesn't have a registered handler yet, retry a few times to help with race + // conditions. This approach is hacky but works well enough for now. const expectedErrorMsg: string = `No handler was found to process the request of type ${requestType}`; const maxAttempts: number = 10; for (let attemptsRemaining = maxAttempts; attemptsRemaining > 0; attemptsRemaining--) { @@ -218,11 +225,14 @@ const requestUnsafe = async , TReturn>( /** * Unregisters a local request handler from running on requests. * - * WARNING: DO NOT USE OUTSIDE OF INITIALIZATION. Use unregisterRequestHandler (not created yet as it may never be necessary) + * WARNING: DO NOT USE OUTSIDE OF INITIALIZATION. Use unregisterRequestHandler (not created yet as + * it may never be necessary) * @param requestType the type of request from which to unregister the handler * @param handler function to unregister from running on requests - * @returns true if successfully unregistered, false if registration not found or trying to unregister a handler that is not local. Throws if provided handler is not the correct handler - * Likely will never need to be exported from this file. Just use registerRequestHandler, which returns a matching unsubscriber function that runs this. + * @returns true if successfully unregistered, false if registration not found or trying to + * unregister a handler that is not local. Throws if provided handler is not the correct handler + * Likely will never need to be exported from this file. Just use registerRequestHandler, which + * returns a matching unsubscriber function that runs this. */ async function unregisterRequestHandlerUnsafe( requestType: SerializedRequestType, @@ -238,11 +248,14 @@ async function unregisterRequestHandlerUnsafe( return false; if (requestRegistration.registrationType === 'remote') - // The request handler is someone else's to unregister. Is this egregious enough that we should throw here? This only really happens if you're the server right now as the server holds remote handlers + // The request handler is someone else's to unregister. Is this egregious enough that we should + // throw here? This only really happens if you're the server right now as the server holds + // remote handlers return false; if (requestRegistration.handler !== handler) - // Somehow the handlers do not match. Probably can't happen unless you call this function directly which shouldn't happen. Is this egregious enough that we should throw? I guess...? + // Somehow the handlers do not match. Probably can't happen unless you call this function + // directly which shouldn't happen. Is this egregious enough that we should throw? I guess...? throw new Error(`Handler to unsubscribe from ${requestType} does not match registered handler`); // Check with the server to make sure we can unregister this registration @@ -269,8 +282,10 @@ async function unregisterRequestHandlerUnsafe( * WARNING: DO NOT USE OUTSIDE OF INITIALIZATION. Use registerRequestHandler * @param requestType the type of request on which to register the handler * @param handler function to register to run on requests - * @param handlerType type of handler function - indicates what type of parameters and what return type the handler has - * @returns promise that resolves if the request successfully registered and unsubscriber function to run to stop the passed-in function from handling requests + * @param handlerType type of handler function - indicates what type of parameters and what return + * type the handler has + * @returns promise that resolves if the request successfully registered and unsubscriber function + * to run to stop the passed-in function from handling requests */ async function registerRequestHandlerUnsafe( requestType: SerializedRequestType, @@ -315,7 +330,8 @@ async function registerRequestHandlerUnsafe( ); } - // We have successfully checked that this is the first registration for this requestType. Set up the handler + // We have successfully checked that this is the first registration for this requestType. Set up + // the handler requestRegistrations.set(requestType, { registrationType: 'local', requestType, @@ -362,10 +378,11 @@ const removeNetworkEventEmitterInternal = (eventType: string): boolean => * * WARNING: DO NOT USE OUTSIDE OF INITIALIZATION. Use createNetworkEventEmitter * @param eventType unique network event type for coordinating between processes - * @param emitOnNetwork the function to use to emit the event on the network. Defaults to emitEventOnNetworkUnsafe. - * Should only need to provide this in createNetworkEventEmitter to make this function safe. - * @param register whether to register the emitter aka whether one reference to the emitter has been released - * and therefore the emitter should not be distributed anymore + * @param emitOnNetwork the function to use to emit the event on the network. Defaults to + * emitEventOnNetworkUnsafe. Should only need to provide this in createNetworkEventEmitter to make + * this function safe. + * @param register whether to register the emitter aka whether one reference to the emitter has been + * released and therefore the emitter should not be distributed anymore * @returns event emitter whose event works between processes */ const createNetworkEventEmitterUnsafe = ( @@ -378,6 +395,8 @@ const createNetworkEventEmitterUnsafe = ( if (existingEmitter.isRegistered) throw new Error(`type ${eventType} is already registered to a network event emitter`); existingEmitter.isRegistered = register; + // Assert as emitter with this generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return existingEmitter.emitter as PapiEventEmitter; } const newNetworkEventEmitter = new PapiNetworkEventEmitter( @@ -385,6 +404,8 @@ const createNetworkEventEmitterUnsafe = ( () => removeNetworkEventEmitterInternal(eventType), ); networkEventEmitters.set(eventType, { + // Assert as emitter with an unknown type. + // eslint-disable-next-line no-type-assertion/no-type-assertion emitter: newNetworkEventEmitter as PapiNetworkEventEmitter, isRegistered: register, }); @@ -413,7 +434,8 @@ const unregisterRemoteRequestHandler = async ( return false; if (requestRegistration.registrationType === 'local' || requestRegistration.clientId !== clientId) - // The request handler is not theirs to unregister. Is this egregious enough that we should throw here? + // The request handler is not theirs to unregister. Is this egregious enough that we should + // throw here? return false; // We can unregister this handler! Remove it from the registrations @@ -441,7 +463,8 @@ const registerRemoteRequestHandler = async ( validateRequestTypeFormatting(requestType); - // Once we have checked that this is the first registration for this requestType, set up the handler + // Once we have checked that this is the first registration for this requestType, set up the + // handler requestRegistrations.set(requestType, { registrationType: 'remote', requestType, @@ -488,7 +511,8 @@ let unsubscribeServerRequestHandlers: UnsubscriberAsync | undefined; // #region functions passed down to INetworkConnector in initialize and helpers for those functions /** - * Calls the appropriate request handler according to the request type and returns a promise of the response + * Calls the appropriate request handler according to the request type and returns a promise of the + * response * @param requestType type of request to handle * @param request the request to handle * @returns promise of response to the request @@ -499,9 +523,11 @@ const handleRequestLocal: RequestHandler = async ( ): Promise> => { const registration = requestRegistrations.get(requestType); - // Result should always be defined if success is true (and not defined if success is false), which seems to be the case in this function. - // However, for some reason, TypeScript can't seem to tell that result is defined if success is true. + // Result should always be defined if success is true (and not defined if success is false), which + // seems to be the case in this function. However, for some reason, TypeScript can't seem to tell + // that result is defined if success is true. // So we will just coerce it to start undefined but pretend it's TReturn. + // eslint-disable-next-line no-type-assertion/no-type-assertion let result: TReturn = undefined as unknown as TReturn; let success = false; let errorMessage = ''; @@ -515,11 +541,16 @@ const handleRequestLocal: RequestHandler = async ( switch (registration.handlerType) { case RequestHandlerType.Args: try { + // Assert type according to `handlerType`. + // eslint-disable-next-line no-type-assertion/no-type-assertion + const handler = registration.handler as ArgsRequestHandler; result = await (incomingRequest.contents - ? (registration.handler as ArgsRequestHandler)( + ? handler( + // Assert `contents` type to some unknown array. + // eslint-disable-next-line no-type-assertion/no-type-assertion ...(incomingRequest.contents as unknown as unknown[]), ) - : (registration.handler as ArgsRequestHandler)()); + : handler()); success = true; } catch (e) { errorMessage = getErrorMessage(e); @@ -527,6 +558,8 @@ const handleRequestLocal: RequestHandler = async ( break; case RequestHandlerType.Contents: try { + // Assert type according to `handlerType`. + // eslint-disable-next-line no-type-assertion/no-type-assertion result = await (registration.handler as ContentsRequestHandler)(incomingRequest.contents); success = true; } catch (e) { @@ -535,8 +568,12 @@ const handleRequestLocal: RequestHandler = async ( break; case RequestHandlerType.Complex: { try { + // Assert type according to `handlerType`. + // eslint-disable-next-line no-type-assertion/no-type-assertion const response = await (registration.handler as ComplexRequestHandler)(incomingRequest); - // Break out the contents of the ComplexResponse to use existing variables. Should we destructure instead to future-proof for other fields? It was not playing well with Typescript + // Break out the contents of the ComplexResponse to use existing variables. Should we + // destructure instead to future-proof for other fields? It was not playing well with + // Typescript. success = response.success; if (response.success) result = response.contents; else errorMessage = response.errorMessage; @@ -570,7 +607,8 @@ const handleRequestLocal: RequestHandler = async ( const routeRequest: RequestRouter = (requestType: string): number => { const registration = requestRegistrations.get(requestType); if (!registration) - // We are the client and we need to send the request to the server or we are the server and we need to return an error + // We are the client and we need to send the request to the server or we are the server and we + // need to return an error return CLIENT_ID_SERVER; if (registration.registrationType === 'local') // We will handle this request here @@ -638,6 +676,8 @@ export const initialize = () => { const registrationUnsubscribers = Object.entries(serverRequestHandlers).map( ([requestType, handler]) => + // Re-assert type after passing through `map`. + // eslint-disable-next-line no-type-assertion/no-type-assertion registerRequestHandlerUnsafe(requestType as SerializedRequestType, handler), ); // Wait to successfully register all requests @@ -686,8 +726,10 @@ const registerRequestHandlerInternal = createSafeRegisterFn( * Register a local request handler to run on requests. * @param requestType the type of request on which to register the handler * @param handler function to register to run on requests - * @param handlerType type of handler function - indicates what type of parameters and what return type the handler has - * @returns promise that resolves if the request successfully registered and unsubscriber function to run to stop the passed-in function from handling requests + * @param handlerType type of handler function - indicates what type of parameters and what return + * type the handler has + * @returns promise that resolves if the request successfully registered and unsubscriber function + * to run to stop the passed-in function from handling requests */ export function registerRequestHandler( requestType: SerializedRequestType, @@ -734,9 +776,10 @@ const emitEventOnNetwork = async (eventType: string, event: T) => { * @returns event emitter whose event works between connections */ export const createNetworkEventEmitter = (eventType: string): PapiEventEmitter => - // Note: running createNetworkEventEmitterUnsafe without initializing is not technically an initialization - // problem. However, emitting a network event before initializing is. As such, we create an emitter here - // without awaiting initialization, but we pass in emitEventOnNetwork, which does wait for initialization. + // Note: running createNetworkEventEmitterUnsafe without initializing is not technically an + // initialization problem. However, emitting a network event before initializing is. As such, we + // create an emitter here without awaiting initialization, but we pass in emitEventOnNetwork, + // which does wait for initialization. createNetworkEventEmitterUnsafe(eventType, emitEventOnNetwork); /** @@ -746,8 +789,13 @@ export const createNetworkEventEmitter = (eventType: string): PapiEventEmitte */ export const getNetworkEvent = (eventType: string): PapiEvent => { const existingEmitter = networkEventEmitters.get(eventType); + // Return event with the generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion if (existingEmitter) return existingEmitter.emitter.event as PapiEvent; - // We didn't find an existing emitter, so create one but don't mark it as registered because you can't emit the event from this function + // We didn't find an existing emitter, so create one but don't mark it as registered because you + // can't emit the event from this function. + // Return event with the generic type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return createNetworkEventEmitterUnsafe(eventType, emitEventOnNetwork, false) .event as PapiEvent; }; @@ -758,7 +806,8 @@ export const getNetworkEvent = (eventType: string): PapiEvent => { * Creates a function that is a request function with a baked requestType. * This is also nice because you get TypeScript type support using this function. * @param requestType requestType for request function - * @returns function to call with arguments of request that performs the request and resolves with the response contents + * @returns function to call with arguments of request that performs the request and resolves with + * the response contents */ export const createRequestFunction = , TReturn>( requestType: SerializedRequestType, diff --git a/src/shared/services/project-data-provider.service.ts b/src/shared/services/project-data-provider.service.ts index 7bbc01434e..bc0a9e1480 100644 --- a/src/shared/services/project-data-provider.service.ts +++ b/src/shared/services/project-data-provider.service.ts @@ -99,7 +99,7 @@ export async function getProjectDataProvider( projectId: string, ): Promise { const metadata = await projectLookupService.getMetadataForProject(projectId); - const projectType = metadata.projectType as keyof ProjectDataTypes; + const { projectType } = metadata; const pdpFactoryId: string = getProjectDataProviderFactoryId(projectType); const pdpFactory = await networkObjectService.get>( pdpFactoryId, diff --git a/src/shared/services/settings.service.ts b/src/shared/services/settings.service.ts index 3eb54c961c..008e7dbab7 100644 --- a/src/shared/services/settings.service.ts +++ b/src/shared/services/settings.service.ts @@ -13,7 +13,8 @@ const onDidUpdateSettingEmitters = new Map< /** * Retrieves the value of the specified setting * @param key The string id of the setting for which the value is being retrieved - * @returns The value of the specified setting, parsed to an object. Returns `null` if setting is not present or no value is available + * @returns The value of the specified setting, parsed to an object. Returns `null` if setting is + * not present or no value is available */ const getSetting = ( key: SettingName, @@ -25,13 +26,16 @@ const getSetting = ( /** * Sets the value of the specified setting * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `null` is the equivalent of deleting the setting + * @param newSetting The value that is to be stored. Setting the new value to `null` is the + * equivalent of deleting the setting */ const setSetting = ( key: SettingName, newSetting: Nullable, ) => { localStorage.setItem(key, JSON.stringify(newSetting)); + // Assert type of the particular SettingName of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion const emitter = onDidUpdateSettingEmitters.get(key) as | PapiEventEmitter> | undefined; @@ -39,7 +43,8 @@ const setSetting = ( }; /** - * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the callback function is executed. + * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the + * callback function is executed. * @param key The string id of the setting for which the value is being subscribed to * @param callback The function that will be called whenever the specified setting is updated * @returns Unsubscriber that should be called whenever the subscription should be deleted @@ -48,11 +53,15 @@ const subscribeToSetting = ( key: SettingName, callback: (newSetting: Nullable) => void, ): Unsubscriber => { + // Assert type of the particular SettingName of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion let emitter = onDidUpdateSettingEmitters.get(key) as | PapiEventEmitter> | undefined; if (!emitter) { emitter = new PapiEventEmitter>(); + // Assert type of the general SettingTypes of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion onDidUpdateSettingEmitters.set(key, emitter as PapiEventEmitter>); } return emitter.subscribe(callback); diff --git a/src/shared/services/web-view.service.ts b/src/shared/services/web-view.service.ts index 131b1f6f79..0bd8c72b3f 100644 --- a/src/shared/services/web-view.service.ts +++ b/src/shared/services/web-view.service.ts @@ -364,8 +364,8 @@ export const removeTab = async (tabId: string): Promise => { * * Not exposed on the papi */ -export const addTab = async ( - savedTabInfo: SavedTabInfo, +export const addTab = async ( + savedTabInfo: SavedTabInfo & { data?: TData }, layout: Layout, ): Promise => { return (await papiDockLayoutVar.promise).addTabToDock(savedTabInfo, layout); @@ -448,6 +448,8 @@ export const getWebView = async ( let existingSavedWebView: SavedWebViewDefinition | undefined; // Look for existing webview if (optionsDefaulted.existingId) { + // Expect this to be a tab. + // eslint-disable-next-line no-type-assertion/no-type-assertion const existingWebView = (await papiDockLayoutVar.promise).dockLayout.find( optionsDefaulted.existingId === '?' ? // If they provided '?', that means look for any webview with a matching webViewType @@ -455,7 +457,8 @@ export const getWebView = async ( // This is not a webview if (!('data' in item)) return false; - // Find any webview with the specified webViewType + // Find any webview with the specified webViewType. Type assert the unknown `data`. + // eslint-disable-next-line no-type-assertion/no-type-assertion return (item.data as WebViewDefinition).webViewType === webViewType; } : // If they provided any other string, look for a webview with that id @@ -464,6 +467,8 @@ export const getWebView = async ( if (existingWebView) { // We found the webview! Save it to send to the web view provider existingSavedWebView = convertWebViewDefinitionToSaved( + // Type assert the unknown `data`. + // eslint-disable-next-line no-type-assertion/no-type-assertion existingWebView.data as WebViewDefinition, ); // Load the web view state since the web view provider doesn't have access to the data store @@ -534,6 +539,8 @@ export const getWebView = async ( specificSrcPolicy = "'unsafe-inline'"; break; default: { + // Defaults to React webview definition. + // eslint-disable-next-line no-type-assertion/no-type-assertion const reactWebView = webView as WebViewDefinitionReact; // Add the component as a script @@ -674,6 +681,8 @@ function removeForbiddenElements(mutationList: MutationRecord[]) { m.addedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return; + // This is an element node. + // eslint-disable-next-line no-type-assertion/no-type-assertion const element = node as Element; const tag = element.tagName.toLowerCase(); @@ -762,6 +771,8 @@ export const initialize = () => { // Register built-in requests // TODO: make a registerRequestHandlers function that we use here and in NetworkService.initialize? const unsubPromises = Object.entries(rendererRequestHandlers).map(([requestType, handler]) => + // Fix type after passing through the `map` function. + // eslint-disable-next-line no-type-assertion/no-type-assertion networkService.registerRequestHandler(requestType as SerializedRequestType, handler), ); diff --git a/src/shared/utils/papi-util.test.ts b/src/shared/utils/papi-util.test.ts index 204b43c4a5..13e134e41f 100644 --- a/src/shared/utils/papi-util.test.ts +++ b/src/shared/utils/papi-util.test.ts @@ -32,13 +32,16 @@ describe('PAPI Utils', () => { it('will throw on deserialize with no separator', () => { const CATEGORY = 'myCategory'; + // eslint-disable-next-line no-type-assertion/no-type-assertion expect(() => deserializeRequestType(CATEGORY as SerializedRequestType)).toThrow(); }); it('will throw on serialize if either input is undefined or empty', () => { const CATEGORY = 'myCategory'; const DIRECTIVE = 'myDirective'; + // eslint-disable-next-line no-type-assertion/no-type-assertion const undefStr = undefined as unknown as string; + // eslint-disable-next-line no-type-assertion/no-type-assertion const nullStr = null as unknown as string; expect(() => serializeRequestType('', DIRECTIVE)).toThrow(); diff --git a/src/shared/utils/util.ts b/src/shared/utils/util.ts index b0b753be90..076a5be657 100644 --- a/src/shared/utils/util.ts +++ b/src/shared/utils/util.ts @@ -45,6 +45,8 @@ export function isString(o: unknown): o is string { export function debounce void>(fn: T, delay = 300): T { if (isString(fn)) throw new Error('Tried to debounce a string! Could be XSS'); let timeout: ReturnType; + // Ensure the right return type. + // eslint-disable-next-line no-type-assertion/no-type-assertion return ((...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); @@ -90,6 +92,8 @@ function isErrorWithMessage(error: unknown): error is ErrorWithMessage { typeof error === 'object' && error !== null && 'message' in error && + // Type assert `error` to check it's `message`. + // eslint-disable-next-line no-type-assertion/no-type-assertion typeof (error as Record).message === 'string' ); } diff --git a/src/stories/table.stories.tsx b/src/stories/table.stories.tsx index 79113f137a..71cb1686da 100644 --- a/src/stories/table.stories.tsx +++ b/src/stories/table.stories.tsx @@ -497,6 +497,8 @@ export const MiscellaneousFunctions: Story = { onCopy: ({ sourceRow, sourceColumnKey }: TableCopyEvent) => { if (window.isSecureContext) { + // CopyEvent in `react-data-grid` should declare its property as `sourceColumnKey: keyof TRow` + // eslint-disable-next-line no-type-assertion/no-type-assertion navigator.clipboard.writeText(sourceRow[sourceColumnKey as keyof Row]); } }, @@ -513,6 +515,8 @@ export const MiscellaneousFunctions: Story = { return targetRow; } + // CopyEvent in `react-data-grid` should declare its property as `sourceColumnKey: keyof TRow` + // eslint-disable-next-line no-type-assertion/no-type-assertion return { ...targetRow, [targetColumnKey]: sourceRow[sourceColumnKey as keyof Row] }; }, },