1// Copyright 2019 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package bpdoc
16
17import (
18	"fmt"
19	"go/ast"
20	"go/doc"
21	"html/template"
22	"reflect"
23	"slices"
24	"strconv"
25	"strings"
26	"unicode"
27	"unicode/utf8"
28
29	"github.com/google/blueprint/proptools"
30)
31
32//
33// Utility functions for PropertyStruct and Property
34//
35
36func (ps *PropertyStruct) Clone() *PropertyStruct {
37	ret := *ps
38	ret.Properties = slices.Clone(ret.Properties)
39	for i, prop := range ret.Properties {
40		ret.Properties[i] = prop.Clone()
41	}
42
43	return &ret
44}
45
46func (p *Property) Clone() Property {
47	ret := *p
48	ret.Properties = slices.Clone(ret.Properties)
49	for i, prop := range ret.Properties {
50		ret.Properties[i] = prop.Clone()
51	}
52
53	return ret
54}
55
56func (p *Property) Equal(other Property) bool {
57	return p.Name == other.Name && p.Type == other.Type && p.Tag == other.Tag &&
58		p.Text == other.Text && p.Default == other.Default &&
59		stringArrayEqual(p.OtherNames, other.OtherNames) &&
60		htmlArrayEqual(p.OtherTexts, other.OtherTexts) &&
61		p.SameSubProperties(other)
62}
63
64func (ps *PropertyStruct) SetDefaults(defaults reflect.Value) {
65	setDefaults(ps.Properties, defaults)
66}
67
68func setDefaults(properties []Property, defaults reflect.Value) {
69	for i := range properties {
70		prop := &properties[i]
71		fieldName := proptools.FieldNameForProperty(prop.Name)
72		f := defaults.FieldByName(fieldName)
73		if (f == reflect.Value{}) {
74			panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
75		}
76
77		if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
78			continue
79		}
80
81		if f.Kind() == reflect.Interface {
82			f = f.Elem()
83		}
84
85		if f.Kind() == reflect.Ptr {
86			if f.IsNil() {
87				continue
88			}
89			f = f.Elem()
90		}
91
92		if f.Kind() == reflect.Struct {
93			setDefaults(prop.Properties, f)
94		} else {
95			prop.Default = fmt.Sprintf("%v", f.Interface())
96		}
97	}
98}
99
100func stringArrayEqual(a, b []string) bool {
101	if len(a) != len(b) {
102		return false
103	}
104
105	for i := range a {
106		if a[i] != b[i] {
107			return false
108		}
109	}
110
111	return true
112}
113
114func htmlArrayEqual(a, b []template.HTML) bool {
115	if len(a) != len(b) {
116		return false
117	}
118
119	for i := range a {
120		if a[i] != b[i] {
121			return false
122		}
123	}
124
125	return true
126}
127
128func (p *Property) SameSubProperties(other Property) bool {
129	if len(p.Properties) != len(other.Properties) {
130		return false
131	}
132
133	for i := range p.Properties {
134		if !p.Properties[i].Equal(other.Properties[i]) {
135			return false
136		}
137	}
138
139	return true
140}
141
142func (ps *PropertyStruct) GetByName(name string) *Property {
143	return getByName(name, "", &ps.Properties)
144}
145
146func (ps *PropertyStruct) Nest(nested *PropertyStruct) {
147	ps.Properties = nestUnique(ps.Properties, nested.Properties)
148}
149
150// Adds a target element to src if it does not exist in src
151func nestUnique(src []Property, target []Property) []Property {
152	var ret []Property
153	ret = append(ret, src...)
154	for _, elem := range target {
155		isUnique := true
156		for _, retElement := range ret {
157			if elem.Equal(retElement) {
158				isUnique = false
159				break
160			}
161		}
162		if isUnique {
163			ret = append(ret, elem)
164		}
165	}
166	return ret
167}
168
169func getByName(name string, prefix string, props *[]Property) *Property {
170	for i := range *props {
171		if prefix+(*props)[i].Name == name {
172			return &(*props)[i]
173		} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
174			return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
175		}
176	}
177	return nil
178}
179
180func (p *Property) Nest(nested *PropertyStruct) {
181	p.Properties = nestUnique(p.Properties, nested.Properties)
182}
183
184func (p *Property) SetAnonymous() {
185	p.Anonymous = true
186}
187
188func newPropertyStruct(t *doc.Type) (*PropertyStruct, error) {
189	typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
190	ps := PropertyStruct{
191		Name: t.Name,
192		Text: t.Doc,
193	}
194
195	structType, ok := typeSpec.Type.(*ast.StructType)
196	if !ok {
197		return nil, fmt.Errorf("type of %q is not a struct", t.Name)
198	}
199
200	var err error
201	ps.Properties, err = structProperties(structType)
202	if err != nil {
203		return nil, err
204	}
205
206	return &ps, nil
207}
208
209func structProperties(structType *ast.StructType) (props []Property, err error) {
210	for _, f := range structType.Fields.List {
211		names := f.Names
212		if names == nil {
213			// Anonymous fields have no name, use the type as the name
214			// TODO: hide the name and make the properties show up in the embedding struct
215			if t, ok := f.Type.(*ast.Ident); ok {
216				names = append(names, t)
217			}
218		}
219		for _, n := range names {
220			var name, tag, text string
221			if n != nil {
222				name = proptools.PropertyNameForField(n.Name)
223			}
224			if f.Doc != nil {
225				text = f.Doc.Text()
226			}
227			if f.Tag != nil {
228				tag, err = strconv.Unquote(f.Tag.Value)
229				if err != nil {
230					return nil, err
231				}
232			}
233			typ, innerProps, err := getType(f.Type)
234			if err != nil {
235				return nil, err
236			}
237
238			props = append(props, Property{
239				Name:       name,
240				Type:       typ,
241				Tag:        reflect.StructTag(tag),
242				Text:       formatText(text),
243				Properties: innerProps,
244			})
245		}
246	}
247
248	return props, nil
249}
250
251func getType(expr ast.Expr) (typ string, innerProps []Property, err error) {
252	var t ast.Expr
253	if star, ok := expr.(*ast.StarExpr); ok {
254		t = star.X
255	} else {
256		t = expr
257	}
258	switch a := t.(type) {
259	case *ast.ArrayType:
260		var elt string
261		elt, innerProps, err = getType(a.Elt)
262		if err != nil {
263			return "", nil, err
264		}
265		typ = "list of " + elt
266	case *ast.InterfaceType:
267		typ = "interface"
268	case *ast.Ident:
269		typ = a.Name
270	case *ast.StructType:
271		innerProps, err = structProperties(a)
272		if err != nil {
273			return "", nil, err
274		}
275	case *ast.IndexExpr:
276		// IndexExpr is used to represent generic type arguments
277		if !isConfigurableAst(a.X) {
278			var writer strings.Builder
279			if err := ast.Fprint(&writer, nil, expr, nil); err != nil {
280				return "", nil, err
281			}
282			return "", nil, fmt.Errorf("unknown type %s", writer.String())
283		}
284		var innerType string
285		innerType, innerProps, err = getType(a.Index)
286		if err != nil {
287			return "", nil, err
288		}
289		typ = "configurable " + innerType
290	default:
291		typ = fmt.Sprintf("%T", expr)
292	}
293
294	return typ, innerProps, nil
295}
296
297func isConfigurableAst(expr ast.Expr) bool {
298	switch e := expr.(type) {
299	case *ast.Ident:
300		return e.Name == "Configurable"
301	case *ast.SelectorExpr:
302		if l, ok := e.X.(*ast.Ident); ok && l.Name == "proptools" {
303			if e.Sel.Name == "Configurable" {
304				return true
305			}
306		}
307	}
308	return false
309}
310
311func (ps *PropertyStruct) ExcludeByTag(key, value string) {
312	filterPropsByTag(&ps.Properties, key, value, true)
313}
314
315func (ps *PropertyStruct) IncludeByTag(key, value string) {
316	filterPropsByTag(&ps.Properties, key, value, false)
317}
318
319func filterPropsByTag(props *[]Property, key, value string, exclude bool) {
320	// Create a slice that shares the storage of props but has 0 length.  Appending up to
321	// len(props) times to this slice will overwrite the original slice contents
322	filtered := (*props)[:0]
323	for _, x := range *props {
324		if hasTag(x.Tag, key, value) == !exclude {
325			filterPropsByTag(&x.Properties, key, value, exclude)
326			filtered = append(filtered, x)
327		}
328	}
329
330	*props = filtered
331}
332
333func hasTag(tag reflect.StructTag, key, value string) bool {
334	for _, entry := range strings.Split(tag.Get(key), ",") {
335		if entry == value {
336			return true
337		}
338	}
339	return false
340}
341
342func formatText(text string) template.HTML {
343	var html template.HTML
344	lines := strings.Split(text, "\n")
345	preformatted := false
346	for _, line := range lines {
347		r, _ := utf8.DecodeRuneInString(line)
348		indent := unicode.IsSpace(r)
349		if indent && !preformatted {
350			html += "<pre>\n\n"
351			preformatted = true
352		} else if !indent && line != "" && preformatted {
353			html += "</pre>\n"
354			preformatted = false
355		}
356		html += template.HTML(template.HTMLEscapeString(line)) + "\n"
357	}
358	if preformatted {
359		html += "</pre>\n"
360	}
361	return html
362}
363