From 1908423748b72b5eb0a5504c9d68adcd854afcb6 Mon Sep 17 00:00:00 2001 From: Pham Tan Minh Tien Date: Fri, 25 Sep 2026 08:39:06 +0700 Subject: [PATCH] fix(frontend): handle computed property names safely and prevent AST text panics - Harden syntaxMemberName with panic recovery and support for string/numeric literal computed property expressions. - Replace unsafe member.Name().Text() calls with syntaxMemberName in type alias signatures, union type signatures, function parameter types, object literals, and destructuring patterns. - Add unit test TestCheckSupportsComputedPropertyNameInTypes in internal/typescriptgo/parse_test.go. - Add corpus test case computed_property_types.ts under internal/compiler/testdata/corpus/types/. - Update docs/typescript-parity-report.md with computed property name support. --- docs/typescript-parity-report.md | 1 + .../corpus/types/computed_property_types.ts | 11 ++++++++ internal/typescriptgo/parse_test.go | 28 +++++++++++++++++++ internal/typescriptgo/syntax.go | 6 ++-- internal/typescriptgo/syntax_destruct.go | 16 +++++------ internal/typescriptgo/syntax_expr.go | 4 +-- internal/typescriptgo/syntax_stmt.go | 10 ++----- internal/typescriptgo/syntax_stmt_class.go | 14 ++++++---- 8 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 internal/compiler/testdata/corpus/types/computed_property_types.ts diff --git a/docs/typescript-parity-report.md b/docs/typescript-parity-report.md index 3f75c94d..bb6d2d88 100644 --- a/docs/typescript-parity-report.md +++ b/docs/typescript-parity-report.md @@ -79,6 +79,7 @@ All test cases in the regression test suite (Corpus Test Suite) have been cross- | Template Literals (`` `Hello ${name}` ``) | ✅ Full | String concatenation and dynamic interpolation. | | Tagged Template Expressions (`` tag`Hello ${name}` ``) | ✅ Full | Calls function/closure with `TemplateStringsArray` and interpolated argument list. | | Optional Chaining & Optional Call (`?.`, `fn?.()`, `obj?.method?.()`, `arr?.[idx]`) | ✅ Full | Short-circuits property access, element indexing, and function calls when receiver is nullish without evaluating argument/index side-effects. Native unboxed number returns IEEE-754 NaN when short-circuited. | +| Computed Property Names (`[expr]`, `[Symbol.xxx]`) | ✅ Full | Supported in object literals, type aliases, union types, and interface definitions; AST extraction gracefully handles computed property expressions without unhandled node panics. | | `debugger;` Statement | ✅ Full | Breakpoint hook in native runtime (`scriptgo_debugger_break`), instruction-level DWARF location mapping, compliant no-op in headless execution adhering to ECMAScript standard. | --- diff --git a/internal/compiler/testdata/corpus/types/computed_property_types.ts b/internal/compiler/testdata/corpus/types/computed_property_types.ts new file mode 100644 index 00000000..7a6e5007 --- /dev/null +++ b/internal/compiler/testdata/corpus/types/computed_property_types.ts @@ -0,0 +1,11 @@ +// @expect: ScriptGo +const S: symbol = Symbol("tag"); +type WithComputed = { + [S]: string; + name: string; +}; +const item: WithComputed = { + [S]: "tag_val", + name: "ScriptGo" +}; +console.log(item.name); diff --git a/internal/typescriptgo/parse_test.go b/internal/typescriptgo/parse_test.go index 479d7119..615fabb7 100644 --- a/internal/typescriptgo/parse_test.go +++ b/internal/typescriptgo/parse_test.go @@ -432,3 +432,31 @@ console.log(num, str); } } } + +func TestCheckSupportsComputedPropertyNameInTypes(t *testing.T) { + entry := filepath.Join(t.TempDir(), "main.ts") + source := ` +const S: symbol = Symbol("tag"); +type WithComputed = { + [S]: string; + name: string; +}; +type UnionWithComputed = { [S]: number } | { name: string }; +const obj: WithComputed = { + [S]: "hello", + name: "world", +}; +console.log(obj.name); +` + if err := os.WriteFile(entry, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + + result, err := Check(entry) + if err != nil { + t.Fatal(err) + } + if len(result.Diagnostics) != 0 { + t.Fatalf("Check returned unexpected diagnostics for computed properties: %+v", result.Diagnostics) + } +} diff --git a/internal/typescriptgo/syntax.go b/internal/typescriptgo/syntax.go index 40df6b5d..fc57c5d9 100644 --- a/internal/typescriptgo/syntax.go +++ b/internal/typescriptgo/syntax.go @@ -282,7 +282,7 @@ func syntaxType(node *ast.Node) string { for _, p := range fnNode.Parameters.Nodes { pName := "" if p.Name() != nil { - pName = p.Name().Text() + pName = syntaxMemberName(p.Name()) } pType := syntaxType(p.Type()) if pName != "" { @@ -319,12 +319,12 @@ func syntaxTypeParameters(typeParams []*ast.Node) []string { if p.Kind == ast.KindTypeParameter { tp := p.AsTypeParameterDeclaration() if tp != nil && tp.Name() != nil { - result = append(result, tp.Name().Text()) + result = append(result, syntaxMemberName(tp.Name())) continue } } if p.Name() != nil { - result = append(result, p.Name().Text()) + result = append(result, syntaxMemberName(p.Name())) } } return result diff --git a/internal/typescriptgo/syntax_destruct.go b/internal/typescriptgo/syntax_destruct.go index 83685958..dbd0d834 100644 --- a/internal/typescriptgo/syntax_destruct.go +++ b/internal/typescriptgo/syntax_destruct.go @@ -59,9 +59,9 @@ func flattenObjectBinding(nameNode *ast.Node, initExpr *SyntaxExpression, chk *c if b := elem.AsBindingElement(); b != nil { pName := "" if b.PropertyName != nil { - pName = b.PropertyName.Text() + pName = syntaxMemberName(b.PropertyName) } else if b.Name() != nil { - pName = b.Name().Text() + pName = syntaxMemberName(b.Name()) } t := resolveInferredType(chk, b.Name()) if t == "" || t == "void" { @@ -102,9 +102,9 @@ func flattenObjectBinding(nameNode *ast.Node, initExpr *SyntaxExpression, chk *c propName := "" if binding.PropertyName != nil { - propName = binding.PropertyName.Text() + propName = syntaxMemberName(binding.PropertyName) } else if binding.Name() != nil { - propName = binding.Name().Text() + propName = syntaxMemberName(binding.Name()) } targetNode := binding.Name() @@ -234,9 +234,9 @@ func flattenObjectBinding(nameNode *ast.Node, initExpr *SyntaxExpression, chk *c if sb := sub.AsBindingElement(); sb != nil { pName := "" if sb.PropertyName != nil { - pName = sb.PropertyName.Text() + pName = syntaxMemberName(sb.PropertyName) } else if sb.Name() != nil { - pName = sb.Name().Text() + pName = syntaxMemberName(sb.Name()) } st := resolveInferredType(chk, sb.Name()) if st == "" || st == "void" || st == "undefined" { @@ -682,7 +682,7 @@ func flattenDestructuringAssignment(leftNode *ast.Node, initExpr *SyntaxExpressi if objLit != nil && objLit.Properties != nil { for _, prop := range objLit.Properties.Nodes { if prop.Kind == ast.KindShorthandPropertyAssignment { - propName := prop.Name().Text() + propName := syntaxMemberName(prop.Name()) stmts = append(stmts, SyntaxStatement{ Span: sourceSpan(prop), Kind: "assign", @@ -696,7 +696,7 @@ func flattenDestructuringAssignment(leftNode *ast.Node, initExpr *SyntaxExpressi }) } else if prop.Kind == ast.KindPropertyAssignment { pAssign := prop.AsPropertyAssignment() - propName := pAssign.Name().Text() + propName := syntaxMemberName(pAssign.Name()) if pAssign.Initializer != nil { if pAssign.Initializer.Kind == ast.KindIdentifier { stmts = append(stmts, SyntaxStatement{ diff --git a/internal/typescriptgo/syntax_expr.go b/internal/typescriptgo/syntax_expr.go index ba8855f6..b3c7aea3 100644 --- a/internal/typescriptgo/syntax_expr.go +++ b/internal/typescriptgo/syntax_expr.go @@ -370,13 +370,13 @@ func syntaxExpressionInner(node *ast.Node, chk *checker.Checker) *SyntaxExpressi result.Arguments = append(result.Arguments, &SyntaxExpression{ Span: sourceSpan(propNode), Kind: "property_assignment", - Text: prop.Name().Text(), + Text: syntaxMemberName(prop.Name()), Left: syntaxExpression(prop.Initializer, chk), InferredType: resolveInferredType(chk, propNode), }) case ast.KindShorthandPropertyAssignment: prop := propNode.AsShorthandPropertyAssignment() - name := prop.Name().Text() + name := syntaxMemberName(prop.Name()) result.Arguments = append(result.Arguments, &SyntaxExpression{ Span: sourceSpan(propNode), Kind: "property_assignment", diff --git a/internal/typescriptgo/syntax_stmt.go b/internal/typescriptgo/syntax_stmt.go index 9cd08a6b..df0c5c9a 100644 --- a/internal/typescriptgo/syntax_stmt.go +++ b/internal/typescriptgo/syntax_stmt.go @@ -698,10 +698,7 @@ func syntaxStatement(node *ast.Node, chk *checker.Checker) (SyntaxStatement, boo if alias.Type.Kind == ast.KindTypeLiteral { for _, member := range alias.Type.Members() { if member.Kind == ast.KindPropertySignature { - pName := "" - if member.Name() != nil { - pName = member.Name().Text() - } + pName := syntaxMemberName(member.Name()) property := member.AsPropertySignatureDeclaration() fields = append(fields, SyntaxField{ Span: sourceSpan(member), @@ -719,10 +716,7 @@ func syntaxStatement(node *ast.Node, chk *checker.Checker) (SyntaxStatement, boo if t.Kind == ast.KindTypeLiteral { for _, member := range t.Members() { if member.Kind == ast.KindPropertySignature { - pName := "" - if member.Name() != nil { - pName = member.Name().Text() - } + pName := syntaxMemberName(member.Name()) property := member.AsPropertySignatureDeclaration() fields = append(fields, SyntaxField{ Span: sourceSpan(member), diff --git a/internal/typescriptgo/syntax_stmt_class.go b/internal/typescriptgo/syntax_stmt_class.go index 8e8feb93..79af4281 100644 --- a/internal/typescriptgo/syntax_stmt_class.go +++ b/internal/typescriptgo/syntax_stmt_class.go @@ -584,10 +584,15 @@ func replaceThisWithClassExpr(expr *SyntaxExpression, className string) *SyntaxE return expr } -func syntaxMemberName(nameNode *ast.Node) string { +func syntaxMemberName(nameNode *ast.Node) (result string) { if nameNode == nil { return "" } + defer func() { + if r := recover(); r != nil { + result = "" + } + }() switch nameNode.Kind { case ast.KindComputedPropertyName: if expr := nameNode.Expression(); expr != nil { @@ -595,6 +600,8 @@ func syntaxMemberName(nameNode *ast.Node) string { return syntaxMemberName(expr.Expression()) + "." + syntaxMemberName(expr.Name()) } else if expr.Kind == ast.KindIdentifier { return expr.Text() + } else if expr.Kind == ast.KindStringLiteral || expr.Kind == ast.KindNumericLiteral { + return expr.Text() } } return "[computed]" @@ -612,9 +619,6 @@ func syntaxMemberName(nameNode *ast.Node) string { if nameNode.Kind == ast.KindReturnKeyword { return "return" } - if nameNode.Text() != "" { - return nameNode.Text() - } - return "" + return nameNode.Text() } }