diff --git a/MODULE.bazel b/MODULE.bazel index abb44a08..8de034c5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -14,7 +14,7 @@ bazel_dep(name = "bazel_skylib", version = "1.8.1") bazel_dep(name = "gazelle", version = "0.42.0", repo_name = "bazel_gazelle") bazel_dep(name = "protobuf", version = PROTOBUF_VERSION, repo_name = "com_google_protobuf") bazel_dep(name = "rules_cc", version = "0.1.1") -bazel_dep(name = "rules_go", version = "0.52.0") +bazel_dep(name = "rules_go", version = "0.60.0") bazel_dep(name = "rules_java", version = "8.15.2") bazel_dep(name = "rules_jvm_external", version = "6.10") bazel_dep(name = "bazel_lib", version = "3.0.0") @@ -183,7 +183,7 @@ use_repo( ) go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") -go_sdk.download(version = "1.23.6") +go_sdk.download(version = "1.25.0") go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//:go.mod") @@ -195,6 +195,7 @@ use_repo( "com_github_google_go_cmp", "com_github_google_uuid", "com_github_hashicorp_golang_lru", + "com_github_odvcencio_gotreesitter", "com_github_rs_zerolog", "com_github_sergi_go_diff", "com_github_stretchr_testify", diff --git a/go.mod b/go.mod index fa52f36c..5c310d38 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,24 @@ module github.com/bazel-contrib/rules_jvm -go 1.22.9 - -toolchain go1.22.12 +go 1.25.0 require ( github.com/aristanetworks/goarista v0.0.0-20220328143245-64c8d3945829 github.com/bazelbuild/bazel-gazelle v0.42.0 github.com/bazelbuild/buildtools v0.0.0-20250204160707-ad48c76ab9b5 - github.com/bazelbuild/rules_go v0.52.0 + github.com/bazelbuild/rules_go v0.60.0 github.com/google/btree v1.1.3 - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru v1.0.2 + github.com/odvcencio/gotreesitter v0.18.0 github.com/rs/zerolog v1.33.0 github.com/sergi/go-diff v1.2.0 github.com/stretchr/testify v1.7.0 go.starlark.net v0.0.0-20250205221240-492d3672b3f4 - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d + golang.org/x/tools v0.34.0 golang.org/x/tools/go/vcs v0.1.0-deprecated - google.golang.org/grpc v1.70.0 + google.golang.org/grpc v1.71.0 ) require ( @@ -28,12 +27,12 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6c479f0b..13502c58 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/bazelbuild/bazel-gazelle v0.42.0 h1:BpkUzE3H2l6buJYFTKgzVMecJimQgWwYu github.com/bazelbuild/bazel-gazelle v0.42.0/go.mod h1:SRCc60YGZ27y+BqLzQ+nMh249+FyZz7YtX/V2ng+/z4= github.com/bazelbuild/buildtools v0.0.0-20250204160707-ad48c76ab9b5 h1:ls2GvHSkr+EXNZBBGVrPewx3nc54YBHCHd6v4BOlz9g= github.com/bazelbuild/buildtools v0.0.0-20250204160707-ad48c76ab9b5/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= -github.com/bazelbuild/rules_go v0.52.0 h1:+ozpngVAW67pCAwfhepaXSSrG3yHcj8K9hNAxSYBno4= -github.com/bazelbuild/rules_go v0.52.0/go.mod h1:M+YrupNArA7OiTlv++rFUgQ6Sm+ZXbQ5HPUj0cGa0us= +github.com/bazelbuild/rules_go v0.60.0 h1:apGSxTTrFUyLNvX9NQmF4CbntWAO0/S5eALeVgB/6Qk= +github.com/bazelbuild/rules_go v0.60.0/go.mod h1:CYcohJVxs4n7eftbC39GCqaEJm3E1EME+6QAkGguKoI= github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -21,8 +21,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= @@ -39,6 +39,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/odvcencio/gotreesitter v0.18.0 h1:hYiUp3lFXlB+YEplv1KxRFM8IKAv1e5QcJcC3nKIsVI= +github.com/odvcencio/gotreesitter v0.18.0/go.mod h1:MSmkQmznhGkdLcyQxiM813bi014e1Y1cpcDnm50meHs= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -51,45 +53,48 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.starlark.net v0.0.0-20250205221240-492d3672b3f4 h1:eBP+boBfJoGU3irqbxGTcTlKcbNwJCOdbmsnDq56nak= go.starlark.net v0.0.0-20250205221240-492d3672b3f4/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/java/gazelle/BUILD.bazel b/java/gazelle/BUILD.bazel index 631285d4..4132c5c4 100644 --- a/java/gazelle/BUILD.bazel +++ b/java/gazelle/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "constants.go", "generate.go", "lang.go", + "parser_router.go", "resolve.go", ], importpath = "github.com/bazel-contrib/rules_jvm/java/gazelle", @@ -26,8 +27,10 @@ go_library( "//java/gazelle/private/kotlin", "//java/gazelle/private/logconfig", "//java/gazelle/private/maven", + "//java/gazelle/private/parser", "//java/gazelle/private/sorted_multiset", "//java/gazelle/private/sorted_set", + "//java/gazelle/private/tsparser", "//java/gazelle/private/types", "@bazel_gazelle//config", "@bazel_gazelle//label", diff --git a/java/gazelle/configure.go b/java/gazelle/configure.go index 038b76d2..43ecc955 100644 --- a/java/gazelle/configure.go +++ b/java/gazelle/configure.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/bazel-contrib/rules_jvm/java/gazelle/javaconfig" - "github.com/bazel-contrib/rules_jvm/java/gazelle/private/javaparser" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/maven" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/types" "github.com/bazelbuild/bazel-gazelle/config" @@ -279,11 +278,7 @@ func (jc *Configurer) Configure(c *config.Config, rel string, f *rule.File) { } if jc.lang.parser == nil { - runner, err := javaparser.NewRunner(jc.lang.logger, c.RepoRoot, jc.lang.javaLogLevel) - if err != nil { - jc.lang.logger.Fatal().Err(err).Msg("could not start javaparser") - } - jc.lang.parser = runner + jc.lang.parser = newParserRouter(jc.lang.logger, c.RepoRoot, jc.lang.javaLogLevel) } if jc.lang.mavenResolver == nil { diff --git a/java/gazelle/generate.go b/java/gazelle/generate.go index cc251515..76ac7587 100644 --- a/java/gazelle/generate.go +++ b/java/gazelle/generate.go @@ -11,8 +11,8 @@ import ( "github.com/bazel-contrib/rules_jvm/java/gazelle/javaconfig" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java" - "github.com/bazel-contrib/rules_jvm/java/gazelle/private/javaparser" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/maven" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/sorted_set" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/types" "github.com/bazelbuild/bazel-gazelle/label" @@ -118,7 +118,7 @@ func (l javaLang) GenerateRules(args language.GenerateArgs) language.GenerateRes sort.Strings(srcFilenamesRelativeToPackage) var err error - javaPkg, err = l.parser.ParsePackage(context.Background(), &javaparser.ParsePackageRequest{ + javaPkg, err = l.parser.ParsePackage(context.Background(), &parser.ParsePackageRequest{ Rel: args.Rel, Files: srcFilenamesRelativeToPackage, }) diff --git a/java/gazelle/lang.go b/java/gazelle/lang.go index a9f2cf14..4022ee1a 100644 --- a/java/gazelle/lang.go +++ b/java/gazelle/lang.go @@ -7,9 +7,9 @@ import ( "github.com/bazel-contrib/rules_jvm/java/gazelle/javaconfig" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java_export_index" - "github.com/bazel-contrib/rules_jvm/java/gazelle/private/javaparser" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/logconfig" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/maven" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/sorted_multiset" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/types" "github.com/bazelbuild/bazel-gazelle/config" @@ -25,7 +25,7 @@ type javaLang struct { language.BaseLifecycleManager resolve.Resolver - parser *javaparser.Runner + parser parser.Parser logger zerolog.Logger javaLogLevel string mavenResolver maven.Resolver @@ -265,7 +265,7 @@ func (l javaLang) Fix(c *config.Config, f *rule.File) { func (l javaLang) DoneGeneratingRules() { if l.parser != nil { - l.parser.ServerManager().Shutdown() + shutdownParser(l.parser) } l.javaExportIndex.FinalizeIndex() } @@ -287,5 +287,13 @@ func (s shutdownServerOnFatalLogHook) Run(e *zerolog.Event, level zerolog.Level, if level != zerolog.FatalLevel { return } - s.l.parser.ServerManager().Shutdown() + shutdownParser(s.l.parser) +} + +func shutdownParser(p parser.Parser) { + shutdowner, ok := p.(interface{ Shutdown() }) + if !ok { + return + } + shutdowner.Shutdown() } diff --git a/java/gazelle/parser_router.go b/java/gazelle/parser_router.go new file mode 100644 index 00000000..e7624cd7 --- /dev/null +++ b/java/gazelle/parser_router.go @@ -0,0 +1,65 @@ +package gazelle + +import ( + "context" + "strings" + "sync" + + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/javaparser" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/tsparser" + "github.com/rs/zerolog" +) + +type parserRouter struct { + javaParser *tsparser.Runner + + logger zerolog.Logger + repoRoot string + javaLogLevel string + + mu sync.Mutex + javacParser parser.Parser + javacParserErr error +} + +func newParserRouter(logger zerolog.Logger, repoRoot, javaLogLevel string) parser.Parser { + return &parserRouter{ + javaParser: tsparser.NewRunner(logger, repoRoot), + logger: logger, + repoRoot: repoRoot, + javaLogLevel: javaLogLevel, + } +} + +func (p *parserRouter) ParsePackage(ctx context.Context, in *parser.ParsePackageRequest) (*java.Package, error) { + for _, filename := range in.Files { + if strings.HasSuffix(filename, ".kt") { + javacParser, err := p.kotlinParser() + if err != nil { + return nil, err + } + return javacParser.ParsePackage(ctx, in) + } + } + return p.javaParser.ParsePackage(ctx, in) +} + +func (p *parserRouter) Shutdown() { + p.javaParser.Shutdown() + p.mu.Lock() + javacParser := p.javacParser + p.mu.Unlock() + shutdownParser(javacParser) +} + +func (p *parserRouter) kotlinParser() (parser.Parser, error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.javacParser != nil || p.javacParserErr != nil { + return p.javacParser, p.javacParserErr + } + p.javacParser, p.javacParserErr = javaparser.NewRunner(p.logger, p.repoRoot, p.javaLogLevel) + return p.javacParser, p.javacParserErr +} diff --git a/java/gazelle/private/javaparser/BUILD.bazel b/java/gazelle/private/javaparser/BUILD.bazel index 4ca15f2d..46ef1084 100644 --- a/java/gazelle/private/javaparser/BUILD.bazel +++ b/java/gazelle/private/javaparser/BUILD.bazel @@ -8,6 +8,7 @@ go_library( deps = [ "//java/gazelle/private/java", "//java/gazelle/private/javaparser/proto/gazelle/java/javaparser/v0:gazelle_java_build_v0_go_proto", + "//java/gazelle/private/parser", "//java/gazelle/private/servermanager", "//java/gazelle/private/sorted_multiset", "//java/gazelle/private/sorted_set", diff --git a/java/gazelle/private/javaparser/javaparser.go b/java/gazelle/private/javaparser/javaparser.go index c116c30e..f655a014 100644 --- a/java/gazelle/private/javaparser/javaparser.go +++ b/java/gazelle/private/javaparser/javaparser.go @@ -7,6 +7,7 @@ import ( "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java" pb "github.com/bazel-contrib/rules_jvm/java/gazelle/private/javaparser/proto/gazelle/java/javaparser/v0" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/servermanager" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/sorted_multiset" "github.com/bazel-contrib/rules_jvm/java/gazelle/private/sorted_set" @@ -43,12 +44,13 @@ func (r *Runner) ServerManager() *servermanager.ServerManager { return r.serverManager } -type ParsePackageRequest struct { - Rel string - Files []string +func (r *Runner) Shutdown() { + if r.serverManager != nil { + r.serverManager.Shutdown() + } } -func (r Runner) ParsePackage(ctx context.Context, in *ParsePackageRequest) (*java.Package, error) { +func (r Runner) ParsePackage(ctx context.Context, in *parser.ParsePackageRequest) (*java.Package, error) { defer func(t time.Time) { r.logger.Debug(). Str("duration", time.Since(t).String()). diff --git a/java/gazelle/private/parser/BUILD.bazel b/java/gazelle/private/parser/BUILD.bazel new file mode 100644 index 00000000..9393a733 --- /dev/null +++ b/java/gazelle/private/parser/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "parser", + srcs = ["parser.go"], + importpath = "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser", + visibility = ["//java/gazelle:__subpackages__"], + deps = ["//java/gazelle/private/java"], +) + diff --git a/java/gazelle/private/parser/parser.go b/java/gazelle/private/parser/parser.go new file mode 100644 index 00000000..b9070eaa --- /dev/null +++ b/java/gazelle/private/parser/parser.go @@ -0,0 +1,18 @@ +package parser + +import ( + "context" + + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java" +) + +// ParsePackageRequest describes a directory-relative set of Java sources to parse. +type ParsePackageRequest struct { + Rel string + Files []string +} + +// Parser extracts package metadata from Java source files. +type Parser interface { + ParsePackage(ctx context.Context, in *ParsePackageRequest) (*java.Package, error) +} diff --git a/java/gazelle/private/tsparser/BUILD.bazel b/java/gazelle/private/tsparser/BUILD.bazel new file mode 100644 index 00000000..537eabad --- /dev/null +++ b/java/gazelle/private/tsparser/BUILD.bazel @@ -0,0 +1,32 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "tsparser", + srcs = ["tsparser.go"], + importpath = "github.com/bazel-contrib/rules_jvm/java/gazelle/private/tsparser", + visibility = ["//java/gazelle:__subpackages__"], + deps = [ + "//java/gazelle/private/java", + "//java/gazelle/private/parser", + "//java/gazelle/private/sorted_multiset", + "//java/gazelle/private/sorted_set", + "//java/gazelle/private/types", + "@com_github_odvcencio_gotreesitter//:gotreesitter", + "@com_github_odvcencio_gotreesitter//grammars", + "@com_github_rs_zerolog//:zerolog", + ], +) + +go_test( + name = "tsparser_test", + srcs = [ + "tsparser_bench_test.go", + "tsparser_test.go", + ], + embed = [":tsparser"], + deps = [ + "//java/gazelle/private/parser", + "//java/gazelle/private/types", + "@com_github_rs_zerolog//:zerolog", + ], +) diff --git a/java/gazelle/private/tsparser/tsparser.go b/java/gazelle/private/tsparser/tsparser.go new file mode 100644 index 00000000..47013079 --- /dev/null +++ b/java/gazelle/private/tsparser/tsparser.go @@ -0,0 +1,893 @@ +// Package tsparser implements Java metadata extraction using gotreesitter. +// It is a pure-Go replacement for the gRPC-based javaparser that requires +// a Java subprocess. +package tsparser + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/odvcencio/gotreesitter" + "github.com/odvcencio/gotreesitter/grammars" + "github.com/rs/zerolog" + + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/java" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/sorted_multiset" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/sorted_set" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/types" +) + +var ( + tsJavaLang = grammars.DetectLanguage("Test.java").Language() + // ParserPool is concurrency-safe and resets parser state between uses. + tsJavaParserPool = gotreesitter.NewParserPool(tsJavaLang) +) + +// Runner implements Java parsing using gotreesitter (pure Go, no subprocess). +type Runner struct { + logger zerolog.Logger + repoRoot string +} + +// NewRunner creates a tree-sitter-based Java parser. +// No subprocess or JDK required. +func NewRunner(logger zerolog.Logger, repoRoot string) *Runner { + return &Runner{ + logger: logger.With().Str("_c", "javaparser-ts").Logger(), + repoRoot: repoRoot, + } +} + +func (r *Runner) Shutdown() {} + +func (r *Runner) ParsePackage(ctx context.Context, in *parser.ParsePackageRequest) (*java.Package, error) { + defer func(t time.Time) { + r.logger.Debug(). + Str("duration", time.Since(t).String()). + Str("rel", in.Rel). + Msg("parse package done") + }(time.Now()) + + importedClassNames := make(map[string]struct{}) + exportedClassNames := make(map[string]struct{}) + importedPackageNames := make(map[string]struct{}) + mainClassNames := make(map[string]struct{}) + perClassMetadata := make(map[string]java.PerClassMetadata) + + var packageName types.PackageName + packageNames := map[string]struct{}{} + + for _, filename := range in.Files { + info, err := r.parseJavaFile(in.Rel, filename) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", filename, err) + } + + if info.packageName != "" { + packageNames[info.packageName] = struct{}{} + } + if packageName.Name == "" && info.packageName != "" { + packageName = types.NewPackageName(info.packageName) + } + + for _, imp := range info.importedClasses { + importedClassNames[imp] = struct{}{} + } + for _, pkg := range info.importedPackages { + importedPackageNames[pkg] = struct{}{} + } + + for _, cls := range info.exportedClasses { + exportedClassNames[cls] = struct{}{} + } + + for _, cls := range info.mainClasses { + mainClassName := types.NewClassName(packageName, cls) + mainClassNames[mainClassName.FullyQualifiedClassName()] = struct{}{} + } + + for className, meta := range info.perClassMetadata { + perClassMetadata[className] = meta + } + } + if len(packageNames) > 1 { + names := make([]string, 0, len(packageNames)) + for name := range packageNames { + names = append(names, name) + } + sort.Strings(names) + return nil, fmt.Errorf("InvalidArgument: Expected exactly one java package, but saw %d: %s", len(names), strings.Join(names, ", ")) + } + + importedClasses := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess) + for imp := range importedClassNames { + cn, err := types.ParseClassName(imp) + if err != nil { + r.logger.Warn().Str("import", imp).Err(err).Msg("skipping unparseable import") + continue + } + importedClasses.Add(*cn) + } + + exportedClasses := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess) + for cls := range exportedClassNames { + cn, err := types.ParseClassName(cls) + if err != nil { + r.logger.Warn().Str("class", cls).Err(err).Msg("skipping unparseable exported class") + continue + } + exportedClasses.Add(*cn) + } + + importedPackages := sorted_set.NewSortedSetFn([]types.PackageName{}, types.PackageNameLess) + for pkg := range importedPackageNames { + importedPackages.Add(types.NewPackageName(pkg)) + } + + mains := sorted_set.NewSortedSetFn([]types.ClassName{}, types.ClassNameLess) + for cls := range mainClassNames { + cn, err := types.ParseClassName(cls) + if err != nil { + r.logger.Warn().Str("class", cls).Err(err).Msg("skipping unparseable main class") + continue + } + mains.Add(*cn) + } + + return &java.Package{ + Name: packageName, + ImportedClasses: importedClasses, + ExportedClasses: exportedClasses, + ImportedPackagesWithoutSpecificClasses: importedPackages, + Mains: mains, + Files: sorted_set.NewSortedSet(in.Files), + TestPackage: java.IsTestPackage(in.Rel), + PerClassMetadata: perClassMetadata, + }, nil +} + +// --------------------------------------------------------------------------- +// Per-file extraction +// --------------------------------------------------------------------------- + +type javaFileInfo struct { + packageName string + importedClasses []string // FQN of imported classes + importedPackages []string // package names from wildcard imports + exportedClasses []string // FQN of public API dependency types + mainClasses []string // bare names of classes containing main() + perClassMetadata map[string]java.PerClassMetadata +} + +func (r *Runner) parseJavaFile(rel, filename string) (*javaFileInfo, error) { + path := filepath.Join(r.repoRoot, rel, filename) + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + tree, err := r.parseJavaContent(content) + if err != nil { + return nil, err + } + defer tree.Release() + root := tree.RootNode() + + info := &javaFileInfo{ + perClassMetadata: make(map[string]java.PerClassMetadata), + } + // Import map for resolving simple annotation names to FQNs. + importMap := make(map[string]string) + + for i := 0; i < root.NamedChildCount(); i++ { + child := root.NamedChild(i) + switch child.Type(tsJavaLang) { + case "package_declaration": + info.packageName = extractPackageName(child, content) + case "import_declaration": + extractImport(child, content, info, importMap) + } + } + + localClassNames := collectLocalClassNames(tree, content) + for i := 0; i < root.NamedChildCount(); i++ { + child := root.NamedChild(i) + switch child.Type(tsJavaLang) { + case "class_declaration", "interface_declaration", "enum_declaration": + extractTypeDecl(child, content, info, importMap, nil, localClassNames, nil) + } + } + + return info, nil +} + +func (r *Runner) parseJavaContent(content []byte) (*gotreesitter.Tree, error) { + tree, err := tsJavaParserPool.Parse(content) + if err != nil { + return nil, fmt.Errorf("parse: %w", err) + } + if tree == nil || tree.RootNode() == nil { + return nil, fmt.Errorf("parse: nil root") + } + + // Fallback to the Java token-source path when the default parse tree has + // syntax errors. This guards metadata extraction against parser regressions + // in one parse mode while preserving the fast path for healthy trees. + if !tree.RootNode().HasError() { + return tree, nil + } + + ts, err := grammars.NewJavaTokenSource(content, tsJavaLang) + if err != nil { + r.logger.Debug().Err(err).Msg("java token-source fallback unavailable") + return tree, nil + } + + fallbackTree, err := tsJavaParserPool.ParseWithTokenSource(content, ts) + if err != nil { + if fallbackTree != nil { + fallbackTree.Release() + } + r.logger.Debug().Err(err).Msg("java token-source fallback parse failed") + return tree, nil + } + if fallbackTree == nil || fallbackTree.RootNode() == nil { + if fallbackTree != nil { + fallbackTree.Release() + } + r.logger.Debug().Msg("java token-source fallback returned nil root") + return tree, nil + } + if fallbackTree.RootNode().HasError() { + fallbackTree.Release() + return tree, nil + } + + tree.Release() + r.logger.Debug().Msg("java parse recovered by token-source fallback") + return fallbackTree, nil +} + +// --------------------------------------------------------------------------- +// Extraction helpers +// --------------------------------------------------------------------------- + +func extractPackageName(node *gotreesitter.Node, content []byte) string { + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + typ := child.Type(tsJavaLang) + if typ == "scoped_identifier" || typ == "identifier" { + return child.Text(content) + } + } + return "" +} + +func extractImport(node *gotreesitter.Node, content []byte, info *javaFileInfo, importMap map[string]string) { + // Detect "static" keyword among anonymous children. + isStatic := false + for i := 0; i < node.ChildCount(); i++ { + ch := node.Child(i) + if !ch.IsNamed() && ch.Text(content) == "static" { + isStatic = true + break + } + } + + var importPath string + hasAsterisk := false + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + typ := child.Type(tsJavaLang) + if typ == "scoped_identifier" || typ == "identifier" { + importPath = child.Text(content) + } else if typ == "asterisk" { + hasAsterisk = true + } + } + if importPath == "" { + return + } + + if isStatic { + if hasAsterisk { + // import static com.example.Foo.* → depends on class Foo + info.importedClasses = append(info.importedClasses, importPath) + addToImportMap(importMap, importPath) + } else { + // import static com.example.Foo.bar → strip member, keep class + if dot := strings.LastIndex(importPath, "."); dot >= 0 { + className := importPath[:dot] + info.importedClasses = append(info.importedClasses, className) + addToImportMap(importMap, className) + } + } + } else { + if hasAsterisk { + // import com.example.* → wildcard package import + info.importedPackages = append(info.importedPackages, importPath) + } else { + // import com.example.Foo → specific class import + info.importedClasses = append(info.importedClasses, importPath) + addToImportMap(importMap, importPath) + } + } +} + +func addToImportMap(importMap map[string]string, fqn string) { + if dot := strings.LastIndex(fqn, "."); dot >= 0 { + importMap[fqn[dot+1:]] = fqn + } else { + importMap[fqn] = fqn + } +} + +func collectLocalClassNames(tree *gotreesitter.Tree, content []byte) map[string]struct{} { + names := map[string]struct{}{} + cursor := gotreesitter.NewTreeCursorFromTree(tree) + for { + node := cursor.CurrentNode() + if node == nil { + return names + } + switch node.Type(tsJavaLang) { + case "class_declaration", "interface_declaration", "enum_declaration": + if nameNode := node.ChildByFieldName("name", tsJavaLang); nameNode != nil { + names[nameNode.Text(content)] = struct{}{} + } + } + + if cursor.GotoFirstNamedChild() { + continue + } + for { + if cursor.GotoNextNamedSibling() { + break + } + if !cursor.GotoParent() { + return names + } + } + } +} + +func extractTypeDecl(node *gotreesitter.Node, content []byte, info *javaFileInfo, importMap map[string]string, parents []string, localClassNames map[string]struct{}, inheritedTypeParams map[string]struct{}) { + nameNode := node.ChildByFieldName("name", tsJavaLang) + if nameNode == nil { + return + } + className := nameNode.Text(content) + nestedNames := append(append([]string{}, parents...), className) + classFQN := qualifyNestedClassName(info.packageName, nestedNames) + + typeParams := extendTypeParameters(inheritedTypeParams, node, content) + for _, ref := range directTypeRefs(node, content, importMap, info.packageName, localClassNames, typeParams, true) { + info.importedClasses = append(info.importedClasses, ref) + } + + // Class-level annotations. + classAnns := extractAnnotationNames(node, content, importMap, info.packageName) + classAnnSet := sorted_set.NewSortedSetFn(nil, types.ClassNameLess) + for _, ann := range classAnns { + if cn, err := types.ParseClassName(ann); err == nil { + classAnnSet.Add(*cn) + } + } + + methodAnns := sorted_multiset.NewSortedMultiSetFn[string, types.ClassName](types.ClassNameLess) + fieldAnns := sorted_multiset.NewSortedMultiSetFn[string, types.ClassName](types.ClassNameLess) + + bodyNode := node.ChildByFieldName("body", tsJavaLang) + if bodyNode != nil { + for i := 0; i < bodyNode.NamedChildCount(); i++ { + member := bodyNode.NamedChild(i) + memberType := member.Type(tsJavaLang) + + switch memberType { + case "class_declaration", "interface_declaration", "enum_declaration": + extractTypeDecl(member, content, info, importMap, nestedNames, localClassNames, typeParams) + + case "method_declaration": + methodName := findIdentifier(member, content) + methodTypeParams := extendTypeParameters(typeParams, member, content) + for _, ref := range directTypeRefs(member, content, importMap, info.packageName, localClassNames, methodTypeParams, true) { + info.importedClasses = append(info.importedClasses, ref) + } + if body := methodBodyNode(member); body != nil { + for _, ref := range usedTypeRefs(body, content, importMap, info.packageName, localClassNames, methodTypeParams) { + info.importedClasses = append(info.importedClasses, ref) + } + } + if !hasModifier(member, content, "private") { + if returnType := methodReturnTypeNode(member); returnType != nil && returnType.Type(tsJavaLang) != "void_type" { + for _, ref := range typeRefsFromTypeNode(returnType, content, importMap, info.packageName, localClassNames, methodTypeParams, false) { + info.exportedClasses = append(info.exportedClasses, ref) + } + } + } + + if node.Type(tsJavaLang) == "class_declaration" && methodName == "main" && isMainMethod(member, content) { + info.mainClasses = append(info.mainClasses, className) + } + + for _, ann := range extractAnnotationNames(member, content, importMap, info.packageName) { + if cn, err := types.ParseClassName(ann); err == nil { + methodAnns.Add(methodName, *cn) + } + } + + case "field_declaration": + for _, ref := range directTypeRefs(member, content, importMap, info.packageName, localClassNames, typeParams, true) { + info.importedClasses = append(info.importedClasses, ref) + } + fieldNames := extractFieldNames(member, content) + annotations := extractAnnotationNames(member, content, importMap, info.packageName) + for _, fieldName := range fieldNames { + for _, ann := range annotations { + if cn, err := types.ParseClassName(ann); err == nil { + fieldAnns.Add(fieldName, *cn) + } + } + } + default: + for _, ref := range usedTypeRefs(member, content, importMap, info.packageName, localClassNames, typeParams) { + info.importedClasses = append(info.importedClasses, ref) + } + } + } + } + + if classAnnSet.Len() > 0 || len(methodAnns.Keys()) > 0 || len(fieldAnns.Keys()) > 0 { + info.perClassMetadata[classFQN] = java.PerClassMetadata{ + AnnotationClassNames: classAnnSet, + MethodAnnotationClassNames: methodAnns, + FieldAnnotationClassNames: fieldAnns, + } + } +} + +func qualifyNestedClassName(packageName string, names []string) string { + parts := make([]string, 0, len(names)+1) + if packageName != "" { + parts = append(parts, packageName) + } + parts = append(parts, names...) + return strings.Join(parts, ".") +} + +func cloneStringSet(in map[string]struct{}) map[string]struct{} { + if len(in) == 0 { + return map[string]struct{}{} + } + out := map[string]struct{}{} + for k := range in { + out[k] = struct{}{} + } + return out +} + +func extendTypeParameters(in map[string]struct{}, node *gotreesitter.Node, content []byte) map[string]struct{} { + var out map[string]struct{} + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if child.Type(tsJavaLang) != "type_parameters" { + continue + } + for j := 0; j < child.NamedChildCount(); j++ { + typeParam := child.NamedChild(j) + if typeParam.Type(tsJavaLang) != "type_parameter" { + continue + } + name := typeParameterName(typeParam, content) + if name != "" { + if out == nil { + out = cloneStringSet(in) + } + out[name] = struct{}{} + } + } + } + if out != nil { + return out + } + return in +} + +func typeParameterName(node *gotreesitter.Node, content []byte) string { + if name := node.ChildByFieldName("name", tsJavaLang); name != nil { + return name.Text(content) + } + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if child.Type(tsJavaLang) == "type_identifier" { + return child.Text(content) + } + } + return "" +} + +func methodReturnTypeNode(node *gotreesitter.Node) *gotreesitter.Node { + if typ := node.ChildByFieldName("type", tsJavaLang); typ != nil { + if isTypeNode(typ) || typ.Type(tsJavaLang) == "void_type" { + return typ + } + } + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if isTypeNode(child) { + return child + } + } + return nil +} + +func methodBodyNode(node *gotreesitter.Node) *gotreesitter.Node { + if body := node.ChildByFieldName("body", tsJavaLang); body != nil { + return body + } + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + switch child.Type(tsJavaLang) { + case "block", "constructor_body": + return child + } + } + return nil +} + +func directTypeRefs(node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}, includeParameterizedBase bool) []string { + refs := make([]string, 0, 4) + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + switch child.Type(tsJavaLang) { + case "class_body", "interface_body", "enum_body", "constructor_body", "block": + continue + } + appendTypeRefsFromAnyNode(&refs, child, content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + } + return refs +} + +func usedTypeRefs(node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}) []string { + refs := make([]string, 0, 4) + appendTypeRefsFromAnyNode(&refs, node, content, importMap, packageName, localClassNames, typeParams, true) + return refs +} + +func typeRefsFromAnyNode(node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}, includeParameterizedBase bool) []string { + refs := make([]string, 0, 4) + appendTypeRefsFromAnyNode(&refs, node, content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + return refs +} + +func appendTypeRefsFromAnyNode(refs *[]string, node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}, includeParameterizedBase bool) { + if isTypeNode(node) { + appendTypeRefsFromTypeNode(refs, node, content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + return + } + if node.Type(tsJavaLang) == "type_parameter" { + name := typeParameterName(node, content) + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if child.Text(content) == name { + continue + } + appendTypeRefsFromAnyNode(refs, child, content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + } + return + } + for i := 0; i < node.NamedChildCount(); i++ { + appendTypeRefsFromAnyNode(refs, node.NamedChild(i), content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + } +} + +func typeRefsFromTypeNode(node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}, includeParameterizedBase bool) []string { + refs := make([]string, 0, 2) + appendTypeRefsFromTypeNode(&refs, node, content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + return refs +} + +func appendTypeRefsFromTypeNode(refs *[]string, node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}, includeParameterizedBase bool) { + switch node.Type(tsJavaLang) { + case "type_identifier", "scoped_type_identifier": + if ref := resolveTypeName(node.Text(content), importMap, packageName, localClassNames, typeParams); ref != "" { + *refs = append(*refs, ref) + } + case "generic_type": + base := node.ChildByFieldName("type", tsJavaLang) + if includeParameterizedBase && base != nil { + appendTypeRefsFromTypeNode(refs, base, content, importMap, packageName, localClassNames, typeParams, true) + } + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if sameNode(child, base) { + continue + } + appendTypeRefsFromAnyNode(refs, child, content, importMap, packageName, localClassNames, typeParams, true) + } + case "array_type": + for i := 0; i < node.NamedChildCount(); i++ { + appendTypeRefsFromAnyNode(refs, node.NamedChild(i), content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + } + default: + for i := 0; i < node.NamedChildCount(); i++ { + appendTypeRefsFromAnyNode(refs, node.NamedChild(i), content, importMap, packageName, localClassNames, typeParams, includeParameterizedBase) + } + } +} + +func sameNode(a, b *gotreesitter.Node) bool { + if a == nil || b == nil { + return false + } + return a.Symbol() == b.Symbol() && a.StartByte() == b.StartByte() && a.EndByte() == b.EndByte() +} + +func isTypeNode(node *gotreesitter.Node) bool { + switch node.Type(tsJavaLang) { + case "type_identifier", "scoped_type_identifier", "generic_type", "array_type", "wildcard": + return true + } + return false +} + +func resolveTypeName(typeName string, importMap map[string]string, packageName string, localClassNames, typeParams map[string]struct{}) string { + typeName = strings.TrimSpace(typeName) + if typeName == "" { + return "" + } + parts := strings.Split(typeName, ".") + if fqn, ok := importMap[parts[0]]; ok { + return fqn + } + if len(parts) > 1 { + return typeName + } + if _, ok := localClassNames[typeName]; ok { + return "" + } + if _, ok := typeParams[typeName]; ok { + return "" + } + if isJavaLangType(typeName) { + return "" + } + if packageName == "" { + return "" + } + return packageName + "." + typeName +} + +func hasModifier(node *gotreesitter.Node, content []byte, modifier string) bool { + for i := 0; i < node.ChildCount(); i++ { + child := node.Child(i) + if child.Type(tsJavaLang) == "modifiers" { + for j := 0; j < child.ChildCount(); j++ { + token := child.Child(j) + if !token.IsNamed() && token.Text(content) == modifier { + return true + } + } + for _, token := range strings.Fields(child.Text(content)) { + if token == modifier { + return true + } + } + return false + } + } + return false +} + +func extractAnnotationNames(node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string) []string { + var annotations []string + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + switch child.Type(tsJavaLang) { + case "marker_annotation", "annotation": + if name := resolveAnnotationName(child, content, importMap, packageName); name != "" { + annotations = append(annotations, name) + } + case "modifiers": + for j := 0; j < child.NamedChildCount(); j++ { + annNode := child.NamedChild(j) + annType := annNode.Type(tsJavaLang) + if annType != "marker_annotation" && annType != "annotation" { + continue + } + if name := resolveAnnotationName(annNode, content, importMap, packageName); name != "" { + annotations = append(annotations, name) + } + } + } + } + return annotations +} + +func resolveAnnotationName(node *gotreesitter.Node, content []byte, importMap map[string]string, packageName string) string { + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + typ := child.Type(tsJavaLang) + switch typ { + case "identifier": + simple := child.Text(content) + if fqn, ok := importMap[simple]; ok { + return fqn + } + return simple + case "scoped_identifier": + return child.Text(content) + } + } + return "" +} + +func isJavaLangType(name string) bool { + _, ok := javaLangTypes[name] + return ok +} + +var javaLangTypes = map[string]struct{}{ + "Boolean": {}, + "Byte": {}, + "Character": {}, + "Double": {}, + "Float": {}, + "Integer": {}, + "Long": {}, + "Short": {}, + "Void": {}, + "CharSequence": {}, + "Class": {}, + "ClassLoader": {}, + "Comparable": {}, + "Enum": {}, + "Iterable": {}, + "Math": {}, + "Number": {}, + "Object": {}, + "Package": {}, + "Process": {}, + "ProcessBuilder": {}, + "Record": {}, + "Runtime": {}, + "SecurityManager": {}, + "StackTraceElement": {}, + "StrictMath": {}, + "String": {}, + "StringBuffer": {}, + "StringBuilder": {}, + "System": {}, + "Thread": {}, + "ThreadGroup": {}, + "ThreadLocal": {}, + "Appendable": {}, + "AutoCloseable": {}, + "Cloneable": {}, + "Readable": {}, + "Runnable": {}, + "Throwable": {}, + "Error": {}, + "Exception": {}, + "RuntimeException": {}, + "ArithmeticException": {}, + "ArrayIndexOutOfBoundsException": {}, + "ArrayStoreException": {}, + "ClassCastException": {}, + "ClassNotFoundException": {}, + "CloneNotSupportedException": {}, + "EnumConstantNotPresentException": {}, + "IllegalAccessException": {}, + "IllegalArgumentException": {}, + "IllegalMonitorStateException": {}, + "IllegalStateException": {}, + "IllegalThreadStateException": {}, + "IndexOutOfBoundsException": {}, + "InstantiationException": {}, + "InterruptedException": {}, + "NegativeArraySizeException": {}, + "NoSuchFieldException": {}, + "NoSuchMethodException": {}, + "NullPointerException": {}, + "NumberFormatException": {}, + "ReflectiveOperationException": {}, + "SecurityException": {}, + "StringIndexOutOfBoundsException": {}, + "TypeNotPresentException": {}, + "UnsupportedOperationException": {}, + "AbstractMethodError": {}, + "AssertionError": {}, + "BootstrapMethodError": {}, + "ClassCircularityError": {}, + "ClassFormatError": {}, + "ExceptionInInitializerError": {}, + "IncompatibleClassChangeError": {}, + "InternalError": {}, + "LinkageError": {}, + "NoClassDefFoundError": {}, + "NoSuchFieldError": {}, + "NoSuchMethodError": {}, + "OutOfMemoryError": {}, + "StackOverflowError": {}, + "UnknownError": {}, + "UnsatisfiedLinkError": {}, + "UnsupportedClassVersionError": {}, + "VerifyError": {}, + "VirtualMachineError": {}, + "Deprecated": {}, + "FunctionalInterface": {}, + "Override": {}, + "SafeVarargs": {}, + "SuppressWarnings": {}, +} + +func isMainMethod(node *gotreesitter.Node, content []byte) bool { + if !hasModifier(node, content, "public") || !hasModifier(node, content, "static") { + return false + } + // Field map entries may be incomplete for method_declaration, + // so walk children by type instead of using ChildByFieldName. + var hasVoidReturn bool + var hasStringArrayParam bool + for i := 0; i < node.ChildCount(); i++ { + child := node.Child(i) + switch child.Type(tsJavaLang) { + case "void_type": + hasVoidReturn = true + case "formal_parameters": + paramText := child.Text(content) + hasStringArrayParam = isSingleStringMainParam(paramText) + } + } + return hasVoidReturn && hasStringArrayParam +} + +func isSingleStringMainParam(paramText string) bool { + paramText = strings.TrimSpace(paramText) + if len(paramText) < 2 || paramText[0] != '(' || paramText[len(paramText)-1] != ')' { + return false + } + inner := strings.TrimSpace(paramText[1 : len(paramText)-1]) + if inner == "" { + return false + } + // main should have exactly one parameter; reject additional params. + if strings.Contains(inner, ",") { + return false + } + + noSpace := strings.ReplaceAll(inner, " ", "") + return strings.Contains(noSpace, "String[]") || strings.Contains(noSpace, "String...") +} + +// findIdentifier returns the text of the first direct identifier child. +// For method_declaration, this is the method name. +func findIdentifier(node *gotreesitter.Node, content []byte) string { + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if child.Type(tsJavaLang) == "identifier" { + return child.Text(content) + } + } + return "" +} + +func extractFieldNames(node *gotreesitter.Node, content []byte) []string { + var fieldNames []string + for i := 0; i < node.NamedChildCount(); i++ { + child := node.NamedChild(i) + if child.Type(tsJavaLang) == "variable_declarator" { + if fieldName := findIdentifier(child, content); fieldName != "" { + // variable_declarator's first identifier child is the field name. + fieldNames = append(fieldNames, fieldName) + } + } + } + return fieldNames +} diff --git a/java/gazelle/private/tsparser/tsparser_bench_test.go b/java/gazelle/private/tsparser/tsparser_bench_test.go new file mode 100644 index 00000000..947b8cd7 --- /dev/null +++ b/java/gazelle/private/tsparser/tsparser_bench_test.go @@ -0,0 +1,199 @@ +package tsparser + +import ( + "context" + "math/rand" + "os" + "path/filepath" + "sort" + "strconv" + "testing" + + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" + "github.com/rs/zerolog" +) + +type javaCorpusFile struct { + path string + rel string + name string + source []byte +} + +type javaCorpusPackage struct { + rel string + files []string +} + +func benchmarkCorpusEnv(primary, fallback string) string { + if value := os.Getenv(primary); value != "" { + return value + } + return os.Getenv(fallback) +} + +func loadJavaBenchmarkCorpus(b *testing.B) (string, []javaCorpusFile) { + b.Helper() + + root := benchmarkCorpusEnv("RULES_JVM_JAVA_CORPUS", "GOT_JAVA_CORPUS") + if root == "" { + b.Skip("set RULES_JVM_JAVA_CORPUS to a Java corpus root") + } + + var files []javaCorpusFile + if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || filepath.Ext(path) != ".java" { + return nil + } + source, err := os.ReadFile(path) + if err != nil { + return err + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + files = append(files, javaCorpusFile{ + path: path, + rel: rel, + name: filepath.Base(path), + source: source, + }) + return nil + }); err != nil { + b.Fatal(err) + } + if len(files) == 0 { + b.Fatalf("no Java files under %s", root) + } + + switch benchmarkCorpusEnv("RULES_JVM_JAVA_CORPUS_ORDER", "GOT_JAVA_CORPUS_ORDER") { + case "random": + seed := int64(1) + if raw := benchmarkCorpusEnv("RULES_JVM_JAVA_CORPUS_RANDOM_SEED", "GOT_JAVA_CORPUS_RANDOM_SEED"); raw != "" { + parsed, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + b.Fatal(err) + } + seed = parsed + } + r := rand.New(rand.NewSource(seed)) + r.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) + default: + sort.Slice(files, func(i, j int) bool { + if len(files[i].source) == len(files[j].source) { + return files[i].path < files[j].path + } + return len(files[i].source) > len(files[j].source) + }) + } + + maxFiles := 10 + if raw := benchmarkCorpusEnv("RULES_JVM_JAVA_CORPUS_MAX_FILES", "GOT_JAVA_CORPUS_MAX_FILES"); raw != "" { + parsed, err := strconv.Atoi(raw) + if err != nil { + b.Fatal(err) + } + maxFiles = parsed + } + if maxFiles > 0 && len(files) > maxFiles { + files = files[:maxFiles] + } + return root, files +} + +func groupJavaCorpusPackages(files []javaCorpusFile) []javaCorpusPackage { + byRel := make(map[string][]string) + for _, file := range files { + rel := filepath.Dir(file.rel) + if rel == "." { + rel = "" + } + byRel[rel] = append(byRel[rel], file.name) + } + + packages := make([]javaCorpusPackage, 0, len(byRel)) + for rel, names := range byRel { + sort.Strings(names) + packages = append(packages, javaCorpusPackage{rel: rel, files: names}) + } + sort.Slice(packages, func(i, j int) bool { + return packages[i].rel < packages[j].rel + }) + return packages +} + +func javaCorpusBytes(files []javaCorpusFile) int64 { + var total int64 + for _, file := range files { + total += int64(len(file.source)) + } + return total +} + +func BenchmarkJavaCorpusParsePackage(b *testing.B) { + root, files := loadJavaBenchmarkCorpus(b) + packages := groupJavaCorpusPackages(files) + b.ReportAllocs() + b.SetBytes(javaCorpusBytes(files)) + b.ResetTimer() + + runner := NewRunner(zerolog.Nop(), root) + var errors int64 + for i := 0; i < b.N; i++ { + for _, pkg := range packages { + _, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: pkg.rel, + Files: pkg.files, + }) + if err != nil { + errors++ + } + } + } + b.ReportMetric(float64(errors)/float64(b.N), "errors/op") +} + +func BenchmarkJavaCorpusParseOnlyFullTree(b *testing.B) { + _, files := loadJavaBenchmarkCorpus(b) + runner := NewRunner(zerolog.Nop(), "") + b.ReportAllocs() + b.SetBytes(javaCorpusBytes(files)) + b.ResetTimer() + + var errors int64 + for i := 0; i < b.N; i++ { + for _, file := range files { + tree, err := runner.parseJavaContent(file.source) + if err != nil || tree == nil { + errors++ + continue + } + tree.Release() + } + } + b.ReportMetric(float64(errors)/float64(b.N), "errors/op") +} + +func BenchmarkJavaCorpusParseOnlyNoTree(b *testing.B) { + _, files := loadJavaBenchmarkCorpus(b) + b.ReportAllocs() + b.SetBytes(javaCorpusBytes(files)) + b.ResetTimer() + + var errors int64 + for i := 0; i < b.N; i++ { + for _, file := range files { + tree, err := tsJavaParserPool.ParseNoTreeBenchmarkOnly(file.source) + if err != nil || tree == nil { + errors++ + continue + } + tree.Release() + } + } + b.ReportMetric(float64(errors)/float64(b.N), "errors/op") +} diff --git a/java/gazelle/private/tsparser/tsparser_test.go b/java/gazelle/private/tsparser/tsparser_test.go new file mode 100644 index 00000000..f30f8f38 --- /dev/null +++ b/java/gazelle/private/tsparser/tsparser_test.go @@ -0,0 +1,727 @@ +package tsparser + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "sync" + "testing" + + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/parser" + "github.com/bazel-contrib/rules_jvm/java/gazelle/private/types" + "github.com/rs/zerolog" +) + +func newTestRunner(t *testing.T) (*Runner, string) { + t.Helper() + dir := t.TempDir() + logger := zerolog.New(zerolog.NewTestWriter(t)) + return NewRunner(logger, dir), dir +} + +func writeJava(t *testing.T, dir, rel, filename, content string) { + t.Helper() + d := filepath.Join(dir, rel) + if err := os.MkdirAll(d, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(d, filename), []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func TestPackageName(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src/main/java/com/example", "Foo.java", ` +package com.example; + +public class Foo {} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src/main/java/com/example", + Files: []string{"Foo.java"}, + }) + if err != nil { + t.Fatal(err) + } + if pkg.Name.Name != "com.example" { + t.Errorf("package name = %q, want %q", pkg.Name.Name, "com.example") + } +} + +func TestLeadingCommentsBeforePackageRegression(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "LeadingComment.java", ` +// Leading line comment used to trigger parse regressions in some parser modes. +/* Leading block comment with symbols: ; {} [] */ +package com.example; + +import java.util.List; +import static java.util.Collections.emptyList; + +public class LeadingComment { + @Override + public String toString() { + return "ok"; + } + + public static void main(String[] args) { + List values = emptyList(); + System.out.println(values.size()); + } +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"LeadingComment.java"}, + }) + if err != nil { + t.Fatal(err) + } + + if pkg.Name.Name != "com.example" { + t.Fatalf("package name = %q, want %q", pkg.Name.Name, "com.example") + } + + wantImports := []string{"java.util.Collections", "java.util.List"} + gotImports := fqns(pkg.ImportedClasses.SortedSlice()) + if !reflect.DeepEqual(gotImports, wantImports) { + t.Fatalf("imported classes = %v, want %v", gotImports, wantImports) + } + + if pkg.ExportedClasses.Len() != 0 { + t.Fatalf("exported classes = %v, want none", fqns(pkg.ExportedClasses.SortedSlice())) + } + + wantMains := []string{"com.example.LeadingComment"} + gotMains := fqns(pkg.Mains.SortedSlice()) + if !reflect.DeepEqual(gotMains, wantMains) { + t.Fatalf("mains = %v, want %v", gotMains, wantMains) + } + + meta, ok := pkg.PerClassMetadata["com.example.LeadingComment"] + if !ok { + t.Fatal("no PerClassMetadata for LeadingComment") + } + overrideAnns := meta.MethodAnnotationClassNames.Values("toString") + if overrideAnns == nil || overrideAnns.Len() != 1 { + t.Fatalf("toString annotations = %v, want [Override]", overrideAnns) + } + if got := overrideAnns.SortedSlice()[0].FullyQualifiedClassName(); got != "Override" { + t.Fatalf("toString annotation = %q, want %q", got, "Override") + } +} + +func TestParseJavaContentLeadingCommentNoSyntaxError(t *testing.T) { + runner, _ := newTestRunner(t) + + tree, err := runner.parseJavaContent([]byte(` +// Leading comment before package declaration. +package com.example; + +public class ParseTreeSmoke {} +`)) + if err != nil { + t.Fatal(err) + } + defer tree.Release() + + if tree.RootNode().HasError() { + t.Fatalf("parse tree has syntax errors for leading-comment input") + } +} + +func TestImports(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Foo.java", ` +package com.example; + +import java.util.List; +import java.io.*; +import static org.junit.Assert.assertEquals; +import static com.google.common.collect.ImmutableList.*; + +public class Foo {} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"Foo.java"}, + }) + if err != nil { + t.Fatal(err) + } + + // Imported classes: java.util.List, org.junit.Assert, com.google.common.collect.ImmutableList + wantClasses := []string{ + "com.google.common.collect.ImmutableList", + "java.util.List", + "org.junit.Assert", + } + gotClasses := pkg.ImportedClasses.SortedSlice() + if len(gotClasses) != len(wantClasses) { + t.Fatalf("imported classes count = %d, want %d\n got: %v", len(gotClasses), len(wantClasses), fqns(gotClasses)) + } + for i, want := range wantClasses { + if gotClasses[i].FullyQualifiedClassName() != want { + t.Errorf("imported class[%d] = %q, want %q", i, gotClasses[i].FullyQualifiedClassName(), want) + } + } + + // Wildcard package import: java.io + gotPkgs := pkg.ImportedPackagesWithoutSpecificClasses.SortedSlice() + if len(gotPkgs) != 1 || gotPkgs[0].Name != "java.io" { + t.Errorf("imported packages = %v, want [java.io]", gotPkgs) + } +} + +func TestExportedAPIReturnTypes(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Foo.java", ` +package com.example; + +import com.external.PrivateReturn; +import com.external.PublicReturn; + +public class Foo { + public PublicReturn getPublic() { return null; } + protected LocalReturn getProtected() { return null; } + PrivateReturn getPackage() { return null; } + private PrivateReturn getPrivate() { return null; } +} +class LocalReturn {} +`) + writeJava(t, dir, "src", "Baz.java", ` +package com.example; + +import com.external.InterfaceReturn; + +public interface Baz { + InterfaceReturn get(); +} +`) + writeJava(t, dir, "src", "Status.java", ` +package com.example; + +public enum Status { OK, ERROR } +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"Foo.java", "Baz.java", "Status.java"}, + }) + if err != nil { + t.Fatal(err) + } + + got := pkg.ExportedClasses.SortedSlice() + want := []string{"com.external.InterfaceReturn", "com.external.PrivateReturn", "com.external.PublicReturn"} + if len(got) != len(want) { + t.Fatalf("exported classes = %v, want %v", fqns(got), want) + } + for i, w := range want { + if got[i].FullyQualifiedClassName() != w { + t.Errorf("exported[%d] = %q, want %q", i, got[i].FullyQualifiedClassName(), w) + } + } +} + +func TestMainMethod(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "App.java", ` +package com.example; + +public class App { + public static void main(String[] args) { + System.out.println("hello"); + } +} +`) + writeJava(t, dir, "src", "Lib.java", ` +package com.example; + +public class Lib { + public void notMain(String[] args) {} +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"App.java", "Lib.java"}, + }) + if err != nil { + t.Fatal(err) + } + + got := pkg.Mains.SortedSlice() + if len(got) != 1 { + t.Fatalf("mains count = %d, want 1", len(got)) + } + if got[0].BareOuterClassName() != "App" { + t.Errorf("main class = %q, want %q", got[0].BareOuterClassName(), "App") + } +} + +func TestMainMethodVarargs(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "App.java", ` +package com.example; + +public class App { + public static void main(String... args) {} +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"App.java"}, + }) + if err != nil { + t.Fatal(err) + } + + if pkg.Mains.Len() != 1 { + t.Fatalf("mains count = %d, want 1", pkg.Mains.Len()) + } +} + +func TestAnnotations(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "MyTest.java", ` +package com.example; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; + +@RunWith(SpringRunner.class) +public class MyTest { + @Autowired + private String field; + + @Test + public void testSomething() {} + + @Override + public String toString() { return ""; } +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"MyTest.java"}, + }) + if err != nil { + t.Fatal(err) + } + + meta, ok := pkg.PerClassMetadata["com.example.MyTest"] + if !ok { + t.Fatal("no PerClassMetadata for MyTest") + } + + // Class-level: @RunWith + classAnns := meta.AnnotationClassNames.SortedSlice() + t.Logf("class annotations: %v", fqns(classAnns)) + if len(classAnns) != 1 || classAnns[0].BareOuterClassName() != "RunWith" { + t.Errorf("class annotations = %v, want [RunWith]", fqns(classAnns)) + } + + // Method: testSomething has @Test + testAnns := meta.MethodAnnotationClassNames.Values("testSomething") + if testAnns == nil || testAnns.Len() != 1 { + t.Fatalf("testSomething annotations = %v, want 1", testAnns) + } + if testAnns.SortedSlice()[0].BareOuterClassName() != "Test" { + t.Errorf("testSomething annotation = %q, want Test", testAnns.SortedSlice()[0].BareOuterClassName()) + } + + // Method: toString has @Override. + overrideAnns := meta.MethodAnnotationClassNames.Values("toString") + if overrideAnns == nil || overrideAnns.Len() != 1 { + t.Fatalf("toString annotations = %v, want 1", overrideAnns) + } + if overrideAnns.SortedSlice()[0].FullyQualifiedClassName() != "Override" { + t.Errorf("toString annotation = %q, want Override", overrideAnns.SortedSlice()[0].FullyQualifiedClassName()) + } + + // Field: field has @Autowired + fieldAnns := meta.FieldAnnotationClassNames.Values("field") + if fieldAnns == nil || fieldAnns.Len() != 1 { + t.Fatalf("field annotations = %v, want 1", fieldAnns) + } + if fieldAnns.SortedSlice()[0].BareOuterClassName() != "Autowired" { + t.Errorf("field annotation = %q, want Autowired", fieldAnns.SortedSlice()[0].BareOuterClassName()) + } +} + +func TestTestPackageDetection(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src/test/java/com/example", "FooTest.java", ` +package com.example; +public class FooTest {} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src/test/java/com/example", + Files: []string{"FooTest.java"}, + }) + if err != nil { + t.Fatal(err) + } + if !pkg.TestPackage { + t.Error("expected TestPackage=true for src/test/ path") + } +} + +func TestMultipleFiles(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "A.java", ` +package com.example; +import java.util.List; +public class A {} +`) + writeJava(t, dir, "src", "B.java", ` +package com.example; +import java.util.Map; +public class B {} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"A.java", "B.java"}, + }) + if err != nil { + t.Fatal(err) + } + + if pkg.ImportedClasses.Len() != 2 { + t.Errorf("imported classes = %d, want 2", pkg.ImportedClasses.Len()) + } + if pkg.ExportedClasses.Len() != 0 { + t.Errorf("exported classes = %d, want 0", pkg.ExportedClasses.Len()) + } + if pkg.Files.Len() != 2 { + t.Errorf("files = %d, want 2", pkg.Files.Len()) + } +} + +func TestFieldInitializerTypeReferences(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "UsesFieldInitializers.java", ` +package com.example; + +public class UsesFieldInitializers { + Object byConstructor = new Helper(); + Class byClassLiteral = Helper.class; + Runnable byMethodReference = Helper::run; + int[] generatedTable = {1, 2, 3}; +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"UsesFieldInitializers.java"}, + }) + if err != nil { + t.Fatal(err) + } + + got := fqns(pkg.ImportedClasses.SortedSlice()) + want := []string{"com.example.Helper"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("imported classes = %v, want %v", got, want) + } +} + +func TestMultiplePackagesError(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "A.java", ` +package com.example; +public class A {} +`) + writeJava(t, dir, "src", "B.java", ` +package example; +public class B {} +`) + + _, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"A.java", "B.java"}, + }) + if err == nil { + t.Fatal("ParsePackage succeeded, want multiple-package error") + } + want := "InvalidArgument: Expected exactly one java package, but saw 2: com.example, example" + if err.Error() != want { + t.Fatalf("error = %q, want %q", err.Error(), want) + } +} + +func TestNoPackageDeclaration(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Script.java", ` +public class Script { + public static void main(String[] args) {} +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"Script.java"}, + }) + if err != nil { + t.Fatal(err) + } + + if pkg.Name.Name != "" { + t.Errorf("package name = %q, want empty", pkg.Name.Name) + } + if pkg.ExportedClasses.Len() != 0 { + t.Errorf("exported classes = %d, want 0", pkg.ExportedClasses.Len()) + } + if pkg.Mains.Len() != 1 { + t.Errorf("mains = %d, want 1", pkg.Mains.Len()) + } +} + +func TestMainMethodStrictSignature(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Good.java", ` +package com.example; +public class Good { + public static void main(String[] args) {} +} +`) + writeJava(t, dir, "src", "BadExtraArgs.java", ` +package com.example; +public class BadExtraArgs { + public static void main(String[] args, int count) {} +} +`) + writeJava(t, dir, "src", "BadReturn.java", ` +package com.example; +public class BadReturn { + public static int main(String[] args) { return 0; } +} +`) + writeJava(t, dir, "src", "BadVisibility.java", ` +package com.example; +public class BadVisibility { + static void main(String[] args) {} +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{ + "BadExtraArgs.java", + "BadReturn.java", + "BadVisibility.java", + "Good.java", + }, + }) + if err != nil { + t.Fatal(err) + } + + got := pkg.Mains.SortedSlice() + if len(got) != 1 { + t.Fatalf("mains = %v, want [com.example.Good]", fqns(got)) + } + if got[0].FullyQualifiedClassName() != "com.example.Good" { + t.Fatalf("main = %q, want com.example.Good", got[0].FullyQualifiedClassName()) + } +} + +func TestStaticNestedImport(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Foo.java", ` +package com.example; + +import static com.foo.Outer.Inner.VALUE; +import static com.foo.Util.*; + +public class Foo {} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"Foo.java"}, + }) + if err != nil { + t.Fatal(err) + } + + want := []string{"com.foo.Outer.Inner", "com.foo.Util"} + got := fqns(pkg.ImportedClasses.SortedSlice()) + if len(got) != len(want) { + t.Fatalf("imported classes = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("imported classes = %v, want %v", got, want) + } + } +} + +func TestAnnotationResolutionPrecedence(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Annotated.java", ` +package com.example; + +import com.thirdparty.ImportedAnn; + +@ImportedAnn +public class Annotated { + @Deprecated + void run() {} + + @LocalAnn + String field; + + @com.scoped.ScopedAnn + void scoped() {} +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"Annotated.java"}, + }) + if err != nil { + t.Fatal(err) + } + + meta := pkg.PerClassMetadata["com.example.Annotated"] + classAnns := fqns(meta.AnnotationClassNames.SortedSlice()) + wantClassAnns := []string{"com.thirdparty.ImportedAnn"} + if len(classAnns) != len(wantClassAnns) { + t.Fatalf("class annotations = %v, want %v", classAnns, wantClassAnns) + } + for i := range wantClassAnns { + if classAnns[i] != wantClassAnns[i] { + t.Fatalf("class annotations = %v, want %v", classAnns, wantClassAnns) + } + } + + methodAnns := meta.MethodAnnotationClassNames.Values("run") + if methodAnns == nil || methodAnns.Len() != 1 { + t.Fatalf("run method annotations = %v, want [Deprecated]", methodAnns) + } + if methodAnns.SortedSlice()[0].FullyQualifiedClassName() != "Deprecated" { + t.Fatalf("run method annotation = %q, want Deprecated", methodAnns.SortedSlice()[0].FullyQualifiedClassName()) + } + + scopedMethodAnns := meta.MethodAnnotationClassNames.Values("scoped") + if scopedMethodAnns == nil || scopedMethodAnns.Len() != 1 { + t.Fatalf("scoped method annotations = %v, want [com.scoped.ScopedAnn]", scopedMethodAnns) + } + if scopedMethodAnns.SortedSlice()[0].FullyQualifiedClassName() != "com.scoped.ScopedAnn" { + t.Fatalf("scoped method annotation = %q, want com.scoped.ScopedAnn", scopedMethodAnns.SortedSlice()[0].FullyQualifiedClassName()) + } + + fieldAnns := meta.FieldAnnotationClassNames.Values("field") + if fieldAnns == nil || fieldAnns.Len() != 1 { + t.Fatalf("field annotations = %v, want [LocalAnn]", fieldAnns) + } + if fieldAnns.SortedSlice()[0].FullyQualifiedClassName() != "LocalAnn" { + t.Fatalf("field annotation = %q, want LocalAnn", fieldAnns.SortedSlice()[0].FullyQualifiedClassName()) + } +} + +func TestFieldAnnotationsOnMultipleDeclarators(t *testing.T) { + runner, dir := newTestRunner(t) + writeJava(t, dir, "src", "Fields.java", ` +package com.example; + +import javax.inject.Inject; + +public class Fields { + @Inject String a, b; +} +`) + + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"Fields.java"}, + }) + if err != nil { + t.Fatal(err) + } + + meta := pkg.PerClassMetadata["com.example.Fields"] + for _, field := range []string{"a", "b"} { + fieldAnns := meta.FieldAnnotationClassNames.Values(field) + if fieldAnns == nil || fieldAnns.Len() != 1 { + t.Fatalf("field %q annotations = %v, want [javax.inject.Inject]", field, fieldAnns) + } + if fieldAnns.SortedSlice()[0].FullyQualifiedClassName() != "javax.inject.Inject" { + t.Fatalf("field %q annotation = %q, want javax.inject.Inject", field, fieldAnns.SortedSlice()[0].FullyQualifiedClassName()) + } + } +} + +func TestParsePackageConcurrent(t *testing.T) { + dir := t.TempDir() + runner := NewRunner(zerolog.Nop(), dir) + writeJava(t, dir, "src", "App.java", ` +package com.example; + +public class App { + public static void main(String[] args) { + System.out.println("hello"); + } +} +`) + + const workers = 12 + const roundsPerWorker = 20 + + errCh := make(chan error, workers*roundsPerWorker) + var wg sync.WaitGroup + + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < roundsPerWorker; j++ { + pkg, err := runner.ParsePackage(context.Background(), &parser.ParsePackageRequest{ + Rel: "src", + Files: []string{"App.java"}, + }) + if err != nil { + errCh <- fmt.Errorf("parse package: %w", err) + return + } + if pkg.Mains.Len() != 1 { + errCh <- fmt.Errorf("mains count = %d, want 1", pkg.Mains.Len()) + return + } + main := pkg.Mains.SortedSlice()[0] + if main.FullyQualifiedClassName() != "com.example.App" { + errCh <- fmt.Errorf("main class = %q, want %q", main.FullyQualifiedClassName(), "com.example.App") + return + } + } + }() + } + + wg.Wait() + close(errCh) + for err := range errCh { + t.Fatal(err) + } +} + +func fqns(cs []types.ClassName) []string { + out := make([]string, len(cs)) + for i, c := range cs { + out[i] = c.FullyQualifiedClassName() + } + return out +} diff --git a/java/gazelle/private/types/types.go b/java/gazelle/private/types/types.go index 6760e38a..20312e7a 100644 --- a/java/gazelle/private/types/types.go +++ b/java/gazelle/private/types/types.go @@ -104,7 +104,45 @@ func ParseClassName(fullyQualified string) (*ClassName, error) { } func ClassNameLess(l, r ClassName) bool { - return l.FullyQualifiedClassName() < r.FullyQualifiedClassName() + for i := 0; ; i++ { + lb, lok := classNameFQNByteAt(l, i) + rb, rok := classNameFQNByteAt(r, i) + if !lok || !rok { + return !lok && rok + } + if lb != rb { + return lb < rb + } + } +} + +func classNameFQNByteAt(c ClassName, index int) (byte, bool) { + if pkg := c.packageName.Name; pkg != "" { + if index < len(pkg) { + return pkg[index], true + } + if index == len(pkg) { + return '.', true + } + index -= len(pkg) + 1 + } + + if index < len(c.bareOuterClassName) { + return c.bareOuterClassName[index], true + } + index -= len(c.bareOuterClassName) + + for _, inner := range c.innerClassNames { + if index == 0 { + return '.', true + } + index-- + if index < len(inner) { + return inner[index], true + } + index -= len(inner) + } + return 0, false } type ResolveInput struct { diff --git a/java/gazelle/private/types/types_test.go b/java/gazelle/private/types/types_test.go index 60b04bea..137c6b43 100644 --- a/java/gazelle/private/types/types_test.go +++ b/java/gazelle/private/types/types_test.go @@ -2,6 +2,7 @@ package types import ( "reflect" + "sort" "testing" ) @@ -83,3 +84,45 @@ func TestParseClassName(t *testing.T) { }) } } + +func TestClassNameLessMatchesFullyQualifiedStringOrder(t *testing.T) { + names := []string{ + "A", + "a.A", + "a.Z", + "a.b.A", + "a.b.C", + "a.b.C.Inner", + "a.b.C.Inner.Nested", + "a.z.A", + "com.example.Simple", + "com.example.Simple.Inner", + "com.google.common.base.Strings", + "com.google.gson.Gson", + } + + classes := make([]ClassName, 0, len(names)) + for _, name := range names { + className, err := ParseClassName(name) + if err != nil { + t.Fatalf("ParseClassName(%q): %v", name, err) + } + classes = append(classes, *className) + } + + got := append([]ClassName(nil), classes...) + sort.Slice(got, func(i, j int) bool { + return ClassNameLess(got[i], got[j]) + }) + + want := append([]ClassName(nil), classes...) + sort.Slice(want, func(i, j int) bool { + return want[i].FullyQualifiedClassName() < want[j].FullyQualifiedClassName() + }) + + for i := range want { + if got[i].FullyQualifiedClassName() != want[i].FullyQualifiedClassName() { + t.Fatalf("sorted[%d] = %q, want %q", i, got[i].FullyQualifiedClassName(), want[i].FullyQualifiedClassName()) + } + } +}