Adds .golangci.yml (errcheck, govet, staticcheck, revive, gocyclo, gocritic, nilerr, errorlint, etc.). Fixes all lint issues found: - RpcError/wrapRpc renamed to RPCError/wrapRPC (revive var-naming) - Shadow vars resolved in process.go, outline.go, project.go - nilerr suppressed with justifying comments in git/collect.go and auto/detector.go (WalkDir / intentional empty-returns) - if-else chain rewritten as switch in format.go (gocritic) - gofmt applied to detector.go and client.go Adds .github/workflows/ci.yml: lint (ubuntu), build+vet+test matrix (ubuntu/windows/macos), and Roslyn helper build (windows). Validated with actionlint (0 issues). Adds .githooks/pre-commit + pre-commit.ps1 for local pre-commit checks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
5.5 KiB
Go
186 lines
5.5 KiB
Go
package helper
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Client is a high-level typed interface to the roslyn-helper subprocess.
|
|
type Client struct {
|
|
proc *Process
|
|
}
|
|
|
|
// NewClient locates the helper binary, starts the process, and verifies it
|
|
// responds to ping. Returns an error if any step fails.
|
|
func NewClient() (*Client, error) {
|
|
helperPath, err := LocateHelper()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proc, err := Start(helperPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("start roslyn helper: %w", err)
|
|
}
|
|
|
|
c := &Client{proc: proc}
|
|
if _, err := c.Ping(); err != nil {
|
|
_ = proc.Close()
|
|
return nil, fmt.Errorf("roslyn helper ping failed: %w", err)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Close shuts down the helper process.
|
|
func (c *Client) Close() error {
|
|
return c.proc.Close()
|
|
}
|
|
|
|
// --- Result types ---
|
|
|
|
// PingResult is returned by the ping method.
|
|
type PingResult struct {
|
|
Pong bool `json:"pong"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// LoadSolutionResult is returned by the loadSolution method.
|
|
type LoadSolutionResult struct {
|
|
Loaded bool `json:"loaded"`
|
|
ProjectCount int `json:"projectCount"`
|
|
DocumentCount int `json:"documentCount"`
|
|
}
|
|
|
|
// ProjectSummary is the top-level result of the projectSummary method.
|
|
type ProjectSummary struct {
|
|
SolutionPath string `json:"solutionPath"`
|
|
SolutionName string `json:"solutionName"`
|
|
Projects []ProjectInfo `json:"projects"`
|
|
}
|
|
|
|
// ProjectInfo describes a single project in the solution.
|
|
type ProjectInfo struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Type string `json:"type"`
|
|
TargetFrameworks []string `json:"targetFrameworks"`
|
|
OutputType string `json:"outputType"`
|
|
RootNamespace string `json:"rootNamespace"`
|
|
DocumentCount int `json:"documentCount"`
|
|
ProjectReferences []string `json:"projectReferences"`
|
|
PackageReferences []PackageReference `json:"packageReferences"`
|
|
}
|
|
|
|
// PackageReference is a NuGet package dependency.
|
|
type PackageReference struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// --- Methods ---
|
|
|
|
// Ping sends a ping to the helper and returns the pong result.
|
|
func (c *Client) Ping() (*PingResult, error) {
|
|
raw, err := c.proc.Send("ping", nil)
|
|
if err != nil {
|
|
return nil, wrapRPC("ping", err)
|
|
}
|
|
var r PingResult
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
return nil, fmt.Errorf("ping: decode response: %w", err)
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// LoadSolution instructs the helper to load the given .sln or .csproj file.
|
|
func (c *Client) LoadSolution(path string) (*LoadSolutionResult, error) {
|
|
params := map[string]string{"path": path}
|
|
raw, err := c.proc.Send("loadSolution", params)
|
|
if err != nil {
|
|
return nil, wrapRPC("loadSolution", err)
|
|
}
|
|
var r LoadSolutionResult
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
return nil, fmt.Errorf("loadSolution: decode response: %w", err)
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// ProjectSummary retrieves the project summary for the currently loaded solution.
|
|
func (c *Client) ProjectSummary() (*ProjectSummary, error) {
|
|
raw, err := c.proc.Send("projectSummary", nil)
|
|
if err != nil {
|
|
return nil, wrapRPC("projectSummary", err)
|
|
}
|
|
var r ProjectSummary
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
return nil, fmt.Errorf("projectSummary: decode response: %w", err)
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// --- Outline types ---
|
|
|
|
// OutlineResult is the structural outline of a single .cs file.
|
|
type OutlineResult struct {
|
|
Path string `json:"path"`
|
|
Namespace string `json:"namespace"`
|
|
LineCount int `json:"lineCount"`
|
|
Usings []string `json:"usings"`
|
|
Types []OutlineType `json:"types"`
|
|
HasSyntaxErrors bool `json:"hasSyntaxErrors"`
|
|
}
|
|
|
|
// OutlineType describes a type (class, interface, struct, record, enum) in the file.
|
|
type OutlineType struct {
|
|
Kind string `json:"kind"`
|
|
Name string `json:"name"`
|
|
Modifiers []string `json:"modifiers"`
|
|
BaseTypes []string `json:"baseTypes"`
|
|
Members []OutlineMember `json:"members"`
|
|
Nested []OutlineType `json:"nested"`
|
|
}
|
|
|
|
// OutlineMember describes a member of a type (method, property, field, event, constructor).
|
|
type OutlineMember struct {
|
|
Kind string `json:"kind"`
|
|
Signature string `json:"signature"`
|
|
Modifiers []string `json:"modifiers"`
|
|
Line int `json:"line"`
|
|
IsObsolete bool `json:"isObsolete,omitempty"`
|
|
}
|
|
|
|
// Outline requests a structural outline of the given .cs file.
|
|
// Does not require a solution to be loaded.
|
|
func (c *Client) Outline(path string) (*OutlineResult, error) {
|
|
params := map[string]string{"path": path}
|
|
raw, err := c.proc.Send("outline", params)
|
|
if err != nil {
|
|
return nil, wrapRPC("outline", err)
|
|
}
|
|
var r OutlineResult
|
|
if err := json.Unmarshal(raw, &r); err != nil {
|
|
return nil, fmt.Errorf("outline: decode response: %w", err)
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// wrapRPC wraps RPCError values into user-friendly messages.
|
|
func wrapRPC(method string, err error) error {
|
|
var rpcErr *RPCError
|
|
if errors.As(err, &rpcErr) {
|
|
switch rpcErr.Code {
|
|
case "E_NOT_FOUND":
|
|
return fmt.Errorf("solution not found: %s", rpcErr.Message)
|
|
case "E_LOAD_FAILED":
|
|
return fmt.Errorf("failed to load solution: %s", rpcErr.Message)
|
|
case "E_INVALID_PARAMS":
|
|
return fmt.Errorf("invalid request: %s", rpcErr.Message)
|
|
default:
|
|
return fmt.Errorf("%s failed: %s", method, rpcErr.Message)
|
|
}
|
|
}
|
|
return err
|
|
}
|