Skip to content

Commit 36bc620

Browse files
committed
compiler: support arbitrary string js tags
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 for *js.Object special field access we can support arbitrary string values in js tags. Fixes #778
1 parent 82b3220 commit 36bc620

File tree

4 files changed

+63
-4
lines changed

4 files changed

+63
-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, "."), formatJSTag(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, "."), formatJSTag(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, "."), formatJSTag(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, "."), formatJSTag(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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sort"
1313
"strconv"
1414
"strings"
15+
"unicode"
1516

1617
"github.com/gopherjs/gopherjs/compiler/analysis"
1718
"github.com/gopherjs/gopherjs/compiler/typesutil"
@@ -643,3 +644,38 @@ func endsWithReturn(stmts []ast.Stmt) bool {
643644
func encodeIdent(name string) string {
644645
return strings.Replace(url.QueryEscape(name), "%", "$", -1)
645646
}
647+
648+
// formatJSTag returns a string of Javascript code appropriate for accessing
649+
// the property identified by jsTag. If the jsTag value can be safely encoded
650+
// in Javascript using the dot notation this is used, else we fallback to the
651+
// bracket notation. For more details see:
652+
//
653+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors
654+
//
655+
// Uses definition of an identifier from
656+
// https://developer.mozilla.org/en-US/docs/Glossary/Identifier
657+
func formatJSTag(jsTag string) string {
658+
useDot := true
659+
660+
RuneRange:
661+
for i, r := range jsTag {
662+
switch i {
663+
case 0:
664+
if r != '$' && r != '_' && !unicode.IsLetter(r) {
665+
useDot = false
666+
break RuneRange
667+
}
668+
default:
669+
if r != '$' && r != '_' && !unicode.IsLetter(r) && !unicode.IsNumber(r) {
670+
useDot = false
671+
break RuneRange
672+
}
673+
}
674+
}
675+
676+
if useDot {
677+
return "." + jsTag
678+
}
679+
680+
return "[\"" + jsTag + "\"]"
681+
}

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+
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+
}

0 commit comments

Comments
 (0)