diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index 324df715..146dde28 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -72,13 +72,11 @@ jobs: working-directory: ./web run: npm run lint - # Typecheck gate. Currently non-blocking (continue-on-error) because the - # existing codebase still has pre-existing type errors. Once the count - # reaches zero, remove `continue-on-error` to make this a hard merge block. - - name: Typecheck - working-directory: ./web - continue-on-error: true - run: npm run typecheck + # Typecheck gate, held to a ratchet: the type errors that predate the gate + # are recorded in ops/typecheck-baseline.txt, and this fails when any file + # gains one, or when errors were fixed without lowering the baseline. + - name: Typecheck (ratchet) + run: python3 ops/typecheck-ratchet.py - name: Run Tests working-directory: ./web diff --git a/ops/typecheck-baseline.txt b/ops/typecheck-baseline.txt new file mode 100644 index 00000000..89672464 --- /dev/null +++ b/ops/typecheck-baseline.txt @@ -0,0 +1,102 @@ +# count file code — written by ops/typecheck-ratchet.py --update +3 ../shared/lib/multisig-operations.ts TS2339 +3 ../shared/lib/perwerde.ts TS2339 +6 ../shared/lib/presale.ts TS2339 +1 ../shared/lib/referral.ts TS2339 +9 ../shared/lib/scores.ts TS2339 +3 ../shared/lib/staking.ts TS2339 +10 ../shared/lib/validator-pool.ts TS2339 +2 ../shared/lib/welati.ts TS2305 +24 ../shared/lib/welati.ts TS2339 +1 ../shared/lib/xcm-bridge.ts TS2353 +1 ../shared/lib/xcm-bridge.ts TS2554 +5 ../shared/lib/xcm-wizard.ts TS2339 +1 ../shared/lib/xcm-wizard.ts TS2345 +1 src/components/AddLiquidityModal.tsx TS2339 +4 src/components/AddLiquidityModal.tsx TS2345 +2 src/components/AddLiquidityModal.tsx TS2352 +3 src/components/LPStakingModal.tsx TS2352 +2 src/components/LPStakingModal.tsx TS2554 +2 src/components/MultisigMembers.tsx TS2322 +5 src/components/MultisigMembers.tsx TS2339 +1 src/components/MultisigMembers.tsx TS2345 +2 src/components/NetworkStats.tsx TS2339 +1 src/components/PoolDashboard.tsx TS2345 +3 src/components/PoolDashboard.tsx TS2352 +1 src/components/RemoveLiquidityModal.tsx TS2345 +16 src/components/TokenSwap.tsx TS2322 +7 src/components/TokenSwap.tsx TS2339 +8 src/components/TokenSwap.tsx TS2345 +6 src/components/TokenSwap.tsx TS2552 +1 src/components/TransactionHistory.tsx TS2304 +3 src/components/TransactionHistory.tsx TS2339 +1 src/components/XCMTeleportModal.tsx TS2339 +2 src/components/admin/CommissionSetupTab.tsx TS2339 +14 src/components/admin/CommissionVotingTab.tsx TS2339 +1 src/components/admin/CommissionVotingTab.tsx TS2345 +1 src/components/admin/XCMConfigurationWizard.tsx TS2339 +5 src/components/admin/XCMConfigurationWizard.tsx TS2345 +14 src/components/dashboard/CommissionProposalsCard.tsx TS2339 +1 src/components/delegation/DelegationManager.tsx TS2322 +1 src/components/delegation/DelegationManager.tsx TS2345 +1 src/components/dex/AddLiquidityModal.tsx TS2345 +2 src/components/dex/CreatePoolModal.tsx TS2345 +1 src/components/dex/InitializeHezPoolModal.tsx TS2339 +1 src/components/dex/InitializeUsdtModal.tsx TS2339 +1 src/components/dex/PoolBrowser.tsx TS2339 +1 src/components/dex/RemoveLiquidityModal.tsx TS2339 +1 src/components/dex/RemoveLiquidityModal.tsx TS2345 +3 src/components/dex/SwapInterface.tsx TS2345 +1 src/components/forum/DiscussionThread.tsx TS2339 +1 src/components/forum/DiscussionThread.tsx TS2740 +1 src/components/governance/ElectionsInterface.tsx TS2345 +2 src/components/governance/GovernanceHistory.tsx TS2339 +10 src/components/governance/MyVotes.tsx TS2339 +2 src/components/notifications/NotificationCenter.tsx TS2322 +2 src/components/notifications/NotificationCenter.tsx TS2739 +2 src/components/p2p/AdList.tsx TS2322 +2 src/components/p2p/AdList.tsx TS2339 +2 src/components/perwerde/CourseList.tsx TS2353 +1 src/components/security/PermissionEditor.tsx TS2304 +1 src/components/security/PermissionEditor.tsx TS2552 +1 src/components/security/SecurityAudit.tsx TS2304 +1 src/components/security/SessionMonitor.tsx TS2552 +1 src/components/theme-provider.tsx TS2322 +1 src/components/trading/PriceChart.tsx TS2345 +1 src/components/trading/PriceChart.tsx TS2352 +4 src/components/wallet/WalletButton.tsx TS2339 +2 src/contexts/AuthContext.tsx TS2322 +1 src/contexts/ReferralContext.tsx TS2345 +10 src/hooks/use-toast.ts TS2339 +2 src/hooks/use-toast.ts TS2395 +3 src/hooks/useDelegation.ts TS2339 +6 src/hooks/useGovernance.ts TS2339 +1 src/hooks/useMobileBridge.ts TS2345 +8 src/pages/AdminPanel.tsx TS2322 +3 src/pages/AdminPanel.tsx TS2345 +1 src/pages/Citizens.tsx TS2345 +5 src/pages/Dashboard.tsx TS2322 +2 src/pages/Dashboard.tsx TS2339 +1 src/pages/Dashboard.tsx TS2769 +1 src/pages/EducationPlatform.tsx TS2353 +4 src/pages/Elections.tsx TS2339 +3 src/pages/Elections.tsx TS2345 +1 src/pages/Elections.tsx TS2352 +8 src/pages/Explorer.tsx TS2339 +2 src/pages/P2PDispute.tsx TS2554 +2 src/pages/P2PTrade.tsx TS2322 +3 src/pages/P2PTrade.tsx TS2339 +2 src/pages/Presale.tsx TS2339 +2 src/pages/Presale.tsx TS2345 +4 src/pages/TelegramConnect.tsx TS2339 +2 src/pages/WalletDashboard.tsx TS2339 +2 src/pages/citizens/CitizensIssues.tsx TS2339 +3 src/pages/citizens/GovernmentEntrance.tsx TS2339 +1 src/pages/education/CertificatesPage.tsx TS2339 +1 src/pages/education/CertificatesPage.tsx TS2769 +1 src/pages/finance/TaxZekatPage.tsx TS2339 +2 src/pages/launchpad/CreatePresale.tsx TS2345 +29 src/pages/launchpad/PresaleDetail.tsx TS2339 +6 src/pages/launchpad/PresaleDetail.tsx TS2345 +1 src/pages/launchpad/PresaleDetail.tsx TS2362 +4 src/pages/launchpad/PresaleList.tsx TS2339 diff --git a/ops/typecheck-ratchet.py b/ops/typecheck-ratchet.py new file mode 100755 index 00000000..adab56be --- /dev/null +++ b/ops/typecheck-ratchet.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Hold web's type errors to a ratchet. + +web/ has type errors that predate its typecheck gate; fixing them all at once +would mean editing hundreds of lines of production code with little test +cover, so the gate used to run with continue-on-error and could not fail. +This makes it a real gate without that big bang: the errors that exist are +recorded per file and error code in ops/typecheck-baseline.txt, and the check +fails when any file gains an error of any code. It also fails when errors were +fixed but the baseline was not lowered with them, so a gain cannot quietly be +given back later. + + python3 ops/typecheck-ratchet.py # check (run from the repo root) + python3 ops/typecheck-ratchet.py --update # rewrite the baseline (only ever lower it) + +Line numbers are left out of the signature on purpose: they move with every +edit above an error, and a gate that fails on that is a gate people disable. +""" +import collections +import pathlib +import re +import subprocess +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +BASELINE = ROOT / 'ops' / 'typecheck-baseline.txt' +ERROR = re.compile(r'^(?P[^(\s][^(]*)\(\d+,\d+\): error (?PTS\d+):') + + +def current() -> collections.Counter: + out = subprocess.run(['npm', 'run', '--silent', 'typecheck'], cwd=ROOT / 'web', + capture_output=True, text=True) + counts = collections.Counter() + for line in (out.stdout + out.stderr).splitlines(): + m = ERROR.match(line) + if m: + counts[f"{m['file']} {m['code']}"] += 1 + if out.returncode != 0 and not counts: + sys.exit(f'typecheck failed without reporting type errors:\n{out.stdout}{out.stderr}') + return counts + + +def baseline() -> collections.Counter: + counts = collections.Counter() + for line in BASELINE.read_text().splitlines(): + if line and not line.startswith('#'): + n, sig = line.split(' ', 1) + counts[sig] = int(n) + return counts + + +def main() -> int: + now = current() + if '--update' in sys.argv: + lines = [f'{n} {sig}' for sig, n in sorted(now.items())] + BASELINE.write_text('# count file code — written by ops/typecheck-ratchet.py --update\n' + + '\n'.join(lines) + '\n') + print(f'baseline: {sum(now.values())} errors in {len(now)} file/code pairs') + return 0 + + base = baseline() + worse = {sig: (base.get(sig, 0), n) for sig, n in now.items() if n > base.get(sig, 0)} + better = {sig: (n, now.get(sig, 0)) for sig, n in base.items() if now.get(sig, 0) < n} + for sig, (was, isnow) in sorted(worse.items()): + print(f'NEW {sig}: {was} -> {isnow}') + for sig, (was, isnow) in sorted(better.items()): + print(f'fixed {sig}: {was} -> {isnow}') + print(f'type errors: {sum(now.values())} (baseline {sum(base.values())})') + if worse: + print('New type errors. Fix them; the baseline only ever goes down.') + return 1 + if better: + # Fail here too: a gain that is not written into the baseline can be + # given back later without the gate noticing. + print('Fewer errors than the baseline: run with --update and commit the new baseline in this change.') + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json index cb77c616..a4cc77c7 100644 --- a/web/tsconfig.app.json +++ b/web/tsconfig.app.json @@ -29,10 +29,12 @@ "@pezkuwi/lib": ["../shared/lib"], "@pezkuwi/lib/*": ["../shared/lib/*"], "@pezkuwi/utils": ["../shared/utils"], + "@pezkuwi/utils/*": ["../shared/utils/*"], "@pezkuwi/theme": ["../shared/theme"], "@pezkuwi/types": ["../shared/types"], "@pezkuwi/components/*": ["../shared/components/*"], - "@shared/*": ["../shared/*"] + "@shared/*": ["../shared/*"], + "*": ["./node_modules/@types/*", "./node_modules/*"] }, "types": ["vite/client"] },