Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 146 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,44 @@ export default [
'no-console': 'off',
'n/no-process-exit': 'off',
'n/hashbang': 'off',
// `_` / `__` as a deliberate throwaway binding — `catch (_)`, a
// discarded destructuring slot. Narrow on purpose: the pattern matches
// UNDERSCORES ONLY, so a real name that happens to start with `_` is
// still reported. v9 drives plain `.js` through the CORE rule (the
// `@typescript-eslint` swap is per-file-type), so it is set here.
// Tests import devDependencies by definition; this rule is about what
// ships in the published package, which tests/ never does.
'n/no-unpublished-import': 'off',
// 🔴 COMMENTS ONLY, and the exception is load-bearing. Four
// api-direct specs document the `testIgnore` glob that excludes them
// from the gate-19 UI run, and a block comment cannot contain the
// literal `**` + `/` because that closes it at `*/`. The files carry a
// ZERO-WIDTH SPACE (U+200B) between the two to break the sequence.
//
// Deleting the character to satisfy the rule would terminate the
// comment early and break the file. In CODE an invisible character is
// a genuine hazard — a look-alike inside an identifier or a string —
// and the rule still catches that. In a comment it cannot change
// behaviour.
'no-irregular-whitespace': ['error', { skipComments: true }],
},
},

{
// `_` / `__` as a deliberate throwaway binding — `catch (_)`, a discarded
// destructuring slot. Narrow on purpose: the pattern matches UNDERSCORES
// ONLY, so a real name that happens to start with `_` is still reported.
//
// 🔴 `.js` / `.mjs` ONLY, NOT `.ts`. The CORE rule is not TypeScript-aware:
// applied to a `.ts` file it reads the parameter NAMES inside a function
// TYPE as bindings and reports them unused. Measured on humaniq —
//
// t?: (app: string, key: string) => string
//
// produced four `no-unused-vars` errors for `app` and `key`, which are
// documentation, not variables. The same mis-scoping made every unused
// `catch (e)` in a `.ts` spec report TWICE, once per rule.
//
// v9 already turns the core rule off for `.ts` and drives
// `@typescript-eslint/no-unused-vars` instead; naming `.ts` here switched
// it back on. TypeScript files are handled by the block below.
files: ['tests/**/*.js', 'tests/**/*.mjs'],
rules: {
'no-unused-vars': [
'error',
{
Expand All @@ -165,12 +198,117 @@ export default [
ignoreRestSiblings: true,
},
],
// Tests import devDependencies by definition; this rule is about what
// ships in the published package, which tests/ never does.
'n/no-unpublished-import': 'off',
},
},

{
// The TypeScript half of the block above. Same intent, same patterns, on
// the rule that actually understands the language: it knows a name inside
// a function type is not a binding, so type annotations stay quiet while a
// genuinely dead local is still reported.
files: ['tests/**/*.ts', 'tests/**/*.tsx'],
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
varsIgnorePattern: '^_+$',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_+$',
argsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
},

{
// 🔴 Node-side CLI tooling under `scripts/`, which is COMMONJS. Flat
// config defaults every `.js` to ESM with browser-ish globals, so without
// this block eslint reports the CommonJS wrapper itself as undefined
// identifiers. Measured on this app: 52 of the 233 errors under
// `tests/` + `scripts/` were `no-undef`, ALL of them in `scripts/`, and
// all five names were the environment rather than a typo — `process` 23,
// `require` 20, `__dirname` 6, `__filename` 2, `module` 1.
//
// This is describing the environment, not relaxing a rule, and it is the
// same argument the test-globals block below makes: declaring them keeps
// `no-undef` able to do its real job, which is catching a genuinely
// misspelled identifier. Suppressing the rule instead would bury that.
//
// `no-console` is off because printing its report is what a CLI checker
// is FOR.
//
// 🔴 NO `n/*` ENTRIES HERE, DELIBERATELY. `eslint-plugin-n` is NOT
// registered for these files under eslint 10 + @nextcloud/eslint-config
// 9, so `'n/no-process-exit': 'off'` would be dead config that reads as
// if it were doing something. Measured both ways on this app: 0 `n/`
// findings with the entries and 0 without.
//
// What DID report was the opposite — four `scripts/*.js` carried
// `/* eslint-disable n/no-process-exit */` and `/* eslint-disable
// n/shebang */` left over from the eslintrc era, and an inline disable
// naming an unregistered plugin is itself an error ("Definition for rule
// 'n/shebang' was not found"). Those 8 comments are removed; do not add
// `n/*` rules back to replace them.
//
// ⚠️ `.js` and `.cjs` ONLY. A `scripts/*.mjs` is genuinely ESM and must
// keep the default `sourceType`, or `import` stops parsing there.
files: ['scripts/**/*.js', 'scripts/**/*.cjs'],
languageOptions: {
sourceType: 'commonjs',
globals: {
require: 'readonly',
module: 'writable',
exports: 'writable',
process: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
console: 'readonly',
Buffer: 'readonly',
global: 'readonly',
URL: 'readonly',
TextEncoder: 'readonly',
TextDecoder: 'readonly',
},
},
rules: {
'no-console': 'off',
},
},

{
// The ESM half of the block above. A `scripts/*.mjs` is genuinely a module
// and must keep the default `sourceType`, so it gets Node's globals but
// none of the CommonJS wrapper. Measured: `process` reported undefined 2x
// in hermiq's generate-opengemeenten-icons.mjs and 4x in openregister's
// l10n/runtime-check.mjs, which the `.js`/`.cjs` block deliberately does
// not match.
files: ['scripts/**/*.mjs', 'tests/**/*.mjs'],
languageOptions: {
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
global: 'readonly',
URL: 'readonly',
TextEncoder: 'readonly',
TextDecoder: 'readonly',
},
},
rules: {
'no-console': 'off',
},
},

