embedcheck

package
v0.63.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package embedcheck is the build-time gate for server actions on embeddable surfaces.

G.serverAction does not work inside an embed frame: the action registry is app-global, keyed by (componentID, action) with no relationship to any surface, so honouring an embed grant at /__gofastr/action would let a credential minted for one surface invoke any action registered anywhere. framework/uihost already panics at boot when a surface's screen registers one (enforceNoServerActionsOnEmbeds in embed_actions.go). This package catches the same condition at `gofastr build` / `make build`, before anything runs.

The signal

The property "this action posts to the server" is carried by the ClientJS passed to component.WithClientJS. The compiler rewrites only the canonical "G.serverAction(" spelling. This analyzer also detects legal whitespace before "(", then reports the canonical spelling instead of allowing a dead call to ship.

component.Server(...) and ActionDef.Server look like the marker but are dead API: Server(...) has one call site in the whole repo (a unit test), and On() never sets ActionDef.Server nor does the compiler read it. Keying on either would record a *declaration* rather than the property — the exact failure mode issue #150 rejected a marker interface for — so they are deliberately not matched.

Reachability, and where each step gives up

embed.Surface now carries the screen value, so the link from a surface to the component tree it renders is a Go value graph. findFindings resolves as much of it as go/analysis + go/types honestly can, per package:

  1. embed.Surface{...} composite literals — identified by resolved type, so a same-named struct elsewhere is never mistaken for one.
  2. The Screen field → the app.NewScreen(path, comp) call that built it, following one level of identifier → initializer within the package.
  3. comp → its concrete named type, following an identifier whose declared type is the component.Component interface back to its initializer.
  4. the WHOLE component tree reachable from that type, not just the root: struct fields (including embedded ones, and through pointers, slices, arrays and maps), concrete components handed to the constructor expression that built the root, and concrete components named in the root's own Render / RenderCtx body.
  5. each reachable type's Actions() method → executable component.On(...) calls with a literal component.WithClientJS(...) option containing a G.serverAction call outside JavaScript comments and strings.

Step 4 is why the root is not the unit. A root that renders a child ships the CHILD's compiled actions to the frame — every compiled registry travels in one bundle — so a gate that inspected only the root passed a surface whose button 401s in the customer's page.

Where it stops, and why it says so

Static analysis cannot follow an interface-typed field resolved at runtime, a component type whose Actions() body lives in another package, a component produced by calling a function value, ClientJS that is not a string literal, or a registration nested inside a function literal. Each of those is reported as an Unresolved — NOT as silence. A gate that quietly gives up reads exactly like a gate that checked and found nothing, which is how the rendered-child hole survived a release.

Most Unresolved notes are advisory: the boot walk in framework/uihost reads live component VALUES, so a child held in a field — through an interface, a map key, or an island wrapper — is checked at Mount. One class is not, and carries Blocking: a child built inside Render() whose type lives in another package. It does not exist as a value when the walk runs, and its Actions() body is not in this syntax tree, so neither gate can vouch for it and `gofastr build` stops.

Failing on EVERY note was tried and reverted. It rejected clean island surfaces — the shape the blueprint emits for every island block — plus interface-typed fields the analyzer had already resolved and the fixture named for false positives, and the advertised remedy ("hold the child in a field") is impossible for a wrapper.

Check returns violations alone for callers that want only those; CheckAll returns both and is what the build gate uses.

Index

Constants

This section is empty.

Variables

View Source
var Analyzer = &analysis.Analyzer{
	Name: "check_embed",
	Doc:  "report embeddable surfaces whose screen's component registers a G.serverAction, which is refused inside a frame",
	Run:  runPass,
}

Analyzer is the go/analysis pass. analysistest exercises it directly, and a future `go vet` attachment would run it; the cmd/check-embed CLI and the `gofastr build` gate both call the same findFindings core via Check.

Functions

func CheckAll added in v0.52.0

func CheckAll(pattern string) ([]Finding, []Unresolved, *token.FileSet, error)

CheckAll is Check plus the places the static walk could not follow.

The two are separate returns because they mean different things to a build: a Finding is a violation and must fail it; an Unresolved is the analyzer saying "I could not look here", which the boot walk covers and which must never be mistaken for a clean result. Callers that gate a build print both and fail on the first.

Types

type Finding

type Finding struct {
	Pos       token.Pos
	Surface   string // the surface Name; "<dynamic>" when not a string literal
	Component string // concrete component type name
	Action    string // the On() event name; "<dynamic>" when not a string literal
}

Finding is one provable server action reachable from an embeddable surface.

func Check

func Check(pattern string) ([]Finding, *token.FileSet, error)

Check loads the non-test packages matching pattern (e.g. "./...") and returns any server-action-on-embed finding. It is the shared driver used by the cmd/check-embed CLI and the `gofastr build` gate, so both report identically.

Findings take precedence over load errors: a real violation is the actionable signal and is returned with a nil error. When a package failed to parse or type-check and no findings were produced, Check returns that as an error so the caller can surface an infrastructure failure rather than a false "clean".

func (Finding) Format

func (f Finding) Format() string

Format renders the human-facing message, mirroring the boot-walk panic so a developer sees the same explanation at build time and at boot.

type Unresolved added in v0.52.0

type Unresolved struct {
	Pos     token.Pos
	Surface string // the surface Name; "<dynamic>" when not a string literal
	Reason  string
	// Blocking marks the note class no gate can cover, which is what
	// `gofastr build` refuses to build past.
	Blocking bool
}

Unresolved is one place the static walk could not follow, on the path from an embeddable surface to the components it renders.

It is never a violation — the surface may well be clean. It exists so that "I found nothing" and "I could not look" are not the same output.

Most notes are advisory, because the boot-time walk in framework/uihost/embed_actions.go covers what they describe: it reads live component VALUES, so a child held in a field — including through an interface, a map key, or an island wrapper — is checked at Mount.

Blocking is the exception: a child CONSTRUCTED inside Render() whose type lives in another package is visible to neither gate. It does not exist as a value when the boot walk runs, and its Actions() body is not in this syntax tree. Only that class fails the build.

func (Unresolved) Format added in v0.52.0

func (u Unresolved) Format() string

Format renders the human-facing note.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL