Skip to content

Commit e78570d

Browse files
committed
compiler: support arbitrary value js struct tag values
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 the index operator (where required) we can support arbitrary string values in js-tagged fields for `*js.Object` special structs. Fixes #778
1 parent 558a913 commit e78570d

File tree

4 files changed

+64
-4
lines changed

4 files changed

+64
-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: 29 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,30 @@ 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 a string of JavaScript code appropriate for
650+
// accessing the property identified by jsTag. If the jsTag value can be safely
651+
// encoded in JavaScript using the dot notation this is used, else we fallback
652+
// to the bracket notation. For more details see:
653+
//
654+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors
655+
//
656+
// Uses definition of an identifier from
657+
// https://developer.mozilla.org/en-US/docs/Glossary/Identifier
658+
func formatJSStructTagVal(jsTag string) string {
659+
useDot := true
660+
661+
for i, r := range jsTag {
662+
ok := unicode.IsLetter(r) || (i != 0 && unicode.IsNumber(r)) || r == '$' || r == '_'
663+
if !ok {
664+
useDot = false
665+
break
666+
}
667+
}
668+
669+
if useDot {
670+
return "." + jsTag
671+
}
672+
673+
return "[\"" + template.JSEscapeString(jsTag) + "\"]"
674+
}

js/js_test.go

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

0 commit comments

Comments
 (0)