Code Review: colgen.go

Overall Assessment

The code is generally well-structured and follows many Go idioms. However, there are several areas where improvements can be made for better readability, maintainability, and adherence to Go best practices.

Documentation

Several functions are missing documentation. Here are examples of missing documentation:

// generateFile processes colgen lines and generates a new file with the generated code.
// It initializes a generator, parses rules, loads packages, generates code, formats it,
// and saves the result to a file with "_colgen.go" suffix.
func generateFile(cl colgenLines, filename string) {
    // ...
}

// replaceFile processes injection lines and replaces content in the original file.
// It initializes a replacer, loads packages, generates replacements, and applies them
// to the original file content.
func replaceFile(cl colgenLines, filename string) {
    // ...
}

// colgenLines holds the parsed lines from a source file that contain colgen directives.
type colgenLines struct {
    lines     []string   // Regular colgen directive lines
    injection []string   // Injection directive lines
    assistant []string   // Assistant directive lines
    pkgName   string     // Package name of the source file
}

Error Handling

The error handling is generally good with the use of exitOnErr, but there are some inconsistencies:

// In some places, errors are handled directly:
if err != nil {
    log.Fatal("generation failed: ", err)
}

// While in others, the exitOnErr helper is used:
exitOnErr(err)

Recommendation: Consistently use the exitOnErr helper for better readability.

Function Size and Complexity

Some functions are quite long and could be broken down into smaller, more focused functions:

// Example of how main() could be refactored:
func main() {
    setupLogging()
    parseFlags()
    
    if shouldExit := handleSpecialFlags(); shouldExit {
        return
    }
    
    filename := getFilenameFromEnv()
    cl, err := readFile(filename)
    exitOnErr(err)
    
    if len(cl.assistant) > 0 {
        handleAssistant(cl, filename)
        return
    }
    
    if len(cl.injection) > 0 {
        handleInjections(cl, filename)
    }
    
    if len(cl.lines) > 0 {
        handleGeneration(cl, filename)
    } else {
        log.Println("no colgen lines found")
    }
}

Variable Naming

Some variable names could be more descriptive:

// Instead of:
cl, err := readFile(filename)

// Consider:
colgenDirectives, err := readFile(filename)

Constants

Consider using constants for repeated string literals:

const (
    configFile = ".colgen"
    
    // Add these constants
    packagePrefix = "package "
    defaultVersion = "devel"
)

Error Messages

Some error messages could be more descriptive:

// Instead of:
return "", "", errors.New("invalid AI prompt, \")\" is not found or has invalid position")

// Consider:
return "", "", fmt.Errorf("invalid AI prompt %q: missing closing parenthesis or incorrect format", aiPrompt)

File Operations

File operations could be more consistent with defer patterns:

// Example of consistent file handling:
file, err := os.OpenFile(tp.TestFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
    return fmt.Errorf("opening test file: %w", err)
}
defer file.Close()

_, err = file.WriteString(r)
if err != nil {
    return fmt.Errorf("writing to test file: %w", err)
}

Function Signatures

Some functions could benefit from more idiomatic return values:

// Instead of:
func extractAIPrompts(aiPrompt string) (mode colgen.AssistMode, name colgen.AssistantName, err error) {
    // ...
}

// Consider:
func extractAIPrompts(aiPrompt string) (colgen.AssistMode, colgen.AssistantName, error) {
    var mode colgen.AssistMode
    name := colgen.AssistantDeepSeek
    
    // ...
    
    return mode, name, nil
}

Error Wrapping

Use error wrapping for better error context:

// Instead of:
return fmt.Errorf("reading config: %w", err)

// Consider more specific context:
return fmt.Errorf("reading colgen config from %s: %w", cp, err)

Conclusion

The code is generally well-structured and follows many Go idioms. The main areas for improvement are:

  1. Adding missing documentation
  2. Consistent error handling
  3. Breaking down complex functions
  4. More descriptive variable names
  5. Using constants for string literals
  6. More descriptive error messages
  7. Consistent file operations
  8. More idiomatic function signatures
  9. Better error wrapping for context

These changes would make the code more maintainable, readable, and idiomatic.