this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 318 lines 9.9 kB view raw
1// Copyright 2025 CUE Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package cueexperiment 16 17import ( 18 "fmt" 19 "maps" 20 "reflect" 21 "slices" 22 "strings" 23 24 "cuelang.org/go/internal/mod/semver" 25) 26 27// This contains experiments that are configured per file. 28 29// File defines the experiments that can be set per file. Users can activate 30// experiments by setting them using a file-based attribute @experiment() in 31// a CUE file. When an experiment is first introduced, it is disabled by 32// default. 33// 34// preview: the version from when the experiment was introduced. 35// stable: the version from when it is permanently set to true. 36// withdrawn: results in an error if the user attempts to use the flag. 37type File struct { 38 // version is the module version of the file that was compiled. 39 version string 40 41 // experiments is a comma-separated list of experiments that are enabled 42 // for this file. This is for documentation purposes only, as the 43 // experiments are already set in the struct fields. 44 experiments string 45 46 // Testing is used to enable experiments for testing. 47 // 48 // TODO: we could later use it for enabling testing features, such as 49 // testing-specific builtins. 50 Testing bool `experiment:"preview:v0.13.0"` 51 52 // Accepted_ is for testing purposes only. It should be removed when an 53 // experiment is accepted and can be used to test this feature instead. 54 Accepted_ bool `experiment:"preview:v0.13.0,stable:v0.15.0"` 55 56 // StructCmp enables comparison of structs. This also defines the == 57 // operator to be defined on all values. For instance, comparing 1 and 58 // "foo" will return false, whereas previously it would return an error. 59 // 60 // Proposal: https://cuelang.org/issue/2583 61 // Spec change: https://cuelang.org/cl/1217013 62 // Spec change: https://cuelang.org/cl/1217014 63 StructCmp bool `experiment:"preview:v0.14.0,stable:v0.15.0"` 64 65 // ExplicitOpen enables the postfix ... operator to explicitly open 66 // closed structs, allowing additional fields to be added. 67 // 68 // Proposal: https://cuelang.org/issue/4032 69 // Spec change: https://cuelang.org/cl/1221642 70 // Requires cue fix when upgrading 71 ExplicitOpen bool `experiment:"preview:v0.15.0"` 72 73 // AliasV2 enables the use of 'self' identifier to refer to the 74 // enclosing struct and enables the postfix alias syntax (~X and ~(K,V)). 75 // The file where this experiment is enabled disallows the use of old prefix 76 // alias syntax (X=). 77 // 78 // Proposal: https://cuelang.org/issue/4014 79 // Spec change: https://cuelang.org/cl/1222377 80 // Requires cue fix when upgrading 81 AliasV2 bool `experiment:"preview:v0.15.0"` 82} 83 84// LanguageVersion returns the language version of the file or "" if no language 85// version is associated with it. 86func (f *File) LanguageVersion() string { 87 return f.version 88} 89 90// NewFile parses the given comma-separated list of experiments for 91// the given version and returns a PerFile struct with the experiments enabled. 92// A empty version indicates the default version. 93func NewFile(version string, experiments ...string) (*File, error) { 94 // TODO: cash versions for a given version where there is no experiment 95 // string. 96 m := parseExperiments(experiments...) 97 f := &File{ 98 version: version, 99 experiments: strings.Join(slices.Sorted(maps.Keys(m)), ","), 100 } 101 102 if err := parseConfig(f, version, m); err != nil { 103 return nil, err 104 } 105 return f, nil 106} 107 108// IsPreview returns true if the experiment exists and can be used 109// for the given version. 110func IsPreview(experiment, version string) bool { 111 return isPreview(experiment, version, File{}) 112} 113 114func isPreview(experiment, version string, t any) bool { 115 expInfo := getExperimentInfoT(experiment, t) 116 if expInfo == nil { 117 return false 118 } 119 return expInfo.isValidForVersion(version) 120} 121 122func (e *experimentInfo) isValidForVersion(version string) bool { 123 // Check if experiment is available for this version 124 if version != "" && e.Preview != "" { 125 if semver.Compare(version, e.Preview) < 0 { 126 return false 127 } 128 } 129 130 // Check if experiment is rejected for this version 131 if e.Withdrawn != "" { 132 if version == "" || semver.Compare(version, e.Withdrawn) >= 0 { 133 return false 134 } 135 } 136 137 return true 138} 139 140// IsStable returns true if the experiment is stable (no longer 141// experimental) for the given version. 142func IsStable(experiment, version string) bool { 143 expInfo := getExperimentInfo(experiment) 144 if expInfo == nil { 145 return false 146 } 147 return expInfo.isStableForVersion(version) 148} 149 150func (e *experimentInfo) isStableForVersion(version string) bool { 151 if e.Stable == "" { 152 return false 153 } 154 return version == "" || semver.Compare(version, e.Stable) >= 0 155} 156 157// CanApplyFix validates whether an experiment fix can be applied 158// to a file with the given version and existing experiments. 159func CanApplyFix(experiment, version, target string) error { 160 return canApplyExperimentFix(experiment, version, target, File{}) 161} 162 163func canApplyExperimentFix(experiment, version, target string, t any) error { 164 expInfo := getExperimentInfoT(experiment, t) 165 if expInfo == nil { 166 return fmt.Errorf("unknown experiment %q", experiment) 167 } 168 169 // Check if experiment is valid for this version 170 if !expInfo.isValidForVersion(target) { 171 if version != "" && expInfo.Preview != "" && 172 semver.Compare(target, expInfo.Preview) < 0 { 173 const msg = "experiment %q requires language version %s or later, have %s" 174 return fmt.Errorf(msg, experiment, expInfo.Preview, version) 175 } 176 177 if expInfo.Withdrawn != "" { 178 if version == "" || semver.Compare(target, expInfo.Withdrawn) >= 0 { 179 const msg = "experiment %q is withdrawn in language version %s" 180 return fmt.Errorf(msg, experiment, expInfo.Withdrawn) 181 } 182 } 183 } 184 185 // Check if experiment is already stable (cannot fix) 186 if expInfo.isStableForVersion(version) { 187 const msg = "experiment %q is already stable as of language version %s - cannot apply fix" 188 return fmt.Errorf(msg, experiment, expInfo.Stable) 189 } 190 191 return nil 192} 193 194// GetActive returns all experiments that are active (can be enabled) 195// for the given version, but not yet accepted. 196func GetActive(origVersion, targetVersion string) []string { 197 return getActiveExperiments(origVersion, targetVersion, File{}) 198} 199 200func getActiveExperiments(origVersion, targetVersion string, t any) []string { 201 var active []string 202 203 ft := reflect.TypeOf(t) 204 for i := 0; i < ft.NumField(); i++ { 205 field := ft.Field(i) 206 tagStr, ok := field.Tag.Lookup("experiment") 207 if !ok { 208 continue 209 } 210 name := strings.ToLower(field.Name) 211 expInfo := parseExperimentTag(tagStr) 212 213 // Skip if not yet available for this version 214 if targetVersion != "" && expInfo.Preview != "" && semver.Compare(targetVersion, expInfo.Preview) < 0 { 215 continue 216 } 217 218 // Skip if already stable 219 if expInfo.Stable != "" && (targetVersion == "" || semver.Compare(origVersion, expInfo.Stable) >= 0) { 220 continue 221 } 222 223 // Skip if withdrawn 224 if expInfo.Withdrawn != "" { 225 continue 226 } 227 228 active = append(active, name) 229 } 230 231 slices.Sort(active) 232 return active 233} 234 235// GetUpgradable returns all experiments that are stable 236// (possibly in later versions), that can be upgraded from the current 237// version (must be lower than stable) to the desired version. 238func GetUpgradable(origVersion, targetVersion string) []string { 239 return getUpgradeExperiments(origVersion, targetVersion, File{}) 240} 241 242func getUpgradeExperiments(origVersion, targetVersion string, t any) []string { 243 var accepted []string 244 if origVersion == "" { 245 panic("original version is empty") 246 } 247 248 ft := reflect.TypeOf(t) 249 for i := 0; i < ft.NumField(); i++ { 250 field := ft.Field(i) 251 tagStr, ok := field.Tag.Lookup("experiment") 252 if !ok { 253 continue 254 } 255 name := strings.ToLower(field.Name) 256 expInfo := parseExperimentTag(tagStr) 257 258 if expInfo.Stable != "" && 259 semver.Compare(targetVersion, expInfo.Preview) >= 0 && 260 semver.Compare(origVersion, expInfo.Stable) < 0 { 261 accepted = append(accepted, name) 262 } 263 } 264 265 slices.Sort(accepted) 266 return accepted 267} 268 269// ShouldRemoveAttribute returns true if the experiment attribute 270// should be removed because the experiment is stable for the given version. 271func ShouldRemoveAttribute(experiment, version string) bool { 272 return IsStable(experiment, version) 273} 274 275// experimentInfo holds parsed experiment lifecycle information 276type experimentInfo struct { 277 Preview string 278 Stable string 279 Withdrawn string 280} 281 282// getExperimentInfo returns experiment lifecycle info for the given experiment name 283func getExperimentInfo(experiment string) *experimentInfo { 284 return getExperimentInfoT(experiment, File{}) 285} 286 287func getExperimentInfoT(experiment string, t any) *experimentInfo { 288 ft := reflect.TypeOf(t) 289 for i := 0; i < ft.NumField(); i++ { 290 field := ft.Field(i) 291 if strings.EqualFold(field.Name, experiment) { 292 if tagStr, ok := field.Tag.Lookup("experiment"); ok { 293 return parseExperimentTag(tagStr) 294 } 295 } 296 } 297 return nil 298} 299 300// parseExperimentTag parses experiment tag string into experimentInfo 301func parseExperimentTag(tagStr string) *experimentInfo { 302 info := &experimentInfo{} 303 for f := range strings.SplitSeq(tagStr, ",") { 304 key, rest, _ := strings.Cut(f, ":") 305 if !semver.IsValid(rest) { 306 panic(fmt.Sprintf("invalid semver in experiment tag %q: %q", key, rest)) 307 } 308 switch key { 309 case "preview": 310 info.Preview = rest 311 case "stable": 312 info.Stable = rest 313 case "withdrawn": 314 info.Withdrawn = rest 315 } 316 } 317 return info 318}