|
| 1 | +# ores-lint |
| 2 | + |
| 3 | +A vendored, dependency-free lint baseline for every JavaScript/TypeScript, |
| 4 | +Rust, Dart/Flutter and Gleam repo in the org fleet. Everything it needs is in |
| 5 | +this directory — there is nothing to install from a registry and nothing to |
| 6 | +keep in version sync. |
| 7 | + |
| 8 | +Only universally accepted linters are used as hosts: |
| 9 | + |
| 10 | +| language | host linter | custom house rules | |
| 11 | +|---|---|---| |
| 12 | +| TypeScript / JS | ESLint 9+ (flat config) | `ores/require-send`, `ores/semi` | |
| 13 | +| Rust | clippy | `implicit_return` house style; `#[must_use]` on log `Event`; require-send scanner | |
| 14 | +| Dart / Flutter | `dart analyze` | require-send scanner | |
| 15 | +| Gleam | `gleam format --check` + `gleam check` | require-send scanner | |
| 16 | + |
| 17 | +There is no ESLint-quality plugin host for Gleam, and Dart's `custom_lint` / |
| 18 | +Rust's `dylint` would pull registry packages into every repo. The one house |
| 19 | +rule that those hosts cannot express — **logger chains must end in `send()`** — |
| 20 | +is implemented as a vendored ESLint rule for TS and a small Node scanner for |
| 21 | +the other three languages. |
| 22 | + |
| 23 | +## Running it |
| 24 | + |
| 25 | +```sh |
| 26 | +sh .ores-lint/lint.sh # lints whatever this repo contains |
| 27 | +sh .ores-lint/selftest.sh # verifies the toolkit still works after a toolchain upgrade |
| 28 | +``` |
| 29 | + |
| 30 | +For JS repos it is also wired into `npm run lint:ores`, and runs automatically |
| 31 | +before `npm run build` (`prebuild`) and before `npm publish` (`prepublishOnly`). |
| 32 | + |
| 33 | +## What it enforces |
| 34 | + |
| 35 | +**JavaScript / TypeScript** — via ESLint (flat config): |
| 36 | + |
| 37 | +| rule | why | |
| 38 | +|---|---| |
| 39 | +| `semi` | house style: semicolons are required, missing ones warn | |
| 40 | +| `ores/require-send` | a logging chain that reaches `.info()`/`.warn()`/… but never calls `.send()` or `.send(boolean)` builds an event that is never delivered | |
| 41 | +| correctness set | `eqeqeq`, `no-unreachable`, `no-dupe-keys`, `use-isnan`, `valid-typeof`, `no-async-promise-executor`, and similar low-false-positive checks | |
| 42 | + |
| 43 | +**Rust** — via clippy, plus rustc `#[must_use]` on ores-otel `Event`: |
| 44 | + |
| 45 | +| lint | why | |
| 46 | +|---|---| |
| 47 | +| `clippy::implicit_return` | house style: prefer an explicit `return` at tail position | |
| 48 | +| `clippy::correctness`, `clippy::suspicious` | real defects | |
| 49 | +| `unwrap_used`, `expect_used`, `panic_in_result_fn`, `todo`, `dbg_macro` | things that should not reach a publish | |
| 50 | + |
| 51 | +**Dart / Flutter** — via `dart analyze` (or `flutter analyze`). If the repo has |
| 52 | +no `analysis_options.yaml`, rollout drops a baseline of analyzer-shipped |
| 53 | +correctness lints. Existing files are never overwritten. Prefer |
| 54 | +`package:lints` / `package:flutter_lints` when the package already depends on |
| 55 | +them — those are the Dart equivalents of `eslint:recommended`. |
| 56 | + |
| 57 | +**Gleam** — via `gleam format --check` and `gleam check`. Unused values already |
| 58 | +catch many forgotten `send` calls; the scanner catches the rest (assigned |
| 59 | +events, unfinished pipes). |
| 60 | + |
| 61 | +**require-send (all four languages).** The ores-otel logger builds an event |
| 62 | +through method chaining (TS/Rust/Dart) or pipes (Gleam). Delivery is |
| 63 | +`send()`, `send(boolean)`, or `send_with_store(...)`. Forgetting that call |
| 64 | +means the event is built and then dropped — unless shutdown recovers it, which |
| 65 | +is a fallback, not the API. Tests that deliberately build unsent events are |
| 66 | +skipped unless `ORES_LINT_REQUIRE_SEND_INCLUDE_TESTS=1`. |
| 67 | + |
| 68 | +## Overriding a finding on one line |
| 69 | + |
| 70 | +This is expected. Some tests, shutdown-recovery fixtures, and rare control |
| 71 | +flow should not call `send()`. Use the comment that matches the host linter, |
| 72 | +or the unified ores-lint form which works in every language: |
| 73 | + |
| 74 | +``` |
| 75 | +// ores-lint-disable-next-line require-send |
| 76 | +// ores-lint-disable-line require-send |
| 77 | +// ores-lint-disable-file require-send |
| 78 | +``` |
| 79 | + |
| 80 | +Language-native equivalents, also honoured: |
| 81 | + |
| 82 | +| language | one-line override | |
| 83 | +|---|---| |
| 84 | +| TypeScript | `// eslint-disable-next-line ores/require-send` | |
| 85 | +| Rust (`#[must_use]`) | `#[allow(unused_must_use)]` on the next statement | |
| 86 | +| Dart analyzer | `// ignore: name_of_lint` (analyzer rules only; require-send uses the ores-lint form) | |
| 87 | +| Gleam | the ores-lint form above | |
| 88 | + |
| 89 | +Do not disable the rule for a whole file unless the file is a generated |
| 90 | +fixture. Prefer the next-line form. |
| 91 | + |
| 92 | +## The two things worth knowing |
| 93 | + |
| 94 | + |
| 95 | +**1. Implicit-return findings are capped.** `clippy::implicit_return` fires once |
| 96 | +per implicit return, which on a real crate is hundreds of identical lines. The |
| 97 | +lint stays fully enabled so nothing is missed, but `rust.sh` collapses it into a |
| 98 | +single warning showing at most 5 concrete locations plus `... and N more`. The |
| 99 | +same cap applies to every ESLint rule via `eslint/formatter.mjs`. Change it with |
| 100 | +`ORES_LINT_MAX_EXAMPLES`. |
| 101 | + |
| 102 | +**2. `clippy::needless_return` had to be disabled.** It ships enabled in |
| 103 | +clippy's default `style` group and warns on exactly the explicit returns this |
| 104 | +house style asks for. Enabling `implicit_return` without allowing |
| 105 | +`needless_return` makes the two lints contradict each other on every function in |
| 106 | +the crate. `selftest.sh` asserts this stays true. |
| 107 | + |
| 108 | +## Scope: sub-projects and repo boundaries |
| 109 | + |
| 110 | +The linter does **not** assume the repo root is the only project. |
| 111 | + |
| 112 | +- **Rust** — `rust.sh` finds every crate in the repo, including ones under |
| 113 | + `apps/` or `clients/`. Crates that are workspace members of an already-linted |
| 114 | + root are skipped (via `cargo metadata --no-deps`) so nothing is linted twice, |
| 115 | + and findings from every crate are aggregated into **one** capped report. |
| 116 | +- **JS/TS** — a flat config at the repo root makes `eslint .` reach nested |
| 117 | + packages, so the config goes in even when the JS lives in a subdirectory. |
| 118 | + |
| 119 | +**Nested git repositories are a hard boundary.** A repo checked out inside |
| 120 | +another repo gets its own ores-lint install; the parent must not lint it, or the |
| 121 | +same findings get reported twice under the wrong repo name and the same |
| 122 | +`package.json` gets wired with two conflicting relative paths. `rollout.mjs` |
| 123 | +records those boundaries in `.ores-lint/nested-repos.json`, and both halves of |
| 124 | +the linter read it. |
| 125 | + |
| 126 | +To exclude a repo entirely — vendored upstream forks, for instance — drop an |
| 127 | +empty `.ores-lint-ignore` file at its root. |
| 128 | + |
| 129 | +## Legacy config migration |
| 130 | + |
| 131 | +ESLint 9+ reads flat config **only**. Three older mechanisms are silently |
| 132 | +ignored, which means any repo still relying on them has not been linted at all: |
| 133 | + |
| 134 | +| legacy mechanism | status | |
| 135 | +|---|---| |
| 136 | +| `.eslintrc*` | ignored entirely; rules are dead | |
| 137 | +| `eslintConfig` key in `package.json` | ignored entirely | |
| 138 | +| `.eslintignore` | ignored, with a warning | |
| 139 | + |
| 140 | +`audit.mjs` reports every repo in each category. `.eslintignore` is ported |
| 141 | +automatically into flat-config `ignores` by `base.mjs` (gitignore semantics |
| 142 | +preserved), so its intent keeps applying. The other two need a human decision |
| 143 | +and are migrated per repo — porting the rules that still make sense, and saying |
| 144 | +in a comment which ones were dropped and why. |
| 145 | + |
| 146 | +## Warn-only, by design |
| 147 | + |
| 148 | +`lint.sh` exits 0 no matter what it finds. It is wired into build and publish |
| 149 | +hooks across hundreds of repos, so it is built to be incapable of breaking one |
| 150 | +unless a human opts in. |
| 151 | + |
| 152 | +To make findings blocking for a single repo, create `.ores-lint/local.sh`: |
| 153 | + |
| 154 | +```sh |
| 155 | +ORES_LINT_STRICT=1 |
| 156 | +``` |
| 157 | + |
| 158 | +`local.sh` is yours — the rollout script never overwrites it. Everything else in |
| 159 | +this directory is managed and will be replaced on the next rollout. |
| 160 | + |
| 161 | +## Knobs |
| 162 | + |
| 163 | +| variable | default | meaning | |
| 164 | +|---|---|---| |
| 165 | +| `ORES_LINT_MAX_EXAMPLES` | `5` | example locations shown per rule | |
| 166 | +| `ORES_LINT_STRICT` | `0` | `1` makes any finding exit non-zero | |
| 167 | +| `ORES_LINT_SKIP_JS` / `ORES_LINT_SKIP_RUST` / `ORES_LINT_SKIP_DART` / `ORES_LINT_SKIP_GLEAM` | `0` | skip one language host | |
| 168 | +| `ORES_LINT_SKIP_REQUIRE_SEND` | `0` | skip the cross-language send() scanner | |
| 169 | +| `ORES_LINT_REQUIRE_SEND_INCLUDE_TESTS` | `0` | `1` also scans `test/` / `*_test.dart` / etc. | |
| 170 | +| `ORES_LINT_RUST_ALL_TARGETS` | `0` | `1` also lints tests/benches/examples | |
| 171 | +| `ORES_LINT_RUST_EXTRA` | — | extra flags appended to the clippy invocation | |
| 172 | + |
| 173 | +## Graceful degradation |
| 174 | + |
| 175 | +Nothing here is allowed to fail loudly for an environmental reason. ESLint not |
| 176 | +installed, too old, clippy not installed, dart/flutter not installed, gleam not |
| 177 | +installed, no TypeScript parser available, crate deps not fetchable — each is |
| 178 | +reported as an actionable skip, not an error. |
| 179 | +Repo-specific ESLint config that already existed is never overwritten. |
| 180 | +The same is true of `analysis_options.yaml`. |
| 181 | + |
| 182 | +CI follows the same model: the workflow runs `npm i -g eslint typescript-eslint` |
| 183 | +and never runs `npm install` for the repo itself, so linting a PR does not |
| 184 | +require the repo's dependency tree to resolve. |
| 185 | + |
| 186 | +## Per-repo customisation |
| 187 | + |
| 188 | +`eslint.config.mjs` at the repo root takes options: |
| 189 | + |
| 190 | +```js |
| 191 | +export default await oresConfig({ |
| 192 | + requireSend: { loggerNames: ['myLogger'], terminalMethods: ['send', 'flush'] }, |
| 193 | + rules: { 'no-console': 'warn' }, |
| 194 | + ignores: ['**/generated/**'], |
| 195 | +}); |
| 196 | +``` |
| 197 | + |
| 198 | +Once you edit that file the rollout script leaves it alone. |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## Fleet operations (from the `codes` directory) |
| 203 | + |
| 204 | +```sh |
| 205 | +node .ores-lint-toolkit/audit.mjs # report the fleet's lint posture |
| 206 | +node .ores-lint-toolkit/audit.mjs --json out.json # ...as machine-readable data |
| 207 | +node .ores-lint-toolkit/rollout.mjs --dry-run # preview |
| 208 | +node .ores-lint-toolkit/rollout.mjs # install / re-install everywhere |
| 209 | +node .ores-lint-toolkit/rollout.mjs --only ores-otel |
| 210 | +node .ores-lint-toolkit/rollout.mjs --shard 0/8 # one slice of a fleet-wide run |
| 211 | +node .ores-lint-toolkit/verify.mjs # assert every repo is correctly installed |
| 212 | +``` |
| 213 | + |
| 214 | +A full rollout over ~900 repos takes a few minutes. `--shard k/n` splits it into |
| 215 | +bounded chunks, which matters when the runner has a per-command time limit. |
| 216 | + |
| 217 | +Re-run the rollout after editing anything in `.ores-lint-toolkit/` — it is |
| 218 | +idempotent and propagates the change to every repo. |
0 commit comments