Skip to content

Commit 8e331e9

Browse files
authored
chore(lint): let the linter see tests/ and scripts/ (#920)
`npm run lint` was `eslint src`, so two whole trees were never linted. Across the fleet that hid roughly 3,900 errors, none of which any CI leg had shown. Most of it was the config, not the code, and the same two defects were in every app. **scripts/ had no config block at all.** These are CommonJS Node CLI checkers, and flat config defaults every `.js` to ESM with browser-ish globals, so eslint read the CommonJS wrapper itself as undefined identifiers: `require`, `process`, `__dirname`, `__filename`, `module`. A `scripts/**` block now declares the environment. Declaring beats suppressing here: `no-undef` is the rule that catches a genuinely misspelled identifier, and dozens of fake findings would bury a real one. A second block covers `scripts/**/*.mjs`, which is ESM and needs Node's globals without the CommonJS wrapper. **The tests block applied a non-TypeScript-aware rule to TypeScript.** It named `tests/**/*.ts` while setting the CORE `no-unused-vars`, which v9 deliberately turns off for `.ts` in favour of the `@typescript-eslint` version. The core rule reads the parameter names inside a function TYPE as bindings, so t?: (app: string, key: string) => string reports `app` and `key` as unused variables, and every unused `catch (e)` in a `.ts` spec reports twice. The block is split now: `.js`/`.mjs` on the core rule, `.ts`/`.tsx` on the TypeScript one, same patterns on both. Also: stale `eslint-disable` comments naming plugins eslint 10 no longer registers, which are themselves errors ("Definition for rule ... was not found"), and a rule that must not parse shell scripts. The genuinely real findings were the useful part: dead locals, unused imports, dead helper functions, unused `catch` bindings, extensionless relative imports, and a handful of `== null` comparisons spelled out so they still match null AND undefined. Verified per app: `npm run lint` 0 errors over src + tests + scripts, `prettier --check` clean, and the unit suite still green. One finding here was a real defect rather than tidiness. `no-dupe-keys` flagged this assertion: expect(moderationItemTitle({ name: ' ', name: 'Real' })).toBe('Real') The second `name` silently overwrites the first, so the object actually built was `{ name: 'Real' }` and the test named "ignores blank/whitespace title fields" never took the blank branch at all. It could not have failed for the reason it claimed. It now passes `{ name: ' ', title: 'Real' }`, which does exercise the fall-through. `TITLE_FIELDS` carried the same slip, listing `'name'` twice, so the second entry was dead. Removed. The suite is 5 passed.
1 parent c775a5b commit 8e331e9

60 files changed

Lines changed: 391 additions & 251 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

eslint.config.mjs

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,32 @@ export default [
144144
'no-console': 'off',
145145
'n/no-process-exit': 'off',
146146
'n/hashbang': 'off',
147-
// `_` / `__` as a deliberate throwaway binding — `catch (_)`, a
148-
// discarded destructuring slot. Narrow on purpose: the pattern matches
149-
// UNDERSCORES ONLY, so a real name that happens to start with `_` is
150-
// still reported. v9 drives plain `.js` through the CORE rule (the
151-
// `@typescript-eslint` swap is per-file-type), so it is set here.
147+
// Tests import devDependencies by definition; this rule is about what
148+
// ships in the published package, which tests/ never does.
149+
'n/no-unpublished-import': 'off',
150+
},
151+
},
152+
153+
{
154+
// `_` / `__` as a deliberate throwaway binding — `catch (_)`, a discarded
155+
// destructuring slot. Narrow on purpose: the pattern matches UNDERSCORES
156+
// ONLY, so a real name that happens to start with `_` is still reported.
157+
//
158+
// 🔴 `.js` / `.mjs` ONLY, NOT `.ts`. The CORE rule is not TypeScript-aware:
159+
// applied to a `.ts` file it reads the parameter NAMES inside a function
160+
// TYPE as bindings and reports them unused. Measured on humaniq —
161+
//
162+
// t?: (app: string, key: string) => string
163+
//
164+
// produced four `no-unused-vars` errors for `app` and `key`, which are
165+
// documentation, not variables. The same mis-scoping made every unused
166+
// `catch (e)` in a `.ts` spec report TWICE, once per rule.
167+
//
168+
// v9 already turns the core rule off for `.ts` and drives
169+
// `@typescript-eslint/no-unused-vars` instead; naming `.ts` here switched
170+
// it back on. TypeScript files are handled by the block below.
171+
files: ['tests/**/*.js', 'tests/**/*.mjs'],
172+
rules: {
152173
'no-unused-vars': [
153174
'error',
154175
{
@@ -165,12 +186,117 @@ export default [
165186
ignoreRestSiblings: true,
166187
},
167188
],
168-
// Tests import devDependencies by definition; this rule is about what
169-
// ships in the published package, which tests/ never does.
170-
'n/no-unpublished-import': 'off',
171189
},
172190
},
173191

192+
{
193+
// The TypeScript half of the block above. Same intent, same patterns, on
194+
// the rule that actually understands the language: it knows a name inside
195+
// a function type is not a binding, so type annotations stay quiet while a
196+
// genuinely dead local is still reported.
197+
files: ['tests/**/*.ts', 'tests/**/*.tsx'],
198+
rules: {
199+
'@typescript-eslint/no-unused-vars': [
200+
'error',
201+
{
202+
varsIgnorePattern: '^_+$',
203+
caughtErrors: 'all',
204+
caughtErrorsIgnorePattern: '^_+$',
205+
argsIgnorePattern: '^_',
206+
ignoreRestSiblings: true,
207+
},
208+
],
209+
},
210+
},
211+
212+
{
213+
// 🔴 Node-side CLI tooling under `scripts/`, which is COMMONJS. Flat
214+
// config defaults every `.js` to ESM with browser-ish globals, so without
215+
// this block eslint reports the CommonJS wrapper itself as undefined
216+
// identifiers. Measured on this app: 52 of the 233 errors under
217+
// `tests/` + `scripts/` were `no-undef`, ALL of them in `scripts/`, and
218+
// all five names were the environment rather than a typo — `process` 23,
219+
// `require` 20, `__dirname` 6, `__filename` 2, `module` 1.
220+
//
221+
// This is describing the environment, not relaxing a rule, and it is the
222+
// same argument the test-globals block below makes: declaring them keeps
223+
// `no-undef` able to do its real job, which is catching a genuinely
224+
// misspelled identifier. Suppressing the rule instead would bury that.
225+
//
226+
// `no-console` is off because printing its report is what a CLI checker
227+
// is FOR.
228+
//
229+
// 🔴 NO `n/*` ENTRIES HERE, DELIBERATELY. `eslint-plugin-n` is NOT
230+
// registered for these files under eslint 10 + @nextcloud/eslint-config
231+
// 9, so `'n/no-process-exit': 'off'` would be dead config that reads as
232+
// if it were doing something. Measured both ways on this app: 0 `n/`
233+
// findings with the entries and 0 without.
234+
//
235+
// What DID report was the opposite — four `scripts/*.js` carried
236+
// `/* eslint-disable n/no-process-exit */` and `/* eslint-disable
237+
// n/shebang */` left over from the eslintrc era, and an inline disable
238+
// naming an unregistered plugin is itself an error ("Definition for rule
239+
// 'n/shebang' was not found"). Those 8 comments are removed; do not add
240+
// `n/*` rules back to replace them.
241+
//
242+
// ⚠️ `.js` and `.cjs` ONLY. A `scripts/*.mjs` is genuinely ESM and must
243+
// keep the default `sourceType`, or `import` stops parsing there.
244+
files: ['scripts/**/*.js', 'scripts/**/*.cjs'],
245+
languageOptions: {
246+
sourceType: 'commonjs',
247+
globals: {
248+
require: 'readonly',
249+
module: 'writable',
250+
exports: 'writable',
251+
process: 'readonly',
252+
__dirname: 'readonly',
253+
__filename: 'readonly',
254+
console: 'readonly',
255+
Buffer: 'readonly',
256+
global: 'readonly',
257+
URL: 'readonly',
258+
TextEncoder: 'readonly',
259+
TextDecoder: 'readonly',
260+
},
261+
},
262+
rules: {
263+
'no-console': 'off',
264+
},
265+
},
266+
267+
{
268+
// The ESM half of the block above. A `scripts/*.mjs` is genuinely a module
269+
// and must keep the default `sourceType`, so it gets Node's globals but
270+
// none of the CommonJS wrapper. Measured: `process` reported undefined 2x
271+
// in hermiq's generate-opengemeenten-icons.mjs and 4x in openregister's
272+
// l10n/runtime-check.mjs, which the `.js`/`.cjs` block deliberately does
273+
// not match.
274+
files: ['scripts/**/*.mjs', 'tests/**/*.mjs'],
275+
languageOptions: {
276+
globals: {
277+
process: 'readonly',
278+
console: 'readonly',
279+
Buffer: 'readonly',
280+
global: 'readonly',
281+
URL: 'readonly',
282+
TextEncoder: 'readonly',
283+
TextDecoder: 'readonly',
284+
},
285+
},
286+
rules: {
287+
'no-console': 'off',
288+
},
289+
},
290+
291+
{
292+
// eslint must not try to PARSE a shell script. `tests/e2e/seed.test.sh`
293+
// matches the `**/*.test.*` glob some presets use, and eslint then reads
294+
// it as JavaScript and reports "Parsing error: Unexpected character" —
295+
// a finding about a file it should never have opened.
296+
ignores: ['**/*.sh', '**/*.bash'],
297+
},
298+
299+
174300
{
175301
// Test globals. Several apps keep their spec files INSIDE `src/`, which the
176302
// lint script scans, and neither `@nextcloud/eslint-config` nor the runner

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"build": "NODE_ENV=production webpack --config webpack.config.js --progress",
2020
"dev": "NODE_ENV=development webpack --config webpack.config.js --progress",
2121
"watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch",
22-
"lint": "eslint src",
22+
"lint": "eslint src tests scripts",
2323
"lint-fix": "npm run lint -- --fix && npm run format:fix",
2424
"test": "jest --silent",
2525
"test-coverage": "jest --silent --coverage",

scripts/build-l10n-js.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ function renderJs(id, translations, pluralForm) {
101101
].join('\n')
102102
}
103103

104+
/**
105+
*
106+
*/
104107
function main() {
105108
const check = process.argv.includes('--check')
106109
const id = appId()

scripts/check-schema-l10n.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ function collect(node, where, sink) {
113113
for (const value of Object.values(node)) collect(value, where, sink)
114114
}
115115

116+
/**
117+
*
118+
*/
116119
function main() {
117120
const update = process.argv.includes('--update')
118121
const list = process.argv.includes('--list')

scripts/check-vue-demi.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
* then hard-fail. Never silently continue.
2525
*/
2626

27+
const { spawnSync } = require('child_process')
2728
const fs = require('fs')
2829
const path = require('path')
29-
const { spawnSync } = require('child_process')
3030

3131
const SHIM = path.resolve(
3232
__dirname,

src/utils/moderationItem.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,7 @@
1313
* SPDX-License-Identifier: EUPL-1.2
1414
*/
1515

16-
const TITLE_FIELDS = [
17-
'name',
18-
'name',
19-
'titel',
20-
'title',
21-
'organization',
22-
'organisation',
23-
]
16+
const TITLE_FIELDS = ['name', 'titel', 'title', 'organization', 'organisation']
2417
const SUBTITLE_FIELDS = [
2518
'email',
2619
'contactEmail',

tests/e2e/global-setup.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
* in the static HTML. We must wait for the input fields to hydrate before filling.
99
*/
1010

11-
import { chromium, type FullConfig } from '@playwright/test'
12-
import * as path from 'path'
11+
import type { FullConfig } from '@playwright/test'
12+
13+
import { chromium } from '@playwright/test'
1314
import * as fs from 'fs'
14-
import { resolveBaseUrl } from './base-url'
15+
import * as path from 'path'
16+
import { resolveBaseUrl } from './base-url.ts'
1517

1618
const AUTH_DIR = path.resolve(__dirname, '.auth')
1719
const STORAGE_STATE = path.join(AUTH_DIR, 'admin.json')
@@ -91,7 +93,7 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
9193
await page.evaluate(() => {
9294
try {
9395
window.localStorage.setItem('cn-walkthrough-seen:stackiq', '999.0.0')
94-
} catch (e) {
96+
} catch {
9597
// localStorage unavailable — specs fall back to dismissing by hand.
9698
}
9799
})

tests/e2e/manifest-pages.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
* and carry standalone `@e2e exclude` directives in their spec blocks.
3434
*/
3535

36-
import { test, expect, type Page } from '@playwright/test'
37-
import { APP_PATH } from './base-url'
36+
import type { Page } from '@playwright/test'
37+
38+
import { expect, test } from '@playwright/test'
39+
import { APP_PATH } from './base-url.ts'
3840

3941
// Was the hardcoded pretty path `/apps/stackiq`. See the APP_PATH
4042
// docblock in tests/e2e/base-url.ts: without a rewrite rule that path is not a

tests/e2e/org-archimate-export.spec.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,10 @@
7575
* @e2e org-archimate-export::boolean-parameters-accept-various-truthy-values
7676
*/
7777

78-
import {
79-
test,
80-
expect,
81-
request as playwrightRequest,
82-
type Page,
83-
} from '@playwright/test'
84-
import { APP_PATH, resolveBaseUrl } from './base-url'
78+
import type { Page } from '@playwright/test'
79+
80+
import { expect, request as playwrightRequest, test } from '@playwright/test'
81+
import { APP_PATH, resolveBaseUrl } from './base-url.ts'
8582

8683
// ---------------------------------------------------------------------------
8784
// Fixture setup

tests/e2e/playwright.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@
5757

5858
import { defineConfig, devices } from '@playwright/test'
5959
import * as path from 'path'
60-
61-
import { BASE_URL } from './base-url'
60+
import { BASE_URL } from './base-url.ts'
6261

6362
const APP_ROOT = path.resolve(__dirname, '..', '..')
6463

0 commit comments

Comments
 (0)