From 46d7adefe08e5fde1400261278449dccd082d0f4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 3 Dec 2025 03:13:16 +0100 Subject: [PATCH] Enable TypeScript `strictNullChecks` (#35843) A big step towards enabling strict mode in Typescript. There was definitely a good share of potential bugs while refactoring this. When in doubt, I opted to keep the potentially broken behaviour. Notably, the `DOMEvent` type is gone, it was broken and we're better of with type assertions on `e.target`. --------- Signed-off-by: silverwind Signed-off-by: wxiaoguang Co-authored-by: delvh Co-authored-by: wxiaoguang --- eslint.config.ts | 2 +- tsconfig.json | 2 +- web_src/js/bootstrap.ts | 2 +- web_src/js/components/ActivityHeatmap.vue | 4 +- web_src/js/components/ContextPopup.vue | 4 +- web_src/js/components/DashboardRepoList.vue | 4 +- web_src/js/components/DiffCommitSelector.vue | 10 +- web_src/js/components/DiffFileTree.vue | 12 +- web_src/js/components/RepoActionView.vue | 4 +- .../js/components/RepoActivityTopAuthors.vue | 6 +- .../js/components/RepoBranchTagSelector.vue | 45 +++--- web_src/js/components/RepoContributors.vue | 14 +- web_src/js/components/RepoFileSearch.vue | 14 +- web_src/js/components/ViewFileTree.vue | 6 +- web_src/js/components/ViewFileTreeItem.vue | 2 +- web_src/js/components/ViewFileTreeStore.ts | 7 +- web_src/js/features/admin/common.ts | 38 ++--- web_src/js/features/admin/config.ts | 2 +- web_src/js/features/admin/selfcheck.ts | 4 +- web_src/js/features/captcha.ts | 4 +- web_src/js/features/citation.ts | 6 +- web_src/js/features/clipboard.ts | 7 +- web_src/js/features/colorpicker.ts | 15 +- web_src/js/features/common-button.ts | 18 +-- web_src/js/features/common-fetch-action.ts | 2 +- web_src/js/features/common-form.ts | 8 +- web_src/js/features/common-issue-list.ts | 11 +- web_src/js/features/common-organization.ts | 2 +- web_src/js/features/common-page.ts | 4 +- .../js/features/comp/ComboMarkdownEditor.ts | 53 +++---- web_src/js/features/comp/Cropper.ts | 11 +- .../js/features/comp/EditorMarkdown.test.ts | 2 +- web_src/js/features/comp/EditorMarkdown.ts | 7 +- web_src/js/features/comp/EditorUpload.ts | 11 +- web_src/js/features/comp/LabelEdit.ts | 28 ++-- web_src/js/features/comp/ReactionSelector.ts | 9 +- web_src/js/features/comp/SearchUserBox.ts | 2 +- web_src/js/features/comp/TextExpander.ts | 3 +- web_src/js/features/comp/WebHookEditor.ts | 9 +- web_src/js/features/copycontent.ts | 2 +- web_src/js/features/dropzone.ts | 17 +-- .../js/features/eventsource.sharedworker.ts | 15 +- web_src/js/features/file-view.ts | 6 +- web_src/js/features/heatmap.ts | 2 +- web_src/js/features/imagediff.ts | 16 +-- web_src/js/features/install.ts | 56 ++++---- web_src/js/features/notification.ts | 6 +- web_src/js/features/oauth2-settings.ts | 8 +- web_src/js/features/pull-view-file.ts | 20 +-- web_src/js/features/repo-branch.ts | 18 +-- web_src/js/features/repo-code.ts | 22 +-- web_src/js/features/repo-commit.ts | 4 +- web_src/js/features/repo-common.ts | 18 +-- web_src/js/features/repo-diff-commit.ts | 14 +- web_src/js/features/repo-diff.ts | 41 +++--- web_src/js/features/repo-editor.ts | 36 ++--- web_src/js/features/repo-graph.ts | 6 +- web_src/js/features/repo-home.ts | 20 +-- web_src/js/features/repo-issue-content.ts | 8 +- web_src/js/features/repo-issue-edit.ts | 42 +++--- web_src/js/features/repo-issue-list.ts | 30 ++-- web_src/js/features/repo-issue-pull.ts | 16 +-- .../features/repo-issue-sidebar-combolist.ts | 18 +-- web_src/js/features/repo-issue-sidebar.ts | 12 +- web_src/js/features/repo-issue.ts | 133 +++++++++--------- web_src/js/features/repo-legacy.ts | 4 +- web_src/js/features/repo-migrate.ts | 10 +- web_src/js/features/repo-migration.ts | 4 +- web_src/js/features/repo-milestone.ts | 4 +- web_src/js/features/repo-new.ts | 28 ++-- web_src/js/features/repo-projects.ts | 40 +++--- web_src/js/features/repo-release.ts | 18 +-- web_src/js/features/repo-search.ts | 6 +- .../features/repo-settings-branches.test.ts | 6 +- web_src/js/features/repo-settings-branches.ts | 4 +- web_src/js/features/repo-settings.ts | 32 ++--- web_src/js/features/repo-unicode-escape.ts | 4 +- web_src/js/features/repo-view-file-tree.ts | 6 +- web_src/js/features/repo-wiki.ts | 4 +- web_src/js/features/sshkey-helper.ts | 2 +- web_src/js/features/tablesort.ts | 6 +- web_src/js/features/user-auth-webauthn.ts | 10 +- web_src/js/features/user-auth.ts | 2 +- web_src/js/features/user-settings.ts | 6 +- web_src/js/htmx.ts | 4 +- web_src/js/markup/anchors.ts | 4 +- web_src/js/markup/codecopy.ts | 2 +- web_src/js/markup/mermaid.ts | 2 +- web_src/js/markup/refissue.ts | 2 +- web_src/js/markup/render-iframe.ts | 2 +- web_src/js/markup/tasklist.ts | 14 +- web_src/js/modules/diff-file.ts | 4 +- web_src/js/modules/fetch.ts | 4 +- web_src/js/modules/fomantic/tab.ts | 2 +- web_src/js/modules/observer.ts | 4 +- web_src/js/modules/sortable.ts | 4 +- web_src/js/modules/tippy.ts | 6 +- web_src/js/modules/toast.ts | 9 +- web_src/js/standalone/devtest.ts | 10 +- .../js/standalone/external-render-iframe.ts | 2 +- web_src/js/standalone/swagger.ts | 2 +- web_src/js/types.ts | 2 +- web_src/js/utils.ts | 36 ++--- web_src/js/utils/dom.test.ts | 6 +- web_src/js/utils/dom.ts | 25 ++-- web_src/js/webcomponents/absolute-date.ts | 8 +- web_src/js/webcomponents/origin-url.ts | 2 +- web_src/js/webcomponents/overflow-menu.ts | 12 +- 108 files changed, 686 insertions(+), 658 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index c849cdbc62..c2fddc856c 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -205,7 +205,7 @@ export default defineConfig([ '@typescript-eslint/no-non-null-asserted-optional-chain': [2], '@typescript-eslint/no-non-null-assertion': [0], '@typescript-eslint/no-redeclare': [0], - '@typescript-eslint/no-redundant-type-constituents': [0], // rule does not properly work without strickNullChecks + '@typescript-eslint/no-redundant-type-constituents': [2], '@typescript-eslint/no-require-imports': [2], '@typescript-eslint/no-restricted-imports': [0], '@typescript-eslint/no-restricted-types': [0], diff --git a/tsconfig.json b/tsconfig.json index 1daf4b7233..2466faf592 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,7 +40,7 @@ "strictBindCallApply": true, "strictBuiltinIteratorReturn": true, "strictFunctionTypes": true, - "strictNullChecks": false, + "strictNullChecks": true, "stripInternal": true, "verbatimModuleSyntax": true, "types": [ diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index 4d3f39f5bf..a94e1d66b0 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -35,7 +35,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); - msgDiv.querySelector('.ui.message').textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); + msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); msgContainer.prepend(msgDiv); } diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index d805817630..7c7e0cd94c 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -5,7 +5,7 @@ import {onMounted, shallowRef} from 'vue'; import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap'; defineProps<{ - values?: HeatmapValue[]; + values: HeatmapValue[]; locale: { textTotalContributions: string; heatMapLocale: Partial; @@ -28,7 +28,7 @@ const endDate = shallowRef(new Date()); onMounted(() => { // work around issue with first legend color being rendered twice and legend cut off - const legend = document.querySelector('.vch__external-legend-wrapper'); + const legend = document.querySelector('.vch__external-legend-wrapper')!; legend.setAttribute('viewBox', '12 0 80 10'); legend.style.marginRight = '-12px'; }); diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 31db902adc..733144aae1 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -11,15 +11,17 @@ const props = defineProps<{ }>(); const loading = shallowRef(false); -const issue = shallowRef(null); +const issue = shallowRef(null); const renderedLabels = shallowRef(''); const errorMessage = shallowRef(''); const createdAt = computed(() => { + if (!issue?.value) return ''; return new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); }); const body = computed(() => { + if (!issue?.value) return ''; const body = issue.value.body.replace(/\n+/g, ' '); return body.length > 85 ? `${body.substring(0, 85)}…` : body; }); diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index e938814ec6..e1f8475ea8 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -110,9 +110,9 @@ export default defineComponent({ }, mounted() { - const el = document.querySelector('#dashboard-repo-list'); + const el = document.querySelector('#dashboard-repo-list')!; this.changeReposFilter(this.reposFilter); - fomanticQuery(el.querySelector('.ui.dropdown')).dropdown(); + fomanticQuery(el.querySelector('.ui.dropdown')!).dropdown(); this.textArchivedFilterTitles = { 'archived': this.textShowOnlyArchived, diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index e9aa3c6744..fcc7af1fa0 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -23,7 +23,7 @@ type CommitListResult = { export default defineComponent({ components: {SvgIcon}, data: () => { - const el = document.querySelector('#diff-commit-select'); + const el = document.querySelector('#diff-commit-select')!; return { menuVisible: false, isLoading: false, @@ -35,7 +35,7 @@ export default defineComponent({ mergeBase: el.getAttribute('data-merge-base'), commits: [] as Array, hoverActivated: false, - lastReviewCommitSha: '', + lastReviewCommitSha: '' as string | null, uniqueIdMenu: generateElemId('diff-commit-selector-menu-'), uniqueIdShowAll: generateElemId('diff-commit-selector-show-all-'), }; @@ -165,7 +165,7 @@ export default defineComponent({ }, /** Called when user clicks on since last review */ changesSinceLastReviewClick() { - window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`); + window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1)!.id}${this.queryParams}`); }, /** Clicking on a single commit opens this specific commit */ commitClicked(commitId: string, newWindow = false) { @@ -193,7 +193,7 @@ export default defineComponent({ // find all selected commits and generate a link const firstSelected = this.commits.findIndex((x) => x.selected); const lastSelected = this.commits.findLastIndex((x) => x.selected); - let beforeCommitID: string; + let beforeCommitID: string | null = null; if (firstSelected === 0) { beforeCommitID = this.mergeBase; } else { @@ -204,7 +204,7 @@ export default defineComponent({ if (firstSelected === lastSelected) { // if the start and end are the same, we show this single commit window.location.assign(`${this.issueLink}/commits/${afterCommitID}${this.queryParams}`); - } else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1).id) { + } else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1)!.id) { // if the first commit is selected and the last commit is selected, we show all commits window.location.assign(`${this.issueLink}/files${this.queryParams}`); } else { diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 981d10c1c1..e2934b967e 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -12,14 +12,14 @@ const store = diffTreeStore(); onMounted(() => { // Default to true if unset store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false'; - document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', toggleVisibility); + document.querySelector('.diff-toggle-file-tree-button')!.addEventListener('click', toggleVisibility); hashChangeListener(); window.addEventListener('hashchange', hashChangeListener); }); onUnmounted(() => { - document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', toggleVisibility); + document.querySelector('.diff-toggle-file-tree-button')!.removeEventListener('click', toggleVisibility); window.removeEventListener('hashchange', hashChangeListener); }); @@ -33,7 +33,7 @@ function expandSelectedFile() { if (store.selectedItem) { const box = document.querySelector(store.selectedItem); const folded = box?.getAttribute('data-folded') === 'true'; - if (folded) setFileFolding(box, box.querySelector('.fold-file'), false); + if (folded) setFileFolding(box, box.querySelector('.fold-file')!, false); } } @@ -48,10 +48,10 @@ function updateVisibility(visible: boolean) { } function updateState(visible: boolean) { - const btn = document.querySelector('.diff-toggle-file-tree-button'); + const btn = document.querySelector('.diff-toggle-file-tree-button')!; const [toShow, toHide] = btn.querySelectorAll('.icon'); - const tree = document.querySelector('#diff-file-tree'); - const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text'); + const tree = document.querySelector('#diff-file-tree')!; + const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text')!; btn.setAttribute('data-tooltip-content', newTooltip); toggleElem(tree, visible); toggleElem(toShow, !visible); diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 00748ee9bb..357a2ba10e 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -402,7 +402,7 @@ export default defineComponent({ } // auto-scroll to the last log line of the last step - let autoScrollJobStepElement: HTMLElement; + let autoScrollJobStepElement: HTMLElement | undefined; for (let stepIndex = 0; stepIndex < this.currentJob.steps.length; stepIndex++) { if (!autoScrollStepIndexes.get(stepIndex)) continue; autoScrollJobStepElement = this.getJobStepLogsContainer(stepIndex); @@ -468,7 +468,7 @@ export default defineComponent({ } const logLine = this.elStepsContainer().querySelector(selectedLogStep); if (!logLine) return; - logLine.querySelector('.line-num').click(); + logLine.querySelector('.line-num')!.click(); }, }, }); diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 5a925f9943..1d04fa5239 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,7 +1,7 @@