platform_build_blueprint/bootstrap/bpdoc/properties.go
Spandan Das 1109cd96b7 bpdocs for struct types created using reflection
struct types defined using reflection have empty PkgPaths(), due to
which they were not added to the generated bpdocs. This CL allows
us to generate bpdocs for such struct types.

For each field in the struct, the algorithm checks the following and
creates a PropertyStruct object accordingly
1. If field is a primitive type (Base condition)
2. If field is another exported struct (Base condition)
3. If field is another struct created using reflection (Recurse)

Test: m soong_docs
Test: java_sdk_library_import before
https://ci.android.com/builds/submitted/7710820/linux/latest/view/java.html#java_sdk_library_import
and after
https://spandandas.users.x20web.corp.google.com/docs/java.html#java_sdk_library_import
Bug: 172797653

Change-Id: I0349e405fd290d427fa0b38a109b4212aace50c6
2021-09-20 22:33:05 +00:00

332 lines
7.4 KiB
Go

// Copyright 2019 Google Inc. All rights reserved.
//
// Licensed 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 bpdoc
import (
"fmt"
"go/ast"
"go/doc"
"html/template"
"reflect"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/google/blueprint/proptools"
)
//
// Utility functions for PropertyStruct and Property
//
func (ps *PropertyStruct) Clone() *PropertyStruct {
ret := *ps
ret.Properties = append([]Property(nil), ret.Properties...)
for i, prop := range ret.Properties {
ret.Properties[i] = prop.Clone()
}
return &ret
}
func (p *Property) Clone() Property {
ret := *p
ret.Properties = append([]Property(nil), ret.Properties...)
for i, prop := range ret.Properties {
ret.Properties[i] = prop.Clone()
}
return ret
}
func (p *Property) Equal(other Property) bool {
return p.Name == other.Name && p.Type == other.Type && p.Tag == other.Tag &&
p.Text == other.Text && p.Default == other.Default &&
stringArrayEqual(p.OtherNames, other.OtherNames) &&
htmlArrayEqual(p.OtherTexts, other.OtherTexts) &&
p.SameSubProperties(other)
}
func (ps *PropertyStruct) SetDefaults(defaults reflect.Value) {
setDefaults(ps.Properties, defaults)
}
func setDefaults(properties []Property, defaults reflect.Value) {
for i := range properties {
prop := &properties[i]
fieldName := proptools.FieldNameForProperty(prop.Name)
f := defaults.FieldByName(fieldName)
if (f == reflect.Value{}) {
panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
}
if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
continue
}
if f.Kind() == reflect.Interface {
f = f.Elem()
}
if f.Kind() == reflect.Ptr {
if f.IsNil() {
continue
}
f = f.Elem()
}
if f.Kind() == reflect.Struct {
setDefaults(prop.Properties, f)
} else {
prop.Default = fmt.Sprintf("%v", f.Interface())
}
}
}
func stringArrayEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func htmlArrayEqual(a, b []template.HTML) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func (p *Property) SameSubProperties(other Property) bool {
if len(p.Properties) != len(other.Properties) {
return false
}
for i := range p.Properties {
if !p.Properties[i].Equal(other.Properties[i]) {
return false
}
}
return true
}
func (ps *PropertyStruct) GetByName(name string) *Property {
return getByName(name, "", &ps.Properties)
}
func (ps *PropertyStruct) Nest(nested *PropertyStruct) {
ps.Properties = nestUnique(ps.Properties, nested.Properties)
}
// Adds a target element to src if it does not exist in src
func nestUnique(src []Property, target []Property) []Property {
var ret []Property
ret = append(ret, src...)
for _, elem := range target {
isUnique := true
for _, retElement := range ret {
if elem.Equal(retElement) {
isUnique = false
break
}
}
if isUnique {
ret = append(ret, elem)
}
}
return ret
}
func getByName(name string, prefix string, props *[]Property) *Property {
for i := range *props {
if prefix+(*props)[i].Name == name {
return &(*props)[i]
} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
}
}
return nil
}
func (p *Property) Nest(nested *PropertyStruct) {
p.Properties = nestUnique(p.Properties, nested.Properties)
}
func (p *Property) SetAnonymous() {
p.Anonymous = true
}
func newPropertyStruct(t *doc.Type) (*PropertyStruct, error) {
typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
ps := PropertyStruct{
Name: t.Name,
Text: t.Doc,
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return nil, fmt.Errorf("type of %q is not a struct", t.Name)
}
var err error
ps.Properties, err = structProperties(structType)
if err != nil {
return nil, err
}
return &ps, nil
}
func structProperties(structType *ast.StructType) (props []Property, err error) {
for _, f := range structType.Fields.List {
names := f.Names
if names == nil {
// Anonymous fields have no name, use the type as the name
// TODO: hide the name and make the properties show up in the embedding struct
if t, ok := f.Type.(*ast.Ident); ok {
names = append(names, t)
}
}
for _, n := range names {
var name, tag, text string
if n != nil {
name = proptools.PropertyNameForField(n.Name)
}
if f.Doc != nil {
text = f.Doc.Text()
}
if f.Tag != nil {
tag, err = strconv.Unquote(f.Tag.Value)
if err != nil {
return nil, err
}
}
typ, innerProps, err := getType(f.Type)
if err != nil {
return nil, err
}
props = append(props, Property{
Name: name,
Type: typ,
Tag: reflect.StructTag(tag),
Text: formatText(text),
Properties: innerProps,
})
}
}
return props, nil
}
func getType(expr ast.Expr) (typ string, innerProps []Property, err error) {
var t ast.Expr
if star, ok := expr.(*ast.StarExpr); ok {
t = star.X
} else {
t = expr
}
switch a := t.(type) {
case *ast.ArrayType:
var elt string
elt, innerProps, err = getType(a.Elt)
if err != nil {
return "", nil, err
}
typ = "list of " + elt
case *ast.InterfaceType:
typ = "interface"
case *ast.Ident:
typ = a.Name
case *ast.StructType:
innerProps, err = structProperties(a)
if err != nil {
return "", nil, err
}
default:
typ = fmt.Sprintf("%T", expr)
}
return typ, innerProps, nil
}
func (ps *PropertyStruct) ExcludeByTag(key, value string) {
filterPropsByTag(&ps.Properties, key, value, true)
}
func (ps *PropertyStruct) IncludeByTag(key, value string) {
filterPropsByTag(&ps.Properties, key, value, false)
}
func filterPropsByTag(props *[]Property, key, value string, exclude bool) {
// Create a slice that shares the storage of props but has 0 length. Appending up to
// len(props) times to this slice will overwrite the original slice contents
filtered := (*props)[:0]
for _, x := range *props {
if hasTag(x.Tag, key, value) == !exclude {
filterPropsByTag(&x.Properties, key, value, exclude)
filtered = append(filtered, x)
}
}
*props = filtered
}
func hasTag(tag reflect.StructTag, key, value string) bool {
for _, entry := range strings.Split(tag.Get(key), ",") {
if entry == value {
return true
}
}
return false
}
func formatText(text string) template.HTML {
var html template.HTML
lines := strings.Split(text, "\n")
preformatted := false
for _, line := range lines {
r, _ := utf8.DecodeRuneInString(line)
indent := unicode.IsSpace(r)
if indent && !preformatted {
html += "<pre>\n\n"
preformatted = true
} else if !indent && line != "" && preformatted {
html += "</pre>\n"
preformatted = false
}
html += template.HTML(template.HTMLEscapeString(line)) + "\n"
}
if preformatted {
html += "</pre>\n"
}
return html
}