Motivation
A consumer who vendors the async-lib source into their own repo ran PMD over it
and got 7 findings (6 critical, 1 major) — NcssCount, UnusedLocalVariable ×5,
AvoidHardcodingId. We can't reproduce that locally because async-lib runs no
PMD at all, and the formatting/lint tools we do have are not enforced where it
counts.
Current state (verified):
- Prettier — exists (
prettier, prettier:verify scripts) but enforced only
in the local pre-commit hook (lint-staged + husky), which any dev can skip
with git commit --no-verify.
- ESLint — exists but scoped to
**/{aura,lwc}/** only; also local pre-commit
only. (ESLint does not analyze Apex — it never will catch the PMD findings.)
- PMD — none. No ruleset, no scanner, nothing.
- CI (
.github/workflows/ci.yml → shared cicd-template/salesforce-ci) runs
only deploy + Apex tests + Codecov. No prettier, no eslint, no PMD.
Net: the only real gate is Apex tests. Formatting and static analysis are advisory
and bypassable. We ship code that fails a standard PMD ruleset, and our consumers
see it before we do.
Goal
Every change to the lib must pass Prettier, ESLint, and PMD in CI — not just
in a local hook that can be skipped.
Proposal
1. Add PMD with a tuned Apex ruleset
- Store
pmd/ruleset.xml. Maintainer supplies the base ruleset (the common
one already used across other btcdev projects) — do not seed from scratch. Then
tune it here:
- Test classes: relax or exclude the noisy rules for
*Test.cls
(NcssCount, UnusedLocalVariable, AvoidHardcodingId are expected in a
4800-line test class with fake Ids and scaffold vars). Prefer a path/pattern
exclusion for test sources over weakening the rule globally.
- Intentional idiom:
QueueableJob.getFullClassName deliberately assigns an
unused local to force a TypeException (Apex forbids a bare cast statement).
Suppress narrowly via @SuppressWarnings('PMD.UnusedLocalVariable') on that
method (or a ruleset exclusion), with a comment explaining why.
- Add npm scripts:
pmd (run) and pmd:verify (fail on findings).
2. Keep Prettier + ESLint, make them non-bypassable
- Prettier stays as-is; ESLint config already no-ops cleanly when there is no
LWC/Aura (see verify-static.sh). No behavior change — just add them to CI.
3. Enforce all three in CI (the actual ask)
No such CI check exists today (verified: the reusable salesforce-ci runs only
deploy + Apex tests + Codecov). So this is net-new. A static-analysis gate that
fails the PR:
- Run
prettier:verify + eslint + pmd:verify on every PR/push.
- Two placement options (decision needed):
- A. Local job in this repo's
ci.yml alongside the reusable salesforce-ci
call — a static-analysis job that fails independently. Self-contained.
- B. Extend the shared
cicd-template to include a lint/PMD stage — benefits
every btcdev lib at once, but is a cross-repo change affecting other packages.
- Recommendation: A first (unblocks async-lib now), then upstream to the
template (B) once the ruleset is proven.
4. Wire into the agent static gate + implementer agent steps
- Add
pmd:verify to scripts/agent/verify-static.sh so verify.sh and agents
catch findings before CI, keeping the local/CI checks in sync.
- Update the implementer agent definition so its workflow explicitly runs the
static gate (prettier + eslint + PMD) as a step and treats any finding as a
blocker before it commits — the agent must know PMD is now part of "done".
5. Keep pre-commit as fast local feedback
lint-staged stays (prettier + eslint) for quick loops. CI is the authority.
Baseline decision
The existing 7 findings must reach zero before the gate can be required:
QueueableJob:229 → suppress (intentional idiom).
- AsyncTest findings → covered by test-source exclusion, OR fix the real ones
(drop genuinely unused locals, replace hardcoded Ids with generated ones,
consider splitting AsyncTest for NcssCount).
- Decide: fix, suppress, or baseline (accept current, block only new).
Prefer fix/suppress over a baseline file so the tree is genuinely clean.
Tooling decision
- PMD runner:
sf code-analyzer (Salesforce Code Analyzer plugin, current) vs
sf scanner (legacy) vs standalone pmd binary vs a pmd-github-action.
Pick one; sf code-analyzer aligns with the existing sf-CLI toolchain.
- Pin the PMD/analyzer version so findings are reproducible across dev and CI.
Scope
- Tooling + config + CI only. No product code change beyond the one suppression
and any real finding fixes.
Out of scope
- New lint rules beyond a sane PMD default + our tuning.
- Reformatting unrelated code (Prettier already governs formatting).
Open questions
- Exclude test sources from PMD wholesale, or tune per-rule for tests?
- Fix vs suppress vs baseline for the current AsyncTest findings?
- Which PMD runner + pinned version?
Resolved from discussion: base ruleset supplied by maintainer (common btcdev
ruleset); CI placement = local job (A) first, upstream to shared template later;
no CI static check exists today.
Acceptance criteria
pmd/ruleset.xml committed; pmd + pmd:verify npm scripts.
- Tree passes
pmd:verify at zero findings (via fixes + the documented suppression).
- CI fails a PR on any Prettier, ESLint, or PMD violation (option A wired at least).
scripts/agent/verify-static.sh runs pmd:verify.
- Implementer agent definition updated: static gate (prettier + eslint + PMD) is an
explicit step and a commit blocker.
- Docs/CONTRIBUTING note: changes must pass prettier + eslint + PMD; how to run
them locally.
Motivation
A consumer who vendors the async-lib source into their own repo ran PMD over it
and got 7 findings (6 critical, 1 major) —
NcssCount,UnusedLocalVariable×5,AvoidHardcodingId. We can't reproduce that locally because async-lib runs noPMD at all, and the formatting/lint tools we do have are not enforced where it
counts.
Current state (verified):
prettier,prettier:verifyscripts) but enforced onlyin the local pre-commit hook (
lint-staged+ husky), which any dev can skipwith
git commit --no-verify.**/{aura,lwc}/**only; also local pre-commitonly. (ESLint does not analyze Apex — it never will catch the PMD findings.)
.github/workflows/ci.yml→ sharedcicd-template/salesforce-ci) runsonly deploy + Apex tests + Codecov. No prettier, no eslint, no PMD.
Net: the only real gate is Apex tests. Formatting and static analysis are advisory
and bypassable. We ship code that fails a standard PMD ruleset, and our consumers
see it before we do.
Goal
Every change to the lib must pass Prettier, ESLint, and PMD in CI — not just
in a local hook that can be skipped.
Proposal
1. Add PMD with a tuned Apex ruleset
pmd/ruleset.xml. Maintainer supplies the base ruleset (the commonone already used across other btcdev projects) — do not seed from scratch. Then
tune it here:
*Test.cls(
NcssCount,UnusedLocalVariable,AvoidHardcodingIdare expected in a4800-line test class with fake Ids and scaffold vars). Prefer a path/pattern
exclusion for test sources over weakening the rule globally.
QueueableJob.getFullClassNamedeliberately assigns anunused local to force a
TypeException(Apex forbids a bare cast statement).Suppress narrowly via
@SuppressWarnings('PMD.UnusedLocalVariable')on thatmethod (or a ruleset exclusion), with a comment explaining why.
pmd(run) andpmd:verify(fail on findings).2. Keep Prettier + ESLint, make them non-bypassable
LWC/Aura (see
verify-static.sh). No behavior change — just add them to CI.3. Enforce all three in CI (the actual ask)
No such CI check exists today (verified: the reusable
salesforce-ciruns onlydeploy + Apex tests + Codecov). So this is net-new. A static-analysis gate that
fails the PR:
prettier:verify+eslint+pmd:verifyon every PR/push.ci.ymlalongside the reusablesalesforce-cicall — a
static-analysisjob that fails independently. Self-contained.cicd-templateto include a lint/PMD stage — benefitsevery btcdev lib at once, but is a cross-repo change affecting other packages.
template (B) once the ruleset is proven.
4. Wire into the agent static gate + implementer agent steps
pmd:verifytoscripts/agent/verify-static.shsoverify.shand agentscatch findings before CI, keeping the local/CI checks in sync.
static gate (prettier + eslint + PMD) as a step and treats any finding as a
blocker before it commits — the agent must know PMD is now part of "done".
5. Keep pre-commit as fast local feedback
lint-stagedstays (prettier + eslint) for quick loops. CI is the authority.Baseline decision
The existing 7 findings must reach zero before the gate can be required:
QueueableJob:229→ suppress (intentional idiom).(drop genuinely unused locals, replace hardcoded Ids with generated ones,
consider splitting AsyncTest for
NcssCount).Prefer fix/suppress over a baseline file so the tree is genuinely clean.
Tooling decision
sf code-analyzer(Salesforce Code Analyzer plugin, current) vssf scanner(legacy) vs standalonepmdbinary vs apmd-github-action.Pick one;
sf code-analyzeraligns with the existing sf-CLI toolchain.Scope
and any real finding fixes.
Out of scope
Open questions
Resolved from discussion: base ruleset supplied by maintainer (common btcdev
ruleset); CI placement = local job (A) first, upstream to shared template later;
no CI static check exists today.
Acceptance criteria
pmd/ruleset.xmlcommitted;pmd+pmd:verifynpm scripts.pmd:verifyat zero findings (via fixes + the documented suppression).scripts/agent/verify-static.shrunspmd:verify.explicit step and a commit blocker.
them locally.