build: add ESLint to script/lint and containerize linting (closes #152)
Some checks failed
check / check (push) Failing after 2m16s

script/lint ran `prettier --check .`, byte for byte what script/fmt-check
runs, so make check checked formatting twice and did no static analysis on
a cryptocurrency wallet. Two used-but-not-imported crashes shipped past it.

ESLint is pinned in package.json with @eslint/js recommended as the base and
a flat config in eslint.config.js. no-undef and no-unused-vars are restated
error-level so a future recommended-set change cannot downgrade them.
Globals are declared per tree rather than globally, because a too-wide set
hides the next unimported identifier: browser for the popup and content
scripts, service worker for src/background/ and src/shared/, browser for the
one documented POPUP ONLY module in src/shared/, jest for tests/, node for
build.js, and both for the e2e harnesses, which carry the callbacks they
ship into the page inline.

Two rules new to the recommended set are off, and both would have cost
something to satisfy. no-useless-assignment flags the `password = null` and
`decryptedSecret = null` wipes in approval.js and confirmTx.js: those
assignments are dead by construction, which is the point of them, and the
rule's fix is to delete the wipe. preserve-caught-error would change what
the wallet's error paths throw, which is a decision of its own.

Every remaining violation is fixed: 41 unused bindings and 53 undefined
identifiers. Unused catch bindings became `catch {`, which the repo already
used; the shared init(ctx) view signature keeps its parameter as _ctx in the
three views that do not read it. src/shared/uniswap.js keeps its unused
V2_SWAP_EXACT_OUT decoder behind a scoped disable, because deleting it would
widen the gap it represents rather than close it (#283).

Linting is containerized. script/lint builds the Dockerfile's new lint stage
so the ESLint deciding whether this repo is green is the pinned one and not
whatever the host has; AUTISTMASK_LINT_NATIVE, set only in that image, is
what makes make check inside the CI build lint in place instead of recursing
into docker. The check stage takes a COPY --from=lint dependency so a lint
failure fails the whole build early rather than racing it.

No --fix anywhere in the lint path: make check remains non-mutating.

The README claim that a used-but-not-imported identifier is invisible to
make check, and the same claim in script/test-e2e, are no longer true and
are corrected.
This commit is contained in:
2026-08-14 04:16:47 +00:00
parent 0be20d7270
commit 7270480e0b
32 changed files with 647 additions and 83 deletions

View File

@@ -79,7 +79,7 @@
for (const cb of cbs) {
try {
cb(data);
} catch (e) {
} catch {
// ignore listener errors
}
}

View File

@@ -12,7 +12,6 @@ const {
setBackRenderer,
pushCurrentView,
goBack,
clearViewStack,
} = require("./views/helpers");
const { applyTheme } = require("./theme");
// Renders a view the popup lands on without having navigated to it forward:

View File

@@ -194,7 +194,7 @@ async function importPrivateKey(ctx) {
let addr;
try {
addr = addressFromPrivateKey(key);
} catch (e) {
} catch {
showFlash("Invalid private key.");
return;
}
@@ -246,7 +246,7 @@ async function importXprvKey(ctx) {
let result;
try {
result = hdWalletFromXprv(xprv);
} catch (e) {
} catch {
showFlash(
"That extended private key is not valid. Please check it and try again.",
);

View File

@@ -12,7 +12,7 @@ const {
goBack,
pushCurrentView,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
const {
fetchRecentTransactions,
@@ -44,7 +44,6 @@ function show() {
state.selectedToken = null;
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
const wi = state.selectedWallet;
const ai = state.selectedAddress;
$("address-title").textContent =
wallet.name + " \u2014 Address " + (ai + 1);
@@ -246,7 +245,6 @@ function renderTransactions(txs) {
row.addEventListener("click", () => {
const idx = parseInt(row.dataset.tx, 10);
const tx = loadedTxs[idx];
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
tx.fromEns = ensNameMap.get(tx.from) || null;
tx.toEns = ensNameMap.get(tx.to) || null;
ctx.showTransactionDetail(tx);

View File

@@ -16,13 +16,9 @@ const {
goBack,
pushCurrentView,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
const {
formatUsd,
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const { formatUsd, getPrice } = require("../../shared/prices");
const {
fetchRecentTransactions,
filterTransactions,

View File

@@ -9,7 +9,7 @@ const {
attachCopyHandlers,
onViewLeave,
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { state, saveState } = require("../../shared/state");
const { networkByChainId } = require("../../shared/networks");
const {
formatEther,
@@ -537,7 +537,7 @@ function clearSignPassword() {
hideError("approve-sign-error");
}
function init(ctx) {
function init(_ctx) {
onViewLeave("approve-tx", clearTxPassword);
onViewLeave("approve-sign", clearSignPassword);

View File

@@ -2,20 +2,12 @@
// Shows transaction details, warnings, errors. On Sign & Send,
// reads inline password, decrypts secret, signs and broadcasts.
const {
parseEther,
parseUnits,
formatEther,
formatUnits,
Contract,
} = require("ethers");
const { parseEther, parseUnits, formatEther, Contract } = require("ethers");
const {
$,
showError,
hideError,
showView,
showFlash,
flashCopyFeedback,
addressTitle,
escapeHtml,
renderAddressHtml,
@@ -23,7 +15,7 @@ const {
goBack,
onViewLeave,
} = require("./helpers");
const { state, currentNetwork } = require("../../shared/state");
const { state } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices");
@@ -399,7 +391,7 @@ function clearPassword() {
hideError("confirm-tx-password-error");
}
function init(ctx) {
function init(_ctx) {
onViewLeave("confirm-tx", clearPassword);
$("btn-confirm-send").addEventListener("click", async () => {
@@ -421,7 +413,7 @@ function init(ctx) {
wallet.encryptedSecret,
password,
);
} catch (e) {
} catch {
showError(
"confirm-tx-password-error",
"That password is incorrect. Please try again.",

View File

@@ -73,7 +73,7 @@ function init(_ctx) {
// Verify password against the wallet's encrypted data
try {
await decryptWithPassword(wallet.encryptedSecret, pw);
} catch (_e) {
} catch {
$("delete-wallet-flash").textContent =
"That password is incorrect. Please try again.";
$("delete-wallet-flash").style.visibility = "visible";

View File

@@ -1,11 +1,7 @@
// Shared DOM helpers used by all views.
const { isDebug } = require("../../shared/log");
const {
formatUsd,
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const { formatUsd, getPrice } = require("../../shared/prices");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { markViewRendered } = require("../viewRouter");

View File

@@ -2,7 +2,6 @@ const {
$,
showView,
showFlash,
flashCopyFeedback,
balanceLinesForAddress,
isoDate,
timeAgo,

View File

@@ -57,7 +57,7 @@ function show() {
attachCopyHandlers("view-receive");
}
function init(ctx) {
function init(_ctx) {
$("btn-receive-copy").addEventListener("click", () => {
const addr = $("receive-address-block").dataset.full;
if (addr) {

View File

@@ -4,7 +4,6 @@ const {
$,
showFlash,
addressTitle,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
goBack,
@@ -210,7 +209,7 @@ function init(_ctx) {
}
resolvedTo = resolved;
ensName = to;
} catch (e) {
} catch {
showFlash("Failed to resolve ENS name.");
return;
}

View File

@@ -14,7 +14,6 @@ const {
parseDustThresholdGwei,
} = require("../dustThreshold");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { onChainSwitch } = require("../../shared/chainSwitch");
const { log, debugFetch, setRuntimeDebug } = require("../../shared/log");
const deleteWallet = require("./deleteWallet");

View File

@@ -23,8 +23,6 @@ const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval");
let ctx;
/**
* Determine a human-readable transaction type string from tx fields.
*/
@@ -166,7 +164,7 @@ function render() {
if (el) el.classList.add("hidden");
}
loadFullTxDetails(tx.hash, tx.to, tx.isContractCall);
loadFullTxDetails(tx.hash, tx.to);
const isoStr = isoDate(tx.timestamp);
$("tx-detail-time").innerHTML =
@@ -274,7 +272,7 @@ function populateOnChainDetails(txData) {
}
}
async function loadFullTxDetails(txHash, toAddress, isContractCall) {
async function loadFullTxDetails(txHash, toAddress) {
const section = $("tx-detail-calldata-section");
const actionEl = $("tx-detail-calldata-action");
const detailsEl = $("tx-detail-calldata-details");
@@ -349,8 +347,9 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
}
}
// The ctx this view is initialized with is unused: this module is the leaf of
// the navigation, and the other views reach it through their own ctx.
function init(_ctx) {
ctx = _ctx;
$("btn-tx-back").addEventListener("click", () => {
goBack();
});

View File

@@ -12,7 +12,7 @@ const {
clearViewStack,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { state, currentNetwork } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
@@ -229,10 +229,6 @@ function tokenLabel(address) {
return t ? t.symbol : null;
}
function etherscanTokenLink(address) {
return `${currentNetwork().explorerUrl}/token/${address}`;
}
function decodedDetailsHtml(decoded) {
if (!decoded || !decoded.details) return "";
let html = `<div class="border border-border border-dashed p-2 mb-3">`;

View File

@@ -21,7 +21,7 @@ async function refreshPrices() {
const fetched = await getTopTokenPrices(25);
Object.assign(prices, fetched);
lastFetchedAt = now;
} catch (e) {
} catch {
// prices stay stale on error
}
}

View File

@@ -82,7 +82,7 @@ function toFixedPoint(value) {
if (text === "") return null;
try {
return parseUnits(text, SCALE_DECIMALS);
} catch (e) {
} catch {
return null;
}
}

View File

@@ -102,6 +102,11 @@ function decodeV2SwapExactIn(input) {
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
// address[] path, bool payerIsUser)
//
// Nothing calls this: decode() has no 0x09 arm, so a V2 exact-out swap gets
// its command name and no token or amount detail. Kept for the fix, which is
// https://git.eeqj.de/sneak/AutistMask/issues/283.
// eslint-disable-next-line no-unused-vars
function decodeV2SwapExactOut(input) {
try {
const d = coder.decode(

View File

@@ -57,7 +57,7 @@ async function cryptoBackend() {
try {
await WebAssembly.compile(EMPTY_WASM_MODULE);
return "wasm";
} catch (_) {
} catch {
return "asmjs";
}
}