Skip to content

feat(scanner): --strict-types forwarding-call detection, closing #26#31

Merged
flaglint merged 2 commits into
mainfrom
feat/strict-types-forwarding-calls
Jul 8, 2026
Merged

feat(scanner): --strict-types forwarding-call detection, closing #26#31
flaglint merged 2 commits into
mainfrom
feat/strict-types-forwarding-calls

Conversation

@flaglint

@flaglint flaglint commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements ADR 006's approach A (the "forwarding function" pattern) for issue #26 — a method value captured in one function, passed as an argument, invoked from inside a different callee — then extends it past the ADR's own original scope once real-world verification against the ADR's own motivating repo (e2b-dev/infra) showed the actual shape needed more than a single hop.

The real chain

A package-level var (e.g. SnapshotFeatureFlag) built by a factory (NewBoolFlag) that stores a literal into a struct field, read back through a trivial accessor method (Key), forwarded through two levels of function wrapping (a "pass-through" wrapper method that fixes the SDK method internally, then a generic "direct forwarding" function that actually invokes the callback) before reaching the SDK call.

Four new whole-scan passes, chained together in the same fixed-point spirit as factory.go's cross-file resolution (ADR 004) and issue #18's Pass B loop — still no go/ssa/go/callgraph (ADR 006's approach B):

  • accessorFields — trivial single-field-accessor methods
  • factoryFieldParams — constructors storing a parameter into a composite-literal field
  • flagDescriptorLiterals — package-level vars built via a known factory with a literal argument
  • findEvalSummaries — direct-forwarding functions, then pass-through functions to a fixed point (a chain can deepen one hop per round)

Two real bugs found and fixed during verification against e2b-dev/infra itself

  1. Interface-method identity mismatch: flag.Key() where flag typedFlag[T] resolves via go/types to the interface's abstract method object — a different *types.Func than the concrete type's own implementation, which is the only one with a body to learn a field from. Fixed by keying by (type name, method name) instead of object identity.
  2. Wrong argument picked as "the key": the original code took the first argument referencing one of a function's own parameters — wrong whenever an unrelated parameter (almost always ctx, conventionally first) comes before the real flag-descriptor argument. Fixed by collecting every candidate position and selecting the correct one at detection time by matching methodSpecs[resolved method].keyArgIndex.

Also fixed (caught by go vet): (*types.Func).Signature() requires go1.23, but the project's own go.mod targets go1.22.0 — would have broken the Go 1.22 CI job. Replaced with the older .Type().(*types.Signature) pattern.

Verification

  • Two new buildable fixtures: testdata/strict/forwarding_call (direct forwarding, generic-instantiated variant matching the issue's exact repro, one-hop pass-through, false-positive guard, genuinely-dynamic-key guard) and testdata/strict/flag_descriptor (the full two-hop chain, built to precisely mirror e2b-dev/infra's real shape — including parameter order, since the first draft accidentally omitted the ctx-comes-first detail and passed despite bug feat(core): add shared types, config loader, and fingerprint generator #2 still being present).
  • Full test suite green.
  • Re-verified directly against e2b-dev/infra's own featureflags package: 7 real usages detected, all correctly resolved, zero warnings.
  • Re-verified against weaviate: unchanged, same 4 real usages, no regression.

Summary by CodeRabbit

  • New Features

    • Improved strict scanning to detect more flag usages, including forwarding patterns and flag-descriptor chains.
    • Added broader test coverage for direct, pass-through, and dynamic-key scenarios.
  • Bug Fixes

    • Reduced missed detections when flag keys are passed through helper functions or wrapped client calls.
    • Better distinguishes valid SDK calls from similar-looking non-matching code.

Implements ADR 006's approach A (the "forwarding function" pattern) for
issue #26 — a method value captured in one function, passed as an
argument, invoked from inside a different callee — then extends it past
the ADR's own original scope once real-world verification against the
ADR's own motivating repo (e2b-dev/infra) showed the actual shape needed
more than a single hop.

The real chain: a package-level var (e.g. SnapshotFeatureFlag) built by a
factory (NewBoolFlag) that stores a literal into a struct field, read
back through a trivial accessor method (Key), forwarded through TWO
levels of function wrapping (a "pass-through" wrapper method that fixes
the SDK method internally, then a generic "direct forwarding" function
that actually invokes the callback) before reaching the SDK call. Closing
this needed four new whole-scan passes, chained together in the same
fixed-point spirit as factory.go's cross-file resolution (ADR 004) and
issue #18's Pass B loop — still no go/ssa/go/callgraph (ADR 006's
approach B):

- accessorFields: trivial single-field-accessor methods
  (`func (f S) Key() string { return f.name }`)
- factoryFieldParams: constructors that store a parameter into a
  composite-literal field of their own return type
- flagDescriptorLiterals: package-level vars built via a known factory
  with a literal argument
- findEvalSummaries: direct-forwarding functions (F calls its own
  callback parameter), then pass-through functions to a fixed point (G
  calls a function with an already-known summary, forwarding one of G's
  own parameters into its flag-descriptor position) — a chain can deepen
  one hop per round, closing the real two-level case

Two real bugs found and fixed during verification against e2b-dev/infra
itself, not caught by the (initially too-simple) fixtures:

1. A call through an interface-typed parameter (`flag.Key()` where
   `flag typedFlag[T]`) resolves via go/types to the *interface's*
   abstract method object — a completely different *types.Func than the
   concrete type's own implementation, even though only the concrete
   implementation has a body to learn a field from. Fixed by keying
   accessorFields by (type name, method name) instead of *types.Func
   identity, resolved against the concrete type only once a real value
   reaches an actual call site.
2. findDirectForwarding took the *first* argument referencing one of a
   function's own parameters as "the flag key" — wrong whenever an
   unrelated parameter (almost always ctx, conventionally first) comes
   before the real flag-descriptor argument in the callback invocation.
   Fixed by collecting *every* candidate argument position
   (evalSummary.keyCandidates) and selecting the correct one at
   detection time by matching methodSpecs[resolved method].keyArgIndex,
   never by position in the source.

Also fixed (caught by `go vet`, not just golangci-lint in CI):
`(*types.Func).Signature()` requires go1.23; the project's own go.mod
targets go1.22.0, so this would have broken the Go 1.22 CI job. Replaced
with the older `.Type().(*types.Signature)` pattern.

Verified: two new buildable fixtures — testdata/strict/forwarding_call
(direct forwarding, a generic-instantiated variant matching issue #26's
own repro exactly, a one-hop pass-through, a false-positive guard, and a
genuinely-dynamic-key guard) and testdata/strict/flag_descriptor (the
full two-hop accessor/factory/package-var chain, built to precisely
mirror e2b-dev/infra's real shape including parameter order — the first
draft of this fixture accidentally omitted the ctx-comes-first detail
and passed despite bug #2 above still being present, which is why it was
rebuilt to match the real source exactly rather than trusted on its own).
Full test suite green. Re-verified directly against e2b-dev/infra's own
featureflags package: 7 real usages detected, all correctly resolved
(JSONVariationCtx calls through the same two-hop chain), zero warnings.
Re-verified against weaviate: unchanged, same 4 real usages, no
regression.

Fixes #26

Signed-off-by: Krishan Kant Sharma <krishansharma0327@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6148f066-892c-4931-b242-8764e95952f2

📥 Commits

Reviewing files that changed from the base of the PR and between f4db6be and 63bc30f.

📒 Files selected for processing (14)
  • internal/scanner/forwarding.go
  • internal/scanner/strict.go
  • internal/scanner/strict_test.go
  • internal/scanner/testdata/strict/flag_descriptor/.gitignore
  • internal/scanner/testdata/strict/flag_descriptor/go.mod
  • internal/scanner/testdata/strict/flag_descriptor/main.go
  • internal/scanner/testdata/strict/flag_descriptor/stubsdk/client.go
  • internal/scanner/testdata/strict/flag_descriptor/stubsdk/go.mod
  • internal/scanner/testdata/strict/flag_descriptor/unrelated/unrelated.go
  • internal/scanner/testdata/strict/forwarding_call/.gitignore
  • internal/scanner/testdata/strict/forwarding_call/go.mod
  • internal/scanner/testdata/strict/forwarding_call/main.go
  • internal/scanner/testdata/strict/forwarding_call/stubsdk/client.go
  • internal/scanner/testdata/strict/forwarding_call/stubsdk/go.mod

📝 Walkthrough

Walkthrough

Adds a new static-analysis module (forwarding.go) that detects flag-evaluation calls forwarded through intermediate functions via a two-hop pattern, wires it into ScanStrict replacing strictTypesUsages, and adds two test fixtures with corresponding strict-scan tests.

Changes

Forwarding call detection

Layer / File(s) Summary
Core data structures and helpers
internal/scanner/forwarding.go (lines 1-89, 265-288, 520-557)
Defines evalSummary, keySource, accessorKey types plus buildParamIndex, calleeFuncObject, and namedTypeName helpers.
Accessor, factory, and literal resolution
internal/scanner/forwarding.go (lines 90-264, 558-638)
Implements accessorFields, factoryFieldParams, flagDescriptorLiterals, and resolveFlagDescriptorKey to trace flag key literals through constructors, struct fields, and accessor methods.
Forwarding summary computation
internal/scanner/forwarding.go (lines 289-519)
Implements findEvalSummaries, findDirectForwarding, and findPassThrough to compute per-function evaluation summaries via fixed-point iteration.
Call-site detection and ScanStrict wiring
internal/scanner/forwarding.go (lines 639-735), internal/scanner/strict.go
Implements detectForwardingCallUsages to emit types.FlagUsage entries, and adds forwardingCallUsages wired into ScanStrict in place of strictTypesUsages.
Forwarding call test fixture
internal/scanner/testdata/strict/forwarding_call/*
Adds a module fixture exercising direct forwarding, generic forwarding, unrelated-type non-detection, pass-through wrapping, and dynamic-key non-detection, plus a stub SDK.
Flag descriptor chain test fixture
internal/scanner/testdata/strict/flag_descriptor/*
Adds a module fixture with a typed flag descriptor, constructor, client pass-through method, an unrelated colliding-accessor package, and a stub SDK.
Strict scan tests
internal/scanner/strict_test.go
Adds TestScanStrict_positiveForwardingCall and TestScanStrict_positiveFlagDescriptorChain validating Phase 1 finds zero usages while ScanStrict resolves expected flag keys, call types, and metadata.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant WrapperFunc
  participant SDKClient
  participant DetectForwardingCallUsages
  participant EvalSummaries

  Caller->>WrapperFunc: usePassThrough(literal key)
  WrapperFunc->>SDKClient: MakeClient()
  WrapperFunc->>SDKClient: callDirect(client.BoolVariation, key)

  Note over EvalSummaries: findEvalSummaries pre-pass
  EvalSummaries->>EvalSummaries: findDirectForwarding(callDirect)
  EvalSummaries->>EvalSummaries: findPassThrough(passThroughWrapper)

  DetectForwardingCallUsages->>EvalSummaries: match call site to evalSummary
  DetectForwardingCallUsages->>DetectForwardingCallUsages: resolveFlagDescriptorKey(key arg)
  DetectForwardingCallUsages-->>Caller: emit types.FlagUsage (flag key, SDK, DetectedBy=strict-types)
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/strict-types-forwarding-calls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Independent review found: accessorKey (forwarding.go) was keyed by bare
type name + method name only, with no package qualifier. Two unrelated
types in different packages of the scanned repo, both named the same
(e.g. "BoolFlag") and both trivial single-field accessor methods named
the same (e.g. "Key"), would silently collide in the shared cross-package
accessorFields index — whichever package's entry a given map iteration
visited last would win, non-deterministically (Go's map iteration order
is randomized per process), and the "losing" type's field name would be
used for the other type's lookups.

Fixed by package-qualifying both sides: accessorFields (write side) now
takes the owning package's path and keys by "pkgPath.TypeName"; namedTypeName
(read side, used by resolveFlagDescriptorKey to resolve a concrete call-
site value's type) does the same via named.Obj().Pkg().Path().

Verified: added a fixture package (testdata/strict/flag_descriptor/
unrelated) declaring a type also named BoolFlag with an also-named Key()
method, reading a *different* field ("identifier", not "name") — proving
the real chain still reliably resolves the correct flag key regardless.
Ran the existing chain test 5 times in a row to rule out map-order-
dependent flakiness (there wasn't any, confirming the fix, since the keys
are now genuinely distinct rather than accidentally non-colliding by luck
of iteration order). Full test suite green.

Signed-off-by: Krishan Kant Sharma <krishansharma0327@gmail.com>
@flaglint flaglint merged commit db0f4d9 into main Jul 8, 2026
12 of 13 checks passed
@flaglint flaglint deleted the feat/strict-types-forwarding-calls branch July 8, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants