blob: 8bec5073431a1ccb77431095f8fbe9f14164390e [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Command importcounter will calculate and print per-package and aggregate metrics
// about Go dependencies. Ex:
//
// go run main.go --patterns go.chromium.org/luci/...
//
// Will print CSV to stdout, listing each subpackage of go.chromium.org/luci/...
// along with some per-subpackge counts on each row. Finally, it will produce
// some aggregate counts for the entire set of packages.
package main
import (
"encoding/csv"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"golang.org/x/tools/go/packages"
)
var (
patterns stringListValue
)
func init() {
flag.Var(&patterns, "patterns", "package path patterns to examine")
}
type stats struct {
// PackageCount is the number of packages analyzed.
PackageCount int
// TotalImportCount is the total number of import statements,
// treating each line inside a factored import block as an individual import.
TotalImportCount int
// DistinctImportCount is the total number of distinct packages imported.
DistinctImportCount int
// TotalExternalImportCount is the total number of import statements
// for packages outside of the path patterns specified in the patterns
// flag.
TotalExternalImportCount int
// DistinctExternalImportCount is the total number of distinct external
// see TotalExternalImportCount above) packages imported.
DistinctExternalImportCount int
// FileCount is the number of files analyzed.
FileCount int
// LOC is the total number of lines of code in the analyzed files.
LOC int
}
// loc returns the total lines of code contained in the go source files
// located in goFiles.
func loc(goFiles []string) int {
sum := 0
for _, fn := range goFiles {
data, err := ioutil.ReadFile(fn)
if err != nil {
log.Fatalf("reading %s: %+v", fn, err)
}
sum += len(strings.Split(string(data), "\n"))
}
return sum
}
// patMatch returns true if pkg is a package name that equals or falls under
// pat, if pat ends with "/...".
func patMatch(pat string, pkg string) bool {
// TODO: something more advanced. This won't work in every case.
prefix := strings.Replace(pat, "/...", "", -1)
return strings.HasPrefix(pkg, prefix)
}
// countExternal returns the number of package names (keys) in pkgs that do not match the
// patterns in in patterns.
func countExternal(patterns []string, pkgs map[string]*packages.Package) int {
ret := 0
for k := range pkgs {
external := false
for _, pat := range patterns {
if !patMatch(pat, k) {
external = true
}
}
if external {
ret++
}
}
return ret
}
func main() {
flag.Parse()
cfg := &packages.Config{
// Consider using packages.NeedDeps if we want more info about imported packages.
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports,
}
pkgs, err := packages.Load(cfg, patterns...)
if err != nil {
log.Fatalf("loading packages: %+v", err)
}
sum := stats{
PackageCount: len(pkgs),
}
importSet := map[string]*packages.Package{}
out := [][]string{}
for _, p := range pkgs {
totalImports := len(p.Imports)
totalExternalImports := countExternal(patterns, p.Imports)
totalLOC := loc(p.GoFiles)
fileCount := len(p.GoFiles)
sum.TotalImportCount += totalImports
sum.TotalExternalImportCount += totalExternalImports
for _, imp := range p.Imports {
importSet[imp.PkgPath] = imp
}
sum.FileCount += fileCount
sum.LOC += totalLOC
out = append(out, []string{
p.PkgPath,
fmt.Sprintf("%d", totalImports),
fmt.Sprintf("%d", totalExternalImports),
fmt.Sprintf("%d", fileCount),
fmt.Sprintf("%d", totalLOC),
})
}
w := csv.NewWriter(os.Stdout)
w.WriteAll(out)
sum.DistinctImportCount = len(importSet)
sum.DistinctExternalImportCount = countExternal(patterns, importSet)
fmt.Printf("Aggregate: %+v\n", sum)
}
// stringListValue is a flag.Value that accumulates strings.
// e.g. --flag=one --flag=two would produce []string{"one", "two"}.
type stringListValue []string
func newStringListValue(val []string, p *[]string) *stringListValue {
*p = val
return (*stringListValue)(p)
}
func (ss *stringListValue) Get() interface{} { return []string(*ss) }
func (ss *stringListValue) String() string { return fmt.Sprintf("%q", *ss) }
func (ss *stringListValue) Set(s string) error { *ss = append(*ss, s); return nil }