0010. Static analysis with SonarQube Cloud
- Status: Accepted
- Date: 2026-10-21
Context
ESLint, CodeQL, Stryker and the 100% coverage gate (ADR 0005) each check one thing. The maintainer wants one more layer on top of them: SonarQube Cloud, with the strictest quality gate the project can keep green, and zero open issues on both new and overall code (#185).
SonarQube Cloud's automatic analysis cannot import coverage reports, so the analysis must run in CI.
Decision
Where it runs: the
SonarQube Cloudjob inci.yml, on every pull request and every push tomaster. It runspnpm test:coverage, then the official scan action pinned by commit SHA, and waits for the quality gate. The action is LGPL-3.0 and runs only in CI, so the dependency review allows it by name (ADR 0008, C17). Automatic analysis is turned off for the project. WithoutSONAR_TOKEN(pull requests from forks) the job skips the analysis with a notice. Dependabot has its own copy of the secret, so dependency updates are analyzed too.Scope:
sonar-project.properties. Test code (unit tests, E2E specs, the fake WhatsApp page) is analyzed with test rules and kept out of duplication and coverage. Coverage exclusions mirrorvitest.config.ts, so Sonar and Vitest agree on 100%.Quality gate "Sonar way for Agentic AI" (built in), which checks new code: severity limits on new bugs, vulnerabilities, code smells and dependency issues, coverage of at least 80% and duplication of at most 3%. The project first cleared its backlog under a custom gate, "YapYap strict", that required zero issues, 100% coverage and A ratings on new and overall code. Once overall code passed it, the maintainer chose the built-in gate and the custom one was deleted (2026-10-22). Vitest still enforces 100% coverage (ADR 0005), so the gate's 80% is only a floor.
Quality profiles "YapYap strict", for TypeScript, JavaScript, CSS, HTML, GitHub Actions, YAML, JSON, XML, secrets and text. Each inherits Sonar way comprehensive, so new built-in rules arrive on their own, and also activates every other rule of its language except these:
Rule Why it is off S1537 (no trailing commas) Prettier adds trailing commas ( trailingComma: all); the two cannot both passS3524 (no parentheses around one parameter) Prettier always parenthesizes arrow function parameters S1451, Web:HeaderCheck (file headers) The license lives in LICENSEand package metadata; per-file headers would repeat it hundreds of timesS1774 (no ternary operator) Ternaries are the idiomatic way to choose a value in TypeScript and JSX; ESLint already limits nesting S2138 (no undefinedassignment)The codebase uses undefinedfor "absent" everywhere, as TypeScript's optional properties do;nullis kept for JSON and DOM APIsS4622 (unions of at most two types) String literal unions such as 'system' | 'light' | 'dark'are how the settings schema and IPC types are modelledWeb:InternationalizationCheck The app chrome is localized in TypeScript (#55), not through server-side resource bundles in HTML text:S7036 (inclusive terminology) Every hit is the name of the default branch, master; renaming the branch is a separate decisionS100 (function names) allows PascalCase as well, because Preact components and ESLint rule visitors (
Program) must use it.Exceptions: an issue is fixed, or the rule is turned off above with a reason. Issues are never accepted, even where the gate would pass. Marking a false positive needs a comment that explains it. Narrow per-file exceptions live in
sonar-project.properties, each with its reason: console output andPATHlookups in command-line scripts, the main entry's promise chain (no top-level await), and two E2E rules the analyzer misreads for Playwright (assertions and conditional skips).Required check:
SonarQube Cloudis required in.github/rulesets/master.json. It was added only once overall code passed the strict gate, so the backlog never blocked unrelated work. The README shows the gate and the main measures as badges.
Consequences
- Every pull request gets a quality gate result and a decoration with its new issues, coverage and duplication.
- The CI job repeats the unit tests to produce coverage next to the scan; this costs about a minute and keeps the job self-contained.
- A rule that conflicts with a project convention is turned off with a reason here rather than worked around in code.