Adding Lockfile Parsers¶
TL;DR
Create a pkg/lockfile/<format>.go file that exports a Parse<Format> function
returning []PackageEntry, register it in detect.go, add tests with fixture
data, and run make all to validate.
Who is this for?
Contributors who want gouvernante to support a new package-manager lockfile
format (e.g. bun.lock, deno.lock, Gradle lockfiles).
Overview¶
gouvernante discovers which lockfile format is present and delegates to
format-specific parsers in pkg/lockfile/. Each parser is a single Go file
that converts the on-disk format into a flat slice of PackageEntry values the
scanner can reason about.
Step-by-Step¶
1. Create the parser file¶
Add pkg/lockfile/<format>.go (e.g. bun.go).
The file must export exactly one public function with this signature:
// Parse<Format> reads a <format> lockfile at path and returns every resolved
// package entry.
func Parse<Format>(path string) ([]PackageEntry, error)
Concrete example for Bun:
package lockfile
import (
"encoding/json"
"fmt"
"os"
)
// ParseBun reads a bun.lock file at path and returns every resolved package
// entry.
func ParseBun(path string) ([]PackageEntry, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading bun lockfile: %w", err)
}
// bun.lock is a JSON-like format; parse it here.
var raw bunLockfile
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("parsing bun lockfile: %w", err)
}
entries := make([]PackageEntry, 0, len(raw.Packages))
for name, meta := range raw.Packages {
entries = append(entries, PackageEntry{
Name: name,
Version: meta.Version,
})
}
return entries, nil
}
// bunLockfile is the on-disk representation of bun.lock.
type bunLockfile struct {
Packages map[string]bunPackageMeta `json:"packages"`
}
type bunPackageMeta struct {
Version string `json:"version"`
}
Key rules:
- For JSON-based formats: use
encoding/jsonfrom the standard library. - For YAML-based formats: use
goccy/go-yaml(already a project dependency). - For text-based formats: use
bufio.Scannerwith line-by-line parsing. - Wrap errors with
%wso callers can inspect them. - Keep unexported helper types in the same file.
2. Register the parser in detect.go¶
Open pkg/lockfile/detect.go and add an entry to the detection table inside
DetectAndParse() so that auto-detection picks up the new format:
lockfiles := []struct {
name string
parser func(string) ([]PackageEntry, error)
}{
{"pnpm-lock.yaml", ParsePnpmLock},
{"package-lock.json", ParsePackageLockJSON},
{"yarn.lock", ParseYarnLock},
{"bun.lock", ParseBun}, // <-- add this line
}
Also add the corresponding case to the ParseFile() switch statement in the
same file:
case "bun.lock":
parser = ParseBun
The scanner walks the table top-to-bottom and uses the first match, so order matters only when a project contains multiple lockfiles.
3. Write tests¶
Create pkg/lockfile/<format>_test.go (e.g. bun_test.go).
Place fixture files under pkg/lockfile/testdata/:
pkg/lockfile/testdata/bun/
basic.lock # minimal happy-path fixture
empty.lock # edge case: zero packages
malformed.lock # edge case: invalid JSON
Use table-driven tests:
package lockfile_test
import (
"path/filepath"
"testing"
"gouvernante/pkg/lockfile"
)
func TestParseBun(t *testing.T) {
tests := []struct {
name string
fixture string
wantCount int
wantErr bool
}{
{"basic", "testdata/bun/basic.lock", 3, false},
{"empty", "testdata/bun/empty.lock", 0, false},
{"malformed", "testdata/bun/malformed.lock", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entries, err := lockfile.ParseBun(filepath.Join(".", tt.fixture))
if (err != nil) != tt.wantErr {
t.Fatalf("ParseBun() error = %v, wantErr %v", err, tt.wantErr)
}
if len(entries) != tt.wantCount {
t.Errorf("got %d entries, want %d", len(entries), tt.wantCount)
}
})
}
}
4. Run the full pipeline¶
make all
This runs fmt-check, lint, test (with the race detector), and build in
sequence. Every check must pass before the parser is considered ready.
Common Pitfalls¶
| Mistake | Fix |
|---|---|
Returning nil instead of an empty slice |
Always return make([]PackageEntry, 0) for zero-package files. |
| Forgetting to close a file handle | Prefer os.ReadFile for small-to-medium lockfiles. |
Bare errors.New without context |
Wrap with fmt.Errorf("parsing <format>: %w", err). |
| Adding an unapproved external library | Use the standard library or goccy/go-yaml (already available). For anything else, open an issue first. |
Self-Assessment Checklist¶
- Parser file follows the
Parse<Format>(path string) ([]PackageEntry, error)signature. - Parser is registered in
detect.go. - Tests cover happy path, empty input, and malformed input.
- Fixture data is committed under
pkg/lockfile/testdata/. -
make allpasses with zero warnings. - Godoc comment on the exported function.
- Errors are wrapped with
%w.
Next Steps¶
- Writing Rules -- define detection rules that the scanner matches against parsed lockfile entries.
- Testing -- deeper dive into the project's testing conventions.
- Code Style -- formatting and linting expectations.