github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/internal/experiments/experiments.go (about)

     1  // Package experiments managed the list of experimental feature flags supported
     2  // by GopherJS.
     3  //
     4  // GOPHERJS_EXPERIMENT environment variable can be used to control which features
     5  // are enabled.
     6  package experiments
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"reflect"
    13  	"strconv"
    14  	"strings"
    15  )
    16  
    17  var (
    18  	// ErrInvalidDest is a kind of error returned by parseFlags() when the dest
    19  	// argument does not meet the requirements.
    20  	ErrInvalidDest = errors.New("invalid flag struct")
    21  	// ErrInvalidFormat is a kind of error returned by parseFlags() when the raw
    22  	// flag string format is not valid.
    23  	ErrInvalidFormat = errors.New("invalid flag string format")
    24  )
    25  
    26  // Env contains experiment flag values from the GOPHERJS_EXPERIMENT
    27  // environment variable.
    28  var Env Flags
    29  
    30  func init() {
    31  	if err := parseFlags(os.Getenv("GOPHERJS_EXPERIMENT"), &Env); err != nil {
    32  		panic(fmt.Errorf("failed to parse GOPHERJS_EXPERIMENT flags: %w", err))
    33  	}
    34  }
    35  
    36  // Flags contains flags for currently supported experiments.
    37  type Flags struct {
    38  	Generics bool `flag:"generics"`
    39  }
    40  
    41  // parseFlags parses the `raw` flags string and populates flag values in the
    42  // `dest`.
    43  //
    44  // `raw` is a comma-separated experiment flag list: `<flag1>,<flag2>,...`. Each
    45  // flag may be either `<name>` or `<name>=<value>`. Omitting value is equivalent
    46  // to "<name> = true". Spaces around name and value are trimmed during
    47  // parsing. Flag name can't be empty. If the same flag is specified multiple
    48  // times, the last instance takes effect.
    49  //
    50  // `dest` must be a pointer to a struct, which fields will be populated with
    51  // flag values. Mapping between flag names and fields is established with the
    52  // `flag` field tag. Fields without a flag tag will be left unpopulated.
    53  // If multiple fields are associated with the same flag result is unspecified.
    54  //
    55  // Flags that don't have a corresponding field are silently ignored. This is
    56  // done to avoid fatal errors when an experiment flag is removed from code, but
    57  // remains specified in user's environment.
    58  //
    59  // Currently only boolean flag values are supported, as defined by
    60  // `strconv.ParseBool()`.
    61  func parseFlags(raw string, dest any) error {
    62  	ptr := reflect.ValueOf(dest)
    63  	if ptr.Type().Kind() != reflect.Pointer || ptr.Type().Elem().Kind() != reflect.Struct {
    64  		return fmt.Errorf("%w: must be a pointer to a struct", ErrInvalidDest)
    65  	}
    66  	if ptr.IsNil() {
    67  		return fmt.Errorf("%w: must not be nil", ErrInvalidDest)
    68  	}
    69  	fields := fieldMap(ptr.Elem())
    70  
    71  	if raw == "" {
    72  		return nil
    73  	}
    74  	entries := strings.Split(raw, ",")
    75  
    76  	for _, entry := range entries {
    77  		entry = strings.TrimSpace(entry)
    78  		var key, val string
    79  		if idx := strings.IndexRune(entry, '='); idx != -1 {
    80  			key = strings.TrimSpace(entry[0:idx])
    81  			val = strings.TrimSpace(entry[idx+1:])
    82  		} else {
    83  			key = entry
    84  			val = "true"
    85  		}
    86  
    87  		if key == "" {
    88  			return fmt.Errorf("%w: empty flag name", ErrInvalidFormat)
    89  		}
    90  
    91  		field, ok := fields[key]
    92  		if !ok {
    93  			// Unknown field value, possibly an obsolete experiment, ignore it.
    94  			continue
    95  		}
    96  		if field.Type().Kind() != reflect.Bool {
    97  			return fmt.Errorf("%w: only boolean flags are supported", ErrInvalidDest)
    98  		}
    99  		b, err := strconv.ParseBool(val)
   100  		if err != nil {
   101  			return fmt.Errorf("%w: can't parse %q as boolean for flag %q", ErrInvalidFormat, val, key)
   102  		}
   103  		field.SetBool(b)
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // fieldMap returns a map of struct fieldMap keyed by the value of the "flag" tag.
   110  //
   111  // `s` must be a struct. Fields without a "flag" tag are ignored. If multiple
   112  // fieldMap have the same flag, the last field wins.
   113  func fieldMap(s reflect.Value) map[string]reflect.Value {
   114  	typ := s.Type()
   115  	result := map[string]reflect.Value{}
   116  	for i := 0; i < typ.NumField(); i++ {
   117  		if val, ok := typ.Field(i).Tag.Lookup("flag"); ok {
   118  			result[val] = s.Field(i)
   119  		}
   120  	}
   121  	return result
   122  }