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 }