diff --git a/bindings/ast.go b/bindings/ast.go index 1db50c7..9f28408 100644 --- a/bindings/ast.go +++ b/bindings/ast.go @@ -2,10 +2,7 @@ package bindings import ( "fmt" - "go/token" "go/types" - - "github.com/dop251/goja" ) type Node interface { @@ -47,12 +44,6 @@ func (i Identifier) Ref() string { return i.Prefix + i.Name } -// Source is the golang file that an entity is sourced from. -type Source struct { - File string - Position token.Position -} - type HeritageType string const ( @@ -76,26 +67,6 @@ func HeritageClauseExtends(args ...ExpressionType) *HeritageClause { } } -func (s Source) Comment(n *goja.Object) Comment { - return Comment{ - SingleLine: true, - Text: fmt.Sprintf("From %s", s.File), - TrailingNewLine: false, - Node: n, - } -} - -type Comment struct { - // Single or multi-line comment - SingleLine bool - Text string - TrailingNewLine bool - - Node *goja.Object -} - -func (c *Comment) isNode() {} - type Modifier string const ( diff --git a/bindings/bindings.go b/bindings/bindings.go index 8ce053c..5768208 100644 --- a/bindings/bindings.go +++ b/bindings/bindings.go @@ -26,6 +26,12 @@ func (b *Bindings) ToTypescriptNode(ety Node) (*goja.Object, error) { var err error switch node := ety.(type) { + case *HeritageClause: + siObj, err = b.HeritageClause(node) + case *PropertySignature: + siObj, err = b.PropertySignature(node) + case *TypeParameter: + siObj, err = b.TypeParameter(node) case DeclarationType: // Defer to the ExpressionType implementation siObj, err = b.ToTypescriptDeclarationNode(node) @@ -36,7 +42,30 @@ func (b *Bindings) ToTypescriptNode(ety Node) (*goja.Object, error) { return nil, fmt.Errorf("unsupported node type for typescript serialization: %T", node) } - return siObj, err + if err != nil { + return nil, err + } + + // Append any source comments + if hasSource, ok := ety.(HasSource); ok { + cmt, set := hasSource.SourceComment() + if set { + siObj, err = b.CommentGojaObject([]SyntheticComment{cmt}, siObj) + if err != nil { + return nil, xerrors.Errorf("source comment declaration: %w", err) + } + } + } + + // Append any other comments + if commented, ok := ety.(Commentable); ok { + siObj, err = b.CommentGojaObject(commented.Comments(), siObj) + if err != nil { + return nil, xerrors.Errorf("comment declaration: %w", err) + } + } + + return siObj, nil } func (b *Bindings) ToTypescriptDeclarationNode(ety DeclarationType) (*goja.Object, error) { @@ -132,7 +161,7 @@ func (b *Bindings) Reference(ref *ReferenceType) (*goja.Object, error) { var args []interface{} for _, arg := range ref.Arguments { - v, err := b.ToTypescriptExpressionNode(arg) + v, err := b.ToTypescriptNode(arg) if err != nil { return nil, fmt.Errorf("reference argument: %w", err) } @@ -156,7 +185,7 @@ func (b *Bindings) PropertySignature(sig *PropertySignature) (*goja.Object, erro return nil, err } - siObj, err := b.ToTypescriptExpressionNode(sig.Type) + siObj, err := b.ToTypescriptNode(sig.Type) if err != nil { return nil, fmt.Errorf("property field type: %w", err) } @@ -195,31 +224,17 @@ func (b *Bindings) Interface(ti *Interface) (*goja.Object, error) { var fields []interface{} for _, field := range ti.Fields { - v, err := b.PropertySignature(field) + v, err := b.ToTypescriptNode(field) if err != nil { return nil, err } - if len(field.FieldComments) > 0 { - for _, text := range field.FieldComments { - v, err = b.Comment(Comment{ - SingleLine: true, - Text: text, - TrailingNewLine: false, - Node: v, - }) - if err != nil { - return nil, fmt.Errorf("comment field %q: %w", field.Name, err) - } - } - } - fields = append(fields, v) } var typeParams []interface{} for _, tp := range ti.Parameters { - v, err := b.TypeParameter(tp) + v, err := b.ToTypescriptNode(tp) if err != nil { return nil, err } @@ -228,7 +243,7 @@ func (b *Bindings) Interface(ti *Interface) (*goja.Object, error) { var heritage []interface{} for _, h := range ti.Heritage { - v, err := b.HeritageClause(h) + v, err := b.ToTypescriptNode(h) if err != nil { return nil, err } @@ -247,24 +262,6 @@ func (b *Bindings) Interface(ti *Interface) (*goja.Object, error) { } obj := res.ToObject(b.vm) - if ti.Source.File != "" { - obj, err = b.Comment(ti.Source.Comment(obj)) - if err != nil { - return nil, xerrors.Errorf("source comment interface: %w", err) - } - } - - for _, c := range ti.Comments { - obj, err = b.Comment(Comment{ - SingleLine: true, - Text: c, - TrailingNewLine: true, - Node: obj, - }) - if err != nil { - return nil, xerrors.Errorf("comment interface: %w", err) - } - } return obj, nil } @@ -314,7 +311,7 @@ func (b *Bindings) Array(arrType ExpressionType) (*goja.Object, error) { return nil, err } - siObj, err := b.ToTypescriptExpressionNode(arrType) + siObj, err := b.ToTypescriptNode(arrType) if err != nil { return nil, fmt.Errorf("array type: %w", err) } @@ -332,7 +329,7 @@ func (b *Bindings) Tuple(length int, tupleType ExpressionType) (*goja.Object, er return nil, err } - siObj, err := b.ToTypescriptExpressionNode(tupleType) + siObj, err := b.ToTypescriptNode(tupleType) if err != nil { return nil, fmt.Errorf("array type: %w", err) } @@ -350,14 +347,14 @@ func (b *Bindings) Alias(alias *Alias) (*goja.Object, error) { return nil, err } - siObj, err := b.ToTypescriptExpressionNode(alias.Type) + siObj, err := b.ToTypescriptNode(alias.Type) if err != nil { return nil, fmt.Errorf("alias type: %w", err) } var typeParams []interface{} for _, tp := range alias.Parameters { - v, err := b.TypeParameter(tp) + v, err := b.ToTypescriptNode(tp) if err != nil { return nil, err } @@ -375,9 +372,6 @@ func (b *Bindings) Alias(alias *Alias) (*goja.Object, error) { } obj := res.ToObject(b.vm) - if alias.Source.File != "" { - return b.Comment(alias.Source.Comment(obj)) - } return obj, nil } @@ -425,7 +419,7 @@ func (b *Bindings) Union(ty *UnionType) (*goja.Object, error) { var types []any for _, t := range ty.Types { - v, err := b.ToTypescriptExpressionNode(t) + v, err := b.ToTypescriptNode(t) if err != nil { return nil, fmt.Errorf("union type: %w", err) } @@ -454,25 +448,6 @@ func (b *Bindings) Null() (*goja.Object, error) { return res.ToObject(b.vm), nil } -func (b *Bindings) Comment(comment Comment) (*goja.Object, error) { - commentF, err := b.f("addSyntheticComment") - if err != nil { - return nil, err - } - - res, err := commentF(goja.Undefined(), - comment.Node, - b.vm.ToValue(true), - b.vm.ToValue(true), - b.vm.ToValue(" "+comment.Text), - b.vm.ToValue(true), - ) - if err != nil { - return nil, xerrors.Errorf("call addSyntheticComment: %w", err) - } - return res.ToObject(b.vm), nil -} - func (b *Bindings) StringLiteral(value string) (*goja.Object, error) { literalF, err := b.f("stringLiteral") if err != nil { @@ -533,7 +508,7 @@ func (b *Bindings) ArrayLiteral(value *ArrayLiteralType) (*goja.Object, error) { var elements []interface{} for _, elem := range value.Elements { - v, err := b.ToTypescriptExpressionNode(elem) + v, err := b.ToTypescriptNode(elem) if err != nil { return nil, fmt.Errorf("array literal element: %w", err) } @@ -553,7 +528,7 @@ func (b *Bindings) VariableStatement(stmt *VariableStatement) (*goja.Object, err return nil, err } - siObj, err := b.ToTypescriptExpressionNode(stmt.Declarations) + siObj, err := b.ToTypescriptNode(stmt.Declarations) if err != nil { return nil, fmt.Errorf("alias type: %w", err) } @@ -567,9 +542,6 @@ func (b *Bindings) VariableStatement(stmt *VariableStatement) (*goja.Object, err } obj := res.ToObject(b.vm) - if stmt.Source.File != "" { - return b.Comment(stmt.Source.Comment(obj)) - } return obj, nil } @@ -582,7 +554,7 @@ func (b *Bindings) VariableDeclarationList(list *VariableDeclarationList) (*goja var decls []interface{} for _, decl := range list.Declarations { - v, err := b.ToTypescriptExpressionNode(decl) + v, err := b.ToTypescriptNode(decl) if err != nil { return nil, err } @@ -609,7 +581,7 @@ func (b *Bindings) VariableDeclaration(decl *VariableDeclaration) (*goja.Object, var declType goja.Value = goja.Undefined() if decl.Type != nil { - declType, err = b.ToTypescriptExpressionNode(decl.Type) + declType, err = b.ToTypescriptNode(decl.Type) if err != nil { return nil, fmt.Errorf("alias type: %w", err) } @@ -617,7 +589,7 @@ func (b *Bindings) VariableDeclaration(decl *VariableDeclaration) (*goja.Object, var declInit goja.Value = goja.Undefined() if decl.Initializer != nil { - declInit, err = b.ToTypescriptExpressionNode(decl.Initializer) + declInit, err = b.ToTypescriptNode(decl.Initializer) if err != nil { return nil, fmt.Errorf("alias type: %w", err) } @@ -643,7 +615,7 @@ func (b *Bindings) OperatorNode(value *OperatorNodeType) (*goja.Object, error) { return nil, err } - obj, err := b.ToTypescriptExpressionNode(value.Type) + obj, err := b.ToTypescriptNode(value.Type) if err != nil { return nil, fmt.Errorf("operator type: %w", err) } @@ -663,7 +635,7 @@ func (b *Bindings) EnumMember(value *EnumMember) (*goja.Object, error) { obj := goja.Undefined() if value.Value != nil { - obj, err = b.ToTypescriptExpressionNode(value.Value) + obj, err = b.ToTypescriptNode(value.Value) if err != nil { return nil, fmt.Errorf("enum member type: %w", err) } @@ -684,7 +656,7 @@ func (b *Bindings) EnumDeclaration(e *Enum) (*goja.Object, error) { var members []any for _, m := range e.Members { - v, err := b.ToTypescriptExpressionNode(m) + v, err := b.ToTypescriptNode(m) if err != nil { return nil, fmt.Errorf("enum type: %w", err) } @@ -701,9 +673,6 @@ func (b *Bindings) EnumDeclaration(e *Enum) (*goja.Object, error) { } obj := res.ToObject(b.vm) - if e.Source.File != "" { - return b.Comment(e.Source.Comment(obj)) - } return obj, nil } @@ -716,26 +685,11 @@ func (b *Bindings) TypeLiteralNode(node *TypeLiteralNode) (*goja.Object, error) var members []interface{} for _, member := range node.Members { - v, err := b.PropertySignature(member) + v, err := b.ToTypescriptNode(member) if err != nil { return nil, err } - // Add field comments if they exist - if len(member.FieldComments) > 0 { - for _, text := range member.FieldComments { - v, err = b.Comment(Comment{ - SingleLine: true, - Text: text, - TrailingNewLine: false, - Node: v, - }) - if err != nil { - return nil, fmt.Errorf("comment field %q: %w", member.Name, err) - } - } - } - members = append(members, v) } @@ -755,7 +709,7 @@ func (b *Bindings) TypeIntersection(node *TypeIntersection) (*goja.Object, error var types []interface{} for _, t := range node.Types { - v, err := b.ToTypescriptExpressionNode(t) + v, err := b.ToTypescriptNode(t) if err != nil { return nil, fmt.Errorf("intersection type: %w", err) } @@ -768,3 +722,31 @@ func (b *Bindings) TypeIntersection(node *TypeIntersection) (*goja.Object, error } return res.ToObject(b.vm), nil } + +func (b *Bindings) CommentGojaObject(comments []SyntheticComment, object *goja.Object) (*goja.Object, error) { + if len(comments) == 0 { + return object, nil + } + + commentF, err := b.f("addSyntheticComment") + if err != nil { + return nil, err + } + + node := object + for _, c := range comments { + res, err := commentF(goja.Undefined(), + node, + b.vm.ToValue(c.Leading), + b.vm.ToValue(c.SingleLine), + b.vm.ToValue(" "+c.Text), + b.vm.ToValue(c.TrailingNewLine), + ) + if err != nil { + return nil, xerrors.Errorf("call addSyntheticComment: %w", err) + } + node = res.ToObject(b.vm) + } + + return node, nil +} diff --git a/bindings/comments.go b/bindings/comments.go new file mode 100644 index 0000000..8aa349f --- /dev/null +++ b/bindings/comments.go @@ -0,0 +1,65 @@ +package bindings + +import ( + "fmt" + "go/token" +) + +// Commentable indicates if the AST node supports adding comments. +// Any number of comments are supported and can be attached to a typescript AST node. +type Commentable interface { + Comment(comment SyntheticComment) + Comments() []SyntheticComment +} + +// SyntheticComment is the state of a comment for a given AST node. +// See the compiler for how these are serialized. +type SyntheticComment struct { + Leading bool + SingleLine bool + Text string + TrailingNewLine bool +} + +type SupportComments struct { + comments []SyntheticComment +} + +// LeadingComment is a helper function for the most common type of comment. +func (s *SupportComments) LeadingComment(text string) { + s.Comment(SyntheticComment{ + Leading: true, + SingleLine: true, + Text: text, + TrailingNewLine: false, + }) +} + +func (s *SupportComments) Comment(comment SyntheticComment) { + s.comments = append(s.comments, comment) +} + +func (s *SupportComments) Comments() []SyntheticComment { + return s.comments +} + +type HasSource interface { + SourceComment() (SyntheticComment, bool) +} + +// Source is the golang file that an entity is sourced from. +type Source struct { + File string + Position token.Position +} + +// SourceComment returns a synthetic comment indicating the source file. +// If the source file is empty, the second return is false. +func (s Source) SourceComment() (SyntheticComment, bool) { + return SyntheticComment{ + Leading: true, + SingleLine: true, + Text: fmt.Sprintf("From %s", s.File), + TrailingNewLine: false, + }, s.File != "" +} diff --git a/bindings/declarations.go b/bindings/declarations.go index 4fb74eb..2fea9c8 100644 --- a/bindings/declarations.go +++ b/bindings/declarations.go @@ -13,8 +13,7 @@ type Interface struct { Fields []*PropertySignature Parameters []*TypeParameter Heritage []*HeritageClause - // Comments maybe should be its own AST node? - Comments []string + SupportComments Source } @@ -28,8 +27,7 @@ type PropertySignature struct { Modifiers []Modifier QuestionToken bool Type ExpressionType - // FieldComments maybe should be its own AST node? - FieldComments []string + SupportComments } func (*PropertySignature) isNode() {} @@ -39,6 +37,7 @@ type Alias struct { Modifiers []Modifier Type ExpressionType Parameters []*TypeParameter + SupportComments Source } diff --git a/config/mutations.go b/config/mutations.go index 5d3f7a5..7035516 100644 --- a/config/mutations.go +++ b/config/mutations.go @@ -230,14 +230,14 @@ func (r *anyLintIgnore) Visit(node bindings.Node) (w walk.Visitor) { } } if anyParam { - node.Comments = append(node.Comments, "biome-ignore lint lint/complexity/noUselessTypeConstraint: golang does 'any' for generics, typescript does not like it") + node.LeadingComment("biome-ignore lint lint/complexity/noUselessTypeConstraint: golang does 'any' for generics, typescript does not like it") } for _, field := range node.Fields { h := &hasAnyVisitor{} walk.Walk(h, field.Type) if h.hasAnyValue { - field.FieldComments = append(field.FieldComments, "biome-ignore lint lint/complexity/noUselessTypeConstraint: ignore linter") + node.LeadingComment("biome-ignore lint lint/complexity/noUselessTypeConstraint: ignore linter") } } diff --git a/convert.go b/convert.go index c4ac288..0918d8e 100644 --- a/convert.go +++ b/convert.go @@ -775,7 +775,11 @@ func (ts *Typescript) buildStruct(obj types.Object, st *types.Struct) (*bindings } tsField.Type = tsType.Value tsi.Parameters = append(tsi.Parameters, tsType.TypeParameters...) - tsField.FieldComments = tsType.RaisedComments + // TODO: Better handle comments. The raised comments should probably be set to + // empty after consumed? + for _, c := range tsType.RaisedComments { + tsField.LeadingComment(c) + } // Some tag support // TODO: Add more tag support?