mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(verifier): named() and cross-extractor read guard in goja
This commit is contained in:
1 parent
fe86e91aff
commit
973ada4983
2 files changed
+54
-10
No files matched your search
@@ -10,8 +10,10 @@ import (
|
||||
|
||||
type extractorState struct {
|
||||
getter goja.Callable
|
||||
handle *goja.Object
|
||||
name string
|
||||
// currentValue/previousValue back the handle's current/previous accessors.
|
||||
currentValue goja.Value
|
||||
previousValue goja.Value
|
||||
// prev/curr cache the JSON-encoded extractor values from the prior and
|
||||
// current PushSnapshot, used by ChangedExtractors to surface per-step
|
||||
// diffs in the trace.
|
||||
@@ -170,14 +172,43 @@ func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value {
|
||||
name = fmt.Sprintf("extractor_%d", len(v.extractors))
|
||||
}
|
||||
|
||||
handle := v.runtime.NewObject()
|
||||
_ = handle.Set("current", goja.Undefined())
|
||||
_ = handle.Set("previous", goja.Undefined())
|
||||
state := &extractorState{
|
||||
getter: getter,
|
||||
name: name,
|
||||
currentValue: goja.Undefined(),
|
||||
previousValue: goja.Undefined(),
|
||||
}
|
||||
|
||||
v.extractors = append(v.extractors, &extractorState{getter: getter, handle: handle, name: name})
|
||||
handle := v.runtime.NewObject()
|
||||
_ = handle.DefineAccessorProperty("current", v.runtime.ToValue(func(goja.FunctionCall) goja.Value {
|
||||
v.checkNotExtracting("current")
|
||||
return state.currentValue
|
||||
}), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
_ = handle.DefineAccessorProperty("previous", v.runtime.ToValue(func(goja.FunctionCall) goja.Value {
|
||||
v.checkNotExtracting("previous")
|
||||
return state.previousValue
|
||||
}), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
_ = handle.Set("named", func(call goja.FunctionCall) goja.Value {
|
||||
state.name = call.Argument(0).String()
|
||||
return handle
|
||||
})
|
||||
|
||||
v.extractors = append(v.extractors, state)
|
||||
return handle
|
||||
}
|
||||
|
||||
// checkNotExtracting panics with a JS error when an extractor getter tries to
|
||||
// read another extractor handle's current/previous. The message is identical to
|
||||
// the web runtime's so authors see one diagnostic across engines.
|
||||
func (v *Verifier) checkNotExtracting(slot string) {
|
||||
if v.extracting {
|
||||
panic(v.runtime.NewGoError(fmt.Errorf(
|
||||
"reading .%s of an extractor inside another extractor is not allowed; extractor getters may read only from the state argument",
|
||||
slot,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// bindAlways accepts either a predicate function (legacy shape) or a formula
|
||||
// handle (new shape). Both produce a formula handle tagged with
|
||||
// __sanderlingFormulaSpec.
|
||||
|
||||
@@ -49,6 +49,11 @@ type Verifier struct {
|
||||
// order, so the runner can surface them in the run report.
|
||||
unsupported []string
|
||||
unsupportedSeen map[string]bool
|
||||
|
||||
// extracting is true only while an extractor getter is running. The handle's
|
||||
// current/previous accessors consult it so a getter that reaches into
|
||||
// another extractor's handle throws instead of reading a stale value.
|
||||
extracting bool
|
||||
}
|
||||
|
||||
// UnsupportedVerbs returns the verbs the picker requested that this platform
|
||||
@@ -276,19 +281,27 @@ func (v *Verifier) PushSnapshot(input SnapshotInput) error {
|
||||
// Predicate thunks read these slots but never trigger advancement, so
|
||||
// invoking a thunk multiple times between snapshots is value-stable.
|
||||
for index, extractor := range v.extractors {
|
||||
previous := extractor.handle.Get("current")
|
||||
_ = extractor.handle.Set("previous", previous)
|
||||
newValue, err := extractor.getter(goja.Undefined(), state)
|
||||
extractor.previousValue = extractor.currentValue
|
||||
newValue, err := v.runExtractor(extractor, state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("extractor %d: %w", index, err)
|
||||
}
|
||||
_ = extractor.handle.Set("current", newValue)
|
||||
extractor.currentValue = newValue
|
||||
extractor.prev = extractor.curr
|
||||
extractor.curr = encodeExtractorValue(newValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runExtractor invokes an extractor's getter with the extracting flag set, so a
|
||||
// getter that reads another extractor's current/previous throws. The flag is
|
||||
// cleared even if the getter panics.
|
||||
func (v *Verifier) runExtractor(extractor *extractorState, state goja.Value) (goja.Value, error) {
|
||||
v.extracting = true
|
||||
defer func() { v.extracting = false }()
|
||||
return extractor.getter(goja.Undefined(), state)
|
||||
}
|
||||
|
||||
// encodeExtractorValue produces a stable JSON encoding of an extractor's
|
||||
// current value for diff comparison. goja values that don't survive Export
|
||||
// (e.g. wrapped host functions) yield nil; callers treat nil as "unknown" and
|
||||
@@ -358,7 +371,7 @@ func (v *Verifier) OverrideExtractorValues(overrides map[int]json.RawMessage) (s
|
||||
if conversionErr != nil {
|
||||
return skipped, fmt.Errorf("extractor override %d: %w", index, conversionErr)
|
||||
}
|
||||
_ = v.extractors[index].handle.Set("current", value)
|
||||
v.extractors[index].currentValue = value
|
||||
}
|
||||
return skipped, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user