diff --git a/compiler/expressions.go b/compiler/expressions.go index d87b8c0d0..42fe624b6 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -516,9 +516,9 @@ func (c *funcContext) translateExpr(expr ast.Expr) *expression { fields, jsTag := c.translateSelection(sel, e.Pos()) if jsTag != "" { if _, ok := sel.Type().(*types.Signature); ok { - return c.formatExpr("$internalize(%1e.%2s.%3s, %4s, %1e.%2s)", e.X, strings.Join(fields, "."), jsTag, c.typeName(sel.Type())) + return c.formatExpr("$internalize(%1e.%2s%3s, %4s, %1e.%2s)", e.X, strings.Join(fields, "."), formatJSStructTagVal(jsTag), c.typeName(sel.Type())) } - return c.internalize(c.formatExpr("%e.%s.%s", e.X, strings.Join(fields, "."), jsTag), sel.Type()) + return c.internalize(c.formatExpr("%e.%s%s", e.X, strings.Join(fields, "."), formatJSStructTagVal(jsTag)), sel.Type()) } return c.formatExpr("%e.%s", e.X, strings.Join(fields, ".")) case types.MethodVal: @@ -669,7 +669,7 @@ func (c *funcContext) translateExpr(expr ast.Expr) *expression { case types.FieldVal: fields, jsTag := c.translateSelection(sel, f.Pos()) if jsTag != "" { - call := c.formatExpr("%e.%s.%s(%s)", f.X, strings.Join(fields, "."), jsTag, externalizeArgs(e.Args)) + call := c.formatExpr("%e.%s%s(%s)", f.X, strings.Join(fields, "."), formatJSStructTagVal(jsTag), externalizeArgs(e.Args)) switch sig.Results().Len() { case 0: return call diff --git a/compiler/statements.go b/compiler/statements.go index 43a526d5d..b83396235 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -703,7 +703,7 @@ func (c *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } fields, jsTag := c.translateSelection(sel, l.Pos()) if jsTag != "" { - return fmt.Sprintf("%s.%s.%s = %s;", c.translateExpr(l.X), strings.Join(fields, "."), jsTag, c.externalize(rhsExpr.String(), sel.Type())) + return fmt.Sprintf("%s.%s%s = %s;", c.translateExpr(l.X), strings.Join(fields, "."), formatJSStructTagVal(jsTag), c.externalize(rhsExpr.String(), sel.Type())) } return fmt.Sprintf("%s.%s = %s;", c.translateExpr(l.X), strings.Join(fields, "."), rhsExpr) case *ast.StarExpr: diff --git a/compiler/utils.go b/compiler/utils.go index 8e385229d..d5452e0a6 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -12,6 +12,8 @@ import ( "sort" "strconv" "strings" + "text/template" + "unicode" "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/typesutil" @@ -643,3 +645,29 @@ func endsWithReturn(stmts []ast.Stmt) bool { func encodeIdent(name string) string { return strings.Replace(url.QueryEscape(name), "%", "$", -1) } + +// formatJSStructTagVal returns JavaScript code for accessing an object's property +// identified by jsTag. It prefers the dot notation over the bracket notation when +// possible, since the dot notation produces slightly smaller output. +// +// For example: +// +// "my_name" -> ".my_name" +// "my name" -> `["my name"]` +// +// For more information about JavaScript property accessors and identifiers, see +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors and +// https://developer.mozilla.org/en-US/docs/Glossary/Identifier. +// +func formatJSStructTagVal(jsTag string) string { + for i, r := range jsTag { + ok := unicode.IsLetter(r) || (i != 0 && unicode.IsNumber(r)) || r == '$' || r == '_' + if !ok { + // Saw an invalid JavaScript identifier character, + // so use bracket notation. + return `["` + template.JSEscapeString(jsTag) + `"]` + } + } + // Safe to use dot notation without any escaping. + return "." + jsTag +} diff --git a/js/js_test.go b/js/js_test.go index cbac7c7db..39afc530d 100644 --- a/js/js_test.go +++ b/js/js_test.go @@ -597,3 +597,26 @@ func TestTypeSwitchJSObject(t *testing.T) { } } } + +func TestStructWithNonIdentifierJSTag(t *testing.T) { + type S struct { + *js.Object + Name string `js:"@&\"'<>//my name"` + } + s := S{Object: js.Global.Get("Object").New()} + + // externalise a value via field + s.Name = "Paul" + + // internalise via field + got := s.Name + if want := "Paul"; got != want { + t.Errorf("value via field with non-identifier js tag gave %q, want %q", got, want) + } + + // verify we can do a Get with the struct tag + got = s.Get("@&\"'<>//my name").String() + if want := "Paul"; got != want { + t.Errorf("value via js.Object.Get gave %q, want %q", got, want) + } +}