package ezconf import ( "go/ast" "go/parser" "go/token" "os" "path/filepath" "regexp" "strings" "github.com/pkg/errors" ) // ParseConfigFile parses a Go source file and extracts ENV comments from struct fields func ParseConfigFile(filename string) ([]EnvVar, error) { content, err := os.ReadFile(filename) if err != nil { return nil, errors.Wrap(err, "failed to read file") } fset := token.NewFileSet() file, err := parser.ParseFile(fset, filename, content, parser.ParseComments) if err != nil { return nil, errors.Wrap(err, "failed to parse file") } envVars := make([]EnvVar, 0) // Walk through the AST ast.Inspect(file, func(n ast.Node) bool { // Look for struct type declarations typeSpec, ok := n.(*ast.TypeSpec) if !ok { return true } structType, ok := typeSpec.Type.(*ast.StructType) if !ok { return true } // Iterate through struct fields for _, field := range structType.Fields.List { var comment string // Try to get from doc comment (comment before field) if field.Doc != nil && len(field.Doc.List) > 0 { comment = field.Doc.List[0].Text comment = strings.TrimPrefix(comment, "//") comment = strings.TrimSpace(comment) } // Try to get from inline comment (comment after field) if comment == "" && field.Comment != nil && len(field.Comment.List) > 0 { comment = field.Comment.List[0].Text comment = strings.TrimPrefix(comment, "//") comment = strings.TrimSpace(comment) } // Parse ENV comment if strings.HasPrefix(comment, "ENV ") { envVar, err := parseEnvComment(comment) if err == nil { envVars = append(envVars, *envVar) } } } return true }) return envVars, nil } // ParseConfigPackage parses all Go files in a package directory and extracts ENV comments func ParseConfigPackage(packagePath string) ([]EnvVar, error) { // Find all .go files in the package files, err := filepath.Glob(filepath.Join(packagePath, "*.go")) if err != nil { return nil, errors.Wrap(err, "failed to glob package files") } allEnvVars := make([]EnvVar, 0) for _, file := range files { // Skip test files if strings.HasSuffix(file, "_test.go") { continue } envVars, err := ParseConfigFile(file) if err != nil { // Log error but continue with other files continue } allEnvVars = append(allEnvVars, envVars...) } return allEnvVars, nil } // parseEnvComment parses a field comment to extract environment variable information. // Expected format: ENV ENV_NAME: Description (required ) (default: ) func parseEnvComment(comment string) (*EnvVar, error) { // Check if comment starts with ENV if !strings.HasPrefix(comment, "ENV ") { return nil, errors.New("comment does not start with 'ENV '") } // Remove "ENV " prefix comment = strings.TrimPrefix(comment, "ENV ") // Extract env var name (everything before the first colon) colonIdx := strings.Index(comment, ":") if colonIdx == -1 { return nil, errors.New("missing colon separator") } envVar := &EnvVar{ Name: strings.TrimSpace(comment[:colonIdx]), } // Extract description and optional parts remainder := strings.TrimSpace(comment[colonIdx+1:]) // Check for (required ...) pattern requiredPattern := regexp.MustCompile(`\(required[^)]*\)`) if requiredPattern.MatchString(remainder) { envVar.Required = true remainder = requiredPattern.ReplaceAllString(remainder, "") } // Check for (default: ...) pattern defaultPattern := regexp.MustCompile(`\(default:\s*([^)]*)\)`) if matches := defaultPattern.FindStringSubmatch(remainder); len(matches) > 1 { envVar.Default = strings.TrimSpace(matches[1]) remainder = defaultPattern.ReplaceAllString(remainder, "") } // What remains is the description envVar.Description = strings.TrimSpace(remainder) return envVar, nil }