{
// eslint must not try to PARSE a shell script. `tests/e2e/seed.test.sh`
// matches the `**/*.test.*` glob some presets use, and eslint then reads
// it as JavaScript and reports "Parsing error: Unexpected character" —
// a finding about a file it should never have opened.
ignores: ['**/*.sh', '**/*.bash'],
},


// eslint-config-prettier LAST OF THE PRESETS, and it has to be: it only turns
// rules OFF, and what it turns off is everything prettier owns — including the
// `@stylistic/*` family v9 introduces (`indent`, `quotes`, `semi`).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build": "NODE_ENV=production webpack --config webpack.config.js --progress",
"dev": "NODE_ENV=development webpack --config webpack.config.js --progress",
"watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch",
"lint": "eslint src",
"lint": "eslint src tests scripts",
"lint-fix": "npm run lint -- --fix",
"test": "vitest run",
"test-coverage": "vitest run --coverage",
Expand Down
3 changes: 3 additions & 0 deletions scripts/build-l10n-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ function renderJs(id, translations, pluralForm) {
].join('\n')
}

/**
*
*/
function main() {
const check = process.argv.includes('--check')
const id = appId()
Expand Down
36 changes: 18 additions & 18 deletions scripts/check-integration-parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
* ---------------------------------------
* A leaf has TWO faces (ADR-019 AD-11/AD-13, ADR-066 decisions 4 and 7):
*
* * a SERVER face — either a `LeafDescriptor` contributed through
* a SERVER face — either a `LeafDescriptor` contributed through
* `RegisterLeafProvidersEvent`, or an `IntegrationProvider` registered on
* OpenRegister's `IntegrationRegistry`. This face is what the
* `openregister.integrations.leaves` capability advertises.
* * a JS face — a `registerIntegration({ id, … })` call that mounts the
* a JS face — a `registerIntegration({ id, … })` call that mounts the
* render pair on `window.OCA.OpenRegister.integrations`.
*
* The two are correlated ONLY by a shared `id`. Nothing at runtime notices when
Expand Down Expand Up @@ -132,7 +132,7 @@ function collectFiles(root, test, maxDepth = 10) {
let entries
try {
entries = fs.readdirSync(dir, { withFileTypes: true })
} catch (e) {
} catch {
return
}
for (const ent of entries) {
Expand Down Expand Up @@ -289,17 +289,17 @@ function resolvePhp(expr, localConsts, globalConsts) {
}
m = /^(?:self|static)::([A-Z0-9_]+)$/.exec(e)
if (m !== null) {
return Object.prototype.hasOwnProperty.call(localConsts, m[1])
return Object.hasOwn(localConsts, m[1])
? localConsts[m[1]]
: null
}
m = /^([A-Za-z_][A-Za-z0-9_]*)::([A-Z0-9_]+)$/.exec(e)
if (m !== null) {
const key = `${m[1]}::${m[2]}`
if (Object.prototype.hasOwnProperty.call(FOREIGN_CONSTANTS, key) === true) {
if (Object.hasOwn(FOREIGN_CONSTANTS, key) === true) {
return FOREIGN_CONSTANTS[key]
}
return Object.prototype.hasOwnProperty.call(globalConsts, key)
return Object.hasOwn(globalConsts, key)
? globalConsts[key]
: null
}
Expand Down Expand Up @@ -351,8 +351,8 @@ function phpLocalConsts(src) {
*
* Two shapes count as a server face, because both are how a leaf reaches the
* `openregister.integrations.leaves` capability:
* * `new LeafDescriptor(…)` — the ADR-066 collect-event contribution;
* * an `IntegrationProvider` (extends `AbstractIntegrationProvider` or
* `new LeafDescriptor(…)` — the ADR-066 collect-event contribution;
* an `IntegrationProvider` (extends `AbstractIntegrationProvider` or
* implements `IntegrationProviderInterface`) whose `getId()` returns a
* literal — the `IntegrationRegistry::addProvider()` path.
*
Expand All @@ -371,7 +371,7 @@ function collectServerFaces() {
let src
try {
src = fs.readFileSync(file, 'utf8')
} catch (e) {
} catch {
continue
}
sources.set(file, src)
Expand Down Expand Up @@ -504,7 +504,7 @@ function resolveJs(expr, locals) {
}
if (
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(e) === true
&& Object.prototype.hasOwnProperty.call(locals, e) === true
&& Object.hasOwn(locals, e) === true
) {
return locals[e]
}
Expand Down Expand Up @@ -588,7 +588,7 @@ function collectJsRegistrations() {
let src
try {
src = fs.readFileSync(file, 'utf8')
} catch (e) {
} catch {
continue
}
// BOTH SUPPORTED REGISTRATION APIs, NOT ONE.
Expand Down Expand Up @@ -737,7 +737,7 @@ function collectOrSchemas() {
let doc
try {
doc = JSON.parse(fs.readFileSync(file, 'utf8'))
} catch (e) {
} catch {
continue
}
const declared = doc && doc.components && doc.components.schemas
Expand Down Expand Up @@ -959,7 +959,7 @@ function main() {
continue
}
counts.R6++
if (Object.prototype.hasOwnProperty.call(schemas, slug) === false) {
if (Object.hasOwn(schemas, slug) === false) {
failures.push(
`✗ [R6 offlineConfig] leaf offlineConfig.${key} = "${slug}" (${r.file}) names a `
+ `schema this repo does not declare in lib/Settings/** — the leaf would query a `
Expand Down Expand Up @@ -996,12 +996,12 @@ function main() {
.map(([rule, n]) => `${rule}:${n}`)
.join(' ')
if (failures.length === 0) {
// eslint-disable-next-line no-console
console.log(
`✓ integration parity: ${scope} — all rules pass (assertions run per rule: ${perRule})`,
)
if (Object.values(counts).every((n) => n === 0) === true) {
// eslint-disable-next-line no-console
console.error(
'✗ integration parity: every rule had ZERO subject matter, yet gate-24 selected this '
+ 'repo as one that registers leaves. That contradiction means this checker failed to '
Expand All @@ -1014,15 +1014,15 @@ function main() {
// The header carries no `✗` on purpose: gate-24 counts violations by
// grepping `^✗` in this log, so every violation — and only a violation —
// starts a line with it.
// eslint-disable-next-line no-console
console.error(
`integration parity gate FAILED — ${failures.length} violation(s) over ${scope}:`,
)
for (const f of failures) {
// eslint-disable-next-line no-console
console.error(f)
}
// eslint-disable-next-line no-console
console.error(`\nAssertions run per rule: ${perRule}`)
process.exit(1)
}
Expand Down
17 changes: 13 additions & 4 deletions scripts/check-l10n.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#!/usr/bin/env node
/* eslint-disable jsdoc/require-param */
/* eslint-disable n/no-process-exit */
/* eslint-disable no-console */
/* eslint-disable n/shebang */

/**
* l10n/i18n consistency checker.
*
Expand All @@ -19,7 +17,6 @@

const fs = require('fs')
const path = require('path')

const {
loadJsTranslations,
walk,
Expand All @@ -40,6 +37,9 @@ const DIM = '\x1b[2m'
const BOLD = '\x1b[1m'
const RESET = '\x1b[0m'

/**
*
*/
function rel(p) {
return path.relative(ROOT, p)
}
Expand Down Expand Up @@ -106,6 +106,9 @@ const NON_DISPLAY_ATTRS = new Set([
'back-route', // Vue Router route name passed to $router.push({ name })
])

/**
*
*/
function findUnwrapped(vueFiles, keys) {
const hits = []
for (const file of vueFiles) {
Expand Down Expand Up @@ -166,12 +169,18 @@ function findUnwrapped(vueFiles, keys) {
return hits
}

/**
*
*/
function printSection(title, color, body) {
console.log(`${color}${BOLD}${title}${RESET}`)
console.log(body)
console.log('')
}

/**
*
*/
function main() {
const { app, translations } = loadJsTranslations(L10N_FILE)
const keys = new Set(Object.keys(translations))
Expand Down
3 changes: 3 additions & 0 deletions scripts/check-schema-l10n.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ function collect(node, where, sink) {
for (const value of Object.values(node)) collect(value, where, sink)
}

/**
*
*/
function main() {
const update = process.argv.includes('--update')
const list = process.argv.includes('--list')
Expand Down
Loading
Loading