blob: d0ac94b9c5f172a1e436c92ae322aff2fa5ad901 [file] [log] [blame]
// 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 protostat
import (
"context"
"io"
"github.com/bufbuild/protocompile/ast"
"github.com/bufbuild/protocompile/parser"
"github.com/bufbuild/protocompile/reporter"
)
// Stats represents some statistics about one or more Protobuf files.
//
// Note that as opposed to most structs in this codebase, we do not omitempty for
// the fields for JSON or YAML.
type Stats struct {
NumFiles int `json:"num_files" yaml:"num_files"`
NumPackages int `json:"num_packages" yaml:"num_packages"`
NumFilesWithSyntaxErrors int `json:"num_files_with_syntax_errors" yaml:"num_files_with_syntax_errors"`
NumMessages int `json:"num_messages" yaml:"num_messages"`
NumFields int `json:"num_fields" yaml:"num_fields"`
NumEnums int `json:"num_enums" yaml:"num_enums"`
NumEnumValues int `json:"num_enum_values" yaml:"num_enum_values"`
NumExtensions int `json:"num_extensions" yaml:"num_extensions"`
NumServices int `json:"num_services" yaml:"num_services"`
NumMethods int `json:"num_methods" yaml:"num_methods"`
}
// FileWalker goes through all .proto files for GetStats.
type FileWalker interface {
// Walk will invoke f for all .proto files for GetStats.
Walk(ctx context.Context, f func(io.Reader) error) error
}
// GetStats gathers some simple statistics about a set of Protobuf files.
//
// See the packages protostatos and protostatstorage for helpers for the
// os and storage packages.
func GetStats(ctx context.Context, fileWalker FileWalker) (*Stats, error) {
handler := reporter.NewHandler(
reporter.NewReporter(
func(reporter.ErrorWithPos) error {
// never aborts
return nil
},
nil,
),
)
statsBuilder := newStatsBuilder()
if err := fileWalker.Walk(
ctx,
func(file io.Reader) error {
// This can return an error and non-nil AST.
// We do not need the filePath because we do not report errors.
astRoot, err := parser.Parse("", file, handler)
if astRoot == nil {
// No AST implies an I/O error trying to read the
// file contents. No stats to collect.
return err
}
if err != nil {
// There was a syntax error, but we still have a partial
// AST we can examine.
statsBuilder.NumFilesWithSyntaxErrors++
}
examineFile(statsBuilder, astRoot)
return nil
},
); err != nil {
return nil, err
}
statsBuilder.NumPackages = len(statsBuilder.packages)
return statsBuilder.Stats, nil
}
// MergeStats merged multiple stats objects into one single Stats object.
//
// A new object is returned.
func MergeStats(statsSlice ...*Stats) *Stats {
resultStats := &Stats{}
for _, stats := range statsSlice {
resultStats.NumFiles += stats.NumFiles
resultStats.NumPackages += stats.NumPackages
resultStats.NumFilesWithSyntaxErrors += stats.NumFilesWithSyntaxErrors
resultStats.NumMessages += stats.NumMessages
resultStats.NumFields += stats.NumFields
resultStats.NumEnums += stats.NumEnums
resultStats.NumEnumValues += stats.NumEnumValues
resultStats.NumExtensions += stats.NumExtensions
resultStats.NumServices += stats.NumServices
resultStats.NumMethods += stats.NumMethods
}
return resultStats
}
type statsBuilder struct {
*Stats
packages map[ast.Identifier]struct{}
}
func newStatsBuilder() *statsBuilder {
return &statsBuilder{
Stats: &Stats{},
packages: make(map[ast.Identifier]struct{}),
}
}
func examineFile(statsBuilder *statsBuilder, fileNode *ast.FileNode) {
statsBuilder.NumFiles++
for _, decl := range fileNode.Decls {
switch decl := decl.(type) {
case *ast.PackageNode:
statsBuilder.packages[decl.Name.AsIdentifier()] = struct{}{}
case *ast.MessageNode:
examineMessage(statsBuilder, &decl.MessageBody)
case *ast.EnumNode:
examineEnum(statsBuilder, decl)
case *ast.ExtendNode:
examineExtend(statsBuilder, decl)
case *ast.ServiceNode:
statsBuilder.NumServices++
for _, decl := range decl.Decls {
_, ok := decl.(*ast.RPCNode)
if ok {
statsBuilder.NumMethods++
}
}
}
}
}
func examineMessage(statsBuilder *statsBuilder, messageBody *ast.MessageBody) {
statsBuilder.NumMessages++
for _, decl := range messageBody.Decls {
switch decl := decl.(type) {
case *ast.FieldNode, *ast.MapFieldNode:
statsBuilder.NumFields++
case *ast.GroupNode:
statsBuilder.NumFields++
examineMessage(statsBuilder, &decl.MessageBody)
case *ast.OneOfNode:
for _, ooDecl := range decl.Decls {
switch ooDecl := ooDecl.(type) {
case *ast.FieldNode:
statsBuilder.NumFields++
case *ast.GroupNode:
statsBuilder.NumFields++
examineMessage(statsBuilder, &ooDecl.MessageBody)
}
}
case *ast.MessageNode:
examineMessage(statsBuilder, &decl.MessageBody)
case *ast.EnumNode:
examineEnum(statsBuilder, decl)
case *ast.ExtendNode:
examineExtend(statsBuilder, decl)
}
}
}
func examineEnum(statsBuilder *statsBuilder, enumNode *ast.EnumNode) {
statsBuilder.NumEnums++
for _, decl := range enumNode.Decls {
_, ok := decl.(*ast.EnumValueNode)
if ok {
statsBuilder.NumEnumValues++
}
}
}
func examineExtend(statsBuilder *statsBuilder, extendNode *ast.ExtendNode) {
for _, decl := range extendNode.Decls {
switch decl := decl.(type) {
case *ast.FieldNode:
statsBuilder.NumExtensions++
case *ast.GroupNode:
statsBuilder.NumExtensions++
examineMessage(statsBuilder, &decl.MessageBody)
}
}
}