Skip to content

Commit e14987c

Browse files
myitcvdmitshur
authored andcommitted
compiler: support arbitrary value js struct tag values (gopherjs#779)
JavaScript allows for arbitrary strings to be used as index values on an Object. A good example of where this comes up is React where `"data-*"` and `"aria-*"` indexes are used on objects (https://reactjs.org/docs/dom-elements.html). By switching from property selectors to index operator (where required), we support arbitrary string values in js-tagged fields for *js.Object special structs. Fixes gopherjs#778.
1 parent 558a913 commit e14987c

File tree

4 files changed

+55
-4
lines changed

4 files changed

+55
-4
lines changed

compiler/expressions.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,9 +516,9 @@ func (c *funcContext) translateExpr(expr ast.Expr) *expression {
516516
fields, jsTag := c.translateSelection(sel, e.Pos())
517517
if jsTag != "" {
518518
if _, ok := sel.Type().(*types.Signature); ok {
519-
return c.formatExpr("$internalize(%1e.%2s.%3s, %4s, %1e.%2s)", e.X, strings.Join(fields, "."), jsTag, c.typeName(sel.Type()))
519+
return c.formatExpr("$internalize(%1e.%2s%3s, %4s, %1e.%2s)", e.X, strings.Join(fields, "."), formatJSStructTagVal(jsTag), c.typeName(sel.Type()))
520520
}
521-
return c.internalize(c.formatExpr("%e.%s.%s", e.X, strings.Join(fields, "."), jsTag), sel.Type())
521+
return c.internalize(c.formatExpr("%e.%s%s", e.X, strings.Join(fields, "."), formatJSStructTagVal(jsTag)), sel.Type())
522522
}
523523
return c.formatExpr("%e.%s", e.X, strings.Join(fields, "."))
524524
case types.MethodVal:
@@ -669,7 +669,7 @@ func (c *funcContext) translateExpr(expr ast.Expr) *expression {
669669
case types.FieldVal:
670670
fields, jsTag := c.translateSelection(sel, f.Pos())
671671
if jsTag != "" {
672-
call := c.formatExpr("%e.%s.%s(%s)", f.X, strings.Join(fields, "."), jsTag, externalizeArgs(e.Args))
672+
call := c.formatExpr("%e.%s%s(%s)", f.X, strings.Join(fields, "."), formatJSStructTagVal(jsTag), externalizeArgs(e.Args))
673673
switch sig.Results().Len() {
674674
case 0:
675675
return call

compiler/statements.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ func (c *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string {
703703
}
704704
fields, jsTag := c.translateSelection(sel, l.Pos())
705705
if jsTag != "" {
706-
return fmt.Sprintf("%s.%s.%s = %s;", c.translateExpr(l.X), strings.Join(fields, "."), jsTag, c.externalize(rhsExpr.String(), sel.Type()))
706+
return fmt.Sprintf("%s.%s%s = %s;", c.translateExpr(l.X), strings.Join(fields, "."), formatJSStructTagVal(jsTag), c.externalize(rhsExpr.String(), sel.Type()))
707707
}
708708
return fmt.Sprintf("%s.%s = %s;", c.translateExpr(l.X), strings.Join(fields, "."), rhsExpr)
709709
case *ast.StarExpr:

compiler/utils.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"sort"
1313
"strconv"
1414
"strings"
15+
"text/template"
16+
"unicode"
1517

1618
"github.com/gopherjs/gopherjs/compiler/analysis"
1719
"github.com/gopherjs/gopherjs/compiler/typesutil"
@@ -643,3 +645,29 @@ func endsWithReturn(stmts []ast.Stmt) bool {
643645
func encodeIdent(name string) string {
644646
return strings.Replace(url.QueryEscape(name), "%", "$", -1)
645647
}
648+
649+
// formatJSStructTagVal returns JavaScript code for accessing an object's property
650+
// identified by jsTag. It prefers the dot notation over the bracket notation when
651+
// possible, since the dot notation produces slightly smaller output.
652+
//
653+
// For example:
654+
//
655+
// "my_name" -> ".my_name"
656+
// "my name" -> `["my name"]`
657+
//
658+
// For more information about JavaScript property accessors and identifiers, see
659+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors and
660+
// https://developer.mozilla.org/en-US/docs/Glossary/Identifier.
661+
//
662+
func formatJSStructTagVal(jsTag string) string {
663+
for i, r := range jsTag {
664+
ok := unicode.IsLetter(r) || (i != 0 && unicode.IsNumber(r)) || r == '$' || r == '_'
665+
if !ok {
666+
// Saw an invalid JavaScript identifier character,
667+
// so use bracket notation.
668+
return `["` + template.JSEscapeString(jsTag) + `"]`
669+
}
670+
}
671+
// Safe to use dot notation without any escaping.
672+
return "." + jsTag
673+
}

js/js_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,3 +597,26 @@ func TestTypeSwitchJSObject(t *testing.T) {
597597
}
598598
}
599599
}
600+
601+
func TestStructWithNonIdentifierJSTag(t *testing.T) {
602+
type S struct {
603+
*js.Object
604+
Name string `js:"@&\"'<>//my name"`
605+
}
606+
s := S{Object: js.Global.Get("Object").New()}
607+
608+
// externalise a value via field
609+
s.Name = "Paul"
610+
611+
// internalise via field
612+
got := s.Name
613+
if want := "Paul"; got != want {
614+
t.Errorf("value via field with non-identifier js tag gave %q, want %q", got, want)
615+
}
616+
617+
// verify we can do a Get with the struct tag
618+
got = s.Get("@&\"'<>//my name").String()
619+
if want := "Paul"; got != want {
620+
t.Errorf("value via js.Object.Get gave %q, want %q", got, want)
621+
}
622+
}

0 commit comments

Comments
 (0)