~/abhipraya
PPL: Static Analysis in SIRA [Sprint 1, Week 2]
The Tool Stack
| Layer | Tool | Scope |
|---|---|---|
| Frontend lint + format | Biome | TypeScript/TSX, 40+ enforced rules |
| Frontend dead code | Knip | Unused exports, imports, dependencies |
| Frontend types | tsc --noEmit | TypeScript type checking |
| Backend lint + format | Ruff | Python, F401/F841 + style rules |
| Backend types | mypy | Python static types, strict mode |
| CI quality gate | SonarQube | Coverage, code smells, security hotspots |
Pre-commit Hooks (Husky)
Five checks run sequentially on every git commit. A failure at any step blocks the commit:
flowchart LR
C([git commit]) --> B["Biome
lint + format"]
B --> R["Ruff
lint + format"]
R --> T["tsc
typecheck"]
T --> M["mypy
typecheck"]
M --> K["Knip
dead code"]
K --> OK([commit accepted])
B -->|fail| X([blocked])
R -->|fail| X
T -->|fail| X
M -->|fail| X
K -->|fail| X
git push additionally runs the full test suite (vitest + pytest).
Here’s what a blocked commit looks like - Biome catches any type and an unused variable in the same line:
$ git commit -m "test: deliberate lint error"
Running pre-commit checks...
[1/5] Checking frontend lint (Biome)...
src/lib/api.ts:28:19 lint/suspicious/noExplicitAny
× Unexpected any. Specify a different type.
> 28 │ const unused_var: any = "test"
│ ^^^
src/lib/api.ts:28:7 lint/correctness/noUnusedVariables
× This variable unused_var is unused.
> 28 │ const unused_var: any = "test"
│ ^^^^^^^^^^
Checked 41 files in 42ms. Found 3 errors.
husky - pre-commit script failed (code 1)
After removing the line, the same commit succeeds - all 5 checks pass in sequence and the commit is accepted.
Source: .husky/pre-commit, .husky/pre-push
Key Rules Enforced
Biome (apps/web/biome.json) - selected rules:
{
"suspicious": { "noExplicitAny": "error" },
"correctness": { "noUnusedVariables": "error", "noUnusedImports": "error", "useHookAtTopLevel": "error" },
"style": { "useImportType": "error", "noParameterAssign": "error" },
"complexity": { "useOptionalChain": "error" }
}
mypy (apps/api/pyproject.toml):
[tool.mypy]
python_version = "3.12"
strict = true
Strict mode enables --disallow-any-generics, --disallow-untyped-defs, --warn-return-any, and 15 other checks.
Knip (apps/web/knip.config.ts) - all categories set to "error":
rules: {
files: 'error', dependencies: 'error', exports: 'error',
types: 'error', duplicates: 'error', unlisted: 'error',
}
SonarQube in CI (SIRA-60)
SonarQube runs as a dedicated quality stage in the GitLab CI pipeline, after all CI jobs complete. Coverage artifacts from CI are consumed directly:
flowchart TB
subgraph ci [ci stage - parallel]
WL[web:lint]
WT["web:test
generates lcov.info"]
AL[api:lint]
AT["api:test
generates coverage.xml"]
end
subgraph quality [quality stage]
LN[linear:notify]
SQ[sonar-scanner-cli]
end
ci --> quality
WT --> SQ
AT --> SQ
sonar-project.properties at repo root:
sonar.projectKey=SIRA
sonar.sources=apps/web/src,apps/api/src
sonar.tests=apps/api/tests
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info
sonar.python.coverage.reportPaths=apps/api/coverage.xml
Both coverage reports (Python XML + JS LCOV) flow into a single SonarQube project at sonarqube.cs.ui.ac.id. The SonarQube MCP server is also configured in .mcp.json for AI-assisted quality inspection directly from the editor.

Beyond Style - Runtime Failure Prevention
Static analysis in SIRA is not limited to formatting and import order. The configuration catches issues that would manifest as runtime failures in production.
mypy Strict Mode: Untyped Code as a Runtime Risk
strict = true enables a class of checks that catch real runtime failures:
| mypy flag | What it prevents |
|---|---|
--disallow-untyped-defs | Functions with no type annotations - callers can’t reason about null safety |
--disallow-any-generics | list without list[str] - type-erased collections that collapse to Any |
--warn-return-any | Functions returning Any - propagates type unsafety to every caller |
--strict-optional | Unenforced Optional checks - missed None dereferences become AttributeError at runtime |
The last is the most critical: any T | None return must be narrowed before use. mypy surfaces these as type errors before CI, not as production crashes.
Biome noExplicitAny: Blocking the Type Escape Hatch
TypeScript’s any type disables all type inference - any method call or property access on any is unchecked:
// ❌ blocked - data.amount is unchecked; typos and wrong shapes pass silently
function process(data: any) { return data.amount }
// ✅ required - TypeScript verifies .amount_paid exists on PaymentCreate
function process(data: PaymentCreate) { return data.amount_paid }
With any, data.typo compiles and crashes at runtime. With typed parameters, it’s a compile error caught before the branch executes.
Knip: Dead Code as a Behavioral Risk
Unused exports are not just style noise - they represent a behavioral risk:
- Unused functions accumulate bugs that tests never exercise
- Dead branches can drift out of sync with the active codebase’s assumptions
- Unreachable code paths hide logic errors until accidentally reactivated
exports: "error" ensures every exported symbol is consumed. Unused utilities are removed rather than accumulating drift.
SonarQube: Complexity as a Bug Predictor
SonarQube’s cognitive complexity metric flags functions with high branching depth - a reliable predictor of off-by-one and conditional logic errors. This catches behavioral issues that type checkers miss: not “what type is this value” but “is this branching logic reachable and correct.”
Evidence
- MR !26 - SIRA-60 SonarQube CI integration
- Linear SIRA-60
- Source:
sonar-project.properties,apps/web/biome.json,apps/web/knip.config.ts,apps/api/pyproject.toml,.husky/pre-commit,.gitlab-ci.yml