blob: 1428fa8a368ded21e4678cc13a0685671cffc2a6 [file] [log] [blame] [edit]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Note: The names of the structs in this package match the corresponding clang-doc class names.
package clangdoc
import (
"archive/zip"
"io"
"log"
"os"
"path"
"strings"
"gopkg.in/yaml.v2"
)
// Debug can be set to true to add more logging information.
// Or the logging could be migrated to use glog, and make use of Google logging
// flags.
var Debug bool
type Location struct {
LineNumber int `yaml:"LineNumber"`
Filename string `yaml:"Filename"`
}
type Type struct {
Name string `yaml:"Name"`
// Namespace info. This uses slash separators?!?!
Path string `yaml:"Path"`
}
// PathNameToFullyQualified converts a Path + Name that is used for types and references into a
// fully-qualified C++ name.
func PathNameToFullyQualified(path, name string) string {
// The Path seems to use slash separators?!?!?
if len(path) == 0 {
// No scoping.
return name
}
// Replace "std::__2" prefixes which are standard library versioning stuff that the user
// doesn't want to see.
if strings.HasPrefix(path, "std/__2") {
path = strings.Replace(path, "std/__2", "std", 1)
}
return strings.ReplaceAll(path, "/", "::") + "::" + name
}
// QualifiedName returns a fully-qualified name for the type that takes into account the Path.
func (t Type) QualifiedName() string {
return PathNameToFullyQualified(t.Path, t.Name)
}
type CommentInfo struct {
Kind string `yaml:"Kind"`
Text string `yaml:"Text"`
Name string `yaml:"Name"`
Direction string `yaml:"Direction"` // in/out for parameter comments
ParamName string `yaml:"ParamName"`
CloseName string `yaml:"CloseName"`
SelfClosing bool `yaml:"SelfClosing"`
Explicit bool `yaml:"Explicit"`
Args string `yaml:"Args"`
AttrKeys []string `yaml:"AttrKeys"`
AttrValues []string `yaml:"AttrValues"`
Children []CommentInfo `yaml:"Children"`
}
// FieldTypeInfo is a field with a name and a type. It is used for function parameters. See also
// MemberTypeInfo which adds an access tag (basically inheritance).
type FieldTypeInfo struct {
Name string `yaml:"Name"`
Type Type `yaml:"Type"`
DefaultValue string `yaml:"DefaultValue"`
}
type MemberTypeInfo struct {
Name string `yaml:"Name"`
Type Type `yaml:"Type"`
Access string `yaml:"Access"`
// This being present depends on Brett's unlanded clang-doc change.
Description []CommentInfo `yaml:"Description"`
}
func (m MemberTypeInfo) IsPublic() bool {
return m.Access == "Public"
}
func (m MemberTypeInfo) IsPrivate() bool {
// Clang-doc seems to omit access for private members.
return len(m.Access) == 0 || m.Access == "Private"
}
func (m MemberTypeInfo) IsProtected() bool {
return m.Access == "Protected"
}
// ReturnType is a struct which clang-doc doesn't have but it inserts another layer of indirection
// in the YAML file which is modeled with this struct.
type ReturnType struct {
Type Type `yaml:"Type"`
}
type FunctionInfo struct {
USR string `yaml:"USR"`
Name string `yaml:"Name"`
// See RecordInfo.Namespace for documentation.
Namespace []Reference `yaml:"Namespace"`
DefLocation Location `yaml:"DefLocation"` // Definition location
Description []CommentInfo `yaml:"Description"`
Location []Location `yaml:"Location"` // Declaration locations.
Params []FieldTypeInfo `yaml:"Params"`
ReturnType ReturnType `yaml:"ReturnType"`
IsMethod bool `yaml:"IsMethod"`
Access string `yaml:"Access"` // Public, Private, Protected.
}
func (f FunctionInfo) IsPublic() bool {
return f.Access == "Public"
}
func (f FunctionInfo) IsPrivate() bool {
// Clang-doc seems to omit access for private members.
return len(f.Access) == 0 || f.Access == "Private"
}
func (f FunctionInfo) IsProtected() bool {
return f.Access == "Protected"
}
// IdentityKey returns a string which represents the function identity, such that string comparisons
// between functions on their identity keys is sufficient for determining whether two functions are
// the same.
//
// This uses the name, return type, and parameter types. It does not include parameter names nor
// member information (this is used to see if a a function has been covered by a base class so
// we explicitly don't want the enclosing class information).
func (f FunctionInfo) IdentityKey() string {
var result string
if len(f.ReturnType.Type.Path) > 0 {
result += f.ReturnType.Type.Path + "::"
}
result += f.ReturnType.Type.Name + " "
result += f.Name + "("
for i, param := range f.Params {
if i >= 1 {
result += ", "
}
if len(param.Type.Path) > 0 {
result += param.Type.Path + "::"
}
result += param.Type.Name
}
result += ")"
return result
}
type Reference struct {
Type string `yaml:"Type"`
Name string `yaml:"Name"`
USR string `yaml:"USR"`
Path string `yaml:"Path"`
IsInGlobalNamespace bool `yaml:"IsInGlobalNamespace"`
}
// Like ReturnType, this is a struct which clang-doc doesn't have but it inserts another layer of
// indirection in the YAML file which is modeled with this struct.
type BaseType struct {
Type Type `yaml:"Type"`
}
type EnumValueInfo struct {
Name string `yaml:"Name"`
Value string `yaml:"Value"`
Expr string `yaml:"Expr"`
}
type EnumInfo struct {
USR string `yaml:"USR"`
Name string `yaml:"Name"`
Namespace []Reference `yaml:"Namespace"`
DefLocation Location `yaml:"DefLocation"`
Description []CommentInfo `yaml:"Description"`
Scoped bool `yaml:"Scoped"` // True for an enum class.
BaseType BaseType `yaml:"BaseType"` // Defined for explicitly typed enums.
Members []EnumValueInfo `yaml:"Members"`
}
type RecordInfo struct {
Name string `yaml:"Name"`
USR string `yaml:"USR"`
Path string `yaml:"Path"`
// This will be an array of the path to the struct definition. It will include child
// structs and not just namespaces.
//
// The order is going from the namespace closest to the record and moving outward, so to
// reconstruct the C++ name you would iterate in reverse.
//
// [ { Type: "Record", Name: "Outer" },
// { Type: "Namespace", Name: "ns" } ]
//
// Clang-doc generates "GlobalNamespace"-named namespaces for records in the global
// namespace. This seems incorrect but was added for the way some other backends work:
// https://reviews.llvm.org/D66298
//
// If using this to generate a name, "GlobalNamespace" will need to be removed manually.
Namespace []Reference `yaml:"Namespace"`
DefLocation Location `yaml:"DefLocation"`
Description []CommentInfo `yaml:"Description"`
TagType string `yaml:"TagType"` // TODO(brettw) missing in current clang-doc output.
Members []MemberTypeInfo `yaml:"Members"`
// |Bases| recursively lists all base classes as a way to list the inherited functions.
// See also |Parents| and |VirtualParents|.
Bases []*RecordInfo `yaml:"Bases"`
// The classes that this class derives from directly. Contrast to "Bases" which lists all
// base classes recursively.
Parents []Reference `yaml:"Parents"`
VirtualParents []Reference `yaml:"VirtualParents"`
ChildRecordRefs []Reference `yaml:"ChildRecords"`
ChildRecords []*RecordInfo
ChildFunctions []*FunctionInfo `yaml:"ChildFunctions"`
ChildEnums []*EnumInfo `yaml:"ChildEnums"`
// When this record is a base class, these items hold the derived information.
Access string `yaml:"Access"`
IsVirtual bool `yaml:"IsVirtual"`
// I'm not sure what this means, I suspect this is set when the class also appears in the
// Parents or VirtualParents list.
IsParent bool `yaml:"IsParent"`
}
func (r RecordInfo) IsConstructor(f *FunctionInfo) bool {
return r.Name == f.Name
}
func (r RecordInfo) IsDestructor(f *FunctionInfo) bool {
return "~"+r.Name == f.Name
}
type NamespaceInfo struct {
Name string `yaml:"Name"`
USR string `yaml:"USR"`
// "Refs" versions expanded by LoadNamespace().
ChildNamespaceRefs []Reference `yaml:"ChildNamespaces"`
ChildNamespaces []*NamespaceInfo
ChildRecordRefs []Reference `yaml:"ChildRecords"`
ChildRecords []*RecordInfo
ChildFunctions []*FunctionInfo `yaml:"ChildFunctions"`
ChildEnums []*EnumInfo `yaml:"ChildEnums"`
}
// Abstracts how to read files from the input.
type fileReader interface {
// The input names is relative to the root of where the clang-doc outputs are stored. It
// may begin with a slash (this still means relative) so that calling code can always
// prepend a slash when concatenating paths without worrying about whether intermediate
// directories are present.
ReadFile(name string) ([]byte, error)
}
// Reader interface implementation for zipped clang-doc outputs.
type zipInput struct {
reader *zip.Reader
}
func (z zipInput) ReadFile(name string) ([]byte, error) {
if name != "" && name[0] == '/' {
name = name[1:]
}
file, err := z.reader.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, err
}
size := info.Size()
data := make([]byte, 0, size+1)
for {
if len(data) >= cap(data) {
d := append(data[:cap(data)], 0)
data = d[:len(data)]
}
n, err := file.Read(data[len(data):cap(data)])
data = data[:len(data)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return data, err
}
}
}
// Reader interface implementation for a regular directory on disk.
type dirInput struct {
dir string
}
func (d dirInput) ReadFile(name string) ([]byte, error) {
return os.ReadFile(path.Join(d.dir, name))
}
func LoadRecord(reader fileReader, subdir string, rec Reference) *RecordInfo {
// Record (structs, classes, etc.) are stored in a file in the same directory
// with the name of the record. Anonymous records are named by the USR id.
var recname string
if len(rec.Name) == 0 {
recname = "@nonymous_record_" + rec.USR
} else {
recname = rec.Name
}
filename := path.Join(subdir, recname+".yaml")
content, err := reader.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
r := &RecordInfo{}
err = yaml.Unmarshal(content, &r)
if err != nil {
log.Fatalf("error: %v in file %v", err, filename)
}
// Child structs are in a subdirectory with the current struct name.
for _, c := range r.ChildRecordRefs {
r.ChildRecords = append(r.ChildRecords, LoadRecord(reader, c.Path, c))
}
return r
}
// LoadNamespace loads everything in the given namespace.
//
// |dir| is the name of the namespace to load, and |child_ns_dir| is the directory that contains the
// child namespaces of this one.
//
// The global namespace stores its items in a "GlobalNamespace" directory. Child namespaces are
// siblings of this directory if the `IsInGlobalNamespace` attribute is set. Otherwise, the
// directory is described by the `Path` attribute.
func LoadNamespace(reader fileReader, subdir string) *NamespaceInfo {
filename := path.Join(subdir, "index.yaml")
content, err := reader.ReadFile(filename)
if os.IsNotExist(err) {
// index.yaml may not exist if the child namespace has no additional attributes
if Debug {
log.Printf("WARNING: %v.\n", err)
}
// Return an empty namespace object
return &NamespaceInfo{}
} else if err != nil {
log.Fatal(err)
}
ns := &NamespaceInfo{}
err = yaml.Unmarshal(content, ns)
if err != nil {
log.Fatalf("error: %v in file %v", err, filename)
}
for _, c := range ns.ChildNamespaceRefs {
var child_path string
if c.IsInGlobalNamespace {
child_path = c.Name
} else {
child_path = path.Join(c.Path, c.Name)
}
ns.ChildNamespaces = append(ns.ChildNamespaces, LoadNamespace(reader, child_path))
}
for _, c := range ns.ChildRecordRefs {
ns.ChildRecords = append(ns.ChildRecords, LoadRecord(reader, c.Path, c))
}
return ns
}
func loadWithReader(reader fileReader) *NamespaceInfo {
return LoadNamespace(reader, "GlobalNamespace")
}
// Returns the root namespace. All other namespaces will be inside of this.
func LoadDir(dir string) *NamespaceInfo {
reader := dirInput{dir}
return loadWithReader(reader)
}
// Returns the root namespace. All other namespaces will be inside of this.
func LoadZip(zipFile string) *NamespaceInfo {
reader, err := zip.OpenReader(zipFile)
if err != nil {
log.Fatal(err)
}
defer reader.Close()
z := zipInput{&reader.Reader}
return loadWithReader(z)
}