Skip to content

Commit f7d99c1

Browse files
adonovangopherbot
authored andcommitted
go/packages/internal/linecount: count lines in Go packages
This CL adds a command to count source lines of Go packages, with options to filter or group by module, package, or file. It serves as an example of packages.Load. Change-Id: Ifeed917b563e91e8425c74783f8992e7b2d25c81 Reviewed-on: https://go-review.googlesource.com/c/tools/+/691937 Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Commit-Queue: Alan Donovan <[email protected]>
1 parent c00c94d commit f7d99c1

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

go/packages/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ uninterpreted to Load, so that it can interpret them
7676
according to the conventions of the underlying build system.
7777
7878
See the Example function for typical usage.
79+
See also [golang.org/x/tools/go/packages/internal/linecount]
80+
for an example application.
7981
8082
# The driver protocol
8183
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The linecount command shows the number of lines of code in a set of
6+
// Go packages plus their dependencies. It serves as a working
7+
// illustration of the [packages.Load] operation.
8+
//
9+
// Example: show gopls' total source line count, and its breakdown
10+
// between gopls, x/tools, and the std go/* packages. (The balance
11+
// comes from other std packages.)
12+
//
13+
// $ linecount -mode=total ./gopls
14+
// 752124
15+
// $ linecount -mode=total -module=golang.org/x/tools/gopls ./gopls
16+
// 103519
17+
// $ linecount -mode=total -module=golang.org/x/tools ./gopls
18+
// 99504
19+
// $ linecount -mode=total -prefix=go -module=std ./gopls
20+
// 47502
21+
//
22+
// Example: show the top 5 modules contributing to gopls' source line count:
23+
//
24+
// $ linecount -mode=module ./gopls | head -n 5
25+
// 440274 std
26+
// 103519 golang.org/x/tools/gopls
27+
// 99504 golang.org/x/tools
28+
// 40220 honnef.co/go/tools
29+
// 17707 golang.org/x/text
30+
//
31+
// Example: show the top 3 largest files in the gopls module:
32+
//
33+
// $ linecount -mode=file -module=golang.org/x/tools/gopls ./gopls | head -n 3
34+
// 6841 gopls/internal/protocol/tsprotocol.go
35+
// 3769 gopls/internal/golang/completion/completion.go
36+
// 2202 gopls/internal/cache/snapshot.go
37+
package main
38+
39+
import (
40+
"bytes"
41+
"cmp"
42+
"flag"
43+
"fmt"
44+
"log"
45+
"os"
46+
"path"
47+
"slices"
48+
"strings"
49+
"sync"
50+
51+
"golang.org/x/sync/errgroup"
52+
"golang.org/x/tools/go/packages"
53+
)
54+
55+
// TODO(adonovan): filters:
56+
// - exclude comment and blank lines (-nonblank)
57+
// - exclude generated files (-generated=false)
58+
// - exclude non-CompiledGoFiles
59+
// - include OtherFiles (asm, etc)
60+
// - include tests (needs care to avoid double counting)
61+
62+
func usage() {
63+
// See https://go.dev/issue/63659.
64+
fmt.Fprintf(os.Stderr, "Usage: linecount [flags] packages...\n")
65+
flag.PrintDefaults()
66+
fmt.Fprintf(os.Stderr, `
67+
Docs: go doc golang.org/x/tools/go/packages/internal/linecount
68+
https://pkg.go.dev/golang.org/x/tools/go/packages/internal/linecount
69+
`)
70+
}
71+
72+
func main() {
73+
// Parse command line.
74+
log.SetPrefix("linecount: ")
75+
log.SetFlags(0)
76+
var (
77+
mode = flag.String("mode", "file", "group lines by 'module', 'package', or 'file', or show only 'total'")
78+
prefix = flag.String("prefix", "", "only count files in packages whose path has the specified prefix")
79+
onlyModule = flag.String("module", "", "only count files in the specified module")
80+
)
81+
flag.Usage = usage
82+
flag.Parse()
83+
if len(flag.Args()) == 0 {
84+
usage()
85+
os.Exit(1)
86+
}
87+
88+
// Load packages.
89+
cfg := &packages.Config{
90+
Mode: packages.NeedName |
91+
packages.NeedFiles |
92+
packages.NeedImports |
93+
packages.NeedDeps |
94+
packages.NeedModule,
95+
}
96+
pkgs, err := packages.Load(cfg, flag.Args()...)
97+
if err != nil {
98+
log.Fatal(err)
99+
}
100+
if packages.PrintErrors(pkgs) > 0 {
101+
os.Exit(1)
102+
}
103+
104+
// Read files and count lines.
105+
var (
106+
mu sync.Mutex
107+
byFile = make(map[string]int)
108+
byPackage = make(map[string]int)
109+
byModule = make(map[string]int)
110+
)
111+
var g errgroup.Group
112+
g.SetLimit(20) // file system parallelism level
113+
packages.Visit(pkgs, nil, func(p *packages.Package) {
114+
pkgpath := p.PkgPath
115+
module := "std"
116+
if p.Module != nil {
117+
module = p.Module.Path
118+
}
119+
if *prefix != "" && !within(pkgpath, path.Clean(*prefix)) {
120+
return
121+
}
122+
if *onlyModule != "" && module != *onlyModule {
123+
return
124+
}
125+
for _, f := range p.GoFiles {
126+
g.Go(func() error {
127+
data, err := os.ReadFile(f)
128+
if err != nil {
129+
return err
130+
}
131+
n := bytes.Count(data, []byte("\n"))
132+
133+
mu.Lock()
134+
byFile[f] = n
135+
byPackage[pkgpath] += n
136+
byModule[module] += n
137+
mu.Unlock()
138+
139+
return nil
140+
})
141+
}
142+
})
143+
if err := g.Wait(); err != nil {
144+
log.Fatal(err)
145+
}
146+
147+
// Display the result.
148+
switch *mode {
149+
case "file", "package", "module":
150+
var m map[string]int
151+
switch *mode {
152+
case "file":
153+
m = byFile
154+
case "package":
155+
m = byPackage
156+
case "module":
157+
m = byModule
158+
}
159+
type item struct {
160+
name string
161+
count int
162+
}
163+
var items []item
164+
for name, count := range m {
165+
items = append(items, item{name, count})
166+
}
167+
slices.SortFunc(items, func(x, y item) int {
168+
return -cmp.Compare(x.count, y.count)
169+
})
170+
for _, item := range items {
171+
fmt.Printf("%d\t%s\n", item.count, item.name)
172+
}
173+
174+
case "total":
175+
total := 0
176+
for _, n := range byFile {
177+
total += n
178+
}
179+
fmt.Printf("%d\n", total)
180+
181+
default:
182+
log.Fatalf("invalid -mode %q (want file, package, module, or total)", *mode)
183+
}
184+
}
185+
186+
func within(file, dir string) bool {
187+
return file == dir ||
188+
strings.HasPrefix(file, dir) && file[len(dir)] == os.PathSeparator
189+
}

0 commit comments

Comments
 (0)