Skip to content

feat: roslyn analyzers - #9727

Open
NickKhalow wants to merge 13 commits into
devfrom
feat/roslyn-analyzers
Open

feat: roslyn analyzers#9727
NickKhalow wants to merge 13 commits into
devfrom
feat/roslyn-analyzers

Conversation

@NickKhalow

@NickKhalow NickKhalow commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Pull Request Description

What does this PR change?

Adds automated enforcement of the project's code standards (CLAUDE.md / skills docs) in two layers — a fast deterministic regex lint and a set of Roslyn analyzers running inside Unity's compilation and the IDE — plus the mechanical cleanup of the existing violations they surfaced (fix/lint-surfaced-cleanups, merged into this branch), so the rules land on a clean baseline.

Layer 1 — deterministic project-rule lint (scripts/lint/custom-rules.sh)

~20 textually-checkable rules, each with a severity and a pointer to the doc that defines it:

  • BLOCK (fails): Debug.Log outside ReportHub, new ObjectProxy<, CheckNamespace suppressions, LINQ in *System.cs, Camera.main, #nullable disable, non-I interface names, foreign test frameworks (Moq/Xunit/FluentAssertions), async void tests, unguarded TryAddWidget(...)., thread affinity in scene-runtime code, nullable locals, the null-forgiving ! (with carve-outs for the sanctioned = null! DTO idiom and the framework-forced viewInstance!/Instance!/World!), var x = World.Get<...> component copies, StringBuilder.Append($"...").
  • WARN (prints, never blocks): string-literal ReportHub categories, World.Query, raw HTTP outside WebRequests/, exception messages without nameof.

Design properties:

  • Ratchet: only lines added in the diff under inspection are linted (working tree vs HEAD in the Stop hook, commit range in CI) — pre-existing violations never block.
  • Suppression is a visible // lint-ignore: <rule-id> comment reviewers can challenge in the diff.
  • Test/Editor/Plugins/Demo paths are excluded from production-only rules.
  • Exit code 3 distinguishes "a rule pattern itself is broken" from "clean" — a broken rule fails loudly instead of silently passing.
  • Self-tested: scripts/lint/tests/selftest.sh runs fixture files against expected-output files, in CI before every lint.

Layer 2 — DCL.Analyzers, five Roslyn analyzers

Semantic checks the regex layer cannot do (symbol resolution, statement ordering). Severities pinned in Explorer/.editorconfig; corruption-class rules fail the build, the rest are advisory while their real-world precision is established:

ID Severity What it catches
DCLA001 error ref local from World.Get/TryGetRef used after a structural change (Add/Remove/Create/Destroy) relocated it. Reachability-aware: exclusive branches, pre-call arguments, and the x = ref World.Get(...) re-fetch idiom stay silent.
DCLA002 warning Detached UniTask flow (async UniTaskVoid, .Forget()) with no exception handling of its own.
DCLA003 warning Heap allocation in per-frame code: system Update() / [Query] bodies, plus any method tagged with the new [Utility.HotPath] attribute. Throw paths exempt. Body-only — callees are not chased.
DCLA004 warning Pooled Get() rental that provably never escapes or releases.
DCLA005 error Enum crossing a [DllImport] boundary without an explicit underlying type.

