Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ All notable changes to this project are documented here. This project follows
(Principle VI), output changes that are *strictly more accurate* are MINOR, not
breaking.

## [Unreleased] — Control-Flow Graph Foundation

### Added — more accurate conditional analysis (MINOR)

The per-function control-flow graph (previously used only to annotate branches)
is now retained as a compact, queryable reachability + dominance model, and the
conditional-analysis consumers resolve over it instead of a source-text-position
heuristic. This sharpens several cases:

- **Conditional status codes** (the #39 / #50 / #57 pain) are computed
structurally: a status contributes iff its assignment can *reach* the response
write and is not overwritten on every path by a later, call-dominating
assignment. Mutually-exclusive `if`/`else` (and `switch`) arms fan out; an
early-`return` before the write is excluded; an unconditional overwrite shadows
earlier assignments. **A value assigned inside a loop body now reaches a write
after the loop** (the analysis terminates across the back-edge).
- **Helper-internal type-switch binding**: when a handler funnels a value into a
shared responder that `switch v.(type)`s on it, the call-site argument is bound
to the matching arm — `Respond(w, &NotFound{})` fans out only that arm's status,
not every arm. When the argument's concrete type is not statically known (a bare
`error`/`any`), the analyzer degrades to the unconditionally-reachable result
and emits a warning rather than over-approximating.
- **Method dispatch via `if r.Method == http.MethodPost`** now splits into one
operation per method, the same as a `switch r.Method` already did.
- **Branch-dependent response bodies** are attributed to the status on which they
are written (e.g. `FullUser`/200 vs `ErrorBody`/404), never merged.

Each behavior ships with its own targeted fixture (`cfg_helper_typeswitch`,
`cfg_loop_status`, `cfg_method_if_dispatch`, `cfg_branch_bodies`); the existing
framework golden corpus does not exercise these constructs, so it — and the
determinism suite — stays byte-identical.

### Changed — internals

- The conditional-status fan-out's source-position heuristic (`positionAfter`,
`positionLineCol`, and the "last unconditional index") was **removed** in favor
of the structural reachability predicate. When control flow cannot be modeled,
the analyzer falls back to the unconditionally-reachable (single-path) result
and warns — it never guesses a conditional set.

## [Unreleased] — TypeRef Metadata Integration (Phase 2)

### Added — more accurate schema output (MINOR)
Expand Down
39 changes: 39 additions & 0 deletions internal/engine/engine_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,45 @@ func allFrameworks(t *testing.T) []frameworkTestCase {
{name: "writejson_helper", inputDir: "../../testdata/writejson_helper", configFn: spec.DefaultHTTPConfig},
{name: "error_switch_minimal", inputDir: "../../testdata/error_switch_minimal", configFn: spec.DefaultChiConfig},
{name: "error_switch_file_service", inputDir: "../../testdata/error_switch_file_service", configFn: spec.DefaultChiConfig},
// spec 009: a response helper that type-switches on its argument. The
// concrete-type route fans out only the matched arm; the imprecise route
// degrades to the default arm + warns (FR-011/FR-012).
{name: "cfg_helper_typeswitch", inputDir: "../../testdata/cfg_helper_typeswitch", configFn: spec.DefaultHTTPConfig},
// spec 009: a status assigned inside a loop body still reaches the response
// write (FR-010 — the loop back-edge terminates and the value contributes).
{name: "cfg_loop_status", inputDir: "../../testdata/cfg_loop_status", configFn: spec.DefaultHTTPConfig},
// spec 009 US2: an `if r.Method == …` dispatch splits into one operation per
// method, the same as a `switch r.Method` (FR-003).
{name: "cfg_method_if_dispatch", inputDir: "../../testdata/cfg_method_if_dispatch", configFn: spec.DefaultHTTPConfig},
// spec 009 US2 (FR-003): the switch-form mirror of cfg_method_if_dispatch, with
// net/http CONSTANT cases (`case http.MethodGet:`). Must split identically —
// extractCaseValues resolves the constants the same way extractMethodGuard does.
{name: "cfg_method_switch_dispatch", inputDir: "../../testdata/cfg_method_switch_dispatch", configFn: spec.DefaultHTTPConfig},
// spec 009 US2: a method dispatch combined with an INDEPENDENT pre-dispatch
// conditional — the independent 500 is carried onto every method operation
// (CFG: orthogonal to the dispatch), not dropped as the pre-CFG split did.
{name: "cfg_method_if_independent", inputDir: "../../testdata/cfg_method_if_independent", configFn: spec.DefaultHTTPConfig},
// spec 009 US2: cross-function guard — a method arm that splits AND calls a
// helper writing a conditional response. The helper response's branch is in the
// HELPER's CFG, so the classifier must not reason about it against the handler's
// CFG (that would leak it onto the other method); it is conservatively excluded.
{name: "cfg_method_helper_response", inputDir: "../../testdata/cfg_method_helper_response", configFn: spec.DefaultHTTPConfig},
// spec 009 US2: a `fallthrough` into a `switch r.Method` default — the 405 is
// recognised structurally as the dispatch fallback and excluded, NOT leaked onto
// GET/POST despite the fallthrough edge making it reachable from the POST arm.
{name: "cfg_method_switch_fallthrough", inputDir: "../../testdata/cfg_method_switch_fallthrough", configFn: spec.DefaultHTTPConfig},
// spec 009 US2: TWO `switch r.Method` dispatches with an independent 401 between
// them — the dispatch root is scoped to one dispatch's arms, so the 401 is shared
// onto GET+POST, not over-excluded by a root spanning both dispatches.
{name: "cfg_method_two_dispatch", inputDir: "../../testdata/cfg_method_two_dispatch", configFn: spec.DefaultHTTPConfig},
// spec 009 US2: a COMBINED case (`case GET, POST:`) + a `default` — the combined
// arm lowers to one block dominated by itself, so the dispatch root must come from
// the recorded group (all arms incl. default); the 405 stays the fallback and is
// not leaked onto the GET/POST operations the combined case splits into.
{name: "cfg_method_combined_default", inputDir: "../../testdata/cfg_method_combined_default", configFn: spec.DefaultHTTPConfig},
// spec 009 US3: branch-dependent response bodies are attributed to the status
// on which they are written — 200/FullUser vs 404/ErrorBody, not merged (FR-005).
{name: "cfg_branch_bodies", inputDir: "../../testdata/cfg_branch_bodies", configFn: spec.DefaultHTTPConfig},
{name: "bodyless_status", inputDir: "../../testdata/bodyless_status", configFn: spec.DefaultHTTPConfig},
{name: "wrapped_response", inputDir: "../../testdata/wrapped_response", configFn: spec.DefaultHTTPConfig},
{name: "echo_handler_factory", inputDir: "../../testdata/echo_handler_factory", configFn: spec.DefaultEchoConfig},
Expand Down
Loading
Loading