Alanxtl commented on code in PR #3294:
URL: https://github.com/apache/dubbo-go/pull/3294#discussion_r3090371069


##########
tools/variadicrpccheck/scan.go:
##########
@@ -0,0 +1,511 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+       "errors"
+       "fmt"
+       "go/ast"
+       "go/token"
+       "go/types"
+       "path/filepath"
+       "sort"
+       "strings"
+)
+
+import (
+       "golang.org/x/tools/go/packages"
+)
+
+var packagesLoad = packages.Load
+
+type Finding struct {
+       Position   token.Position
+       Kind       string
+       TypeName   string
+       MethodName string
+}
+
+// String renders warning-only output so callers can pipe the tool into CI logs
+// without any post-processing.
+func (f Finding) String() string {
+       return fmt.Sprintf(
+               "%s:%d:%d: warning: %s %s exports variadic RPC method %s; 
prefer []T, request structs, or Triple + Protobuf IDL",
+               f.Position.Filename,
+               f.Position.Line,
+               f.Position.Column,
+               f.Kind,
+               f.TypeName,
+               f.MethodName,
+       )
+}
+
+// Scan loads the requested packages, finds exported variadic RPC contracts,
+// keeps the output order stable, and returns any package-load errors together
+// with the findings collected before the error was seen.
+func Scan(dir string, patterns []string) ([]Finding, error) {
+       pkgs, err := loadPackages(dir, normalizePatterns(patterns))
+       if err != nil {
+               return nil, err
+       }
+
+       findings, loadErrs := collectFindingsAndErrors(pkgs)
+       sortFindings(findings)
+       if len(loadErrs) > 0 {
+               return findings, errors.New(strings.Join(loadErrs, "; "))
+       }
+
+       return findings, nil
+}
+
+// normalizePatterns falls back to the usual recursive package scan when the
+// caller does not pass an explicit pattern list.
+func normalizePatterns(patterns []string) []string {
+       if len(patterns) == 0 {
+               return []string{"./..."}
+       }
+       return patterns
+}
+
+// loadPackages asks go/packages for the syntax and type information needed to
+// inspect both interface declarations and concrete methods.
+func loadPackages(dir string, patterns []string) ([]*packages.Package, error) {
+       cfg := &packages.Config{
+               Mode: packages.NeedName |
+                       packages.NeedFiles |
+                       packages.NeedCompiledGoFiles |
+                       packages.NeedSyntax |
+                       packages.NeedTypes |
+                       packages.NeedTypesInfo,
+               Dir: dir,
+       }
+       return packagesLoad(cfg, patterns...)
+}
+
+// collectFindingsAndErrors scans every loaded package and keeps loader errors
+// separate so we can still emit useful warnings before returning the error.
+func collectFindingsAndErrors(pkgs []*packages.Package) ([]Finding, []string) {
+       findings := make([]Finding, 0)
+       loadErrs := make([]string, 0)
+       for _, pkg := range pkgs {
+               loadErrs = append(loadErrs, packageErrors(pkg)...)
+               findings = append(findings, collectPackageFindings(pkg)...)
+       }
+       return findings, loadErrs
+}
+
+// packageErrors flattens go/packages errors into strings so Scan can combine
+// them into one returned error value.
+func packageErrors(pkg *packages.Package) []string {
+       if pkg == nil || len(pkg.Errors) == 0 {
+               return nil
+       }
+
+       loadErrs := make([]string, 0, len(pkg.Errors))
+       for _, pkgErr := range pkg.Errors {
+               loadErrs = append(loadErrs, pkgErr.Error())
+       }
+       return loadErrs
+}
+
+// sortFindings keeps warning output stable across runs.
+func sortFindings(findings []Finding) {
+       sort.Slice(findings, func(i, j int) bool {
+               if findings[i].Position.Filename != 
findings[j].Position.Filename {
+                       return findings[i].Position.Filename < 
findings[j].Position.Filename
+               }
+               if findings[i].Position.Line != findings[j].Position.Line {
+                       return findings[i].Position.Line < 
findings[j].Position.Line
+               }
+               if findings[i].Position.Column != findings[j].Position.Column {
+                       return findings[i].Position.Column < 
findings[j].Position.Column
+               }
+               if findings[i].Kind != findings[j].Kind {
+                       return findings[i].Kind < findings[j].Kind
+               }
+               if findings[i].TypeName != findings[j].TypeName {
+                       return findings[i].TypeName < findings[j].TypeName
+               }
+               return findings[i].MethodName < findings[j].MethodName
+       })
+}
+
+// collectPackageFindings walks compiled files so build tags and generated-file
+// selection stay aligned with the package loader.
+func collectPackageFindings(pkg *packages.Package) []Finding {
+       findings := make([]Finding, 0)
+       for fileIdx, file := range pkg.Syntax {
+               if fileIdx >= len(pkg.CompiledGoFiles) {
+                       continue
+               }
+
+               filename := pkg.CompiledGoFiles[fileIdx]
+               if shouldSkipFile(filename) {
+                       continue
+               }
+
+               for _, decl := range file.Decls {
+                       switch typedDecl := decl.(type) {
+                       case *ast.GenDecl:
+                               findings = append(findings, 
collectInterfaceFindings(pkg, typedDecl)...)
+                       case *ast.FuncDecl:
+                               if finding, ok := 
collectImplementationFinding(pkg, typedDecl); ok {
+                                       findings = append(findings, finding)
+                               }
+                       }
+               }
+       }
+       return findings
+}
+
+// collectInterfaceFindings catches contract definitions before any concrete
+// implementation exists, including methods inherited through embedded
+// interfaces, which is where new cross-language APIs are often introduced.
+func collectInterfaceFindings(pkg *packages.Package, decl *ast.GenDecl) 
[]Finding {
+       if decl.Tok != token.TYPE {
+               return nil
+       }
+
+       findings := make([]Finding, 0)
+       for _, spec := range decl.Specs {
+               typeSpec, iface, ok := exportedInterfaceSpec(spec)
+               if !ok {
+                       continue
+               }
+
+               findings = append(findings, typeSpecInterfaceFindings(pkg, 
typeSpec, iface)...)
+       }
+
+       return findings
+}
+
+// exportedInterfaceSpec keeps only exported interface declarations and skips
+// unexported or non-interface type specs early.
+func exportedInterfaceSpec(spec ast.Spec) (*ast.TypeSpec, *ast.InterfaceType, 
bool) {
+       typeSpec, ok := spec.(*ast.TypeSpec)
+       if !ok || !typeSpec.Name.IsExported() {
+               return nil, nil, false
+       }
+
+       iface, ok := typeSpec.Type.(*ast.InterfaceType)
+       if !ok {
+               return nil, nil, false
+       }
+
+       return typeSpec, iface, true
+}
+
+// typeSpecInterfaceFindings runs the RPC-style variadic check for one exported
+// interface and preserves positions for both direct and embedded methods.
+func typeSpecInterfaceFindings(pkg *packages.Package, typeSpec *ast.TypeSpec, 
iface *ast.InterfaceType) []Finding {
+       ifaceType, ok := interfaceTypeForSpec(pkg, typeSpec)
+       if !ok {
+               return nil
+       }
+
+       methodPositions := interfaceMethodPositions(pkg, iface)
+       ifaceType.Complete()
+
+       findings := make([]Finding, 0, ifaceType.NumMethods())
+       for i := 0; i < ifaceType.NumMethods(); i++ {
+               if finding, ok := interfaceMethodFinding(pkg, typeSpec, 
ifaceType.Method(i), methodPositions); ok {
+                       findings = append(findings, finding)
+               }
+       }
+       return findings
+}
+
+// interfaceTypeForSpec resolves the go/types interface backing one AST type
+// declaration so later checks can use normalized method signatures.
+func interfaceTypeForSpec(pkg *packages.Package, typeSpec *ast.TypeSpec) 
(*types.Interface, bool) {
+       obj := pkg.TypesInfo.Defs[typeSpec.Name]
+       if obj == nil {
+               return nil, false
+       }
+
+       ifaceType, ok := obj.Type().Underlying().(*types.Interface)
+       return ifaceType, ok
+}
+
+// interfaceMethodFinding returns one finding for an exported variadic 
RPC-style
+// interface method and falls back to the interface declaration position when a
+// more precise method position is unavailable.
+func interfaceMethodFinding(pkg *packages.Package, typeSpec *ast.TypeSpec, 
method *types.Func, methodPositions map[string]token.Pos) (Finding, bool) {
+       if !method.Exported() || !isCandidateRPCMethodName(method.Name()) {
+               return Finding{}, false
+       }
+
+       sig, ok := method.Type().(*types.Signature)
+       if !ok || !isVariadicRPCSignature(sig) {
+               return Finding{}, false
+       }
+
+       pos := methodPositions[method.Name()]
+       if !pos.IsValid() {
+               pos = typeSpec.Name.Pos()
+       }
+
+       return Finding{
+               Position:   pkg.Fset.Position(pos),
+               Kind:       "interface",
+               TypeName:   typeSpec.Name.Name,
+               MethodName: method.Name(),
+       }, true
+}
+
+// collectImplementationFinding covers struct receiver methods used as direct
+// service implementations in non-interface registration flows.
+func collectImplementationFinding(pkg *packages.Package, decl *ast.FuncDecl) 
(Finding, bool) {

Review Comment:
   `collectImplementationFinding` 会把“任意导出类型上的 variadic 导出方法”视作 RPC 实现,不要求 
receiver 实现 `Reference() string` / `common.RPCService`。
   
   试一下
   `type Helper struct{}; func (h *Helper) Merge(ctx context.Context, values 
...string) error`
   扫描器也会报警。这会把很多并未走 `RegisterService` 的普通类型误报成 RPC 实现,和这次 PR 想传达的 “RPC Contract 
Guidance” 不太一致。



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to