Skip to content

Commit

Permalink
implement package parsing go sources and holding comments positions
Browse files Browse the repository at this point in the history
This package intended to replace field-tags directives along with
addition of ability to parse comment directives regardless of structure
definition location.
  • Loading branch information
xobotyi committed Nov 19, 2024
1 parent 8558d2e commit 8c344d3
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 0 deletions.
135 changes: 135 additions & 0 deletions internal/files/comments-cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package files

import (
"go/ast"
"go/parser"
"go/token"
"sync"
)

// CommentGroup is a "baked" representation of an AST's comment group that
// doesn't require further access to the [token.FileSet] it was taken from, as we
// won't have access to the original fset. Among other benefits, it allows
// reducing the number of locks within the accessed fset, as it is accessed only
// twice during conversion.
type CommentGroup struct {
Text []string
Start token.Position
End token.Position
}

func NewCommentGroup(fset *token.FileSet, cg *ast.CommentGroup) CommentGroup {
text := make([]string, 0, len(cg.List))
for _, comment := range cg.List {
text = append(text, comment.Text)
}

return CommentGroup{
Text: text,
Start: fset.PositionFor(cg.Pos(), true),
End: fset.PositionFor(cg.End(), true),
}
}

type CommentsCache struct {
mu sync.RWMutex
comments map[string][]CommentGroup
}

func NewCommentsCache() *CommentsCache {
return &CommentsCache{
mu: sync.RWMutex{},
comments: make(map[string][]CommentGroup, 64), //nolint:mnd
}
}

// ParseFile parses the provided file, including comments, and adds the parsed
// comments to the internal cache. If the file is already parsed and present
// in the cache, the function does nothing.
//
// The function returns an error if the file cannot be parsed.
func (c *CommentsCache) ParseFile(filename string) error {
c.mu.Lock()
defer c.mu.Unlock()

if c.comments[filename] != nil {
return nil
}

fset := token.NewFileSet()

file, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return err //nolint:wrapcheck
}

comments := make([]CommentGroup, len(file.Comments))
for i, cg := range file.Comments {
comments[i] = NewCommentGroup(fset, cg)
}

c.comments[filename] = comments

return nil
}

// AddFile adds file comments to the internal cache. If the filename already
// exists in the cache, the function does nothing.
func (c *CommentsCache) AddFile(fset *token.FileSet, file *ast.File) {
c.mu.Lock()
defer c.mu.Unlock()

// ToDo: add check if file belongs to provided fset.
filename := fset.PositionFor(file.Pos(), true).Filename

if c.comments[filename] != nil {
return
}

comments := make([]CommentGroup, len(file.Comments))
for i, cg := range file.Comments {
comments[i] = NewCommentGroup(fset, cg)
}

c.comments[filename] = comments
}

// Comments returns a list of all comments in the file. If the file is not parsed
// yet, a nil slice is returned.
func (c *CommentsCache) Comments(filename string) []CommentGroup {
c.mu.RLock()
defer c.mu.RUnlock()

return c.comments[filename]
}

// CommentsForPosition retrieves comment groups related to provided position.
// Comment group considered to be related in case it located on previous or same
// row.
func (c *CommentsCache) CommentsForPosition(p token.Position, exclude ...token.Position) []CommentGroup {
comments := c.Comments(p.Filename)
if comments == nil {
return nil
}

// we expect to get 0 to 2 comments (none, prev|same, prev & same)
result := make([]CommentGroup, 0, 2) //nolint:mnd

for _, cg := range comments {
if positionEndRelatesTo(cg.End, p) && !positionEndRelatesTo(cg.End, exclude...) {
result = append(result, cg)
}
}

return result
}

func positionEndRelatesTo(p token.Position, references ...token.Position) bool {
for _, rp := range references {
if rp.Line == p.Line || rp.Line-1 == p.Line {
return true
}
}

return false
}
165 changes: 165 additions & 0 deletions internal/files/comments-cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package files_test

import (
"go/parser"
"go/token"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"dev.gaijin.team/go/go-exhaustruct/v4/internal/files"
)

func TestCommentsCache(t *testing.T) {
t.Parallel()

filename := "./testdata/comment-source.go"

t.Run("ParseFile", func(t *testing.T) {
t.Parallel()

c := files.NewCommentsCache()

require.Error(t, c.ParseFile("./definitely/non/existent"))

require.NoError(t, c.ParseFile(filename))
// add second time to see what everything ok happen in case of consequent call
require.NoError(t, c.ParseFile(filename))

assert.Len(t, c.Comments(filename), 6)

testFileComments(t, c, filename)
})

t.Run("AddFile", func(t *testing.T) {
t.Parallel()

c := files.NewCommentsCache()

fset := token.NewFileSet()
file, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
require.NoError(t, err)

c.AddFile(fset, file)
// add second time to see what everything ok happen in case of consequent call
c.AddFile(fset, file)

require.NoError(t, c.ParseFile(filename))

assert.Len(t, c.Comments(filename), 6)

testFileComments(t, c, filename)
})
}

func testFileComments(t *testing.T, c *files.CommentsCache, filename string) {
t.Helper()

assert.Equal(t, []files.CommentGroup{
{
Text: []string{"// Test before structure name."},
Start: token.Position{
Filename: filename,
Line: 3,
Column: 1,
Offset: 18,
},
End: token.Position{
Filename: filename,
Line: 3,
Column: 31,
Offset: 48,
},
},
{
Text: []string{"// after structure name"},
Start: token.Position{
Filename: filename,
Line: 4,
Column: 20,
Offset: 68,
},
End: token.Position{
Filename: filename,
Line: 4,
Column: 43,
Offset: 91,
},
},
}, c.CommentsForPosition(token.Position{ //nolint:exhaustruct
Filename: filename,
Line: 4,
}))

assert.Equal(t, []files.CommentGroup{
{
Text: []string{"// after field declaration"},
Start: token.Position{
Filename: filename,
Line: 5,
Column: 13,
Offset: 104,
},
End: token.Position{
Filename: filename,
Line: 5,
Column: 39,
Offset: 130,
},
},
}, c.CommentsForPosition(token.Position{ //nolint:exhaustruct
Filename: filename,
Line: 5,
}, token.Position{ //nolint:exhaustruct
Filename: filename,
Line: 4,
}))

assert.Equal(t, []files.CommentGroup{
{
Text: []string{
"// before field declaration",
"// miltiline comment",
"//",
"// with empty lines",
},
Start: token.Position{
Filename: filename,
Line: 6,
Column: 2,
Offset: 132,
},
End: token.Position{
Filename: filename,
Line: 9,
Column: 21,
Offset: 206,
},
},
{
Text: []string{"// after field declaration [2]"},
Start: token.Position{
Filename: filename,
Line: 10,
Column: 13,
Offset: 219,
},
End: token.Position{
Filename: filename,
Line: 10,
Column: 43,
Offset: 249,
},
},
}, c.CommentsForPosition(token.Position{ //nolint:exhaustruct
Filename: filename,
Line: 10,
}, token.Position{ //nolint:exhaustruct
Filename: filename,
Line: 5,
}))

//nolint:exhaustruct
assert.Nil(t, c.CommentsForPosition(token.Position{Filename: "./definitely/non/existent"}))
}
12 changes: 12 additions & 0 deletions internal/files/testdata/comment-source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package testdata

// Test before structure name.
type Test struct { // after structure name
Foo string // after field declaration
// before field declaration
// miltiline comment
//
// with empty lines
Bar string // after field declaration [2]
Baz string // after field declaration [3]
}

0 comments on commit 8c344d3

Please sign in to comment.