From 25e72c040e177e63218a64740257ca3c24558ca7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 10:28:39 -0500 Subject: [PATCH 1/7] feat: preserve field & type comments --- bindings/bindings.go | 2 +- bindings/comments.go | 35 ++++++++++++++++++--- comments.go | 59 +++++++++++++++++++++++++++++++++++ convert.go | 36 +++++++++++++++------ convert_test.go | 3 ++ testdata/comments/comments.go | 28 +++++++++++++++++ testdata/comments/comments.ts | 41 ++++++++++++++++++++++++ 7 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 comments.go create mode 100644 testdata/comments/comments.go create mode 100644 testdata/comments/comments.ts diff --git a/bindings/bindings.go b/bindings/bindings.go index 5768208..ede3a01 100644 --- a/bindings/bindings.go +++ b/bindings/bindings.go @@ -739,7 +739,7 @@ func (b *Bindings) CommentGojaObject(comments []SyntheticComment, object *goja.O node, b.vm.ToValue(c.Leading), b.vm.ToValue(c.SingleLine), - b.vm.ToValue(" "+c.Text), + b.vm.ToValue(c.Text), b.vm.ToValue(c.TrailingNewLine), ) if err != nil { diff --git a/bindings/comments.go b/bindings/comments.go index 8aa349f..9442b7d 100644 --- a/bindings/comments.go +++ b/bindings/comments.go @@ -2,7 +2,9 @@ package bindings import ( "fmt" + "go/ast" "go/token" + "strings" ) // Commentable indicates if the AST node supports adding comments. @@ -25,12 +27,37 @@ type SupportComments struct { comments []SyntheticComment } -// LeadingComment is a helper function for the most common type of comment. -func (s *SupportComments) LeadingComment(text string) { +func (s *SupportComments) ASTCommentGroup(grp *ast.CommentGroup) { + if grp == nil { + return + } + for _, cmt := range grp.List { + s.ASTComment(cmt) + } +} + +func (s *SupportComments) ASTComment(cmt *ast.Comment) { + // TODO: Is there a better way to get just the text of the comment? + text := cmt.Text + text = strings.TrimPrefix(text, "//") + text = strings.TrimPrefix(text, "/*") + text = strings.TrimSuffix(text, "*/") + s.Comment(SyntheticComment{ Leading: true, - SingleLine: true, + SingleLine: !strings.Contains(cmt.Text, "\n"), Text: text, + TrailingNewLine: true, + }) +} + +// 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, + // All go comments are `// ` prefixed, so add a space. + Text: " " + text, TrailingNewLine: false, }) } @@ -59,7 +86,7 @@ func (s Source) SourceComment() (SyntheticComment, bool) { return SyntheticComment{ Leading: true, SingleLine: true, - Text: fmt.Sprintf("From %s", s.File), + Text: fmt.Sprintf(" From %s", s.File), TrailingNewLine: false, }, s.File != "" } diff --git a/comments.go b/comments.go new file mode 100644 index 0000000..09c18d1 --- /dev/null +++ b/comments.go @@ -0,0 +1,59 @@ +package guts + +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/packages" +) + +func (p *GoParser) CommentForObject(obj types.Object) *ast.CommentGroup { + for _, pkg := range p.Pkgs { + if obj.Pkg() != nil && pkg.PkgPath == obj.Pkg().Path() { + return CommentForObject(obj, pkg) + } + } + return &ast.CommentGroup{List: []*ast.Comment{}} +} + +// CommentForObject returns the *ast.CommentGroup associated with the object's declaration. +// It looks up the syntax node that defines the object, and returns its Doc comment (if any). +func CommentForObject(obj types.Object, pkg *packages.Package) *ast.CommentGroup { + if obj == nil || pkg == nil { + return &ast.CommentGroup{List: []*ast.Comment{}} + } + + // obj.Pos() gives us the token.Pos of the declaration + pos := obj.Pos() + for _, f := range pkg.Syntax { + // File covers the object position? + if f.Pos() <= pos && pos <= f.End() { + // Walk the file to find the node at that position + var found *ast.CommentGroup + ast.Inspect(f, func(n ast.Node) bool { + if n == nil { + return false + } + if n.Pos() <= pos && pos <= n.End() { + switch decl := n.(type) { + case *ast.FuncDecl: + if decl.Name != nil && decl.Name.Pos() == pos { + found = decl.Doc + return false + } + case *ast.GenDecl: + for _, spec := range decl.Specs { + if spec.Pos() <= pos && pos <= spec.End() { + found = decl.Doc + return false + } + } + } + } + return true + }) + return found + } + } + return &ast.CommentGroup{List: []*ast.Comment{}} +} diff --git a/convert.go b/convert.go index 0918d8e..c15fe58 100644 --- a/convert.go +++ b/convert.go @@ -39,9 +39,10 @@ type GoParser struct { // This needs to be a producer function, as the AST is mutated directly, // and we cannot have shared references. // Eg: "time.Time" -> "string" - typeOverrides map[string]TypeOverride - config *packages.Config - fileSet *token.FileSet + typeOverrides map[string]TypeOverride + config *packages.Config + fileSet *token.FileSet + preserveComments bool } // NewGolangParser returns a new GoParser object. @@ -80,6 +81,11 @@ func NewGolangParser() (*GoParser, error) { }, nil } +func (p *GoParser) PreserveComments() *GoParser { + p.preserveComments = true + return p +} + // IncludeCustomDeclaration is an advanced form of IncludeCustom. func (p *GoParser) IncludeCustomDeclaration(mappings map[string]TypeOverride) { for k, v := range mappings { @@ -115,6 +121,7 @@ func (p *GoParser) IncludeCustom(mappings map[GolangType]GolangType) error { return exp } } + return nil } @@ -174,9 +181,10 @@ func (p *GoParser) include(directory string, prefix string, reference bool) erro // The returned typescript object can be mutated before serializing. func (p *GoParser) ToTypescript() (*Typescript, error) { typescript := &Typescript{ - typescriptNodes: make(map[string]*typescriptNode), - parsed: p, - skip: p.Skips, + typescriptNodes: make(map[string]*typescriptNode), + parsed: p, + skip: p.Skips, + preserveComments: p.preserveComments, } // Parse all go types to the typescript AST @@ -209,9 +217,10 @@ type Typescript struct { // parsed go code. All names should be unique. If non-unique names exist, that // means packages contain the same named types. // TODO: the key "string" should be replaced with "Identifier" - typescriptNodes map[string]*typescriptNode - parsed *GoParser - skip map[string]struct{} + typescriptNodes map[string]*typescriptNode + parsed *GoParser + skip map[string]struct{} + preserveComments bool // Do not allow calling serialize more than once. // The call affects the state. serialized bool @@ -437,6 +446,11 @@ func (ts *Typescript) parse(obj types.Object) error { if err != nil { return xerrors.Errorf("generate %q: %w", objectIdentifier.Ref(), err) } + + if ts.preserveComments { + cmts := ts.parsed.CommentForObject(obj) + node.ASTCommentGroup(cmts) + } return ts.setNode(objectIdentifier.Ref(), typescriptNode{ Node: node, }) @@ -791,6 +805,10 @@ func (ts *Typescript) buildStruct(obj types.Object, st *types.Struct) (*bindings } } + if ts.preserveComments { + cmts := ts.parsed.CommentForObject(field) + tsField.ASTCommentGroup(cmts) + } tsi.Fields = append(tsi.Fields, tsField) } diff --git a/convert_test.go b/convert_test.go index 57f56a2..c6f4d42 100644 --- a/convert_test.go +++ b/convert_test.go @@ -76,6 +76,9 @@ func TestGeneration(t *testing.T) { gen, err := guts.NewGolangParser() require.NoError(t, err, "new convert") + // PreserveComments will attach golang comments to the typescript nodes. + gen.PreserveComments() + dir := filepath.Join(".", "testdata", f.Name()) err = gen.IncludeGenerate("./" + dir) require.NoErrorf(t, err, "include %q", dir) diff --git a/testdata/comments/comments.go b/testdata/comments/comments.go new file mode 100644 index 0000000..688a510 --- /dev/null +++ b/testdata/comments/comments.go @@ -0,0 +1,28 @@ +package comments + +// CommentedStructure is a struct with a comment. +// +// It actually has 2 comments?! +// TODO: Maybe add a third comment! +type CommentedStructure struct { + Inline string // Field comment + + // Leading comment + Leading string + + Trailing string + // Trailing comment + + // Leading comment + All string // Field comment + // Trailing comment + + /* Another leading comment */ + Block string +} + +/* + BlockComments are not idiomatic in Go, but can be used +*/ +type BlockComment struct { +} diff --git a/testdata/comments/comments.ts b/testdata/comments/comments.ts new file mode 100644 index 0000000..32d4efb --- /dev/null +++ b/testdata/comments/comments.ts @@ -0,0 +1,41 @@ +// Code generated by 'guts'. DO NOT EDIT. + +// From comments/comments.go +/* + BlockComments are not idiomatic in Go, but can be used +*/ +export interface BlockComment { +} + +// From comments/comments.go +// CommentedStructure is a struct with a comment. +// +// It actually has 2 comments?! +// TODO: Maybe add a third comment! +export interface CommentedStructure { + // CommentedStructure is a struct with a comment. + // + // It actually has 2 comments?! + // TODO: Maybe add a third comment! + readonly Inline: string; + // CommentedStructure is a struct with a comment. + // + // It actually has 2 comments?! + // TODO: Maybe add a third comment! + readonly Leading: string; + // CommentedStructure is a struct with a comment. + // + // It actually has 2 comments?! + // TODO: Maybe add a third comment! + readonly Trailing: string; + // CommentedStructure is a struct with a comment. + // + // It actually has 2 comments?! + // TODO: Maybe add a third comment! + readonly All: string; + // CommentedStructure is a struct with a comment. + // + // It actually has 2 comments?! + // TODO: Maybe add a third comment! + readonly Block: string; +} From ca0f56f7d49607ebee8fb8e0bfd7a06dd663f9a6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 10:48:17 -0500 Subject: [PATCH 2/7] refactor --- comments.go | 165 ++++++++++++++++++++++++++++------ testdata/comments/comments.go | 5 +- testdata/comments/comments.ts | 27 ++---- 3 files changed, 150 insertions(+), 47 deletions(-) diff --git a/comments.go b/comments.go index 09c18d1..f96738c 100644 --- a/comments.go +++ b/comments.go @@ -2,58 +2,171 @@ package guts import ( "go/ast" + "go/token" "go/types" + "strings" "golang.org/x/tools/go/packages" + + "github.com/coder/guts/bindings" ) -func (p *GoParser) CommentForObject(obj types.Object) *ast.CommentGroup { +func (p *GoParser) CommentForObject(obj types.Object) []bindings.SyntheticComment { for _, pkg := range p.Pkgs { if obj.Pkg() != nil && pkg.PkgPath == obj.Pkg().Path() { return CommentForObject(obj, pkg) } } - return &ast.CommentGroup{List: []*ast.Comment{}} + return []bindings.SyntheticComment{} } -// CommentForObject returns the *ast.CommentGroup associated with the object's declaration. -// It looks up the syntax node that defines the object, and returns its Doc comment (if any). -func CommentForObject(obj types.Object, pkg *packages.Package) *ast.CommentGroup { +// CommentForObject returns the comment group associated with the object's declaration. +// For functions/methods it returns FuncDecl.Doc. +// For const/var/type it prefers Spec.Doc, else GenDecl.Doc. +// For struct/interface members it returns Field.Doc, else Field.Comment (trailing). +func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.SyntheticComment { if obj == nil || pkg == nil { - return &ast.CommentGroup{List: []*ast.Comment{}} + return nil } - - // obj.Pos() gives us the token.Pos of the declaration pos := obj.Pos() + for _, f := range pkg.Syntax { - // File covers the object position? - if f.Pos() <= pos && pos <= f.End() { - // Walk the file to find the node at that position - var found *ast.CommentGroup - ast.Inspect(f, func(n ast.Node) bool { - if n == nil { + if !covers(f, pos) { + continue + } + + var found []*ast.CommentGroup + ast.Inspect(f, func(n ast.Node) bool { + // The decl nodes "cover" the types they comment on. So we can check quickly if + // the node is relevant. + if n == nil || !covers(n, pos) { + return false + } + + switch nd := n.(type) { + case *ast.FuncDecl: + // Match function/method name token exactly. + if nd.Name != nil && nd.Name.Pos() == pos { + found = nd.Doc return false } - if n.Pos() <= pos && pos <= n.End() { - switch decl := n.(type) { - case *ast.FuncDecl: - if decl.Name != nil && decl.Name.Pos() == pos { - found = decl.Doc + + case *ast.GenDecl: + // Walk specs to prefer per-spec docs over decl docs. + for _, sp := range nd.Specs { + if !covers(sp, pos) { + continue + } + switch spec := sp.(type) { + case *ast.ValueSpec: + // const/var + for _, name := range spec.Names { + if name.Pos() == pos { + if spec.Doc != nil { + found = spec.Doc + } else { + found = nd.Doc + } + return false + } + } + + case *ast.TypeSpec: + // type declarations (struct/interface/alias) + if spec.Name != nil && spec.Name.Pos() == pos { + // comment on the type itself + if spec.Doc != nil { + found = spec.Doc + } else { + found = nd.Doc + } return false } - case *ast.GenDecl: - for _, spec := range decl.Specs { - if spec.Pos() <= pos && pos <= spec.End() { - found = decl.Doc + + // dive into members for struct/interface fields + switch t := spec.Type.(type) { + case *ast.StructType: + if cg := commentForFieldList(t.Fields, pos); cg != nil { + found = cg + return false + } + case *ast.InterfaceType: + if cg := commentForFieldList(t.Methods, pos); cg != nil { + found = cg return false } } } } + + // If we saw the decl but not a more specific match, keep walking. return true - }) - return found + } + + // Keep drilling down until we either match or run out. + return true + }) + + if len(found) > 0 { + return found[0] } + return nil } - return &ast.CommentGroup{List: []*ast.Comment{}} + + return nil +} + +func commentForFieldList(fl *ast.FieldList, pos token.Pos) []bindings.SyntheticComment { + if fl == nil { + return nil + } + cmts := []bindings.SyntheticComment{} + for _, fld := range fl.List { + if !covers(fld, pos) { + continue + } + // Named field or interface method: match any of the Names. + if len(fld.Names) > 0 { + for _, nm := range fld.Names { + if nm.Pos() == pos { + cmts = append(cmts, syntheticComments(true, fld.Doc)...) + cmts = append(cmts, syntheticComments(true, fld.Doc)...) + return cmts + } + } + } else { + // Embedded field (anonymous): no Names; match on the Type span. + if covers(fld.Type, pos) { + cmts = append(cmts, syntheticComments(true, fld.Doc)...) + cmts = append(cmts, syntheticComments(true, fld.Doc)...) + return cmts + } + } + } + return nil +} + +func covers(n ast.Node, p token.Pos) bool { + return n != nil && n.Pos() <= p && p <= n.End() +} + +func syntheticComments(leading bool, grp *ast.CommentGroup) []bindings.SyntheticComment { + cmts := []bindings.SyntheticComment{} + for _, c := range grp.List { + cmts = append(cmts, bindings.SyntheticComment{ + Leading: leading, + SingleLine: !strings.Contains(c.Text, "\n"), + Text: normalizeCommentText(c.Text), + TrailingNewLine: true, + }) + } + return cmts +} + +func normalizeCommentText(text string) string { + // TODO: Is there a better way to get just the text of the comment? + text = strings.TrimPrefix(text, "//") + text = strings.TrimPrefix(text, "/*") + text = strings.TrimSuffix(text, "*/") + return text } diff --git a/testdata/comments/comments.go b/testdata/comments/comments.go index 688a510..9622475 100644 --- a/testdata/comments/comments.go +++ b/testdata/comments/comments.go @@ -14,7 +14,7 @@ type CommentedStructure struct { // Trailing comment // Leading comment - All string // Field comment + All string // Inline comment // Trailing comment /* Another leading comment */ @@ -26,3 +26,6 @@ type CommentedStructure struct { */ type BlockComment struct { } + +// Constant is just a value +const Constant = "value" // Constant comment diff --git a/testdata/comments/comments.ts b/testdata/comments/comments.ts index 32d4efb..8606b8f 100644 --- a/testdata/comments/comments.ts +++ b/testdata/comments/comments.ts @@ -13,29 +13,16 @@ export interface BlockComment { // It actually has 2 comments?! // TODO: Maybe add a third comment! export interface CommentedStructure { - // CommentedStructure is a struct with a comment. - // - // It actually has 2 comments?! - // TODO: Maybe add a third comment! + // Field comment readonly Inline: string; - // CommentedStructure is a struct with a comment. - // - // It actually has 2 comments?! - // TODO: Maybe add a third comment! + // Leading comment readonly Leading: string; - // CommentedStructure is a struct with a comment. - // - // It actually has 2 comments?! - // TODO: Maybe add a third comment! readonly Trailing: string; - // CommentedStructure is a struct with a comment. - // - // It actually has 2 comments?! - // TODO: Maybe add a third comment! + // Leading comment readonly All: string; - // CommentedStructure is a struct with a comment. - // - // It actually has 2 comments?! - // TODO: Maybe add a third comment! + // Another leading comment readonly Block: string; } + +// From comments/comments.go +export const Constant = "value"; From 30b872972a283ed01d8f5279b3d244bb70640c1a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 10:53:37 -0500 Subject: [PATCH 3/7] chore: update all tests with preserved comments --- bindings/comments.go | 36 ++++++----------------------- comments.go | 24 +++++++++---------- convert.go | 4 ++-- testdata/comments/comments.ts | 5 ++-- testdata/generics/generics.ts | 2 ++ testdata/inheritance/inheritance.ts | 4 +++- testdata/nonids/nonids.ts | 1 + testdata/union/union.ts | 1 + 8 files changed, 30 insertions(+), 47 deletions(-) diff --git a/bindings/comments.go b/bindings/comments.go index 9442b7d..599c13f 100644 --- a/bindings/comments.go +++ b/bindings/comments.go @@ -2,15 +2,13 @@ package bindings import ( "fmt" - "go/ast" "go/token" - "strings" ) // 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) + AppendComment(comment SyntheticComment) Comments() []SyntheticComment } @@ -27,33 +25,9 @@ type SupportComments struct { comments []SyntheticComment } -func (s *SupportComments) ASTCommentGroup(grp *ast.CommentGroup) { - if grp == nil { - return - } - for _, cmt := range grp.List { - s.ASTComment(cmt) - } -} - -func (s *SupportComments) ASTComment(cmt *ast.Comment) { - // TODO: Is there a better way to get just the text of the comment? - text := cmt.Text - text = strings.TrimPrefix(text, "//") - text = strings.TrimPrefix(text, "/*") - text = strings.TrimSuffix(text, "*/") - - s.Comment(SyntheticComment{ - Leading: true, - SingleLine: !strings.Contains(cmt.Text, "\n"), - Text: text, - TrailingNewLine: true, - }) -} - // LeadingComment is a helper function for the most common type of comment. func (s *SupportComments) LeadingComment(text string) { - s.Comment(SyntheticComment{ + s.AppendComment(SyntheticComment{ Leading: true, SingleLine: true, // All go comments are `// ` prefixed, so add a space. @@ -62,7 +36,11 @@ func (s *SupportComments) LeadingComment(text string) { }) } -func (s *SupportComments) Comment(comment SyntheticComment) { +func (s *SupportComments) AppendComments(comments []SyntheticComment) { + s.comments = append(s.comments, comments...) +} + +func (s *SupportComments) AppendComment(comment SyntheticComment) { s.comments = append(s.comments, comment) } diff --git a/comments.go b/comments.go index f96738c..92153b3 100644 --- a/comments.go +++ b/comments.go @@ -35,7 +35,7 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe continue } - var found []*ast.CommentGroup + var found []bindings.SyntheticComment ast.Inspect(f, func(n ast.Node) bool { // The decl nodes "cover" the types they comment on. So we can check quickly if // the node is relevant. @@ -47,7 +47,7 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe case *ast.FuncDecl: // Match function/method name token exactly. if nd.Name != nil && nd.Name.Pos() == pos { - found = nd.Doc + found = syntheticComments(true, nd.Doc) return false } @@ -63,9 +63,9 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe for _, name := range spec.Names { if name.Pos() == pos { if spec.Doc != nil { - found = spec.Doc + found = syntheticComments(true, nd.Doc) } else { - found = nd.Doc + found = syntheticComments(true, spec.Doc) } return false } @@ -76,9 +76,9 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe if spec.Name != nil && spec.Name.Pos() == pos { // comment on the type itself if spec.Doc != nil { - found = spec.Doc + found = syntheticComments(true, spec.Doc) } else { - found = nd.Doc + found = syntheticComments(true, nd.Doc) } return false } @@ -107,10 +107,7 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe return true }) - if len(found) > 0 { - return found[0] - } - return nil + return found } return nil @@ -130,7 +127,7 @@ func commentForFieldList(fl *ast.FieldList, pos token.Pos) []bindings.SyntheticC for _, nm := range fld.Names { if nm.Pos() == pos { cmts = append(cmts, syntheticComments(true, fld.Doc)...) - cmts = append(cmts, syntheticComments(true, fld.Doc)...) + cmts = append(cmts, syntheticComments(false, fld.Comment)...) return cmts } } @@ -138,7 +135,7 @@ func commentForFieldList(fl *ast.FieldList, pos token.Pos) []bindings.SyntheticC // Embedded field (anonymous): no Names; match on the Type span. if covers(fld.Type, pos) { cmts = append(cmts, syntheticComments(true, fld.Doc)...) - cmts = append(cmts, syntheticComments(true, fld.Doc)...) + cmts = append(cmts, syntheticComments(false, fld.Comment)...) return cmts } } @@ -152,6 +149,9 @@ func covers(n ast.Node, p token.Pos) bool { func syntheticComments(leading bool, grp *ast.CommentGroup) []bindings.SyntheticComment { cmts := []bindings.SyntheticComment{} + if grp == nil { + return cmts + } for _, c := range grp.List { cmts = append(cmts, bindings.SyntheticComment{ Leading: leading, diff --git a/convert.go b/convert.go index c15fe58..f9b52a1 100644 --- a/convert.go +++ b/convert.go @@ -449,7 +449,7 @@ func (ts *Typescript) parse(obj types.Object) error { if ts.preserveComments { cmts := ts.parsed.CommentForObject(obj) - node.ASTCommentGroup(cmts) + node.AppendComments(cmts) } return ts.setNode(objectIdentifier.Ref(), typescriptNode{ Node: node, @@ -807,7 +807,7 @@ func (ts *Typescript) buildStruct(obj types.Object, st *types.Struct) (*bindings if ts.preserveComments { cmts := ts.parsed.CommentForObject(field) - tsField.ASTCommentGroup(cmts) + tsField.AppendComments(cmts) } tsi.Fields = append(tsi.Fields, tsField) } diff --git a/testdata/comments/comments.ts b/testdata/comments/comments.ts index 8606b8f..aed850a 100644 --- a/testdata/comments/comments.ts +++ b/testdata/comments/comments.ts @@ -13,13 +13,12 @@ export interface BlockComment { // It actually has 2 comments?! // TODO: Maybe add a third comment! export interface CommentedStructure { - // Field comment - readonly Inline: string; + readonly Inline: string; // Field comment // Leading comment readonly Leading: string; readonly Trailing: string; // Leading comment - readonly All: string; + readonly All: string; // Inline comment // Another leading comment readonly Block: string; } diff --git a/testdata/generics/generics.ts b/testdata/generics/generics.ts index cdd7879..9c9dbc3 100644 --- a/testdata/generics/generics.ts +++ b/testdata/generics/generics.ts @@ -25,6 +25,7 @@ export interface Complex { readonly dynamic: Fields; readonly comparable: boolean | null; @@ -48,6 +49,7 @@ export interface FieldsDiffOrder; } diff --git a/testdata/inheritance/inheritance.ts b/testdata/inheritance/inheritance.ts index c8d623a..ccfd3f6 100644 --- a/testdata/inheritance/inheritance.ts +++ b/testdata/inheritance/inheritance.ts @@ -21,8 +21,10 @@ export interface FooBarPtr extends Bar, GenBar { } // From codersdk/inheritance.go +// FooBuzz has a json tag for the embedded +// See: https://go.dev/play/p/-p6QYmY8mtR export interface FooBuzz { - readonly foo: Buzz; + readonly foo: Buzz; // Json tag changes the inheritance readonly bazz: string; } diff --git a/testdata/nonids/nonids.ts b/testdata/nonids/nonids.ts index 62649c2..795759b 100644 --- a/testdata/nonids/nonids.ts +++ b/testdata/nonids/nonids.ts @@ -2,6 +2,7 @@ // From nonids/nonids.go export interface Foo { + // Hyphen is an odd case, but this field is not ignored readonly "-": string; readonly "hyphenated-string": string; readonly "1numbered": number; diff --git a/testdata/union/union.ts b/testdata/union/union.ts index 15f2eb1..326de9c 100644 --- a/testdata/union/union.ts +++ b/testdata/union/union.ts @@ -1,6 +1,7 @@ // Code generated by 'guts'. DO NOT EDIT. // From union/union.go +// Repeated constraints are redundant export interface Repeated { readonly Value: T; } From 74ec75e99149e7a35dd5b09bf416f179e4713ce0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 11:00:19 -0500 Subject: [PATCH 4/7] add more test data --- convert.go | 3 +++ testdata/comments/comments.go | 13 +++++++++++++ testdata/comments/comments.ts | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/convert.go b/convert.go index f9b52a1..1cc380f 100644 --- a/convert.go +++ b/convert.go @@ -81,6 +81,9 @@ func NewGolangParser() (*GoParser, error) { }, nil } +// PreserveComments will attempt to preserve any comments associated with +// the golang types. This feature is still a work in progress, and may not +// preserve all comments or match all expectations. func (p *GoParser) PreserveComments() *GoParser { p.preserveComments = true return p diff --git a/testdata/comments/comments.go b/testdata/comments/comments.go index 9622475..8bab61a 100644 --- a/testdata/comments/comments.go +++ b/testdata/comments/comments.go @@ -19,6 +19,18 @@ type CommentedStructure struct { /* Another leading comment */ Block string + + /* Multi + Line + Comment + */ + MultiLine string +} + +type InheritedCommentedStructure struct { + // CommentedStructure is a commented field, but in typescript it has no place + // since it is covered by an "extend" clause. Not sure where to put it. + CommentedStructure } /* @@ -28,4 +40,5 @@ type BlockComment struct { } // Constant is just a value +// Constants are not yet supported for comments const Constant = "value" // Constant comment diff --git a/testdata/comments/comments.ts b/testdata/comments/comments.ts index aed850a..53707fb 100644 --- a/testdata/comments/comments.ts +++ b/testdata/comments/comments.ts @@ -21,7 +21,16 @@ export interface CommentedStructure { readonly All: string; // Inline comment // Another leading comment readonly Block: string; + /* Multi + Line + Comment + */ + readonly MultiLine: string; } // From comments/comments.go export const Constant = "value"; + +// From comments/comments.go +export interface InheritedCommentedStructure extends CommentedStructure { +} From ab2c41579f72d9002f7c1dd04bd4100554e144d7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 11:04:37 -0500 Subject: [PATCH 5/7] Add ai disclaimer --- comments.go | 1 + 1 file changed, 1 insertion(+) diff --git a/comments.go b/comments.go index 92153b3..f456bfc 100644 --- a/comments.go +++ b/comments.go @@ -24,6 +24,7 @@ func (p *GoParser) CommentForObject(obj types.Object) []bindings.SyntheticCommen // For functions/methods it returns FuncDecl.Doc. // For const/var/type it prefers Spec.Doc, else GenDecl.Doc. // For struct/interface members it returns Field.Doc, else Field.Comment (trailing). +// Disclaimer: A lot of this code was AI generated. Feel free to improve it! func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.SyntheticComment { if obj == nil || pkg == nil { return nil From 908566200fb2a5cdb877042d771b4fbfe1139578 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 11:15:59 -0500 Subject: [PATCH 6/7] cover some more type declarations --- bindings/declarations.go | 1 + comments.go | 20 ++++++++++---------- convert.go | 27 ++++++++++++++++++++------- testdata/comments/comments.go | 3 +-- testdata/comments/comments.ts | 4 +++- testdata/enums/enums.go | 5 ++++- testdata/enums/enums.ts | 1 + 7 files changed, 40 insertions(+), 21 deletions(-) diff --git a/bindings/declarations.go b/bindings/declarations.go index 2fea9c8..80cad6c 100644 --- a/bindings/declarations.go +++ b/bindings/declarations.go @@ -105,6 +105,7 @@ type VariableStatement struct { Modifiers []Modifier Declarations *VariableDeclarationList Source + SupportComments } func (*VariableStatement) isNode() {} diff --git a/comments.go b/comments.go index f456bfc..2be0819 100644 --- a/comments.go +++ b/comments.go @@ -58,16 +58,19 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe if !covers(sp, pos) { continue } + + // nd.Doc are the comments for the entire type/const/var block. + if nd.Doc != nil { + found = append(found, syntheticComments(true, nd.Doc)...) + } + switch spec := sp.(type) { case *ast.ValueSpec: // const/var for _, name := range spec.Names { if name.Pos() == pos { - if spec.Doc != nil { - found = syntheticComments(true, nd.Doc) - } else { - found = syntheticComments(true, spec.Doc) - } + found = append(found, syntheticComments(true, spec.Doc)...) + found = append(found, syntheticComments(false, spec.Comment)...) return false } } @@ -76,11 +79,8 @@ func CommentForObject(obj types.Object, pkg *packages.Package) []bindings.Synthe // type declarations (struct/interface/alias) if spec.Name != nil && spec.Name.Pos() == pos { // comment on the type itself - if spec.Doc != nil { - found = syntheticComments(true, spec.Doc) - } else { - found = syntheticComments(true, nd.Doc) - } + found = append(found, syntheticComments(true, spec.Doc)...) + found = append(found, syntheticComments(false, spec.Comment)...) return false } diff --git a/convert.go b/convert.go index 1cc380f..7c000f0 100644 --- a/convert.go +++ b/convert.go @@ -488,14 +488,21 @@ func (ts *Typescript) parse(obj types.Object) error { return xerrors.Errorf("(map) generate %q: %w", objectIdentifier.Ref(), err) } + aliasNode := &bindings.Alias{ + Name: objectIdentifier, + Modifiers: []bindings.Modifier{}, + Type: ty.Value, + Parameters: ty.TypeParameters, + Source: ts.location(obj), + } + + if ts.preserveComments { + cmts := ts.parsed.CommentForObject(obj) + aliasNode.AppendComments(cmts) + } + return ts.setNode(objectIdentifier.Ref(), typescriptNode{ - Node: &bindings.Alias{ - Name: objectIdentifier, - Modifiers: []bindings.Modifier{}, - Type: ty.Value, - Parameters: ty.TypeParameters, - Source: ts.location(obj), - }, + Node: aliasNode, }) case *types.Interface: // Interfaces are used as generics. Non-generic interfaces are @@ -576,6 +583,12 @@ func (ts *Typescript) parse(obj types.Object) error { if err != nil { return xerrors.Errorf("basic const %q: %w", objectIdentifier.Ref(), err) } + + if ts.preserveComments { + cmts := ts.parsed.CommentForObject(obj) + cnst.AppendComments(cmts) + } + return ts.setNode(objectIdentifier.Ref(), typescriptNode{ Node: cnst, }) diff --git a/testdata/comments/comments.go b/testdata/comments/comments.go index 8bab61a..1660dd7 100644 --- a/testdata/comments/comments.go +++ b/testdata/comments/comments.go @@ -40,5 +40,4 @@ type BlockComment struct { } // Constant is just a value -// Constants are not yet supported for comments -const Constant = "value" // Constant comment +const Constant = "value" // An inline note diff --git a/testdata/comments/comments.ts b/testdata/comments/comments.ts index 53707fb..0c7cf9c 100644 --- a/testdata/comments/comments.ts +++ b/testdata/comments/comments.ts @@ -29,7 +29,9 @@ export interface CommentedStructure { } // From comments/comments.go -export const Constant = "value"; +// Constant is just a value +export const Constant = "value"; // An inline note + // From comments/comments.go export interface InheritedCommentedStructure extends CommentedStructure { diff --git a/testdata/enums/enums.go b/testdata/enums/enums.go index 39cda22..c101673 100644 --- a/testdata/enums/enums.go +++ b/testdata/enums/enums.go @@ -3,7 +3,9 @@ package enums import "time" type ( - EnumString string + // EnumString is a string-based enum + EnumString string + // EnumSliceType is a slice of string-based enums EnumSliceType []EnumString EnumInt int @@ -12,6 +14,7 @@ type ( ) const ( + // EnumFoo is the "foo" value EnumFoo EnumString = "foo" EnumBar EnumString = "bar" EnumBaz EnumString = "baz" diff --git a/testdata/enums/enums.ts b/testdata/enums/enums.ts index 63a40e7..5d438c2 100644 --- a/testdata/enums/enums.ts +++ b/testdata/enums/enums.ts @@ -14,6 +14,7 @@ export enum EnumInt { } // From enums/enums.go +// EnumSliceType is a slice of string-based enums export type EnumSliceType = readonly EnumString[]; // From enums/enums.go From 578afc3e5c149fcbc2641bbce06dfb2f0dd28f7e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 16 Oct 2025 11:17:29 -0500 Subject: [PATCH 7/7] add preserve comments to readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0ed6b87..46f1bc1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,11 @@ See the [simple example](./example/simple) for a basic usage of the library. A l ```go // Step 1: Create a new Golang parser golang, _ := guts.NewGolangParser() + +// Optional: Preserve comments from the golang source code +// This feature is still experimental and may not work in all cases +golang.PreserveComments() + // Step 2: Configure the parser _ = golang.IncludeGenerate("github.com/coder/guts/example/simple") // Step 3: Convert the Golang to the typescript AST