Skip to content

Commit

Permalink
js/common: Helper for Object.freeze from Go
Browse files Browse the repository at this point in the history
  • Loading branch information
codebien committed Apr 20, 2022
1 parent b9879fb commit 46f112b
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 0 deletions.
69 changes: 69 additions & 0 deletions js/common/frozen_object.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package common

import (
"fmt"

"github.com/dop251/goja"
)

// FreezeObject replicates the JavaScript Object.freeze function.
func FreezeObject(rt *goja.Runtime, obj goja.Value) error {
global := rt.GlobalObject().Get("Object").ToObject(rt)
freeze, ok := goja.AssertFunction(global.Get("freeze"))
if !ok {
panic("failed to get the Object.freeze function from the runtime")
}
isFrozen, ok := goja.AssertFunction(global.Get("isFrozen"))
if !ok {
panic("failed to get the Object.isFrozen function from the runtime")
}
fobj := &freezing{
global: global,
rt: rt,
freeze: freeze,
isFrozen: isFrozen,
}
return fobj.deepFreeze(obj)
}

type freezing struct {
rt *goja.Runtime
global goja.Value
freeze goja.Callable
isFrozen goja.Callable
}

func (f *freezing) deepFreeze(val goja.Value) error {
if val != nil && goja.IsNull(val) {
return nil
}

_, err := f.freeze(goja.Undefined(), val)
if err != nil {
return fmt.Errorf("object freeze failed: %w", err)
}

o := val.ToObject(f.rt)
if o == nil {
return nil
}

for _, key := range o.Keys() {
prop := o.Get(key)
if prop == nil {
continue
}
frozen, err := f.isFrozen(goja.Undefined(), prop)
if err != nil {
return err
}
if frozen.ToBoolean() { // prevent cycles
continue
}
if err = f.deepFreeze(prop); err != nil {
return fmt.Errorf("deep freezing the property %s failed: %w", key, err)
}
}

return nil
}
66 changes: 66 additions & 0 deletions js/common/frozen_object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package common

import (
"testing"

"github.com/dop251/goja"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFrozenObject(t *testing.T) {
t.Parallel()

rt := goja.New()
obj := rt.NewObject()
require.NoError(t, obj.Set("foo", "bar"))
require.NoError(t, rt.Set("obj", obj))

v, err := rt.RunString(`obj.foo`)
require.NoError(t, err)
require.Equal(t, "bar", v.String())

// Set a nested object
_, err = rt.RunString(`obj.nested = {propkey: "value1"}`)
require.NoError(t, err)

// Not yet frozen
v, err = rt.RunString(`Object.isFrozen(obj)`)
require.NoError(t, err)
require.False(t, v.ToBoolean())

require.NoError(t, FreezeObject(rt, obj))

// It has been frozen
v, err = rt.RunString(`Object.isFrozen(obj)`)
require.NoError(t, err)
require.True(t, v.ToBoolean())

// It has deeply frozen the properties
vfoo, err := rt.RunString(`Object.isFrozen(obj.foo)`)
require.NoError(t, err)
require.True(t, vfoo.ToBoolean())

// And deeply frozen the nested objects
vnested, err := rt.RunString(`Object.isFrozen(obj.nested)`)
require.NoError(t, err)
require.True(t, vnested.ToBoolean())

nestedProp, err := rt.RunString(`Object.isFrozen(obj.nested.propkey)`)
require.NoError(t, err)
require.True(t, nestedProp.ToBoolean())

// The assign is silently ignored
_, err = rt.RunString(`obj.foo = "bad change"`)
require.NoError(t, err)

v, err = rt.RunString(`obj.foo`)
require.NoError(t, err)
assert.Equal(t, "bar", v.String())

// If the strict mode is enabled then it fails
v, err = rt.RunString(`'use strict'; obj.foo = "bad change"`)
require.NotNil(t, err)
assert.Contains(t, err.Error(), "Cannot assign to read only property 'foo'")
assert.Nil(t, v)
}
1 change: 1 addition & 0 deletions js/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*
*/

// Package common contains helpers for interacting with the JavaScript runtime.
package common

import (
Expand Down

0 comments on commit 46f112b

Please sign in to comment.