taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Meslint.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", }, }, {