-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement package parsing go sources and holding comments positions
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
Showing
3 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |