blob: 5b43e3316b7670b2146ecae3832e9e623392b876 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Lint and dependency-licence policy
Two things are described here: which ESLint plugins this project uses and why, and the licence
policy every dependency is measured against. They belong together because the first was changed by
the second.
## The licence policy
Every dependency, direct and transitive, is classified against the
[ASF third-party licensing policy](https://www.apache.org/legal/resolved.html):
| Category | Outcome | Meaning |
| -------- | --------- | ------------------------------------------------------------------------------------------------------------------------- |
| **A** | PASS | Permissive. May be included and depended on freely. |
| **B** | REVIEW | Reported, does not fail. May generally be included in binary form, with attribution obligations the PMC must be aware of. |
| **X** | **BLOCK** | May not be included in an Apache product. |
| Unknown | **BLOCK** | A licence nobody has classified is a licence nobody has cleared. |
Enforced by `scripts/check-dependency-licenses.mjs`, wired into the `License Compliance` job:
```bash
npm run check:licenses # the whole tree — this is what CI runs
npm run check:licenses:production # only what ships
npm run check:licenses:selftest # proves the classifier still classifies
npm run check:licenses -- --json # machine-readable, for a release review
```
### Why the scan covers devDependencies
They are not distributed, so on a narrow reading they are out of scope. But they are named in
`package.json`, and `package.json` travels in a source release — which is exactly how the one
Category X dependency this project had came to matter. A Category X build tool is a question the
PMC has to answer either way, so the scan surfaces it instead of scoping it out.
### Why "Unknown" blocks
The failure worth preventing is not a GPL dependency arriving with a banner. It is a package whose
`license` field reads `SEE LICENSE IN LICENSE.txt`, or nothing at all, arriving on a transitive
bump and going unnoticed for two releases. Treating that as a pass makes the check decorative.
Clearing an Unknown means one of two things, and neither is widening a category list to make the
build green:
1. Identify the licence and add its SPDX identifier to the correct category list, or
2. record it in `ACKNOWLEDGED` in the script, with the reason and who it is pending on.
`ACKNOWLEDGED` currently holds one entry — `BlueOak-1.0.0`, which is plainly permissive, is not
named in either ASF list, and reaches this tree only through build tooling. It is treated as
Category A pending PMC confirmation, and that is recorded rather than assumed.
### The classifier is tested
`npm run check:licenses:selftest` runs 26 expressions through the classifier and asserts the
verdict for each — that `LGPL-3.0-only` and `SSPL-1.0` come out X, that `MPL-2.0` comes out B, that
`(MIT OR GPL-3.0)` resolves under MIT while `(MIT AND GPL-3.0)` does not, and that an undeclared or
unrecognised licence comes out Unknown.
This exists because the tree is clean. A green scan over a clean tree says nothing about whether
the rules still bite; the self-test is what says it, and CI runs both.
## The plugins
| Plugin | Licence | Why it is here |
| ----------------------------------- | ---------- | --------------------------------------------------------------------------- |
| `@eslint/js`, `typescript-eslint` | MIT | Language correctness. |
| `angular-eslint` | MIT | Angular and template rules, including the accessibility set. |
| `eslint-plugin-unicorn` | MIT | Correctness rules, `unopinionated` set. |
| `eslint-plugin-security` | Apache-2.0 | eval, unsafe regex, non-literal filesystem paths, timing-unsafe comparison. |
| `eslint-plugin-import` | MIT | Import resolution and duplicate-import correctness. |
| `eslint-import-resolver-typescript` | ISC | Teaches the above about `tsconfig` paths. |
| `eslint-rules/` (this repository) | Apache-2.0 | Rules with no permissively-licensed equivalent. |
`unicorn` is pinned to `^65.0.1` rather than the current major: from 66 onwards it requires ESLint
≥ 10.4, and this project is on ESLint 9. Bumping it means bumping ESLint first, and forcing the
resolution with `--legacy-peer-deps` is not available — the `Dependency Integrity` CI job exists to
forbid exactly that.
The `unopinionated` unicorn set is used rather than `recommended`. The recommended set carries a
large stylistic component — filename casing, abbreviation expansion, `for…of` over `.forEach` —
that would rewrite a great deal of working code for no defect fixed.
## What was removed, and what replaced it
`eslint-plugin-sonarjs` was removed. It is **LGPL-3.0-only**, an ASF Category X licence. It was a
devDependency, never bundled and absent from the production tree, but it was named in
`package.json`.
It contributed 217 enabled rules. Most were already covered — `eslint:recommended` and
`typescript-eslint` were enabled alongside it and overlap heavily (`no-fallthrough`,
`no-unused-vars`, `no-delete-var`, `no-empty-character-class`, `no-labels`, and others are
duplicates). The rest fall out like this:
| What sonarjs covered | What covers it now |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Cognitive complexity | `local/cognitive-complexity` — see below |
| `code-eval`, `pseudo-random`, `xml-parser-xxe`, `post-message`, `file-permissions`, `insecure-cookie`, `cookie-no-httponly` | `eslint-plugin-security` |
| `unused-import`, duplicate and unresolved imports | `eslint-plugin-import`, `@typescript-eslint/no-unused-vars` |
| `no-primitive-wrappers`, `no-array-delete`, `no-alphabetical-sort`, `no-global-this`, `new-operator-misuse`, `different-types-comparison` | `eslint-plugin-unicorn` |
| `no-fallthrough`, `no-unused-vars`, `no-delete-var`, `no-labels`, `block-scoped-var`, `no-empty-character-class` | `eslint:recommended` / `typescript-eslint` (already enabled) |
| `todo-tag`, `fixme-tag` | Nothing — but the tree has **zero** TODO/FIXME/HACK in hand-written source today, and a reviewer sees one in a diff |
### Rules with no replacement
Stated plainly, because a migration that claims parity it does not have is worse than one that
names its gaps:
- **`no-duplicate-string`** — this project had it on at `error` (it is off in sonarjs's own
recommended set). It is why `NAV_CONFIG` names its icons as constants and the e2e specs hoist
their selectors. Nothing permissive implements it. New code can now repeat a literal without a
lint failure; review is the only thing catching it.
- **`no-hardcoded-passwords`**, **`no-hardcoded-ip`**, **`sql-queries`** — `eslint-plugin-security`
covers neither credential nor SQL-shape detection. For this application the loss is small: it
issues no SQL, and CodeQL and the secret-scanning on the repository cover credentials from a
different angle.
- **`no-identical-functions`**, **`no-duplicated-branches`**, **`no-nested-conditional`**,
**`no-invariant-returns`**, **`no-gratuitous-expressions`** — the deeper dataflow rules. No
permissive plugin implements them.
- **`deprecation`** — flags calls to `@deprecated` API. `@typescript-eslint/no-deprecated` covers
this but requires typed linting (`parserOptions.projectService`), which this config does not
enable because it changes the cost of every lint run. Worth doing deliberately.
- **`assertions-in-tests`**, **`no-skipped-tests`**, **`no-empty-test-file`**,
**`no-fixed-wait-in-tests`** — the test-quality rules.
## The local cognitive-complexity rule
`eslint-rules/cognitive-complexity.js`, enabled at the same threshold of **15** that was enforced
before, so this is a like-for-like swap and not a quiet relaxation.
It is implemented here rather than taken from npm because there is nothing to take:
`eslint-plugin-cognitive-complexity` is an npm security placeholder, and the alternatives are
single-author packages at v0.x. Trading a licence problem for a supply-chain one is not a trade.
The metric is a published specification with independent implementations in many languages, and it
is short: increment for each break in linear flow, increment again by the current nesting depth for
structures that nest, ignore structures that let several statements read as one. `else` and
`else if` take the flat increment without the nesting surcharge. A nested function is scored on its
own rather than charged to its parent.
### `??` is not counted
`a ?? b` states a default; it is not a branch the reader has to follow. Counting it made an early
draft of this rule **stricter than the rule it replaced** — a real template handler in
`loan-product-form.component.ts` holds 23 `??` defaults and almost no other branching, passed under
sonarjs, and failed the draft. That behaviour is now pinned by a test.
### Its tests
`npm run test:eslint-rules` — 26 cases, run by CI in the `TS/ESLint` job. They assert **exact
scores** at a threshold of one below, rather than that something was reported, because the way this
rule would fail quietly is by drifting stricter or laxer than what it replaced while still
reporting.
## Rules switched off, and why
Each of these is off deliberately, and the reason is in `eslint.config.js` beside it:
| Rule | Why |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unicorn/prefer-global-this` | Would defeat `no-restricted-globals`. That rule names `localStorage` and `sessionStorage` and matches a bare global — `globalThis.localStorage` sails past it. The adapter boundary is a trust boundary (`security.md` §4); a style rule does not get to open a hole in it. |
| `security/detect-object-injection` | Reports every `obj[key]` with a non-literal key. All 35 reports were that shape. Leaving it on trains reviewers to skim warnings. |
| `unicorn/no-array-for-each` | `.forEach` is not a defect. |
| `unicorn/no-negated-condition` | Which branch reads better depends on which is the common case. |
| `unicorn/no-useless-undefined` | `signal<T \| undefined>(undefined)` and an explicit `return undefined` are how this codebase states "absent" in a typed signature. |
| `unicorn/prefer-top-level-await` | Changes a module's evaluation semantics, and both reports are in the native-federation bootstrap. A deliberate change, not a lint autofix. |
## The suppression baseline
27 pre-existing violations of the newly-added rules are recorded in `eslint-suppressions.json` —
mostly `unicorn/no-array-sort`, which wants `toSorted()` in place of an in-place `.sort()`. That is
a real bug class (mutating an array the caller still holds) and a semantic change, so it is
baselined rather than mass-fixed in a licence PR.
The file is a ratchet, not an amnesty: CI runs `eslint --prune-suppressions`, which **fails** when
the file lists a violation that no longer exists. A fixed violation cannot come back, and a new one
fails immediately. The count only falls.
The 416 adapter-boundary suppressions are counted separately by the GA gate and are unaffected by
this change.