blob: fa4104358adb67459ba715206835bba02eb7fe77 [file] [log] [blame]
// 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"`
}
// PathNameToFullyQualified converts a Path + Name that is used for types and references into a
// fully-qualified C++ name.
//
// TODO(https://fxbug.dev/42070116): This will skip template parameters which are not encoded in the path.
// Implementing this properly will require looking up each USR in the index and getting the template
// parameters. If you can get a QualName (on a Reference) it will contain the template parameters
// and should be used instead.
func PathNameToFullyQualified(path, name string) string {
// The Path uses slash separators.
if len(path) == 0 {
// No scoping.
return name
}
if path == "GlobalNamespace" {
// Clang-doc generates a "GlobalNamespace" at the toplevel.
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
}
type Reference struct {
Type string `yaml:"Type"` // e.g. "Namespace", "Record".
Name string `yaml:"Name"`
QualName string `yaml:"QualName"`
USR string `yaml:"USR"`
Path string `yaml:"Path"`
IsInGlobalNamespace bool `yaml:"IsInGlobalNamespace"`
}
// Makes a simple reference with no USR (useful for tests).
func MakeReference(name string, qualName string, path string) Reference {
return Reference{
Name: name,
QualName: qualName,
Path: path,
IsInGlobalNamespace: len(path) == 0,
}
}
func MakeGlobalReference(name string) Reference {
return MakeReference(name, name, "")
}
type Type struct {
Reference Reference `yaml:"Type"`
}
func MakeType(name string, qualName string, path string) Type {
return Type{Reference: MakeReference(name, qualName, path)}
}
func MakeGlobalType(name string) Type {
return MakeType(name, 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"`
}
type TemplateParamInfo struct {
Contents string `yaml:"Contents"`
}
type TemplateSpecializationInfo struct {
SpecializationOf string `yaml:"SpecializationOf"`
Params []TemplateParamInfo `yaml:"Params"`
}
type TemplateInfo struct {
Params []TemplateParamInfo `yaml:"Params"`
// Will be null if this is not a specialization.
Specialization *TemplateSpecializationInfo `yaml:"Specialization"`
}
// 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"`
TypeRef Reference `yaml:"Type"`
DefaultValue string `yaml:"DefaultValue"`
}
type MemberTypeInfo struct {
Name string `yaml:"Name"`
TypeRef Reference `yaml:"Type"`
FullTypeName string `yaml:"FullTypeName"`
Access string `yaml:"Access"`
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"
}
type FunctionInfo struct {
USR string `yaml:"USR"`
Name string `yaml:"Name"`
// See RecordInfo.Namespace for documentation.
Namespace []Reference `yaml:"Namespace"`
// The declaration location will be empty if there is no separate definition location
// (inlined functions or functions not forward-defined). See GetLocation() below.
DeclLocations []Location `yaml:"Location"`
DefLocation Location `yaml:"DefLocation"`
Description []CommentInfo `yaml:"Description"`
Params []FieldTypeInfo `yaml:"Params"`
ReturnType Type `yaml:"ReturnType"`
IsMethod bool `yaml:"IsMethod"`
Access string `yaml:"Access"` // Public, Private, Protected.
Template *TemplateInfo `yaml:"Template"` // Null for non-templates.
}
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"
}
// Used for getting the canonical location of the function, returns the first declaration location
// or the definition location.
func (f FunctionInfo) GetLocation() Location {
if len(f.DeclLocations) == 0 {
return f.DefLocation
}
return f.DeclLocations[0]
}
// 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 {
result := f.ReturnType.Reference.QualName + " " + f.Name
// Template specialization args.
if f.Template != nil && f.Template.Specialization != nil {
for i, param := range f.Template.Specialization.Params {
if i > 0 {
result += ", "
}
result += param.Contents
}
}
// Parameter types.
result += "("
for i, param := range f.Params {
if i >= 1 {
result += ", "
}
result += param.TypeRef.QualName
}
result += ")"
return result
}
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 Type `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"`
Members []MemberTypeInfo `yaml:"Members"`
Template *TemplateInfo `yaml:"Template"` // Null for non-templates.
// |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"`
ChildTypedefs []*TypedefInfo `yaml:"ChildTypedefs"`
// 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"`
}
type TypedefInfo struct {
USR string `yaml:"USR"`
Name string `yaml:"Name"`
Namespace []Reference `yaml:"Namespace"`
Description []CommentInfo `yaml:"Description"`
DefLocation Location `yaml:"DefLocation"`
Underlying Reference `yaml:"Underlying"`
IsUsing bool `yaml:"IsUsing"` // False means "typedef".
}
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"`
ChildTypedefs []*TypedefInfo `yaml:"ChildTypedefs"`
}
// 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, filename string) *RecordInfo {
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 records reference other files in the same directory, named according to the unique
// USR key.
for _, c := range r.ChildRecordRefs {
r.ChildRecords = append(r.ChildRecords, LoadRecord(reader, c.USR+".yaml"))
}
return r
}
// LoadNamespace loads everything in the given namespace from the given file.
func LoadNamespace(reader fileReader, filename string) *NamespaceInfo {
content, err := reader.ReadFile(filename)
if os.IsNotExist(err) {
// The file may not exist if the child namespace has no additional attributes
if Debug {
log.Printf("WARNING: %v.\n", err)
}
}
ns := &NamespaceInfo{}
err = yaml.Unmarshal(content, ns)
if err != nil {
log.Fatalf("error: %v in file %v", err, filename)
}
// The child namespaces and records reference other files in the same directory, named
// according to the unique USR key.
for _, c := range ns.ChildNamespaceRefs {
ns.ChildNamespaces = append(ns.ChildNamespaces,
LoadNamespace(reader, c.USR+".yaml"))
}
for _, c := range ns.ChildRecordRefs {
ns.ChildRecords = append(ns.ChildRecords,
LoadRecord(reader, c.USR+".yaml"))
}
return ns
}
func loadWithReader(reader fileReader) *NamespaceInfo {
return LoadNamespace(reader, "index.yaml")
}
// 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)
}