Merge changes Iaac671d4,If4390c22 * changes: eslint: synthesize node_modules for Bazel lint_test Revert "Suppress no-unnecessary-type-assertion for intentional assertion"
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts index afd4856..e1762c4 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -495,7 +495,6 @@ if (!this.groupMembers) return; const el = e.target as GrButton; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const index = Number(el.getAttribute('data-index')!); const keys = this.groupMembers[index]; const item = @@ -547,7 +546,6 @@ if (!this.includedGroups) return; const el = e.target as GrButton; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const index = Number(el.getAttribute('data-index')!); const keys = this.includedGroups[index];
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts index 208b4da..31a107a 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1616,7 +1616,6 @@ } override updated() { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const tabs = [...queryAll<HTMLElement>(this.tabs!, 'md-secondary-tab')]; const tabIndex = tabs.findIndex(t => t.dataset['name'] === this.activeTab);
diff --git a/polygerrit-ui/app/elements/chat-panel/chat-panel_test.ts b/polygerrit-ui/app/elements/chat-panel/chat-panel_test.ts index 777ce3c..84a2052 100644 --- a/polygerrit-ui/app/elements/chat-panel/chat-panel_test.ts +++ b/polygerrit-ui/app/elements/chat-panel/chat-panel_test.ts
@@ -128,7 +128,6 @@ const policy = element.shadowRoot!.querySelector('.ai-policy'); assert.isOk(policy); assert.include( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion policy.textContent!, 'Review agent may display inaccurate info' );
diff --git a/polygerrit-ui/app/elements/chat-panel/citations-box_test.ts b/polygerrit-ui/app/elements/chat-panel/citations-box_test.ts index 74e0a4c..09b1192 100644 --- a/polygerrit-ui/app/elements/chat-panel/citations-box_test.ts +++ b/polygerrit-ui/app/elements/chat-panel/citations-box_test.ts
@@ -179,7 +179,6 @@ '.citations-summary-message' ); assert.isOk(summary); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion assert.include(summary.textContent!, '2 citations'); const items = element.shadowRoot?.querySelectorAll('.citation-item');
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts index df027c9..6e3d7cc 100644 --- a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts +++ b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
@@ -132,7 +132,6 @@ assert.isFalse(element.isExpanded); const summaryDiv: HTMLElement = - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion element.shadowRoot!.querySelector('.summary')!; summaryDiv.click(); await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts index eb7d77b..5f5e1775 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
@@ -235,7 +235,6 @@ private showKey(e: Event) { const el = e.target as GrButton; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const index = Number(el.getAttribute('data-index')!); this.keyToView = this.keys[index]; this.viewKeyModal.showModal(); @@ -243,7 +242,6 @@ private handleDeleteKey(e: Event) { const el = e.target as GrButton; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const index = Number(el.getAttribute('data-index')!); this.keysToRemove.push(this.keys[index]); this.keys.splice(index, 1);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts index 0d101ab..40c1774 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -471,7 +471,6 @@ await element.updateComplete; const chips = element.accountChips; const chipsOneSpy = sinon.spy(chips[1], 'focus'); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(input.input!, 'ArrowLeft'); assert.isTrue(chipsOneSpy.called); const chipsZeroSpy = sinon.spy(chips[0], 'focus');
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts index b577f0d..3230e48 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
@@ -204,7 +204,6 @@ const stub = sinon.stub(element.cursor, 'next'); assertIsDefined(element.dropdown); assert.isFalse(element.dropdown.open); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(element!.shadowRoot!.querySelector('#trigger')!, 'ArrowDown'); await element.updateComplete; assert.isTrue(element.dropdown.open); @@ -220,7 +219,6 @@ assertIsDefined(element.dropdown); const stub = sinon.stub(element.cursor, 'previous'); assert.isFalse(element.dropdown.open); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(element!.shadowRoot!.querySelector('#trigger')!, 'ArrowUp'); await element.updateComplete; assert.isTrue(element.dropdown.open); @@ -237,7 +235,6 @@ // Because enter and space are handled by the same fn, we need only to // test one. assert.isFalse(element.dropdown.open); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(element!.shadowRoot!.querySelector('#trigger')!, ' '); await element.updateComplete; assert.isTrue(element.dropdown.open);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts index a4ea783..3108166 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
@@ -274,7 +274,6 @@ await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(autocomplete.input!, Key.ESC); await waitUntil(() => autocomplete.suggestionsDropdown!.isHidden); @@ -286,11 +285,9 @@ await element.open(); await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden); // Press esc to close suggestions. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(autocomplete.input!, Key.ESC); await waitUntil(() => autocomplete.suggestionsDropdown!.isHidden); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(autocomplete.input!, Key.ESC); await element.updateComplete; @@ -309,7 +306,6 @@ await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden); await autocomplete.latestSuggestionUpdateComplete; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(autocomplete.input!, Key.ENTER); await waitUntil(() => autocomplete.suggestionsDropdown!.isHidden); @@ -331,12 +327,10 @@ await autocomplete.latestSuggestionUpdateComplete; // Press enter to close suggestions. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(autocomplete.input!, Key.ENTER); await waitUntil(() => autocomplete.suggestionsDropdown!.isHidden); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion pressKey(autocomplete.input!, Key.ENTER); await element.updateComplete;
diff --git a/polygerrit-ui/app/eslint-bazel.config.js b/polygerrit-ui/app/eslint-bazel.config.js index ad7690e..9724124 100644 --- a/polygerrit-ui/app/eslint-bazel.config.js +++ b/polygerrit-ui/app/eslint-bazel.config.js
@@ -4,13 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -// This file has a special settings for bazel. -// The settings is required because bazel uses different location -// for node_modules. +// Bazel-specific ESLint wrapper. +// +// This file is intentionally thin: +// - `eslint.config.js` remains the source of truth for rules +// - this file only adapts module resolution for Bazel runfiles +// +// In Bazel test mode, npm dependencies are located under runfiles +// directories such as ui_dev_npm/node_modules. Extend the Node resolver +// so that ESLint can locate those packages. const {defineConfig, globalIgnores} = require('eslint/config'); const js = require('@eslint/js'); const {FlatCompat} = require('@eslint/eslintrc'); +const path = require('path'); +const fs = require('fs'); const compat = new FlatCompat({ baseDirectory: __dirname, @@ -18,21 +26,50 @@ allConfig: js.configs.all, }); -function getBazelSettings() { - const runFilesDir = process.env['RUNFILES_DIR']; - if (!runFilesDir) { - // eslint is executed with 'bazel run ...' to fix the source code. It runs - // against real source code, no special paths for node_modules is set. - return {}; +function pathExists(p) { + try { + return fs.existsSync(p); + // eslint-disable-next-line no-unused-vars + } catch (unusedError) { + return false; } - // eslint is executed with 'bazel test...'. Set path to required node_modules +} + +function getRunfilesRoot() { + return process.env.RUNFILES_DIR || process.env.TEST_SRCDIR || ''; +} + +function getNodeModulesPaths() { + const runfilesRoot = getRunfilesRoot(); + + if (runfilesRoot) { + // Bazel test mode: collect node_modules from runfiles + return [ + path.join(runfilesRoot, 'ui_dev_npm/node_modules'), + path.join(runfilesRoot, 'ui_npm/node_modules'), + path.join(runfilesRoot, '_main/external/ui_dev_npm/node_modules'), + path.join(runfilesRoot, '_main/external/ui_npm/node_modules'), + path.join(runfilesRoot, '_main/polygerrit-ui/app/node_modules'), + ].filter(pathExists); + } + + // Workspace mode + return [ + path.join(__dirname, 'node_modules'), + path.join(__dirname, '../../node_modules'), + path.join(process.cwd(), 'node_modules'), + path.join(process.cwd(), '../../node_modules'), + ].filter(pathExists); +} + +function getBazelSettings() { + const paths = getNodeModulesPaths(); + if (paths.length === 0) return {}; + return { 'import/resolver': { node: { - paths: [ - `${runFilesDir}/ui_npm/node_modules`, - `${runFilesDir}/ui_dev_npm/node_modules`, - ], + paths, }, }, };
diff --git a/tools/js/eslint-chdir.js b/tools/js/eslint-chdir.js index 5aea704..d9fd479 100644 --- a/tools/js/eslint-chdir.js +++ b/tools/js/eslint-chdir.js
@@ -15,16 +15,118 @@ * limitations under the License. */ -// Eslint 7 introduced a breaking change - it uses the current workdir instead -// of the configuration file directory for resolving relative paths: -// https://eslint.org/docs/user-guide/migrating-to-7.0.0#base-path-change -// This file is loaded before the eslint and sets the current directory -// back to the location of configuration file. +// ESLint resolves relative paths from the current working directory. +// +// In workspace mode (`lint_bin`), this works with the regular Node.js module +// layout under `polygerrit-ui/app/node_modules`. +// +// In Bazel test mode (`lint_test`), npm dependencies are exposed through +// multiple runfiles trees instead. Typed linting with TypeScript does not see +// the same module and type environment from that layout as from the workspace +// layout. +// +// To align `lint_test` with `lint_bin`, synthesize a local +// `polygerrit-ui/app/node_modules` by symlinking entries from the runfiles +// npm trees, then prepend it to NODE_PATH so resolution matches workspace mode. +const fs = require('fs'); +const Module = require('module'); const path = require('path'); -const configParamIndex = - process.argv.findIndex(arg => arg === '-c' || arg === '---config'); -if (configParamIndex >= 0 && configParamIndex + 1 < process.argv.length) { - const dirName = path.dirname(process.argv[configParamIndex + 1]); - process.chdir(dirName); + +function pathExists(filePath) { + try { + return fs.existsSync(filePath); + } catch { + return false; + } } + +function readDirEntries(dirPath) { + try { + return fs.readdirSync(dirPath, {withFileTypes: true}); + } catch { + return []; + } +} + +function ensureDir(dirPath) { + fs.mkdirSync(dirPath, {recursive: true}); +} + +function symlinkIfMissing(targetPath, linkPath) { + if (pathExists(linkPath)) return; + + try { + fs.symlinkSync(targetPath, linkPath); + } catch { + // Ignore races and pre-existing entries. + } +} + +function mergeNodeModules(destinationDir, sourceDirs) { + ensureDir(destinationDir); + + for (const sourceDir of sourceDirs) { + for (const entry of readDirEntries(sourceDir)) { + const sourceEntry = path.join(sourceDir, entry.name); + const destEntry = path.join(destinationDir, entry.name); + + if (entry.name.startsWith('@') && entry.isDirectory()) { + ensureDir(destEntry); + + for (const scopedEntry of readDirEntries(sourceEntry)) { + symlinkIfMissing( + path.join(sourceEntry, scopedEntry.name), + path.join(destEntry, scopedEntry.name) + ); + } + continue; + } + + symlinkIfMissing(sourceEntry, destEntry); + } + } +} + +function getRunfilesRoot() { + return process.env.RUNFILES_DIR || process.env.TEST_SRCDIR || ''; +} + +function getRunfilesNodeModules(runfilesRoot) { + return [ + path.join(runfilesRoot, 'ui_dev_npm/node_modules'), + path.join(runfilesRoot, 'ui_npm/node_modules'), + path.join(runfilesRoot, '_main/node_modules'), + ].filter(pathExists); +} + +function prependNodePath(paths) { + const existing = process.env.NODE_PATH + ? process.env.NODE_PATH.split(path.delimiter).filter(Boolean) + : []; + + process.env.NODE_PATH = [...paths, ...existing].join(path.delimiter); + Module._initPaths(); +} + +function getConfigDirFromArgv(argv) { + const configArgIndex = argv.findIndex(arg => arg === '-c' || arg === '--config'); + if (configArgIndex < 0 || configArgIndex + 1 >= argv.length) return ''; + + return path.dirname(argv[configArgIndex + 1]); +} + +const configDir = getConfigDirFromArgv(process.argv); +if (!configDir) return; + +process.chdir(configDir); + +const runfilesRoot = getRunfilesRoot(); +if (!runfilesRoot) return; + +const runfilesNodeModules = getRunfilesNodeModules(runfilesRoot); +if (runfilesNodeModules.length === 0) return; + +const localNodeModules = path.join(process.cwd(), 'node_modules'); +mergeNodeModules(localNodeModules, runfilesNodeModules); +prependNodePath([localNodeModules, ...runfilesNodeModules]);