commit d01af30894ef2c43baa22212537dc69f2dacb868
parent 4fca497be9a2dae7127aa928d03c1ced487092fc
Author: Florian Dold <dold@taler.net>
Date: Fri, 31 Jul 2026 12:55:41 +0200
qa: trim the lint configuration to correctness rules
Style and formatting preferences are prettier's business, and reporting them as
defects buried the real findings among ~9000. Errors are now genuine bugs,
accessibility and unused-symbol debt stays visible as warnings, and vendored or
generated code is ignored rather than half-heartedly reformatted.
Diffstat:
| M | eslint.config.mjs | | | 176 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
1 file changed, 156 insertions(+), 20 deletions(-)
diff --git a/eslint.config.mjs b/eslint.config.mjs
@@ -23,10 +23,51 @@ const reactPlugin = require(resolve("eslint-plugin-react"));
const reactHooksPlugin = require(resolve("eslint-plugin-react-hooks"));
const importPlugin = require(resolve("eslint-plugin-import"));
const jsxA11yPlugin = require(resolve("eslint-plugin-jsx-a11y"));
-const headerPlugin = require(resolve("eslint-plugin-header"));
const prettierConfig = require(resolve("eslint-config-prettier"));
const globals = require(resolve("globals"));
+/**
+ * Barrel modules of a package, which legitimately re-export everything.
+ */
+const barrelFiles = ["**/index.ts", "**/index.*.ts"];
+
+const barrelImportMessage =
+ "Importing the package barrel from inside the package creates an import " +
+ "cycle. Import from the module that defines the symbol.";
+
+/**
+ * Ban importing a package's own barrel (src/index.ts and its platform
+ * variants) from the modules of that package.
+ *
+ * The relative path to the barrel depends on how deep the importing file
+ * sits, and only that exact path may be banned: a folder's own index.ts is
+ * an ordinary module, and several packages deliberately split a component
+ * across index/state/views. So one config block per depth, each pairing the
+ * files at that depth with the exact way up to the package root.
+ */
+const barrelImportBan = (maxDepth) =>
+ Array.from({ length: maxDepth }, (_, i) => {
+ const depth = i + 1;
+ const subdirs = "*/".repeat(depth - 1);
+ const toRoot = depth === 1 ? "./" : "../".repeat(depth - 1);
+ return {
+ files: [`packages/*/src/${subdirs}*.{ts,tsx}`],
+ rules: {
+ "no-restricted-imports": [
+ "error",
+ {
+ patterns: [
+ {
+ group: [`${toRoot}index.js`, `${toRoot}index.*.js`],
+ message: barrelImportMessage,
+ },
+ ],
+ },
+ ],
+ },
+ };
+ });
+
export default tseslint.config(
{
ignores: [
@@ -38,18 +79,32 @@ export default tseslint.config(
"**/tsconfig.tsbuildinfo",
"**/.eslintrc.js",
"**/.eslintrc.cjs",
+ // Generated bundle, not source.
+ "packages/web-util/src/tailwind.js",
+ // Third-party code kept in-tree. It is maintained by syncing with
+ // upstream, so it has to stay close to it; upstream's style is not ours
+ // to fix, and reformatting it would make the next sync harder.
+ "packages/taler-util/src/globbing/**", // minimatch
+ "packages/taler-util/src/punycode.ts", // punycode.js
+ "packages/taler-util/src/whatwg-url.ts", // jsdom/whatwg-url
+ "packages/taler-util/src/bech32.ts", // BIP-173 reference code
+ "packages/taler-util/src/segwit_addr.ts", // BIP-173 reference code
+ "packages/idb-bridge/src/idbtypes.ts", // TypeScript's DOM IndexedDB types
+ // Web Platform Tests ported verbatim, deliberately kept close to upstream.
+ "packages/idb-bridge/src/idb-wpt-ported/**",
+ // Input fixtures for the pogen extractor, not code that runs.
+ "packages/pogen/example/**",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
- files: ["**/*.{ts,tsx,js,jsx}"],
+ files: ["**/*.{ts,tsx,js,jsx,mjs,cjs}"],
plugins: {
react: reactPlugin,
"react-hooks": reactHooksPlugin,
"jsx-a11y": jsxA11yPlugin,
import: importPlugin,
- header: headerPlugin,
},
languageOptions: {
globals: {
@@ -72,28 +127,109 @@ export default tseslint.config(
},
},
rules: {
- ...reactPlugin.configs.recommended.rules,
+ // ---------------------------------------------------------------
+ // Correctness. These flag code that is, or is about to be, wrong.
+ // ---------------------------------------------------------------
...reactHooksPlugin.configs.recommended.rules,
- ...jsxA11yPlugin.configs.recommended.rules,
+ // Calling a hook conditionally desynchronizes the hook order between
+ // renders, which mixes up one hook's state with another's. These are
+ // real defects; there are too many to unpick here, so they are visible
+ // rather than blocking.
+ "react-hooks/rules-of-hooks": "warn",
+ // A list rendered without keys re-uses component state across items.
+ "react/jsx-key": "error",
+ "react/jsx-no-undef": "error",
+ // Not diagnostics: these two exist so that no-unused-vars can see the
+ // identifiers a JSX expression references. Without them every
+ // component imported for use in JSX is reported as unused.
+ "react/jsx-uses-vars": "error",
+ "react/jsx-uses-react": "error",
+ // Node16 module resolution: an import without the extension fails at
+ // run time, and tsc does not catch it in every configuration.
+ "import/extensions": ["error", "ignorePackages"],
+ // An infinite loop is written as `while (true)` on purpose.
"no-constant-condition": ["error", { checkLoops: false }],
- "prefer-const": ["warn", { destructuring: "all" }],
+ // An empty catch is a deliberate "ignore this", an empty block is not.
+ "no-empty": ["error", { allowEmptyCatch: true }],
+ // `cond && fn()` is a call, not a stray expression; the codebase uses it
+ // throughout. What is left is the genuine case: a statement with no
+ // effect at all.
+ "@typescript-eslint/no-unused-expressions": [
+ "error",
+ { allowShortCircuit: true, allowTernary: true },
+ ],
+
+ // ---------------------------------------------------------------
+ // Known debt. Real problems, too many to fix in one go, so they
+ // stay visible without failing the run.
+ // ---------------------------------------------------------------
+ "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
+ // Accessibility findings are real, but there are too many to clear here.
+ // Downgraded rather than dropped, and only the ones the recommended set
+ // actually turns on -- mapping the whole table would switch on rules
+ // that upstream deliberately leaves off.
+ ...Object.fromEntries(
+ Object.entries(jsxA11yPlugin.configs.recommended.rules)
+ .filter(([, setting]) => {
+ const severity = Array.isArray(setting) ? setting[0] : setting;
+ return severity !== "off" && severity !== 0;
+ })
+ .map(([rule]) => [rule, "warn"]),
+ ),
+
+ // ---------------------------------------------------------------
+ // Style and preference. Prettier owns layout; the rest below is a
+ // matter of taste and should not be reported as a defect.
+ // ---------------------------------------------------------------
+ "no-var": "off",
+ "prefer-const": "off",
+ "no-extra-boolean-cast": "off",
"no-prototype-builtins": "off",
- "@typescript-eslint/no-namespace": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-this-alias": "off",
+ "@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-namespace": "off",
"@typescript-eslint/ban-ts-comment": "off",
- "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
- "@typescript-eslint/explicit-function-return-type": [
- "warn",
- { allowExpressions: true },
- ],
- "@typescript-eslint/no-use-before-define": [
- "error",
- { functions: false, classes: false },
- ],
- "import/extensions": ["error", "ignorePackages"],
- "react/no-unknown-property": 0,
- "react/prop-types": 0,
- "react/no-unescaped-entities": 0,
+ // preact/compat provides the JSX pragma; React is not in scope.
+ "react/react-in-jsx-scope": "off",
+ },
+ },
+ // A module that imports its own package barrel pulls in every other module
+ // of the package, which is how the import cycles here are formed: the cycle
+ // then breaks whichever module happens to be initialized first.
+ ...barrelImportBan(5),
+ {
+ // A barrel re-exporting the package is exactly what these files are for.
+ files: barrelFiles,
+ rules: {
+ "no-restricted-imports": "off",
+ },
+ },
+ {
+ // chai states an assertion as a property access -- `expect(x).undefined`
+ // is the assertion, not a statement someone forgot to finish.
+ files: ["**/*.test.{ts,tsx}"],
+ rules: {
+ "@typescript-eslint/no-unused-expressions": "off",
+ },
+ },
+ {
+ // Shims that hand CommonJS built-ins to esbuild-bundled code. Reaching for
+ // require() is the whole point of the file.
+ files: ["**/import-meta-url.js"],
+ rules: {
+ "@typescript-eslint/no-require-imports": "off",
+ },
+ },
+ {
+ // No React here: taler-harness is a CLI, and its `use*` helpers are test
+ // fixtures, not hooks.
+ files: ["packages/taler-harness/**"],
+ rules: {
+ "react-hooks/rules-of-hooks": "off",
+ "react-hooks/exhaustive-deps": "off",
},
},
{