master
go 992 lines 23.7 KB
Raw
1 package main
2
3 import (
4 "bytes"
5 "fmt"
6 "go/ast"
7 "go/format"
8 "go/parser"
9 "go/token"
10 "log"
11 "math"
12 "os"
13 "path/filepath"
14 "reflect"
15 "strconv"
16 "strings"
17 "time"
18 "unicode"
19
20 "github.com/netdata/netdata/go/plugins/pkg/confopt"
21 )
22
23 // parseConfigFromGoFile parses a Go config file to extract configuration fields
24 // and the defaults supplied by defaultConfig().
25 func (g *DocGenerator) parseConfigFromGoFile() ([]ConfigField, map[string]any, error) {
26 fset := token.NewFileSet()
27 node, err := parser.ParseFile(fset, g.ConfigFile, nil, parser.ParseComments)
28 if err != nil {
29 return nil, nil, fmt.Errorf("failed to parse Go file: %w", err)
30 }
31
32 g.consts = g.extractConstValues(node)
33
34 var fields []ConfigField
35 var configStruct *ast.StructType
36
37 // Find the Config struct
38 ast.Inspect(node, func(n ast.Node) bool {
39 switch x := n.(type) {
40 case *ast.TypeSpec:
41 if x.Name.Name == "Config" {
42 if structType, ok := x.Type.(*ast.StructType); ok {
43 configStruct = structType
44 return false
45 }
46 }
47 }
48 return true
49 })
50
51 if configStruct != nil {
52 fields = extractFieldsFromStruct(g, configStruct, node.Comments)
53 }
54
55 // Try to parse defaults from init.go
56 defaults := g.parseDefaultsFromInitFile()
57 if defaults == nil {
58 return nil, nil, fmt.Errorf("failed to parse defaultConfig() function from init.go - all configuration fields must have defaults defined")
59 }
60
61 // Validate that ALL fields have defaults and apply them
62 var missingDefaults []string
63 for i := range fields {
64 field := &fields[i]
65 if isAutoBoolType(field.GoType) {
66 field.Type = "string"
67 field.Enum = toStringSlice(confopt.AutoBoolEnum)
68 }
69
70 if defaultValue, exists := defaults[field.Name]; exists {
71 field.Default = normalizeDefaultValue(*field, defaultValue)
72 field.Required = false
73 } else if isAutoBoolType(field.GoType) {
74 field.Default = confopt.AutoBoolAuto.String()
75 field.Required = false
76 } else if field.Pointer {
77 field.Default = "<auto>"
78 field.Required = false
79 } else {
80 missingDefaults = append(missingDefaults, field.Name)
81 }
82
83 assignDefaultUIGroup(field)
84 }
85
86 // FAIL HARD if any field lacks a default
87 if len(missingDefaults) > 0 {
88 return nil, nil, fmt.Errorf("defaultConfig() function must provide default values for ALL configuration fields. Missing defaults for: %v", missingDefaults)
89 }
90
91 return fields, defaults, nil
92 }
93
94 func extractFieldsFromStruct(g *DocGenerator, structType *ast.StructType, comments []*ast.CommentGroup) []ConfigField {
95 var fields []ConfigField
96
97 for _, field := range structType.Fields.List {
98 // Skip embedded fields (like framework.Config)
99 if len(field.Names) == 0 {
100 typeName := extractGoType(field.Type)
101 if typeName == "web.HTTPConfig" {
102 g.hasHTTPConfig = true
103 }
104 continue
105 }
106
107 for _, name := range field.Names {
108 // Skip unexported fields
109 if !ast.IsExported(name.Name) {
110 continue
111 }
112
113 configField := extractConfigField(name.Name, field, comments)
114 if configField != nil {
115 fields = append(fields, *configField)
116 }
117 }
118 }
119
120 return fields
121 }
122
123 func extractConfigField(fieldName string, field *ast.Field, comments []*ast.CommentGroup) *ConfigField {
124 // Extract type information
125 fieldType := extractGoType(field.Type)
126 if fieldType == "" {
127 return nil
128 }
129
130 // Extract struct tags
131 yamlName, jsonName, uiTag := extractStructTags(field.Tag)
132 if yamlName == "" {
133 yamlName = convertToSnakeCase(fieldName)
134 }
135 if jsonName == "" {
136 jsonName = yamlName
137 }
138
139 // Skip fields marked as omitempty only or inline
140 if yamlName == "-" || strings.Contains(yamlName, "inline") {
141 return nil
142 }
143
144 // Convert Go type to JSON Schema type
145 isPointer := strings.HasPrefix(fieldType, "*")
146 jsonType := convertToJSONType(fieldType)
147 var itemsType string
148 if sliceElem, ok := getSliceElementGoType(fieldType); ok {
149 itemsType = convertToJSONType(sliceElem)
150 if itemsType == "array" {
151 itemsType = "string"
152 }
153 }
154
155 // Extract documentation from comments
156 description := extractFieldDescription(fieldName, field, comments)
157 title := formatTitleFromJSONName(jsonName)
158 if title == "" {
159 title = camelToWords(fieldName)
160 }
161
162 // Create field
163 configField := &ConfigField{
164 Name: fieldName,
165 JSONName: jsonName,
166 Type: jsonType,
167 Title: title,
168 Required: !strings.Contains(yamlName, "omitempty"),
169 Description: description,
170 ItemsType: itemsType,
171 Pointer: isPointer,
172 GoType: fieldType,
173 }
174
175 applyUITag(configField, uiTag)
176
177 // Set defaults and constraints based on field name and type
178 setFieldDefaults(configField)
179
180 return configField
181 }
182
183 func extractGoType(expr ast.Expr) string {
184 switch t := expr.(type) {
185 case *ast.Ident:
186 return t.Name
187 case *ast.SelectorExpr:
188 // Handle qualified types like time.Duration
189 if ident, ok := t.X.(*ast.Ident); ok {
190 return ident.Name + "." + t.Sel.Name
191 }
192 case *ast.ArrayType:
193 elemType := extractGoType(t.Elt)
194 if elemType == "" {
195 elemType = "interface{}"
196 }
197 return "[]" + elemType
198 case *ast.StarExpr:
199 inner := extractGoType(t.X)
200 if inner == "" {
201 return ""
202 }
203 return "*" + inner
204 }
205 return ""
206 }
207
208 func extractStructTags(tag *ast.BasicLit) (yaml, json, ui string) {
209 if tag == nil {
210 return "", "", ""
211 }
212
213 tagValue := strings.Trim(tag.Value, "`")
214 structTag := reflect.StructTag(tagValue)
215
216 yaml = structTag.Get("yaml")
217 json = structTag.Get("json")
218 ui = structTag.Get("ui")
219
220 return yaml, json, ui
221 }
222
223 func convertToSnakeCase(s string) string {
224 var result strings.Builder
225 for i, r := range s {
226 if i > 0 && 'A' <= r && r <= 'Z' {
227 result.WriteRune('_')
228 }
229 if 'A' <= r && r <= 'Z' {
230 result.WriteRune(r - 'A' + 'a')
231 } else {
232 result.WriteRune(r)
233 }
234 }
235 return result.String()
236 }
237
238 func convertToJSONType(goType string) string {
239 if strings.HasPrefix(goType, "[]") {
240 return "array"
241 }
242 baseType := strings.TrimPrefix(goType, "*")
243 switch goType {
244 case "string":
245 return "string"
246 case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
247 return "integer"
248 case "float32", "float64":
249 return "number"
250 case "bool":
251 return "boolean"
252 default:
253 // Handle pointer wrappers of basic types
254 switch baseType {
255 case "string":
256 return "string"
257 case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
258 return "integer"
259 case "float32", "float64":
260 return "number"
261 case "bool":
262 return "boolean"
263 }
264 // Handle complex types
265 if strings.Contains(goType, "Duration") {
266 return "integer"
267 }
268 return "string" // Default fallback
269 }
270 }
271
272 func extractFieldDescription(fieldName string, field *ast.Field, comments []*ast.CommentGroup) string {
273 extractCommentText := func(list []*ast.Comment) string {
274 lines := make([]string, 0, len(list))
275 for _, c := range list {
276 clean := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(strings.TrimSuffix(c.Text, "*/"), "/*"), "//"))
277 if clean != "" {
278 lines = append(lines, clean)
279 }
280 }
281 return strings.Join(lines, " ")
282 }
283
284 if field.Comment != nil && len(field.Comment.List) > 0 {
285 if text := extractCommentText(field.Comment.List); text != "" {
286 return text
287 }
288 }
289
290 if field.Doc != nil && len(field.Doc.List) > 0 {
291 if text := extractCommentText(field.Doc.List); text != "" {
292 return text
293 }
294 }
295
296 // Check for exact matches for framework fields
297 descriptions := map[string]string{
298 "UpdateEvery": "Data collection frequency in seconds",
299 "Endpoint": "Connection endpoint URL",
300 "ConnectTimeout": "Connection timeout in seconds",
301 "CollectItems": "Enable collection of item metrics",
302 "MaxItems": "Maximum number of items to collect",
303 "ObsoletionIterations": "Number of iterations after which charts become obsolete",
304 }
305
306 if desc, exists := descriptions[fieldName]; exists {
307 return desc
308 }
309
310 // Generate intelligent descriptions based on field patterns
311 fieldLower := strings.ToLower(fieldName)
312
313 // Generic connection-related fields
314 if strings.Contains(fieldLower, "host") {
315 return "Hostname"
316 }
317 if strings.Contains(fieldLower, "port") {
318 return "Port"
319 }
320 if strings.Contains(fieldLower, "user") || strings.Contains(fieldLower, "username") {
321 return "Username"
322 }
323 if strings.Contains(fieldLower, "password") || strings.Contains(fieldLower, "pass") {
324 return "Password"
325 }
326
327 // Collection control fields
328 if strings.HasPrefix(fieldLower, "collect") {
329 resource := camelToWords(strings.TrimPrefix(fieldName, "Collect"))
330 return fmt.Sprintf("Collect %s", resource)
331 }
332
333 // Selector fields
334 if strings.HasSuffix(fieldLower, "selector") {
335 resource := camelToWords(strings.TrimSuffix(fieldName, "Selector"))
336 return fmt.Sprintf("Filter %s", resource)
337 }
338
339 // Timeout fields
340 if strings.Contains(fieldLower, "timeout") {
341 return "Timeout"
342 }
343
344 // SSL/TLS fields
345 if strings.Contains(fieldLower, "ssl") || strings.Contains(fieldLower, "tls") {
346 return "SSL/TLS"
347 }
348
349 // URL/URI fields
350 if strings.Contains(fieldLower, "url") || strings.Contains(fieldLower, "uri") {
351 return "URL"
352 }
353
354 // DSN fields
355 if strings.Contains(fieldLower, "dsn") {
356 return "DSN"
357 }
358
359 // Max/limit fields
360 if strings.HasPrefix(fieldLower, "max") {
361 resource := camelToWords(strings.TrimPrefix(fieldName, "Max"))
362 return fmt.Sprintf("Max %s", resource)
363 }
364
365 // Default to a more descriptive generic description
366 return camelToWords(fieldName)
367 }
368
369 // Helper function to extract resource name from field name
370 func extractResourceFromFieldName(fieldName string) string {
371 // Remove common prefixes/suffixes
372 name := fieldName
373 name = strings.TrimPrefix(name, "Collect")
374 name = strings.TrimPrefix(name, "Max")
375 name = strings.TrimSuffix(name, "Selector")
376 name = strings.TrimSuffix(name, "Config")
377
378 // Convert to lowercase and make plural if needed
379 resource := camelToWords(name)
380 resource = strings.ToLower(resource)
381
382 // Handle some special cases
383 if resource == "system queues" {
384 return "system queues"
385 }
386 if resource == "system channels" {
387 return "system channels"
388 }
389 if resource == "reset queue stats" {
390 return "queue statistics (destructive)"
391 }
392
393 return resource
394 }
395
396 // Helper function to convert CamelCase to words
397 func camelToWords(s string) string {
398 var result strings.Builder
399 for i, r := range s {
400 if i > 0 && 'A' <= r && r <= 'Z' {
401 result.WriteRune(' ')
402 }
403 if i == 0 {
404 result.WriteRune(r)
405 } else {
406 result.WriteRune(unicode.ToLower(r))
407 }
408 }
409 return result.String()
410 }
411
412 var titleAcronyms = map[string]string{
413 "dsn": "DSN",
414 "ssl": "SSL",
415 "tls": "TLS",
416 "ibm": "IBM",
417 "odbc": "ODBC",
418 }
419
420 func formatTitleFromJSONName(name string) string {
421 if name == "" {
422 return ""
423 }
424 parts := strings.Split(name, "_")
425 for i, part := range parts {
426 if part == "" {
427 continue
428 }
429 lower := strings.ToLower(part)
430 if upper, ok := titleAcronyms[lower]; ok {
431 parts[i] = upper
432 continue
433 }
434 parts[i] = strings.ToUpper(lower[:1]) + lower[1:]
435 }
436 return strings.Join(parts, " ")
437 }
438
439 func setFieldDefaults(field *ConfigField) {
440 // Set defaults based on field name patterns
441 switch field.Name {
442 case "UpdateEvery":
443 field.Default = 1
444 field.Minimum = new(1)
445 case "ConnectTimeout":
446 field.Default = 5
447 field.Minimum = new(1)
448 field.Maximum = new(300)
449 case "CollectItems":
450 field.Default = true
451 case "MaxItems":
452 field.Default = 10
453 field.Minimum = new(1)
454 field.Maximum = new(1000)
455 case "Endpoint":
456 field.Default = "dummy://localhost"
457 field.Examples = []string{"dummy://localhost", "tcp://server:1414"}
458 case "ObsoletionIterations":
459 field.Default = 60
460 field.Minimum = new(1)
461
462 // Generic defaults for common field patterns
463 case "Port":
464 field.Minimum = new(1)
465 field.Maximum = new(65535)
466 }
467
468 // Set format hints for specific field types
469 fieldLower := strings.ToLower(field.Name)
470 if strings.Contains(fieldLower, "password") || strings.Contains(fieldLower, "pass") {
471 field.Format = "password"
472 if field.UIWidget == "" {
473 field.UIWidget = "password"
474 }
475 }
476
477 // TODO: In the future, extract defaults from SetDefaults method in config.go
478 // This would make defaults module-specific rather than hardcoded in framework
479 }
480
481 func applyUITag(field *ConfigField, tag string) {
482 if tag == "" {
483 return
484 }
485 opts := parseUIOptions(tag)
486 if group, ok := opts["group"]; ok {
487 field.UIGroup = group
488 }
489 if widget, ok := opts["widget"]; ok {
490 field.UIWidget = widget
491 }
492 if help, ok := opts["help"]; ok {
493 field.UIHelp = help
494 }
495 if placeholder, ok := opts["placeholder"]; ok {
496 field.UIPlaceholder = placeholder
497 }
498 }
499
500 func parseUIOptions(tag string) map[string]string {
501 result := make(map[string]string)
502 if tag == "" {
503 return result
504 }
505 parts := strings.SplitSeq(tag, ",")
506 for part := range parts {
507 part = strings.TrimSpace(part)
508 if part == "" {
509 continue
510 }
511 kv := strings.SplitN(part, ":", 2)
512 key := strings.TrimSpace(kv[0])
513 if key == "" {
514 continue
515 }
516 value := ""
517 if len(kv) == 2 {
518 value = strings.TrimSpace(kv[1])
519 }
520 result[key] = value
521 }
522 return result
523 }
524
525 func assignDefaultUIGroup(field *ConfigField) {
526 if field.UIGroup != "" {
527 return
528 }
529
530 name := field.JSONName
531 lower := strings.ToLower(name)
532
533 switch {
534 case name == "update_every" || name == "vnode" || name == "dsn" || strings.Contains(lower, "endpoint") || strings.Contains(lower, "url"):
535 field.UIGroup = "Connection"
536 case strings.HasPrefix(lower, "collect_"):
537 field.UIGroup = "Collection"
538 case lower == "timeout" || strings.HasSuffix(lower, "_timeout"):
539 field.UIGroup = "Connection"
540 case strings.HasSuffix(lower, "_matching") || strings.HasSuffix(lower, "_selector"):
541 field.UIGroup = "Filters"
542 case strings.HasPrefix(lower, "max_"):
543 field.UIGroup = "Limits"
544 case strings.HasPrefix(lower, "tls_") || strings.Contains(lower, "proxy") || strings.Contains(lower, "header") || strings.Contains(lower, "redirect"):
545 field.UIGroup = "HTTP"
546 case strings.Contains(lower, "password") || strings.Contains(lower, "username") || strings.Contains(lower, "auth") || strings.Contains(lower, "token") || strings.Contains(lower, "api_key"):
547 field.UIGroup = "Auth"
548 default:
549 field.UIGroup = "Advanced"
550 }
551 }
552
553 func toStringSlice(values []confopt.AutoBool) []string {
554 result := make([]string, 0, len(values))
555 for _, v := range values {
556 result = append(result, v.String())
557 }
558 return result
559 }
560
561 func normalizeDefaultValue(field ConfigField, value any) any {
562 if !isAutoBoolType(field.GoType) {
563 return value
564 }
565 switch v := value.(type) {
566 case string:
567 return normalizeAutoBoolLiteral(v)
568 case bool:
569 if v {
570 return confopt.AutoBoolEnabled.String()
571 }
572 return confopt.AutoBoolDisabled.String()
573 case nil:
574 return confopt.AutoBoolAuto.String()
575 default:
576 return confopt.AutoBoolAuto.String()
577 }
578 }
579
580 func normalizeAutoBoolLiteral(value string) string {
581 lower := strings.ToLower(strings.TrimSpace(value))
582 switch lower {
583 case "", confopt.AutoBoolAuto.String():
584 return confopt.AutoBoolAuto.String()
585 case confopt.AutoBoolEnabled.String():
586 return confopt.AutoBoolEnabled.String()
587 case confopt.AutoBoolDisabled.String():
588 return confopt.AutoBoolDisabled.String()
589 }
590
591 if strings.HasSuffix(lower, "autoboolenabled") {
592 return confopt.AutoBoolEnabled.String()
593 }
594 if strings.HasSuffix(lower, "autobooldisabled") {
595 return confopt.AutoBoolDisabled.String()
596 }
597 return confopt.AutoBoolAuto.String()
598 }
599
600 // parseDefaultsFromInitFile parses init.go to find the defaultConfig() function
601 // and extract default values from the returned Config struct
602 func (g *DocGenerator) parseDefaultsFromInitFile() map[string]any {
603 // Construct path to init.go
604 dir := filepath.Dir(g.ConfigFile)
605 initFile := filepath.Join(dir, "init.go")
606
607 // Check if init.go exists
608 if _, err := os.Stat(initFile); os.IsNotExist(err) {
609 log.Printf("ERROR: init.go not found at %s - defaultConfig() function is required", initFile)
610 return nil
611 }
612
613 fset := token.NewFileSet()
614 node, err := parser.ParseFile(fset, initFile, nil, parser.ParseComments)
615 if err != nil {
616 log.Printf("ERROR: Failed to parse init.go: %v", err)
617 return nil
618 }
619
620 defaults := make(map[string]any)
621 foundDefaultConfig := false
622
623 // Find the defaultConfig function
624 ast.Inspect(node, func(n ast.Node) bool {
625 switch x := n.(type) {
626 case *ast.FuncDecl:
627 if x.Name.Name == "defaultConfig" {
628 foundDefaultConfig = true
629 // Look for return statement with Config literal
630 foundReturn := false
631 ast.Inspect(x.Body, func(bodyNode ast.Node) bool {
632 switch stmt := bodyNode.(type) {
633 case *ast.ReturnStmt:
634 if len(stmt.Results) > 0 {
635 if lit, ok := stmt.Results[0].(*ast.CompositeLit); ok {
636 g.extractDefaultsFromLiteral(lit, defaults)
637 foundReturn = true
638 return false
639 }
640 }
641 }
642 return true
643 })
644 if !foundReturn {
645 log.Printf("ERROR: defaultConfig() function found but no valid Config{} return statement")
646 }
647 return false
648 }
649 }
650 return true
651 })
652
653 if !foundDefaultConfig {
654 log.Printf("ERROR: defaultConfig() function not found in %s - this function is mandatory", initFile)
655 return nil
656 }
657
658 if len(defaults) == 0 {
659 log.Printf("ERROR: defaultConfig() function found but extracted no default values")
660 return nil
661 }
662
663 log.Printf("Successfully extracted %d default values from defaultConfig()", len(defaults))
664 return defaults
665 }
666
667 // extractDefaultsFromLiteral extracts field values from a Config{} literal
668 func (g *DocGenerator) extractDefaultsFromLiteral(lit *ast.CompositeLit, defaults map[string]any) {
669 for _, elt := range lit.Elts {
670 if kv, ok := elt.(*ast.KeyValueExpr); ok {
671 if ident, ok := kv.Key.(*ast.Ident); ok {
672 fieldName := ident.Name
673 switch val := kv.Value.(type) {
674 case *ast.CompositeLit:
675 if isArrayLiteral(val) {
676 values := make([]any, 0, len(val.Elts))
677 for _, elt := range val.Elts {
678 if value := g.extractValue(elt); value != nil {
679 values = append(values, value)
680 }
681 }
682 defaults[fieldName] = values
683 continue
684 }
685 nested := make(map[string]any)
686 g.extractDefaultsFromLiteral(val, nested)
687 // Flatten nested composite literals for embedded configs such as framework.Config.
688 if fieldName == "Config" {
689 for k, v := range nested {
690 if _, exists := defaults[k]; !exists {
691 defaults[k] = v
692 }
693 }
694 } else {
695 // Preserve nested context using fieldName prefix to avoid collisions.
696 for k, v := range nested {
697 compositeKey := fmt.Sprintf("%s.%s", fieldName, k)
698 if _, exists := defaults[compositeKey]; !exists {
699 defaults[compositeKey] = v
700 }
701 }
702 }
703 default:
704 if value := g.extractValue(kv.Value); value != nil {
705 defaults[fieldName] = value
706 }
707 }
708 }
709 }
710 }
711 }
712
713 // extractValue converts an AST expression to a Go value
714 func (g *DocGenerator) extractValue(expr ast.Expr) any {
715 switch v := expr.(type) {
716 case *ast.BasicLit:
717 switch v.Kind {
718 case token.STRING:
719 // Remove quotes
720 return strings.Trim(v.Value, `"`)
721 case token.INT:
722 if i, err := strconv.Atoi(v.Value); err == nil {
723 return i
724 }
725 case token.FLOAT:
726 if f, err := strconv.ParseFloat(v.Value, 64); err == nil {
727 return f
728 }
729 }
730 case *ast.Ident:
731 // Handle boolean values
732 switch v.Name {
733 case "true":
734 return true
735 case "false":
736 return false
737 }
738 if g != nil {
739 if val, ok := g.consts[v.Name]; ok {
740 return val
741 }
742 }
743 return exprToString(v)
744 case *ast.CallExpr:
745 if len(v.Args) > 0 {
746 val := g.extractValue(v.Args[0])
747 if val != nil {
748 return val
749 }
750 }
751 return exprToString(v)
752 case *ast.BinaryExpr:
753 left := g.extractValue(v.X)
754 right := g.extractValue(v.Y)
755 if result, ok := evalBinaryExpr(v.Op, left, right); ok {
756 return result
757 }
758 return exprToString(v)
759 case *ast.SelectorExpr:
760 if ident, ok := v.X.(*ast.Ident); ok {
761 if ident.Name == "time" {
762 if duration, ok := timeConstant(v.Sel.Name); ok {
763 return duration
764 }
765 }
766 if ident.Name == "framework" || ident.Name == "confopt" {
767 switch v.Sel.Name {
768 case "AutoBoolAuto":
769 return confopt.AutoBoolAuto.String()
770 case "AutoBoolEnabled":
771 return confopt.AutoBoolEnabled.String()
772 case "AutoBoolDisabled":
773 return confopt.AutoBoolDisabled.String()
774 }
775 }
776 }
777 return exprToString(v)
778 case *ast.UnaryExpr:
779 return g.extractValue(v.X)
780 }
781 return nil
782 }
783
784 func exprToString(expr ast.Expr) string {
785 if expr == nil {
786 return ""
787 }
788 var buf bytes.Buffer
789 if err := format.Node(&buf, token.NewFileSet(), expr); err != nil {
790 return ""
791 }
792 return strings.TrimSpace(buf.String())
793 }
794
795 func getSliceElementGoType(goType string) (string, bool) {
796 if !strings.HasPrefix(goType, "[]") {
797 return "", false
798 }
799 elem := strings.TrimPrefix(goType, "[]")
800 return elem, true
801 }
802
803 func isArrayLiteral(lit *ast.CompositeLit) bool {
804 if lit == nil {
805 return false
806 }
807 if _, ok := lit.Type.(*ast.ArrayType); ok {
808 return true
809 }
810 return false
811 }
812
813 func (g *DocGenerator) extractConstValues(file *ast.File) map[string]any {
814 consts := make(map[string]any)
815 if file == nil {
816 return consts
817 }
818
819 prev := g.consts
820 g.consts = consts
821
822 for _, decl := range file.Decls {
823 gen, ok := decl.(*ast.GenDecl)
824 if !ok || gen.Tok != token.CONST {
825 continue
826 }
827 for _, spec := range gen.Specs {
828 vs, ok := spec.(*ast.ValueSpec)
829 if !ok {
830 continue
831 }
832 for i, name := range vs.Names {
833 if name == nil || name.Name == "_" {
834 continue
835 }
836 var value any
837 if len(vs.Values) > i {
838 value = g.extractValue(vs.Values[i])
839 } else if len(vs.Values) > 0 {
840 value = g.extractValue(vs.Values[0])
841 }
842 if value != nil {
843 consts[name.Name] = value
844 }
845 }
846 }
847 }
848
849 g.consts = prev
850 return consts
851 }
852
853 func evalBinaryExpr(op token.Token, left, right any) (any, bool) {
854 if li, lok := toInt64(left); lok {
855 if ri, rok := toInt64(right); rok {
856 switch op {
857 case token.MUL:
858 return li * ri, true
859 case token.QUO:
860 if ri == 0 {
861 return nil, false
862 }
863 return li / ri, true
864 case token.ADD:
865 return li + ri, true
866 case token.SUB:
867 return li - ri, true
868 case token.REM:
869 if ri == 0 {
870 return nil, false
871 }
872 return li % ri, true
873 }
874 }
875 }
876
877 lf, lok := toFloat64(left)
878 rf, rok := toFloat64(right)
879 if !lok || !rok {
880 return nil, false
881 }
882
883 var res float64
884 switch op {
885 case token.MUL:
886 res = lf * rf
887 case token.QUO:
888 if rf == 0 {
889 return nil, false
890 }
891 res = lf / rf
892 case token.ADD:
893 res = lf + rf
894 case token.SUB:
895 res = lf - rf
896 default:
897 return nil, false
898 }
899
900 if math.IsNaN(res) || math.IsInf(res, 0) {
901 return nil, false
902 }
903 if math.Mod(res, 1) == 0 {
904 return int64(res), true
905 }
906 return res, true
907 }
908
909 func toInt64(v any) (int64, bool) {
910 switch val := v.(type) {
911 case int:
912 return int64(val), true
913 case int8:
914 return int64(val), true
915 case int16:
916 return int64(val), true
917 case int32:
918 return int64(val), true
919 case int64:
920 return val, true
921 case uint:
922 return int64(val), true
923 case uint8:
924 return int64(val), true
925 case uint16:
926 return int64(val), true
927 case uint32:
928 return int64(val), true
929 case uint64:
930 if val > math.MaxInt64 {
931 return 0, false
932 }
933 return int64(val), true
934 case float64:
935 if math.Mod(val, 1) == 0 {
936 return int64(val), true
937 }
938 }
939 return 0, false
940 }
941
942 func toFloat64(v any) (float64, bool) {
943 switch val := v.(type) {
944 case int:
945 return float64(val), true
946 case int8:
947 return float64(val), true
948 case int16:
949 return float64(val), true
950 case int32:
951 return float64(val), true
952 case int64:
953 return float64(val), true
954 case uint:
955 return float64(val), true
956 case uint8:
957 return float64(val), true
958 case uint16:
959 return float64(val), true
960 case uint32:
961 return float64(val), true
962 case uint64:
963 return float64(val), true
964 case float32:
965 return float64(val), true
966 case float64:
967 return val, true
968 }
969 return 0, false
970 }
971
972 func timeConstant(name string) (int64, bool) {
973 switch name {
974 case "Nanosecond":
975 return int64(time.Nanosecond), true
976 case "Microsecond":
977 return int64(time.Microsecond), true
978 case "Millisecond":
979 return int64(time.Millisecond), true
980 case "Second":
981 return int64(time.Second), true
982 case "Minute":
983 return int64(time.Minute), true
984 case "Hour":
985 return int64(time.Hour), true
986 }
987 return 0, false
988 }
989
990 func isAutoBoolType(goType string) bool {
991 return goType == "framework.AutoBool" || goType == "confopt.AutoBool"
992 }