diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58c0c49 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/jstemmer/gotags + +go 1.25.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/parser.go b/parser.go index 4037e12..2361918 100644 --- a/parser.go +++ b/parser.go @@ -362,6 +362,8 @@ func getType(node ast.Node, star bool) (paramType string) { paramType = "interface{}" case *ast.Ellipsis: paramType = fmt.Sprintf("...%s", getType(t.Elt, true)) + case *ast.IndexExpr: // for generics type receiver + paramType = getType(t.X, star) } return } diff --git a/parser_test.go b/parser_test.go index d4c9375..4ac8985 100644 --- a/parser_test.go +++ b/parser_test.go @@ -41,6 +41,7 @@ func (t TagSlice) Dump() { type F map[TagField]string var testCases = []struct { + label string filename string relative bool basepath string @@ -48,7 +49,7 @@ var testCases = []struct { withExtraSymbols bool tags []Tag }{ - {filename: "testdata/const.go", tags: []Tag{ + {label: "const", filename: "testdata/const.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("Constant", 3, "c", F{"access": "public", "type": "string"}), tag("OtherConst", 4, "c", F{"access": "public"}), @@ -57,7 +58,7 @@ var testCases = []struct { tag("C", 8, "c", F{"access": "public"}), tag("D", 9, "c", F{"access": "public"}), }}, - {filename: "testdata/const.go", withExtraSymbols: true, tags: []Tag{ + {label: "const with extra symbols", filename: "testdata/const.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("Constant", 3, "c", F{"access": "public", "type": "string"}), tag("OtherConst", 4, "c", F{"access": "public"}), @@ -72,7 +73,7 @@ var testCases = []struct { tag("Test.C", 8, "c", F{"access": "public"}), tag("Test.D", 9, "c", F{"access": "public"}), }}, - {filename: "testdata/func.go", tags: []Tag{ + {label: "func", filename: "testdata/func.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("Function1", 3, "f", F{"access": "public", "signature": "()", "type": "string"}), tag("function2", 6, "f", F{"access": "private", "signature": "(p1, p2 int, p3 *string)"}), @@ -81,8 +82,9 @@ var testCases = []struct { tag("function5", 15, "f", F{"access": "private", "signature": "()", "type": "string, string, error"}), tag("function6", 18, "f", F{"access": "private", "signature": "(v ...interface{})"}), tag("function7", 21, "f", F{"access": "private", "signature": "(s ...string)"}), + tag("function8", 24, "f", F{"access": "private", "signature": "(input T)"}), }}, - {filename: "testdata/func.go", withExtraSymbols: true, tags: []Tag{ + {label: "func with extra symbols", filename: "testdata/func.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("Test.Function1", 3, "f", F{"access": "public", "signature": "()", "type": "string"}), tag("Test.function2", 6, "f", F{"access": "private", "signature": "(p1, p2 int, p3 *string)"}), @@ -91,6 +93,7 @@ var testCases = []struct { tag("Test.function5", 15, "f", F{"access": "private", "signature": "()", "type": "string, string, error"}), tag("Test.function6", 18, "f", F{"access": "private", "signature": "(v ...interface{})"}), tag("Test.function7", 21, "f", F{"access": "private", "signature": "(s ...string)"}), + tag("Test.function8", 24, "f", F{"access": "private", "signature": "(input T)"}), tag("Function1", 3, "f", F{"access": "public", "signature": "()", "type": "string"}), tag("function2", 6, "f", F{"access": "private", "signature": "(p1, p2 int, p3 *string)"}), tag("function3", 9, "f", F{"access": "private", "signature": "()", "type": "bool"}), @@ -98,27 +101,28 @@ var testCases = []struct { tag("function5", 15, "f", F{"access": "private", "signature": "()", "type": "string, string, error"}), tag("function6", 18, "f", F{"access": "private", "signature": "(v ...interface{})"}), tag("function7", 21, "f", F{"access": "private", "signature": "(s ...string)"}), + tag("function8", 24, "f", F{"access": "private", "signature": "(input T)"}), }}, - {filename: "testdata/import.go", tags: []Tag{ + {label: "import", filename: "testdata/import.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("fmt", 3, "i", F{}), tag("go/ast", 6, "i", F{}), tag("go/parser", 7, "i", F{}), }}, - {filename: "testdata/import.go", withExtraSymbols: true, tags: []Tag{ + {label: "import with extra symbols", filename: "testdata/import.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("fmt", 3, "i", F{}), tag("go/ast", 6, "i", F{}), tag("go/parser", 7, "i", F{}), }}, - {filename: "testdata/interface.go", tags: []Tag{ + {label: "interface", filename: "testdata/interface.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("InterfaceMethod", 4, "m", F{"access": "public", "signature": "(int)", "ntype": "Interface", "type": "string"}), tag("OtherMethod", 5, "m", F{"access": "public", "signature": "()", "ntype": "Interface"}), tag("io.Reader", 6, "e", F{"access": "public", "ntype": "Interface"}), tag("Interface", 3, "n", F{"access": "public", "type": "interface"}), }}, - {filename: "testdata/interface.go", withExtraSymbols: true, tags: []Tag{ + {label: "interface with extra symbols", filename: "testdata/interface.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("InterfaceMethod", 4, "m", F{"access": "public", "signature": "(int)", "ntype": "Interface", "type": "string"}), tag("OtherMethod", 5, "m", F{"access": "public", "signature": "()", "ntype": "Interface"}), @@ -126,7 +130,7 @@ var testCases = []struct { tag("Interface", 3, "n", F{"access": "public", "type": "interface"}), tag("Test.Interface", 3, "n", F{"access": "public", "type": "interface"}), }}, - {filename: "testdata/struct.go", tags: []Tag{ + {label: "struct", filename: "testdata/struct.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("Field1", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), tag("Field2", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), @@ -146,8 +150,11 @@ var testCases = []struct { tag("Dial", 33, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, error"}), tag("Dial2", 39, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, *Struct2"}), tag("Dial3", 42, "f", F{"access": "public", "signature": "()", "type": "*Connection, *Connection"}), + tag("WithGenerics", 45, "t", F{"access": "public", "type": "struct"}), + tag("Field1", 46, "w", F{"access": "public", "ctype": "WithGenerics", "type": "T"}), + tag("Do", 49, "m", F{"access": "public", "ctype": "WithGenerics", "signature": "(input T)"}), }}, - {filename: "testdata/struct.go", withExtraSymbols: true, tags: []Tag{ + {label: "struct with extra symbols", filename: "testdata/struct.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("Field1", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), tag("Field2", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), @@ -183,8 +190,15 @@ var testCases = []struct { tag("Test.Dial2", 39, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, *Struct2"}), tag("Dial3", 42, "f", F{"access": "public", "signature": "()", "type": "*Connection, *Connection"}), tag("Test.Dial3", 42, "f", F{"access": "public", "signature": "()", "type": "*Connection, *Connection"}), + tag("WithGenerics", 45, "t", F{"access": "public", "type": "struct"}), + tag("Test.WithGenerics", 45, "t", F{"access": "public", "type": "struct"}), + tag("Field1", 46, "w", F{"access": "public", "ctype": "WithGenerics", "type": "T"}), + tag("Do", 49, "m", F{"access": "public", "ctype": "WithGenerics", "signature": "(input T)"}), + tag("WithGenerics.Do", 49, "m", F{"access": "public", "ctype": "WithGenerics", "signature": "(input T)"}), + tag("Test.WithGenerics.Do", 49, "m", F{"access": "public", "ctype": "WithGenerics", "signature": "(input T)"}), + tag("Test.Do", 49, "m", F{"access": "public", "ctype": "WithGenerics", "signature": "(input T)"}), }}, - {filename: "testdata/type.go", tags: []Tag{ + {label: "type", filename: "testdata/type.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("testType", 3, "t", F{"access": "private", "type": "int"}), tag("testArrayType", 4, "t", F{"access": "private", "type": "[4]int"}), @@ -195,7 +209,7 @@ var testCases = []struct { tag("testMapType", 9, "t", F{"access": "private", "type": "map[string]bool"}), tag("testChanType", 10, "t", F{"access": "private", "type": "chan bool"}), }}, - {filename: "testdata/type.go", withExtraSymbols: true, tags: []Tag{ + {label: "type with extra symbols", filename: "testdata/type.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("testType", 3, "t", F{"access": "private", "type": "int"}), tag("testArrayType", 4, "t", F{"access": "private", "type": "[4]int"}), @@ -214,7 +228,7 @@ var testCases = []struct { tag("Test.testMapType", 9, "t", F{"access": "private", "type": "map[string]bool"}), tag("Test.testChanType", 10, "t", F{"access": "private", "type": "chan bool"}), }}, - {filename: "testdata/var.go", tags: []Tag{ + {label: "var", filename: "testdata/var.go", tags: []Tag{ tag("Test", 1, "p", F{}), tag("variable1", 3, "v", F{"access": "private", "type": "int"}), tag("variable2", 4, "v", F{"access": "private", "type": "string"}), @@ -223,7 +237,7 @@ var testCases = []struct { tag("C", 8, "v", F{"access": "public"}), tag("D", 9, "v", F{"access": "public"}), }}, - {filename: "testdata/var.go", withExtraSymbols: true, tags: []Tag{ + {label: "var with extra symbols", filename: "testdata/var.go", withExtraSymbols: true, tags: []Tag{ tag("Test", 1, "p", F{}), tag("variable1", 3, "v", F{"access": "private", "type": "int"}), tag("variable2", 4, "v", F{"access": "private", "type": "string"}), @@ -238,18 +252,18 @@ var testCases = []struct { tag("Test.C", 8, "v", F{"access": "public"}), tag("Test.D", 9, "v", F{"access": "public"}), }}, - {filename: "testdata/simple.go", relative: true, basepath: "dir", tags: []Tag{ + {label: "simple", filename: "testdata/simple.go", relative: true, basepath: "dir", tags: []Tag{ {Name: "main", File: "../testdata/simple.go", Address: "1", Type: "p", Fields: F{"line": "1"}}, }}, - {filename: "testdata/simple.go", withExtraSymbols: true, relative: true, basepath: "dir", tags: []Tag{ + {label: "simple with extra symbols", filename: "testdata/simple.go", withExtraSymbols: true, relative: true, basepath: "dir", tags: []Tag{ {Name: "main", File: "../testdata/simple.go", Address: "1", Type: "p", Fields: F{"line": "1"}}, }}, - {filename: "testdata/range.go", minversion: 4, tags: []Tag{ + {label: "range", filename: "testdata/range.go", minversion: 4, tags: []Tag{ tag("main", 1, "p", F{}), tag("fmt", 3, "i", F{}), tag("main", 5, "f", F{"access": "private", "signature": "()"}), }}, - {filename: "testdata/range.go", withExtraSymbols: true, minversion: 4, tags: []Tag{ + {label: "range with extra symbols", filename: "testdata/range.go", withExtraSymbols: true, minversion: 4, tags: []Tag{ tag("main", 1, "p", F{}), tag("fmt", 3, "i", F{}), tag("main", 5, "f", F{"access": "private", "signature": "()"}), @@ -259,44 +273,48 @@ var testCases = []struct { func TestParse(t *testing.T) { for _, testCase := range testCases { - if testCase.minversion > 0 && extractVersionCode(runtime.Version()) < testCase.minversion { - t.Skipf("[%s] skipping test. Version is %s, but test requires at least go1.%d", testCase.filename, runtime.Version(), testCase.minversion) - continue - } - - basepath, err := filepath.Abs(testCase.basepath) - if err != nil { - t.Errorf("[%s] could not determine base path: %s\n", testCase.filename, err) - continue - } + t.Run(testCase.label, func(t *testing.T) { + if testCase.minversion > 0 && extractVersionCode(runtime.Version()) < testCase.minversion { + t.Skipf("[%s] skipping test. Version is %s, but test requires at least go1.%d", testCase.filename, runtime.Version(), testCase.minversion) + } - var extra FieldSet - if testCase.withExtraSymbols { - extra = FieldSet{ExtraTags: true} - } + basepath, err := filepath.Abs(testCase.basepath) + if err != nil { + t.Fatalf("[%s] could not determine base path: %s\n", testCase.filename, err) + } - tags, err := Parse(testCase.filename, testCase.relative, basepath, extra) - if err != nil { - t.Errorf("[%s] Parse error: %s", testCase.filename, err) - continue - } + var extra FieldSet + if testCase.withExtraSymbols { + extra = FieldSet{ExtraTags: true} + } - sort.Sort(TagSlice(tags)) - sort.Sort(TagSlice(testCase.tags)) + tags, err := Parse(testCase.filename, testCase.relative, basepath, extra) + if err != nil { + t.Fatalf("[%s] Parse error: %s", testCase.filename, err) + } - if len(tags) != len(testCase.tags) { - t.Errorf("[%s] len(tags) == %d, want %d", testCase.filename, len(tags), len(testCase.tags)) - continue - } + sort.Sort(TagSlice(tags)) + sort.Sort(TagSlice(testCase.tags)) - for i, tag := range testCase.tags { - if len(tag.File) == 0 { - tag.File = testCase.filename + if len(tags) != len(testCase.tags) { + t.Fatalf( + "[%s] len(tags) == %d, want %d", + testCase.filename, + len(tags), + len(testCase.tags), + ) } - if tags[i].String() != tag.String() { - t.Errorf("[%s] tag(%d)\n is:%s\nwant:%s", testCase.filename, i, tags[i].String(), tag.String()) + + for i, tag := range testCase.tags { + if len(tag.File) == 0 { + tag.File = testCase.filename + } + if tags[i].String() != tag.String() { + t.Fatalf("[%s] tag(%d)\n is:%s\nwant:%s", testCase.filename, i, tags[i].String(), tag.String()) + } + } - } + }) } } diff --git a/testdata/func.go b/testdata/func.go index b378ebc..2f6f66b 100644 --- a/testdata/func.go +++ b/testdata/func.go @@ -20,3 +20,6 @@ func function6(v ...interface{}) { func function7(s ...string) { } + +func function8[T any](input T) { +} diff --git a/testdata/struct.go b/testdata/struct.go index f1d8d4d..7c7744f 100644 --- a/testdata/struct.go +++ b/testdata/struct.go @@ -41,3 +41,10 @@ func Dial2() (*Connection, *Struct2) { func Dial3() (a, b *Connection) { } + +type WithGenerics[T any] struct { + Field1 T +} + +func (w *WithGenerics[T]) Do(input T) { +}