| // 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) |
| } |