Both error-severity rules downgrade to warnings under **/Tests/** (tests exercise structural-change scenarios deliberately).

Integration

  • Explorer/Assets/DCL/DCL.Analyzers.dll (LFS) carries the RoslynAnalyzer label with all platforms disabled — Unity feeds it to csc for every asmdef at or below Assets/DCL/: first-party code only, vendored code untouched. Analyzers also run live in Rider/VS.
  • Built netstandard2.0 against Microsoft.CodeAnalysis 4.3.1, so it loads in any Roslyn host ≥ 4.3 (Unity 6000.x bundles ≥ 4.9). bash scripts/build-analyzers.sh = tests + Release build + DLL sync.
  • The lint Stop hook (scripts/lint/lint-changed.sh) runs the regex layer first — instant feedback before the multi-minute ReSharper load, and ReSharper is skipped while BLOCK findings are unresolved.
  • CI: new custom-lint job (seconds, no Unity, diff-ranged with base detection for PRs / pushes / merge queues; runs the linter selftest first) and analyzers job (runs the analyzer test suite when Analyzers/ changes). Both are forced to run when the linter itself changes so a broken rule cannot merge unexecuted. ~1,450 lines of analyzer tests using the standard Microsoft.CodeAnalysis.Testing markup idiom with metadata-name stubs for external types.

Layer 3 — cleanup of the violations the new tooling surfaced

The branch also merges fix/lint-surfaced-cleanups (104 files, +45/−308, all mechanical), which fixes the existing violations so the new rules land on a clean baseline — without it, DCLA001 (error) would fail the build on pre-existing code the moment this PR merged:

  • CharacterPreviewController.UpdateAvatarAsync held ref avatarShape across structural changes (globalWorld.Create, plus the entity creation/destruction hidden inside Promise.Create/ForgetLoading) — writes through a relocated ref are silently lost. All structural work now completes into locals before the ref is taken. A real, pre-existing lost-write bug found by DCLA001 — verified GREEN on Unity 6000.4.0f1 (219 assemblies).
  • DCLPlayerPrefs: shutdown/cleanup logs migrated to ReportHub (the TODO claiming it was unavailable was stale); the editor-only MenuItem log keeps Debug.Log with a justified // lint-ignore.
  • SituationalReactionPresenter: Camera.main replaced with injected IExposedCameraData.CinemachineBrain?.OutputCamera, threaded through ChatPluginChatPanelPresenter.
  • All 21 banned // ReSharper disable CheckNamespace comments deleted.
  • 37 dead csc.rsp files next to asmrefs deleted — Unity only honors rsp beside an asmdef (verified against the Bee build graph: zero references); orphaned by the assembly consolidation (refactor: consolidate assembly definitions #8961).
  • NRT enabled on the four uncovered assemblies: MarketplaceCredits.API, Character, Quality.RenderFeatures, DCL.Editor (~154 new nullable warnings — no-warning-ratchet label applies).

Review fixes (second push)

All review findings addressed:

  • custom-lint gate: the changes job now emits a lintscripts output; the job runs when only scripts/lint/** changed, so a broken rule can no longer merge with its selftest skipped.
  • Secret scope: custom-lint and analyzers blank the workflow-level Unity credentials at job level — both execute PR-authored code and need no license.
  • lint-changed.sh: no longer suppresses custom-rules.sh stderr; exit code 3 (broken rule pattern) now blocks like BLOCK findings.
  • [HotPath]: narrowed to AttributeTargets.Method — the analyzer doesn't inspect constructors, so the wider target created unchecked annotations.
  • DLL drift closed: the analyzers job deterministically rebuilds the DLL (SDK pinned exactly in Analyzers/global.json, ContinuousIntegrationBuild=true, DebugType=none) and fails on any byte difference vs the committed DLL. build-analyzers.sh uses identical flags; the DLL is recommitted from the deterministic build (byte-stable across clean rebuilds).
  • ReSharper ratchet: filter-warnings.sh excludes DCLA* diagnostics from the warning count — the analyzers have their own enforcement channel. This dropped the lint delta from +329 to +62; the remainder is the deliberate NRT enablement on four assemblies (CS8618/CS8625/CS8765), covered by the no-warning-ratchet label.
  • Portability: the lint selftest golden comparison now strips CR, so it passes on Windows (CRLF) checkouts.

Probe results and hardening (third push)

A deliberate DCLA001 violation was pushed, its CI result recorded, and reverted. The findings drove three further changes:

  • Unity's csc ignores .editorconfig severities — the probe compiled as a warning despite the error pin. DCLA001/DCLA005 now carry DiagnosticSeverity.Error in their descriptors, which is what actually fails the Unity build (verified: the analyzer does run inside Unity's compile and fired at the exact violation line). The .editorconfig pins remain for IDEs and dotnet builds; the **/Tests/** downgrade is therefore IDE-only.
  • The analyzers self-scope away from vendored code — Unity feeds the DLL to package compilations (Library/PackageCache), not just Assets/DCL as originally assumed; DCLA005-at-Error was failing com.decentraland.pulse.transport and com.unity.cloud.ktx. All five analyzers now skip /PackageCache/ syntax trees (VendoredCode.cs, with regression test). First-party violations surfaced by the probe run were fixed instead: UUAVState pins : int, and SituationalReactionPresenter's Cinemachine type leak into the Chat assembly (CS0012) is resolved via a new IExposedCameraData.OutputCamera default member.
  • The drift check caught two real divergences during development — a CRLF-vs-LF source checkout (fixed: Analyzers/** text eol=lf in .gitattributes) and an incremental-build DLL that differed from a clean rebuild (fixed: build-analyzers.sh wipes bin/obj first). Local and CI builds are now byte-identical.

Known limitations

  • DCLA003 is body-only by design: an allocating helper called from Update() is not flagged unless tagged [HotPath].
  • DCLA002/003/004 remain advisory warnings; the codebase currently carries ~620 DCLA002 (detached UniTask) hits that show as warnings in the Unity console — burn-down candidates for follow-up PRs.

Test Instructions

Tooling + mechanical cleanups; the only runtime-behavior surface worth a functional pass is the avatar preview flow touched by the DCLA001 fix.

Test Steps

  1. bash scripts/lint/tests/selftest.sh — regex-layer fixtures pass.
  2. dotnet test Analyzers/DCL.Analyzers.Tests (or bash scripts/build-analyzers.sh) — analyzer suite passes.
  3. Add a Debug.Log to any production .cs file → the Stop hook / custom-lint CI job reports debug-log as BLOCK; remove it.
  4. In an ECS system, obtain ref var c = ref World.Get<T>(e), call World.Remove<T>(e), then read c → DCLA001 appears in the IDE and Unity compile.
  5. Open the Unity project and confirm compilation succeeds with the analyzer DLL active (no diagnostics on the cleaned-up codebase).
  6. QA (avatar preview): in the backpack and builder preview, change wearables, colors, and body shape — the preview must update correctly (the CharacterPreviewController fix reorders promise creation relative to component writes).

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

…ad config

Violations surfaced by the new deterministic lint layer and DCL.Analyzers
(companion PRs), all mechanical:

- CharacterPreviewController.UpdateAvatarAsync held 'ref avatarShape'
  across structural changes (globalWorld.Create, plus the entity
  creation/destruction hidden inside Promise.Create and ForgetLoading) -
  writes through a relocated ref are silently lost. All structural work now
  completes into locals before the ref is taken. Verified on Unity
  6000.4.0f1 (GREEN, 219 assemblies).
- DCLPlayerPrefs: shutdown warning + cleanup-timing logs migrated to
  ReportHub (the TODO claiming it was unavailable is stale - Utility
  references Diagnostics); the editor-only MenuItem log keeps Debug.Log.
- SituationalReactionPresenter: Camera.main replaced with injected
  IExposedCameraData.CinemachineBrain?.OutputCamera, threaded
  ChatContainer -> ChatPlugin -> ChatPanelPresenter.
- All 21 banned '// ReSharper disable CheckNamespace' comments deleted.
- 37 dead csc.rsp files next to asmrefs deleted - Unity only honors rsp
  beside an asmdef (verified against the Bee build graph: zero references);
  orphaned by the assembly consolidation (#8961).
- NRT enabled on the four uncovered assemblies: MarketplaceCredits.API,
  Character, Quality.RenderFeatures, DCL.Editor (~154 new nullable
  warnings; 'no-warning-ratchet' label applies if the ReSharper count
  rises).
Regex rules distilled from CLAUDE.md and the skills, enforced on ADDED
lines only (git diff -U0 parsing) so pre-existing violations never block -
the same ratchet philosophy as the ReSharper warning count. 19 rules (14
BLOCK, 5 WARN), each cited to its source doc in its message, with a
per-line '// lint-ignore: <rule-id>' escape hatch visible to reviewers.

grep -E candidate pass + awk exact verification (~0.5s at 140k added
lines); broken patterns exit loudly; vendored code excluded by pathspec.

Wired twice: the Claude-session Stop hook (fast-fail before the ReSharper
pass) and a custom-lint CI job over the PR's merge-base diff with
::error/::warning annotations; merge_group gets a real base; the watchdog
gains the job plus an always() fix (its condition could never fire).

scripts/lint/tests: fixtures + goldens + selftest.sh run first in CI, so a
dead rule or broken engine fails the build instead of passing silently.
Five semantic rules in Unity's csc via the RoslynAnalyzer asset label (same
integration as the CodeLess and Arch source-generator DLLs), scoped by
placement at Assets/DCL/ to first-party code only:

- DCLA001 (error): ref component used after a structural change;
  reachability-aware (exclusive branches, pre-call args, re-fetch idiom)
- DCLA002 (warning): detached UniTask flow without exception handling
- DCLA003 (warning): heap allocation in Update/[Query]/[HotPath] bodies
- DCLA004 (warning): pooled rental that provably leaks
- DCLA005 (error): FFI enum without explicit underlying type

Severities pinned in Explorer/.editorconfig (tests scoped to warning).
netstandard2.0 / CodeAnalysis 4.3.1; 83 NUnit tests; analyzers CI job feeds
the watchdog; scripts/build-analyzers.sh syncs the LFS DLL. Calibrated
against two Unity 6000.4.0f1 compiles - every FP class carved out with a
regression test; the one true positive (CharacterPreviewController) is
fixed in the companion cleanup PR.
@NickKhalow NickKhalow self-assigned this Aug 13, 2026
@NickKhalow
NickKhalow requested review from a team as code owners August 13, 2026 11:24
@decentraland-bot
decentraland-bot self-requested a review August 13, 2026 11:24
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Build skipped — no changes detected under Explorer/.

Lint

Warnings not reduced: 13169 => 13354 — remove at least 186 warnings to merge.

Warnings/errors in files changed by this PR (85)
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs:183  CSharpWarnings::CS0618  CS0618: Constant 'DCL.FeatureFlags.FeatureFlagsStrings.GPUI_ENABLED' is obsolete: 'GPU Instancer Pro terrain is no longer optional so the flag is not needed'
Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs:83  CSharpWarnings::CS8600  Converting null literal or possible null value into non-nullable type
Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs:145  CSharpWarnings::CS8600  Converting null literal or possible null value into non-nullable type
Assets/DCL/NetworkDefinitions/SceneParcelsConverter.cs:43  CSharpWarnings::CS8600  Converting null literal or possible null value into non-nullable type
Assets/DCL/NetworkDefinitions/SceneParcelsConverter.cs:44  CSharpWarnings::CS8600  Converting null literal or possible null value into non-nullable type
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:16  CSharpWarnings::CS8618  Non-nullable field 'allowedMediaHostnames' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Prefs/DCLPlayerPrefs.cs:45  CSharpWarnings::CS8618  Non-nullable field 'dclPrefs' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:160  CSharpWarnings::CS8618  Non-nullable field 'empty' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:12  CSharpWarnings::CS8618  Non-nullable field 'main' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:159  CSharpWarnings::CS8618  Non-nullable field 'occupied' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:17  CSharpWarnings::CS8618  Non-nullable field 'requiredPermissions' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:158  CSharpWarnings::CS8618  Non-nullable field 'roads' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:14  CSharpWarnings::CS8618  Non-nullable field 'runtimeVersion' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:13  CSharpWarnings::CS8618  Non-nullable field 'scene' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:15  CSharpWarnings::CS8618  Non-nullable field 'sdkVersion' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:163  CSharpWarnings::CS8618  Non-nullable field 'spawn_coordinate' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/NetworkDefinitions/RealmData.cs:103  CSharpWarnings::CS8618  Non-nullable properties 'RealmName', 'CommsAdapter', 'Protocol', 'Hostname', 'WorldCommsSecret' must contain non-null values when exiting constructor. Consider adding the 'required' modifiers or declaring the properties as nullable.
Assets/DCL/PluginSystem/Global/ChatPlugin.cs:510  CSharpWarnings::CS8618  Non-nullable property 'ChatSendMessageAudio' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:72  CSharpWarnings::CS8618  Non-nullable property 'OriginalJson' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ChatPlugin.cs:513  CSharpWarnings::CS8618  Non-nullable property 'ReactionsConfig' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/Multiplayer/Connections/Systems/RoomIndicator/DebugRoomsSystem.Indicator.cs:57  CSharpWarnings::CS8625  Cannot convert null literal to non-nullable reference type
Assets/DCL/NetworkDefinitions/SceneParcelsConverter.cs:39  CSharpWarnings::CS8765  Nullability of type of parameter 'existingValue' in method does not match overridden member 'T? Newtonsoft.Json.JsonConverter<T>.ReadJson(JsonReader, Type, T?, bool, JsonSerializer)' (possibly because of nullability attributes)
Assets/DCL/NetworkDefinitions/SceneParcelsConverter.cs:24  CSharpWarnings::CS8765  Nullability of type of parameter 'value' in method does not match overridden member 'void Newtonsoft.Json.JsonConverter<T>.WriteJson(JsonWriter, T?, JsonSerializer)' (possibly because of nullability attributes)
Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs:31  EnumUnderlyingTypeIsInt  'int' is default enum governing type
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:68  InconsistentNaming  Name 'BACKWARD' does not match rule 'Enum member'. Suggested name is 'Backward'.
Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs:64  InconsistentNaming  Name 'EnableHeadIK' does not match rule 'members_should_be_pascal_case'. Suggested name is 'EnableHeadIk'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:67  InconsistentNaming  Name 'FORWARD' does not match rule 'Enum member'. Suggested name is 'Forward'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:16  InconsistentNaming  Name 'allowedMediaHostnames' does not match rule 'members_should_be_pascal_case'. Suggested name is 'AllowedMediaHostnames'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:24  InconsistentNaming  Name 'authoritativeMultiplayer' does not match rule 'members_should_be_pascal_case'. Suggested name is 'AuthoritativeMultiplayer'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:82  InconsistentNaming  Name 'cameraTarget' does not match rule 'members_should_be_pascal_case'. Suggested name is 'CameraTarget'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:26  InconsistentNaming  Name 'creator' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Creator'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:79  InconsistentNaming  Name 'default' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Default'.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:160  InconsistentNaming  Name 'empty' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Empty'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:25  InconsistentNaming  Name 'featureToggles' does not match rule 'members_should_be_pascal_case'. Suggested name is 'FeatureToggles'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:58  InconsistentNaming  Name 'fixedTime' does not match rule 'members_should_be_pascal_case'. Suggested name is 'FixedTime'.
Assets/DCL/NetworkDefinitions/RealmData.cs:19  InconsistentNaming  Name 'hasSceneURNs' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'hasSceneUrNs'.
Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs:66  InconsistentNaming  Name 'headIK' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'headIk'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:19  InconsistentNaming  Name 'isPortableExperience' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IsPortableExperience'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:22  InconsistentNaming  Name 'landscapeTerrain' does not match rule 'members_should_be_pascal_case'. Suggested name is 'LandscapeTerrain'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:12  InconsistentNaming  Name 'main' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Main'.
Assets/DCL/PluginSystem/Global/ChatPlugin.cs:74  InconsistentNaming  Name 'mainUIView' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'mainUiView'.
Assets/DCL/PluginSystem/Global/ChatPlugin.cs:123  InconsistentNaming  Name 'mainUIView' does not match rule 'parameters_should_be_camel_case'. Suggested name is 'mainUiView'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:77  InconsistentNaming  Name 'name' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Name'.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:159  InconsistentNaming  Name 'occupied' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Occupied'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:81  InconsistentNaming  Name 'position' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Position'.
Assets/DCL/Chat/_Refactor/ChatReactions/Presenters/SituationalReactionPresenter.cs:28  InconsistentNaming  Name 'prevStreamUI' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'prevStreamUi'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:17  InconsistentNaming  Name 'requiredPermissions' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RequiredPermissions'.
Assets/DCL/NetworkDefinitions/WorldManifest.cs:158  InconsistentNaming  Name 'roads' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Roads'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:14  InconsistentNaming  Name 'runtimeVersion' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RuntimeVersion'.
Assets/DCL/NetworkDefinitions/SceneMetadata.cs:13  InconsistentNaming  Name 'scene' does not match rule 'members_should_be_pascal_case'. Suggested name is 'Scene'.

…and 35 more (see the csharp-lint-reports artifact).

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 24954 0 13
PlayMode ✅ Passed 236 0 36

@claude

This comment has been minimized.

@NickKhalow NickKhalow added the no QA needed Used to tag pull requests that does not require QA validation label Aug 13, 2026
Comment thread .github/workflows/test.yml Outdated
Comment thread .github/workflows/test.yml
Comment thread scripts/lint/lint-changed.sh Outdated
Comment thread Explorer/Assets/DCL/Infrastructure/Utility/HotPathAttribute.cs
Comment thread scripts/build-analyzers.sh

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STEP 1 — Context

Loaded: CLAUDE.md, docs/code-style-guidelines.md, .github/prompts/review-instructions.md. Subsystem docs: N/A (tooling PR, no runtime subsystem touched).

Scope. This PR introduces three new layers of automated code-quality enforcement:

  1. Roslyn semantic analyzers (Analyzers/DCL.Analyzers/) — five rules (DCLA001–DCLA005) covering structural-change-after-ref, detached UniTask flows, allocations in system Update, pooled rental leaks, and FFI enum typing. Shipped as Explorer/Assets/DCL/DCL.Analyzers.dll (LFS, RoslynAnalyzer label, all runtime platforms disabled).
  2. Shell-based regex linter (scripts/lint/custom-rules.sh) — deterministic project-rule checks applied only to added lines (ratchet model), covering Debug.Log, ObjectProxy, Camera.main, LINQ in systems, nullable locals, null-forgiving !, and ~15 other CLAUDE.md / skill-sourced patterns. Selftest framework under scripts/lint/tests/.
  3. CI integration (.github/workflows/test.yml) — new custom-lint job (regex rules), analyzers job (Roslyn test suite), watchdog updates, merge-group event support, diff-range output propagation.

The only runtime code is HotPathAttribute.cs — a 12-line marker attribute with no logic.

Changed files: 32 (+3296 −10). All new except the workflow, .gitignore, CLAUDE.md, editorconfig, code-style-guidelines, and lint-changed.sh.


STEP 2 — Root-cause check: PASS

This PR adds preventive tooling that catches code-quality violations at compile time / CI time. It enforces existing project conventions (CLAUDE.md, code-standards skill, docs/code-style-guidelines.md) automatically rather than relying solely on manual review. This is a root-cause approach — the goal is to prevent violations from entering the codebase at all.


STEP 3 — Design & integration: PASS

No new runtime lifecycle units, systems, or state holders. The design decisions are all tooling/infrastructure:

  • Three-layer enforcement is well-layered. Regex rules (fast, deterministic, added-lines-only ratchet) complement Roslyn analyzers (symbol-aware, type-aware) and existing ReSharper inspections (comprehensive but slower). Each layer has a clear strength and they don't duplicate effort. The regex layer explicitly excludes Analyzers/ from its pathspec, and the Roslyn analyzers cover what regex cannot (ref-after-structural-change, UniTask flow analysis, pool leak detection).
  • LFS-committed DLL is the standard Unity pattern for analyzer distribution. The .meta file correctly configures it as RoslynAnalyzer with all runtime platforms disabled — it will only be fed to csc, never included in builds.
  • Selftest framework (scripts/lint/tests/selftest.sh) catches dead rules, engine regressions, and whole-script breakage. CI runs the selftest before every diff lint, so a broken rule fails the build instead of passing silently.
  • CI job dependency graph is correct: watchdog needs [analyzers, custom-lint, lint, test] with always() so it fires on failures (the always() is required — without a status-check function, GitHub's implicit success() would prevent the watchdog from running on failures, which is exactly when it should fire). Skipped jobs (e.g., analyzers when no Analyzers/ files changed) have result == 'skipped', not 'failure', so they don't trigger the watchdog.
  • Security model for PR-head code execution is adequate: pull_request trigger (fork PRs get read-only tokens, no secrets), persist-credentials: false, job-level permissions: contents: read, and timeout limits. Consistent with pre-existing CI jobs that already execute PR-head code. Shell scripts are defensively coded (quoted variables, -- with grep, env-var transport to awk, SIGPIPE-safe grep).

Owner search: N/A — no new runtime units to trace lifecycle ownership for.

Observation — stale DLL risk: The analyzers CI job runs dotnet test on the source, not the committed DLL. A developer could modify analyzer source, have tests pass in CI, but ship a stale DLL with old behavior. scripts/build-analyzers.sh is the manual rebuild mechanism. Consider adding a rebuild-and-diff CI step in a follow-up to close this gap.


STEP 4 — Member audit: PASS

The only new public API is HotPathAttribute — a sealed marker attribute with no members beyond the inherited Attribute base. No consumers yet (the attribute is provided for future annotation of hot-path methods outside ECS systems). No properties or accessors to audit.


STEP 5 — Line-level findings

See inline comments for the two P2 findings with suggestion blocks.

Security review: No security issues found. Shell scripts follow best practices (quoted variables, -- with grep, env-var transport to awk, explicit diff format pins). CI job permissions are minimal (contents: read). DLL .meta correctly disables all runtime platforms. NuGet dependencies are well-known Microsoft/NUnit packages.


STEP 6 — Complexity: COMPLEX

Introduces new CI jobs, a Roslyn analyzer infrastructure with five rules and a comprehensive test suite, a shell-based linting framework with selftest, and modifies the build pipeline's job dependency graph and event handling.


STEP 7 — QA assessment: QA_REQUIRED: NO

All changes are CI/CD workflows, build-time Roslyn analyzers (DLL with all runtime platforms disabled), shell scripts, documentation, and .editorconfig configuration. The only runtime code (HotPathAttribute.cs) is a zero-logic marker attribute. No user-facing behavior is affected.


STEP 8 — Non-blocking warnings

No Explorer/Assets/Scenes/Main.unity in the changed files. No warnings to emit.


STEP 9 — Verdict

Both findings are P2 (minor). No P0 or P1 issues. The analyzers are well-designed with comprehensive test coverage (all five follow Roslyn best practices: CompilationStartAction, concurrent execution, proper symbol resolution, short-circuit when anchor types are absent). The shell linter is defensively coded (grep+awk two-pass, pattern validation with exit 3, selftest framework, explicit diff format pins). The CI integration is correct.

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Introduces Roslyn analyzer infrastructure, shell-based linting framework, and CI pipeline modifications across 32 files
QA_REQUIRED: NO


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

Comment thread Explorer/Assets/DCL/Infrastructure/Utility/HotPathAttribute.cs Outdated
Comment thread .github/workflows/test.yml Outdated
@popuz

popuz commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

@NickKhalow , does it close this ticket #9400 ?

@popuz

popuz commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

additionally, @NickKhalow, will you replace with this all unit tests from [CodeConvention] category or it is supposed to be in the next iteration?

@eordano

eordano commented Aug 13, 2026

Copy link
Copy Markdown
Member

additionally, @NickKhalow, will you replace with this all unit tests from [CodeConvention] category or it is supposed to be in the next iteration?

I pushed some fixes to these warnings here:
0ff656d

@claude

This comment has been minimized.

Comment thread Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs Outdated
…ft check, HotPath targets

- changes job emits lintscripts output; custom-lint runs when only the linter
  changed (previously a lint-only PR skipped the selftest entirely)
- custom-lint and analyzers jobs blank the workflow-level Unity credentials;
  both execute PR-authored code and need no license
- lint-changed.sh no longer swallows custom-rules.sh stderr or exit code 3
  (broken rule pattern) - both now block like BLOCK findings
- HotPathAttribute narrowed to methods: DCLA003 does not inspect constructors,
  so allowing the attribute there created unchecked annotations
- analyzers job rebuilds the DLL deterministically (pinned SDK via
  Analyzers/global.json, ContinuousIntegrationBuild, DebugType=none) and fails
  on drift against the committed DLL; build-analyzers.sh uses identical flags;
  DLL recommitted from the deterministic build
- filter-warnings.sh excludes DCLA* from the ReSharper ratchet count - the
  analyzers have their own enforcement channel (Unity csc + IDE)
- selftest.sh golden comparison strips CR so it passes on CRLF checkouts
…commit

Deliberate ref-after-structural-change violation in a production assembly.
Expected CI outcome: the Unity build job FAILS with error DCLA001. If it stays
green, Unity's csc is not applying the Explorer/.editorconfig severities and
the corruption-class rules need a ruleset or Error-default descriptors.
Revert this commit after recording the result.
@NickKhalow

Copy link
Copy Markdown
Contributor Author

Review findings addressed in c0c32fe6a1:

  • custom-lint now runs when only scripts/lint/** changed (new lintscripts output) — a lint-only PR can no longer skip the selftest
  • custom-lint + analyzers jobs blank the workflow-level Unity credentials (both run PR-authored code, neither needs a license)
  • lint-changed.sh surfaces custom-rules.sh stderr and blocks on exit code 3 (broken rule), not just 2
  • [HotPath] narrowed to methods — the analyzer doesn't inspect constructors
  • DLL drift check added: analyzers job rebuilds deterministically (SDK pinned in Analyzers/global.json, ContinuousIntegrationBuild, DebugType=none) and byte-compares against the committed DLL; build-analyzers.sh uses the same flags and the DLL is recommitted from that build
  • filter-warnings.sh excludes DCLA* from the ReSharper ratchet — the analyzers have their own enforcement channel, which should account for the +329 lint delta
  • lint selftest golden comparison is now CRLF-proof (was failing on Windows checkouts)

⚠️ The Unity build on the current head is expected to FAIL — that's deliberate. f4420ef916 is a REVERT-ME probe: a DCLA001 violation in a production assembly. A red build with error DCLA001 proves Unity's csc applies the .editorconfig severities; a green build means the corruption-class rules need a ruleset / Error-default descriptors before merge. The commit will be reverted once the result is recorded.

@claude

This comment has been minimized.

Comment thread .github/workflows/test.yml Outdated
…ic MVID hashes source bytes

The drift check's CI rebuild (LF checkout) produced a different DLL than the
committed one built from a CRLF checkout. Pin Analyzers/** to LF and recommit
the DLL from an LF build; local and CI hashes now match byte-exactly.
…ignores editorconfig severities

Probe result (CI run 31708955588): DCLA001 fired inside Unity's compile at the
exact violation, but as a WARNING despite the .editorconfig error pin - Unity's
csc does not apply dotnet_diagnostic severities. DCLA001/DCLA005 descriptors
now carry DiagnosticSeverity.Error so the Unity build actually fails; the
editorconfig pins keep governing IDEs and dotnet builds (Tests downgrade is
IDE-only). DLL rebuilt and recommitted.

Also fixes the two violations the probe run surfaced in the merge preview:
- UUAVState pinned ': int' - it crosses the DllImport boundary and DCLA005 at
  Error would otherwise fail the build (explicit int == previous implicit ABI)
- SituationalReactionPresenter used CinemachineBrain?.OutputCamera, forcing a
  Cinemachine reference the Chat assembly lacks (CS0012, broke editmode+lint
  solution builds); IExposedCameraData now exposes OutputCamera as a default
  member so consumers never touch the Cinemachine type
@claude

This comment has been minimized.

…clean

Run 31710811877 showed two things:

- DCLA005 at Error fails vendored package code: Unity feeds the analyzer DLL
  to package compilations too (com.decentraland.pulse.transport ENet enums,
  com.unity.cloud.ktx), not just asmdefs under Assets/DCL as the README
  claimed. Vendored sources can't be fixed here, so every analyzer now skips
  syntax trees under /PackageCache/ (VendoredCode.cs) with a regression test;
  README scope section corrected.

- The drift check caught a self-inflicted mismatch: the previous DLL was
  synced from an INCREMENTAL build whose stale obj/ state produced different
  bytes than CI's clean rebuild. build-analyzers.sh now wipes bin/obj before
  building; DLL recommitted from the clean build.
@claude

This comment has been minimized.

…reference

Windows builds of byte-identical sources can differ from the Linux build
(observed across two drift-gate failures with LF-normalized sources and the
pinned SDK). Make the drift gate self-serving: the job uploads its
deterministic rebuild as 'DCL.Analyzers.dll-canonical' before comparing, so
on drift the fix is to download and commit that artifact.
@claude

This comment has been minimized.

…ypass; fix ref-temporary in preview controller

Three fixes from review round 3 and the drift-gate investigation:

- Root cause of every hash mismatch: the SDK's implicit SourceLink appends
  the git HEAD sha to AssemblyInformationalVersion, so each build embedded
  the CURRENT commit and a committed DLL could never byte-match a rebuild
  at any other commit (the OS was never the issue).
  IncludeSourceRevisionInInformationalVersion=false makes output depend on
  sources and flags only; DLL recommitted, hash now stable across commits.

- The analyzers job gate now also fires when ONLY the committed DLL changed
  (Explorer/Assets/DCL/DCL.Analyzers.dll in the pathspec) - previously a
  swapped binary alone skipped the drift check entirely.

- CharacterPreviewController: ForgetLoading was still invoked through a
  temporary ref into the chunk (World.Get(...).WearablePromise.Forget...),
  keeping 'this' pointed at ECS memory across the structural change inside.
  The old promise is now copied out first; the copy's mutations are
  deliberately discarded since the component slot is overwritten below.
@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @NickKhalow's task in 3m 26s —— View job


Security + code review: re-review after 72e6c31c

  • Diff 72e6c31c against my last pass
  • Verify the SourceRevision root-cause claim
  • Re-check N5 (job gate) and N2 (ref temporary)
  • Re-check workflow / secret / dependency delta
  • Carry forward what's still open

N9 — root cause found, and it retires both of my theories

The commit message has it exactly right, and it's better than anything I proposed:

the SDK's implicit SourceLink appends the git HEAD sha to AssemblyInformationalVersion, so each build embedded the CURRENT commit … the OS was never the issue.

That's the .NET SDK's AddSourceRevisionToInformationalVersion target: SourceRevisionId is populated by the bundled Git source-link task and appended as +<sha> to the informational-version attribute, which lands in the assembly's metadata. Combined with test.yml:216 checking out github.event.pull_request.head.sha, the gate was mathematically unsatisfiable: any local build embeds sha X, committing that DLL produces head sha Y, CI rebuilds at Y and embeds Y — mismatch, forever, on every OS, every round. Three red gates and none of them were the thing I was diagnosing.

IncludeSourceRevisionInInformationalVersion=false is the correct and complete fix, and it's the only remaining commit-dependent input: SourceLink's other output is the .sourcelink.json in the PDB, which DebugType=none already drops. I couldn't run dotnet in this environment to reproduce the bytes, so the confirmation has to come from the gate itself — Unity Test is still in_progress on this head, so treat "green drift gate" as the merge condition, not as established.

Two corrections to my own last comment, since they'd otherwise send someone down a wrong path:

  • Withdrawing the Windows-warning suggestion. I proposed adding a uname guard to build-analyzers.sh telling Windows developers their build can't match CI. With the sha explanation covering all three observed failures, there is no evidence left for cross-OS divergence — that banner would be a fabricated constraint baked into the tooling. Good that it wasn't taken.
  • My PathMap/separator reasoning was speculation dressed as mechanism. It may still be a real edge, but it was never demonstrated here and shouldn't be treated as known.

N14 (new, nit) — one comment still asserts the retired theory. test.yml:233-236 still reads "Windows builds of the same sources can differ byte-wise (observed)". That observation is now fully explained by the sha, so the comment states as established fact something this very commit disproved. The Analyzers/README.md rewrite gets it right — it lists the three real causes and demotes the artifact to "if the gate still fails unexpectedly". Keeping the artifact as an unexplained-drift fallback is sound; keeping the wrong cause next to it isn't.


N5 — closed for the case that mattered

test.yml:133 now carries Explorer/Assets/DCL/DCL.Analyzers.dll in the pathspec, so a PR that swaps only the binary can no longer skip the drift check. That was the security-relevant half. Explorer/.editorconfig is still absent and I'm withdrawing that part of the suggestion — the drift gate doesn't read it and the analyzer tests don't either, so including it would only add noise.

Not covered by any gate, and worth knowing rather than acting on: DCL.Analyzers.dll.meta isn't in the pathspec either. It's correct today (RoslynAnalyzer label, all platforms disabled), but a meta-only edit re-enabling platforms would ship the analyzer into the player build with nothing checking it. Small surface, no mechanism proposed — just don't assume the gate covers it.


N2 — fixed, and fixed for the right reason

CharacterPreviewController.cs:104-105. AssetPromise is a struct (AssetPromise.cs:15), so the copy-out genuinely moves this to the stack before ForgetLoadingDestroyEntity runs world.Destroy(Entity); Entity = Entity.Null;. The discarded write is safe because avatarShape.WearablePromise = wearablePromise overwrites the slot wholesale at line 124 — and the comment says exactly that, which is what makes it reviewable rather than fragile. The method is now ref-safe end to end.

Still worth the two lines in StructuralChangeAfterRefAnalyzer's XML doc: a transient World.Get<T>(e).Field.Method(...) receiver is a real DCLA001 false negative, and this file is the proof it occurs in practice.


Still open

ID Where State
N13 test.yml:196-198 Unchanged. "Tests only; the shipped DLL is synced manually" — the job now rebuilds, publishes the canonical artifact, and drift-gates the binary. Same stale line in the scripts/build-analyzers.sh header.
N12 Explorer/.editorconfig:819-831 Unchanged. "Corruption-class rules fail the build" / "never fail the build" under [**/Tests/**] are both false under Unity. Analyzers/README.md:8-14 now states this correctly and at length; the editorconfig — where a developer looks when a build goes red — still contradicts it.
N8 (wording) Analyzers/README.md:35 "first-party code under Assets/ is analyzed in full" — the filter is "not /PackageCache/", and Assets/Plugins/ holds vendored source too. Scope decision is right; the sentence overstates it.
N1 PR labels no-warning-ratchet still not applied (~154 new nullable warnings from the NRT enablement). Apply →
M3/M4 workflow checkout@v6, setup-dotnet@v4, upload-artifact@v4 tag-pinned rather than SHA-pinned (repo-wide follow-up; none of these jobs hold secrets). No NuGet lock file — with the commit variable eliminated, restore is the last unpinned input the gate depends on. RestorePackagesWithLockFile + --locked-mode closes it.

Dependency / workflow surface (delta only)

No new packages, no registry sources, no native code. Runtime delta is three lines in CharacterPreviewController. The DLL changed (19b4ab29…d0f55f15…, 35840 bytes, unchanged size) — expected, since dropping the informational-version suffix rewrites metadata; I can't inspect it (LFS pointer only in this checkout), so the provenance argument rests entirely on the drift gate passing.

Workflow delta is the one-line pathspec change. Job hardening from earlier rounds is intact: permissions: contents: read, blanked UNITY_* on both PR-code-executing jobs, persist-credentials: false, artifact retention 7 days, no pull_request_target, no prompt files, no new secret consumers.

Worth restating once, because the README's artifact instructions read stronger than they are: the canonical artifact is a build product of PR-side sources and a PR-side .csproj. "CI published it" is not independent attestation — trust comes from Analyzers/** and the csproj being reviewable text in the diff. That's the right achievable property here; it just means the artifact path isn't a way to accept a binary whose sources weren't read.


Verdict

This round found the actual bug, and it was upstream of everything I'd been proposing — the gate was unsatisfiable by construction, not flaky. With IncludeSourceRevisionInInformationalVersion=false the output depends on sources, flags, and the pinned SDK, which is what the invariant needed all along. N5 and N2 are both properly closed.

The one merge condition left is empirical: the drift gate has to go green on the merged head. It hasn't reported yet on this run, and with the severities living in the descriptors, that binary is what decides whether builds fail — this gate is the only thing tying it to reviewable text. Everything else (N12, N13, N14, N8-wording, N1) is documentation and labels.

DEPENDENCY_REVIEW: NEEDS_ATTENTION
· branch feat/roslyn-analyzers

@NickKhalow NickKhalow added the no-warning-ratchet Prevent CI linting checks label Aug 13, 2026
@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9727, run #31721567656

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Apple M1

Metric Baseline Change Δ Result
Samples 4021 (×3) 4160 (×3)
CPU average 22.3 ms (21.7–23.2) 21.5 ms (21.1–22.5) -0.7 ms ⚪ within noise
CPU 1% worst 230.5 ms (224.7–232.1) 200.4 ms (110.5–227.8) -30.2 ms ⚪ within noise
CPU 0.1% worst 238.5 ms (234.0–239.6) 232.0 ms (229.9–235.1) -6.5 ms ⚪ within noise
GPU average 6.9 ms (2.7–7.2) 2.2 ms (1.0–2.8) -4.8 ms 🟢 69% faster
GPU 1% worst 35.3 ms (33.7–37.2) 34.7 ms (34.1–34.8) -0.6 ms ⚪ within noise
GPU 0.1% worst 36.3 ms (35.1–38.0) 36.6 ms (35.3–36.8) 0.3 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

Intel Core i5

Metric Baseline Change Δ Result
Samples 2287 (×3) 2593 (×3)
CPU average 39.2 ms (36.8–39.3) 34.6 ms (33.2–40.4) -4.6 ms ⚪ within noise
CPU 1% worst 378.9 ms (366.5–380.5) 179.1 ms (33.8–475.8) -199.8 ms ⚪ within noise
CPU 0.1% worst 395.9 ms (389.0–396.8) 508.9 ms (37.6–519.0) 113.0 ms ⚪ within noise
GPU average 9.5 ms (9.4–9.6) 9.5 ms (9.4–9.6) 0.0 ms ⚪ within noise
GPU 1% worst 40.2 ms (39.7–40.3) 29.9 ms (20.5–49.7) -10.3 ms ⚪ within noise
GPU 0.1% worst 48.7 ms (44.9–49.0) 53.5 ms (37.6–60.4) 4.9 ms ⚪ within noise
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

eordano added a commit that referenced this pull request Aug 17, 2026
review.md (feat-roslyn-analyzers, PR #9727) judged an earlier sibling
history of this branch. Its two must_fix items are already resolved:
the analyzer/lint-infrastructure work here never merged in
fix/lint-surfaced-cleanups (the split dalkia/popuz demanded already
exists, as separate branches from the same parent), and the requested
CharacterPreviewController regression test belongs to that other
branch, not this one - see FIXNOTES.md for the verified topology.

What's left is mikhail-dcl's nit: PooledRentalLeakAnalyzer (DCLA004)
disqualifies a rental symbol as soon as any assignment of it reaches
an escaping use, so reassigning a local before releasing it silently
drops the earlier, actually-leaked rental - flow-insensitive tracking
at the symbol level, not a gap in the escape analysis. Root cause
documented on the class doc comment (the invariant the code itself
can't show), and locked in with a regression test asserting the
no-report behavior is intentional rather than an oversight, so an
accidental "fix" that starts flagging it fails loudly instead of
silently changing the analyzer's false-negative bias.

Verified: scripts/lint/custom-rules.sh --working-tree (0 findings),
scripts/lint/tests/selftest.sh (OK), scripts/build-analyzers.sh
(99/99 tests, DLL resynced).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018c638dR1vPysCMbYt2qQg5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-dependency no QA needed Used to tag pull requests that does not require QA validation no-warning-ratchet Prevent CI linting checks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants