Support multi-variable selects and typed selects
This adds support for selecting on multiple variables at once, so that you can do AND/OR combindations of them. For example: select(( arch(), os(), ), { ("arm64", "linux"): ["libfoo64"], (default, "linux"): ["libfoo"], (default, "windows"): ["libfoowindows"], (default, default): ["libbar"], }) It also allows for select conditions to be boolean-typed. You can write literal true and false without quotes to select on them. Currently we don't have any boolean-typed variables though, so a fake one was added for testing. Bug: 323382414 Test: m nothing --no-skip-soong-tests Change-Id: Ibe586e7b21865b8734027848cc421594cbd1d8cc
This commit is contained in:
parent
09fe90e407
commit
3311debbb3
9 changed files with 846 additions and 320 deletions
|
@ -571,37 +571,46 @@ func endPos(pos scanner.Position, n int) scanner.Position {
|
||||||
return pos
|
return pos
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectType int
|
type ConfigurableCondition struct {
|
||||||
|
position scanner.Position
|
||||||
|
FunctionName string
|
||||||
|
Args []String
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
func (c *ConfigurableCondition) Equals(other ConfigurableCondition) bool {
|
||||||
SelectTypeUnconfigured SelectType = iota // Used for selects with only one branch, which is "default"
|
if c.FunctionName != other.FunctionName {
|
||||||
SelectTypeReleaseVariable
|
return false
|
||||||
SelectTypeSoongConfigVariable
|
|
||||||
SelectTypeProductVariable
|
|
||||||
SelectTypeVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s SelectType) String() string {
|
|
||||||
switch s {
|
|
||||||
case SelectTypeUnconfigured:
|
|
||||||
return "unconfigured"
|
|
||||||
case SelectTypeReleaseVariable:
|
|
||||||
return "release variable"
|
|
||||||
case SelectTypeSoongConfigVariable:
|
|
||||||
return "soong config variable"
|
|
||||||
case SelectTypeProductVariable:
|
|
||||||
return "product variable"
|
|
||||||
case SelectTypeVariant:
|
|
||||||
return "variant"
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
}
|
||||||
|
if len(c.Args) != len(other.Args) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range c.Args {
|
||||||
|
if c.Args[i] != other.Args[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurableCondition) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(c.FunctionName)
|
||||||
|
sb.WriteRune('(')
|
||||||
|
for i, arg := range c.Args {
|
||||||
|
sb.WriteRune('"')
|
||||||
|
sb.WriteString(arg.Value)
|
||||||
|
sb.WriteRune('"')
|
||||||
|
if i < len(c.Args)-1 {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteRune(')')
|
||||||
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Select struct {
|
type Select struct {
|
||||||
KeywordPos scanner.Position // the keyword "select"
|
KeywordPos scanner.Position // the keyword "select"
|
||||||
Typ SelectType
|
Conditions []ConfigurableCondition
|
||||||
Condition String
|
|
||||||
LBracePos scanner.Position
|
LBracePos scanner.Position
|
||||||
RBracePos scanner.Position
|
RBracePos scanner.Position
|
||||||
Cases []*SelectCase // the case statements
|
Cases []*SelectCase // the case statements
|
||||||
|
@ -640,8 +649,7 @@ func (s *Select) Type() Type {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectCase struct {
|
type SelectCase struct {
|
||||||
// TODO: Support int and bool typed cases
|
Patterns []Expression
|
||||||
Pattern String
|
|
||||||
ColonPos scanner.Position
|
ColonPos scanner.Position
|
||||||
Value Expression
|
Value Expression
|
||||||
}
|
}
|
||||||
|
@ -656,7 +664,7 @@ func (c *SelectCase) String() string {
|
||||||
return "<select case>"
|
return "<select case>"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SelectCase) Pos() scanner.Position { return c.Pattern.LiteralPos }
|
func (c *SelectCase) Pos() scanner.Position { return c.Patterns[0].Pos() }
|
||||||
func (c *SelectCase) End() scanner.Position { return c.Value.End() }
|
func (c *SelectCase) End() scanner.Position { return c.Value.End() }
|
||||||
|
|
||||||
// UnsetProperty is the expression type of the "unset" keyword that can be
|
// UnsetProperty is the expression type of the "unset" keyword that can be
|
||||||
|
|
258
parser/parser.go
258
parser/parser.go
|
@ -28,6 +28,8 @@ var errTooManyErrors = errors.New("too many errors")
|
||||||
|
|
||||||
const maxErrors = 1
|
const maxErrors = 1
|
||||||
|
|
||||||
|
const default_select_branch_name = "__soong_conditions_default__"
|
||||||
|
|
||||||
type ParseError struct {
|
type ParseError struct {
|
||||||
Err error
|
Err error
|
||||||
Pos scanner.Position
|
Pos scanner.Position
|
||||||
|
@ -177,7 +179,6 @@ func (p *parser) next() {
|
||||||
p.comments = append(p.comments, &CommentGroup{Comments: comments})
|
p.comments = append(p.comments, &CommentGroup{Comments: comments})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parseDefinitions() (defs []Definition) {
|
func (p *parser) parseDefinitions() (defs []Definition) {
|
||||||
|
@ -386,11 +387,7 @@ func (p *parser) evaluateOperator(value1, value2 Expression, operator rune,
|
||||||
if _, ok := e2.(*Select); ok {
|
if _, ok := e2.(*Select); ok {
|
||||||
// Promote e1 to a select so we can add e2 to it
|
// Promote e1 to a select so we can add e2 to it
|
||||||
e1 = &Select{
|
e1 = &Select{
|
||||||
Typ: SelectTypeUnconfigured,
|
|
||||||
Cases: []*SelectCase{{
|
Cases: []*SelectCase{{
|
||||||
Pattern: String{
|
|
||||||
Value: "__soong_conditions_default__",
|
|
||||||
},
|
|
||||||
Value: e1,
|
Value: e1,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
@ -563,49 +560,81 @@ func (p *parser) parseSelect() Expression {
|
||||||
result := &Select{
|
result := &Select{
|
||||||
KeywordPos: p.scanner.Position,
|
KeywordPos: p.scanner.Position,
|
||||||
}
|
}
|
||||||
p.accept(scanner.Ident)
|
// Read the "select("
|
||||||
if !p.accept('(') {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch p.scanner.TokenText() {
|
|
||||||
case "release_variable":
|
|
||||||
result.Typ = SelectTypeReleaseVariable
|
|
||||||
case "soong_config_variable":
|
|
||||||
result.Typ = SelectTypeSoongConfigVariable
|
|
||||||
case "product_variable":
|
|
||||||
result.Typ = SelectTypeProductVariable
|
|
||||||
case "variant":
|
|
||||||
result.Typ = SelectTypeVariant
|
|
||||||
default:
|
|
||||||
p.errorf("unknown select type %q, expected release_variable, soong_config_variable, product_variable, or variant", p.scanner.TokenText())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
p.accept(scanner.Ident)
|
p.accept(scanner.Ident)
|
||||||
if !p.accept('(') {
|
if !p.accept('(') {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := p.parseStringValue(); s != nil {
|
// If we see another '(', there's probably multiple conditions and there must
|
||||||
result.Condition = *s
|
// be a ')' after. Set the multipleConditions variable to remind us to check for
|
||||||
} else {
|
// the ')' after.
|
||||||
return nil
|
multipleConditions := false
|
||||||
|
if p.tok == '(' {
|
||||||
|
multipleConditions = true
|
||||||
|
p.accept('(')
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Typ == SelectTypeSoongConfigVariable {
|
// Read all individual conditions
|
||||||
if !p.accept(',') {
|
conditions := []ConfigurableCondition{}
|
||||||
|
for first := true; first || multipleConditions; first = false {
|
||||||
|
condition := ConfigurableCondition{
|
||||||
|
position: p.scanner.Position,
|
||||||
|
FunctionName: p.scanner.TokenText(),
|
||||||
|
}
|
||||||
|
if !p.accept(scanner.Ident) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s := p.parseStringValue(); s != nil {
|
if !p.accept('(') {
|
||||||
result.Condition.Value += ":" + s.Value
|
|
||||||
} else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for p.tok != ')' {
|
||||||
|
if s := p.parseStringValue(); s != nil {
|
||||||
|
condition.Args = append(condition.Args, *s)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.tok == ')' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !p.accept(',') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.accept(')')
|
||||||
|
|
||||||
|
for _, c := range conditions {
|
||||||
|
if c.Equals(condition) {
|
||||||
|
p.errorf("Duplicate select condition found: %s", c.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, condition)
|
||||||
|
|
||||||
|
if multipleConditions {
|
||||||
|
if p.tok == ')' {
|
||||||
|
p.next()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !p.accept(',') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Retry the closing parent to allow for a trailing comma
|
||||||
|
if p.tok == ')' {
|
||||||
|
p.next()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.accept(')') {
|
if multipleConditions && len(conditions) < 2 {
|
||||||
|
p.errorf("Expected multiple select conditions due to the extra parenthesis, but only found 1. Please remove the extra parenthesis.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.Conditions = conditions
|
||||||
|
|
||||||
if !p.accept(',') {
|
if !p.accept(',') {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -615,47 +644,79 @@ func (p *parser) parseSelect() Expression {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNonUnsetValue := false
|
parseOnePattern := func() Expression {
|
||||||
for p.tok == scanner.String {
|
switch p.tok {
|
||||||
c := &SelectCase{}
|
case scanner.Ident:
|
||||||
if s := p.parseStringValue(); s != nil {
|
switch p.scanner.TokenText() {
|
||||||
if strings.HasPrefix(s.Value, "__soong") {
|
case "default":
|
||||||
p.errorf("select branch conditions starting with __soong are reserved for internal use")
|
p.next()
|
||||||
return nil
|
return &String{
|
||||||
|
LiteralPos: p.scanner.Position,
|
||||||
|
Value: default_select_branch_name,
|
||||||
|
}
|
||||||
|
case "true":
|
||||||
|
p.next()
|
||||||
|
return &Bool{
|
||||||
|
LiteralPos: p.scanner.Position,
|
||||||
|
Value: true,
|
||||||
|
}
|
||||||
|
case "false":
|
||||||
|
p.next()
|
||||||
|
return &Bool{
|
||||||
|
LiteralPos: p.scanner.Position,
|
||||||
|
Value: false,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
p.errorf("Expted a string, true, false, or default, got %s", p.scanner.TokenText())
|
||||||
}
|
}
|
||||||
c.Pattern = *s
|
case scanner.String:
|
||||||
} else {
|
if s := p.parseStringValue(); s != nil {
|
||||||
return nil
|
if strings.HasPrefix(s.Value, "__soong") {
|
||||||
|
p.errorf("select branch conditions starting with __soong are reserved for internal use")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
p.errorf("Expted a string, true, false, or default, got %s", p.scanner.TokenText())
|
||||||
}
|
}
|
||||||
c.ColonPos = p.scanner.Position
|
return nil
|
||||||
if !p.accept(':') {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if p.tok == scanner.Ident && p.scanner.TokenText() == "unset" {
|
|
||||||
c.Value = UnsetProperty{Position: p.scanner.Position}
|
|
||||||
p.accept(scanner.Ident)
|
|
||||||
} else {
|
|
||||||
hasNonUnsetValue = true
|
|
||||||
c.Value = p.parseExpression()
|
|
||||||
}
|
|
||||||
if !p.accept(',') {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Cases = append(result.Cases, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default must be last
|
hasNonUnsetValue := false
|
||||||
if p.tok == scanner.Ident {
|
for p.tok != '}' {
|
||||||
if p.scanner.TokenText() != "default" {
|
c := &SelectCase{}
|
||||||
p.errorf("select cases can either be quoted strings or 'default' to match any value")
|
|
||||||
return nil
|
if multipleConditions {
|
||||||
|
if !p.accept('(') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := 0; i < len(conditions); i++ {
|
||||||
|
if p := parseOnePattern(); p != nil {
|
||||||
|
c.Patterns = append(c.Patterns, p)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if i < len(conditions)-1 {
|
||||||
|
if !p.accept(',') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if p.tok == ',' {
|
||||||
|
// allow optional trailing comma
|
||||||
|
p.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !p.accept(')') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if p := parseOnePattern(); p != nil {
|
||||||
|
c.Patterns = append(c.Patterns, p)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c := &SelectCase{Pattern: String{
|
|
||||||
LiteralPos: p.scanner.Position,
|
|
||||||
Value: "__soong_conditions_default__",
|
|
||||||
}}
|
|
||||||
p.accept(scanner.Ident)
|
|
||||||
c.ColonPos = p.scanner.Position
|
c.ColonPos = p.scanner.Position
|
||||||
if !p.accept(':') {
|
if !p.accept(':') {
|
||||||
return nil
|
return nil
|
||||||
|
@ -676,9 +737,64 @@ func (p *parser) parseSelect() Expression {
|
||||||
// If all branches have the value "unset", then this is equivalent
|
// If all branches have the value "unset", then this is equivalent
|
||||||
// to an empty select.
|
// to an empty select.
|
||||||
if !hasNonUnsetValue {
|
if !hasNonUnsetValue {
|
||||||
result.Typ = SelectTypeUnconfigured
|
p.errorf("This select statement is empty, remove it")
|
||||||
result.Condition.Value = ""
|
return nil
|
||||||
result.Cases = nil
|
}
|
||||||
|
|
||||||
|
patternsEqual := func(a, b Expression) bool {
|
||||||
|
switch a2 := a.(type) {
|
||||||
|
case *String:
|
||||||
|
if b2, ok := b.(*String); ok {
|
||||||
|
return a2.Value == b2.Value
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case *Bool:
|
||||||
|
if b2, ok := b.(*Bool); ok {
|
||||||
|
return a2.Value == b2.Value
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// true so that we produce an error in this unexpected scenario
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patternListsEqual := func(a, b []Expression) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if !patternsEqual(a[i], b[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, c := range result.Cases {
|
||||||
|
// Check for duplicates
|
||||||
|
for _, d := range result.Cases[i+1:] {
|
||||||
|
if patternListsEqual(c.Patterns, d.Patterns) {
|
||||||
|
p.errorf("Found duplicate select patterns: %v", c.Patterns)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check that the only all-default cases is the last one
|
||||||
|
if i < len(result.Cases)-1 {
|
||||||
|
isAllDefault := true
|
||||||
|
for _, x := range c.Patterns {
|
||||||
|
if x2, ok := x.(*String); !ok || x2.Value != default_select_branch_name {
|
||||||
|
isAllDefault = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isAllDefault {
|
||||||
|
p.errorf("Found a default select branch at index %d, expected it to be last (index %d)", i, len(result.Cases)-1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ty := UnsetType
|
ty := UnsetType
|
||||||
|
|
|
@ -1225,6 +1225,17 @@ func TestParserError(t *testing.T) {
|
||||||
input: "\x00",
|
input: "\x00",
|
||||||
err: "invalid character NUL",
|
err: "invalid character NUL",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "select with duplicate condition",
|
||||||
|
input: `
|
||||||
|
m {
|
||||||
|
foo: select((arch(), arch()), {
|
||||||
|
(default, default): true,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
err: "Duplicate select condition found: arch()",
|
||||||
|
},
|
||||||
// TODO: test more parser errors
|
// TODO: test more parser errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,48 +142,83 @@ func (p *printer) printSelect(s *Select) {
|
||||||
if len(s.Cases) == 0 {
|
if len(s.Cases) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(s.Cases) == 1 && s.Cases[0].Pattern.Value == "__soong_conditions_default__" {
|
if len(s.Cases) == 1 && len(s.Cases[0].Patterns) == 1 {
|
||||||
p.printExpression(s.Cases[0].Value)
|
if str, ok := s.Cases[0].Patterns[0].(*String); ok && str.Value == default_select_branch_name {
|
||||||
p.pos = s.RBracePos
|
p.printExpression(s.Cases[0].Value)
|
||||||
return
|
p.pos = s.RBracePos
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.requestSpace()
|
p.requestSpace()
|
||||||
p.printToken("select(", s.KeywordPos)
|
p.printToken("select(", s.KeywordPos)
|
||||||
switch s.Typ {
|
multilineConditions := false
|
||||||
case SelectTypeSoongConfigVariable:
|
if len(s.Conditions) > 1 {
|
||||||
p.printToken("soong_config_variable(", s.Condition.LiteralPos)
|
p.printToken("(", s.KeywordPos)
|
||||||
parts := strings.Split(s.Condition.Value, ":")
|
if s.Conditions[len(s.Conditions)-1].position.Line > s.KeywordPos.Line {
|
||||||
namespace := parts[0]
|
multilineConditions = true
|
||||||
variable := parts[1]
|
p.requestNewline()
|
||||||
p.printToken(strconv.Quote(namespace), s.Condition.LiteralPos)
|
p.indent(p.curIndent() + 4)
|
||||||
p.printToken(",", s.Condition.LiteralPos)
|
}
|
||||||
p.requestSpace()
|
}
|
||||||
p.printToken(strconv.Quote(variable), s.Condition.LiteralPos)
|
for i, c := range s.Conditions {
|
||||||
p.printToken(")", s.Condition.LiteralPos)
|
p.printToken(c.FunctionName, c.position)
|
||||||
case SelectTypeReleaseVariable:
|
p.printToken("(", c.position)
|
||||||
p.printToken("release_variable(", s.Condition.LiteralPos)
|
for i, arg := range c.Args {
|
||||||
p.printToken(strconv.Quote(s.Condition.Value), s.Condition.LiteralPos)
|
p.printToken(strconv.Quote(arg.Value), arg.LiteralPos)
|
||||||
p.printToken(")", s.Condition.LiteralPos)
|
if i < len(c.Args)-1 {
|
||||||
case SelectTypeProductVariable:
|
p.printToken(",", arg.LiteralPos)
|
||||||
p.printToken("product_variable(", s.Condition.LiteralPos)
|
p.requestSpace()
|
||||||
p.printToken(strconv.Quote(s.Condition.Value), s.Condition.LiteralPos)
|
}
|
||||||
p.printToken(")", s.Condition.LiteralPos)
|
}
|
||||||
case SelectTypeVariant:
|
p.printToken(")", p.pos)
|
||||||
p.printToken("variant(", s.Condition.LiteralPos)
|
if len(s.Conditions) > 1 {
|
||||||
p.printToken(strconv.Quote(s.Condition.Value), s.Condition.LiteralPos)
|
if multilineConditions {
|
||||||
p.printToken(")", s.Condition.LiteralPos)
|
p.printToken(",", p.pos)
|
||||||
default:
|
p.requestNewline()
|
||||||
panic("should be unreachable")
|
} else if i < len(s.Conditions)-1 {
|
||||||
|
p.printToken(",", p.pos)
|
||||||
|
p.requestSpace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s.Conditions) > 1 {
|
||||||
|
if multilineConditions {
|
||||||
|
p.unindent(p.pos)
|
||||||
|
}
|
||||||
|
p.printToken(")", p.pos)
|
||||||
}
|
}
|
||||||
p.printToken(", {", s.LBracePos)
|
p.printToken(", {", s.LBracePos)
|
||||||
p.requestNewline()
|
p.requestNewline()
|
||||||
p.indent(p.curIndent() + 4)
|
p.indent(p.curIndent() + 4)
|
||||||
for _, c := range s.Cases {
|
for _, c := range s.Cases {
|
||||||
p.requestNewline()
|
p.requestNewline()
|
||||||
if c.Pattern.Value != "__soong_conditions_default__" {
|
if len(c.Patterns) > 1 {
|
||||||
p.printToken(strconv.Quote(c.Pattern.Value), c.Pattern.LiteralPos)
|
p.printToken("(", p.pos)
|
||||||
} else {
|
}
|
||||||
p.printToken("default", c.Pattern.LiteralPos)
|
for i, pat := range c.Patterns {
|
||||||
|
switch pat := pat.(type) {
|
||||||
|
case *String:
|
||||||
|
if pat.Value != default_select_branch_name {
|
||||||
|
p.printToken(strconv.Quote(pat.Value), pat.LiteralPos)
|
||||||
|
} else {
|
||||||
|
p.printToken("default", pat.LiteralPos)
|
||||||
|
}
|
||||||
|
case *Bool:
|
||||||
|
s := "false"
|
||||||
|
if pat.Value {
|
||||||
|
s = "true"
|
||||||
|
}
|
||||||
|
p.printToken(s, pat.LiteralPos)
|
||||||
|
default:
|
||||||
|
panic("Unhandled case")
|
||||||
|
}
|
||||||
|
if i < len(c.Patterns)-1 {
|
||||||
|
p.printToken(",", p.pos)
|
||||||
|
p.requestSpace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(c.Patterns) > 1 {
|
||||||
|
p.printToken(")", p.pos)
|
||||||
}
|
}
|
||||||
p.printToken(":", c.ColonPos)
|
p.printToken(":", c.ColonPos)
|
||||||
p.requestSpace()
|
p.requestSpace()
|
||||||
|
|
|
@ -657,50 +657,44 @@ foo {
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Select with only unsets is removed",
|
name: "Multi-condition select",
|
||||||
input: `
|
input: `
|
||||||
foo {
|
foo {
|
||||||
stuff: select(soong_config_variable("my_namespace", "my_variable"), {
|
stuff: select((arch(), os()), {
|
||||||
"foo": unset,
|
("x86", "linux"): "a",
|
||||||
default: unset,
|
(default, default): "b",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
output: `
|
output: `
|
||||||
foo {
|
foo {
|
||||||
|
stuff: select((arch(), os()), {
|
||||||
|
("x86", "linux"): "a",
|
||||||
|
(default, default): "b",
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Additions of unset selects are removed",
|
name: "Multi-condition select with conditions on new lines",
|
||||||
input: `
|
input: `
|
||||||
foo {
|
foo {
|
||||||
stuff: select(soong_config_variable("my_namespace", "my_variable"), {
|
stuff: select((arch(),
|
||||||
"foo": "a",
|
os()), {
|
||||||
default: "b",
|
("x86", "linux"): "a",
|
||||||
}) + select(soong_config_variable("my_namespace", "my_variable2"), {
|
(default, default): "b",
|
||||||
"foo": unset,
|
|
||||||
default: unset,
|
|
||||||
}) + select(soong_config_variable("my_namespace", "my_variable3"), {
|
|
||||||
"foo": "c",
|
|
||||||
default: "d",
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
// TODO(b/323382414): This is not good formatting, revisit later.
|
|
||||||
// But at least it removes the useless middle select
|
|
||||||
output: `
|
output: `
|
||||||
foo {
|
foo {
|
||||||
stuff: select(soong_config_variable("my_namespace", "my_variable"), {
|
stuff: select((
|
||||||
"foo": "a",
|
arch(),
|
||||||
default: "b",
|
os(),
|
||||||
}) +
|
), {
|
||||||
|
("x86", "linux"): "a",
|
||||||
select(soong_config_variable("my_namespace", "my_variable3"), {
|
(default, default): "b",
|
||||||
"foo": "c",
|
}),
|
||||||
default: "d",
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,21 +14,18 @@
|
||||||
package proptools
|
package proptools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"github.com/google/blueprint/parser"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const default_select_branch_name = "__soong_conditions_default__"
|
|
||||||
|
|
||||||
type ConfigurableElements interface {
|
type ConfigurableElements interface {
|
||||||
string | bool | []string
|
string | bool | []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigurableEvaluator interface {
|
type ConfigurableEvaluator interface {
|
||||||
EvaluateConfiguration(typ parser.SelectType, property, condition string) (string, bool)
|
EvaluateConfiguration(condition ConfigurableCondition, property string) ConfigurableValue
|
||||||
PropertyErrorf(property, fmt string, args ...interface{})
|
PropertyErrorf(property, fmt string, args ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +35,200 @@ type configurableMarker bool
|
||||||
|
|
||||||
var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem()
|
var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem()
|
||||||
|
|
||||||
|
type ConfigurableCondition struct {
|
||||||
|
FunctionName string
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurableCondition) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(c.FunctionName)
|
||||||
|
sb.WriteRune('(')
|
||||||
|
for i, arg := range c.Args {
|
||||||
|
sb.WriteString(strconv.Quote(arg))
|
||||||
|
if i < len(c.Args)-1 {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteRune(')')
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type configurableValueType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
configurableValueTypeString configurableValueType = iota
|
||||||
|
configurableValueTypeBool
|
||||||
|
configurableValueTypeUndefined
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *configurableValueType) patternType() configurablePatternType {
|
||||||
|
switch *v {
|
||||||
|
case configurableValueTypeString:
|
||||||
|
return configurablePatternTypeString
|
||||||
|
case configurableValueTypeBool:
|
||||||
|
return configurablePatternTypeBool
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *configurableValueType) String() string {
|
||||||
|
switch *v {
|
||||||
|
case configurableValueTypeString:
|
||||||
|
return "string"
|
||||||
|
case configurableValueTypeBool:
|
||||||
|
return "bool"
|
||||||
|
case configurableValueTypeUndefined:
|
||||||
|
return "undefined"
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigurableValue represents the value of a certain condition being selected on.
|
||||||
|
// This type mostly exists to act as a sum type between string, bool, and undefined.
|
||||||
|
type ConfigurableValue struct {
|
||||||
|
typ configurableValueType
|
||||||
|
stringValue string
|
||||||
|
boolValue bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurableValue) String() string {
|
||||||
|
switch c.typ {
|
||||||
|
case configurableValueTypeString:
|
||||||
|
return strconv.Quote(c.stringValue)
|
||||||
|
case configurableValueTypeBool:
|
||||||
|
if c.boolValue {
|
||||||
|
return "true"
|
||||||
|
} else {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
case configurableValueTypeUndefined:
|
||||||
|
return "undefined"
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigurableValueString(s string) ConfigurableValue {
|
||||||
|
return ConfigurableValue{
|
||||||
|
typ: configurableValueTypeString,
|
||||||
|
stringValue: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigurableValueBool(b bool) ConfigurableValue {
|
||||||
|
return ConfigurableValue{
|
||||||
|
typ: configurableValueTypeBool,
|
||||||
|
boolValue: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigurableValueUndefined() ConfigurableValue {
|
||||||
|
return ConfigurableValue{
|
||||||
|
typ: configurableValueTypeUndefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type configurablePatternType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
configurablePatternTypeString configurablePatternType = iota
|
||||||
|
configurablePatternTypeBool
|
||||||
|
configurablePatternTypeDefault
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *configurablePatternType) String() string {
|
||||||
|
switch *v {
|
||||||
|
case configurablePatternTypeString:
|
||||||
|
return "string"
|
||||||
|
case configurablePatternTypeBool:
|
||||||
|
return "bool"
|
||||||
|
case configurablePatternTypeDefault:
|
||||||
|
return "default"
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type configurablePattern struct {
|
||||||
|
typ configurablePatternType
|
||||||
|
stringValue string
|
||||||
|
boolValue bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *configurablePattern) matchesValue(v ConfigurableValue) bool {
|
||||||
|
if p.typ == configurablePatternTypeDefault {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if v.typ == configurableValueTypeUndefined {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if p.typ != v.typ.patternType() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch p.typ {
|
||||||
|
case configurablePatternTypeString:
|
||||||
|
return p.stringValue == v.stringValue
|
||||||
|
case configurablePatternTypeBool:
|
||||||
|
return p.boolValue == v.boolValue
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *configurablePattern) matchesValueType(v ConfigurableValue) bool {
|
||||||
|
if p.typ == configurablePatternTypeDefault {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if v.typ == configurableValueTypeUndefined {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p.typ == v.typ.patternType()
|
||||||
|
}
|
||||||
|
|
||||||
|
type configurableCase[T ConfigurableElements] struct {
|
||||||
|
patterns []configurablePattern
|
||||||
|
value *T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configurableCase[T]) Clone() configurableCase[T] {
|
||||||
|
return configurableCase[T]{
|
||||||
|
patterns: slices.Clone(c.patterns),
|
||||||
|
value: copyConfiguredValue(c.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type configurableCaseReflection interface {
|
||||||
|
initialize(patterns []configurablePattern, value interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ configurableCaseReflection = &configurableCase[string]{}
|
||||||
|
|
||||||
|
func (c *configurableCase[T]) initialize(patterns []configurablePattern, value interface{}) {
|
||||||
|
c.patterns = patterns
|
||||||
|
c.value = value.(*T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for the given T, return the reflect.type of configurableCase[T]
|
||||||
|
func configurableCaseType(configuredType reflect.Type) reflect.Type {
|
||||||
|
// I don't think it's possible to do this generically with go's
|
||||||
|
// current reflection apis unfortunately
|
||||||
|
switch configuredType.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return reflect.TypeOf(configurableCase[string]{})
|
||||||
|
case reflect.Bool:
|
||||||
|
return reflect.TypeOf(configurableCase[bool]{})
|
||||||
|
case reflect.Slice:
|
||||||
|
switch configuredType.Elem().Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return reflect.TypeOf(configurableCase[[]string]{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
// Configurable can wrap the type of a blueprint property,
|
// Configurable can wrap the type of a blueprint property,
|
||||||
// in order to allow select statements to be used in bp files
|
// in order to allow select statements to be used in bp files
|
||||||
// for that property. For example, for the property struct:
|
// for that property. For example, for the property struct:
|
||||||
|
@ -51,11 +242,11 @@ var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(n
|
||||||
//
|
//
|
||||||
// my_module {
|
// my_module {
|
||||||
// property_a: "foo"
|
// property_a: "foo"
|
||||||
// property_b: select soong_config_variable: "my_namespace" "my_variable" {
|
// property_b: select(soong_config_variable("my_namespace", "my_variable"), {
|
||||||
// "value_1": "bar",
|
// "value_1": "bar",
|
||||||
// "value_2": "baz",
|
// "value_2": "baz",
|
||||||
// default: "qux",
|
// default: "qux",
|
||||||
// }
|
// })
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// The configurable property holds all the branches of the select
|
// The configurable property holds all the branches of the select
|
||||||
|
@ -67,9 +258,8 @@ var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(n
|
||||||
type Configurable[T ConfigurableElements] struct {
|
type Configurable[T ConfigurableElements] struct {
|
||||||
marker configurableMarker
|
marker configurableMarker
|
||||||
propertyName string
|
propertyName string
|
||||||
typ parser.SelectType
|
conditions []ConfigurableCondition
|
||||||
condition string
|
cases []configurableCase[T]
|
||||||
cases map[string]*T
|
|
||||||
appendWrapper *appendWrapper[T]
|
appendWrapper *appendWrapper[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,40 +302,49 @@ func (c *Configurable[T]) GetOrDefault(evaluator ConfigurableEvaluator, defaultV
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configurable[T]) evaluateNonTransitive(evaluator ConfigurableEvaluator) *T {
|
func (c *Configurable[T]) evaluateNonTransitive(evaluator ConfigurableEvaluator) *T {
|
||||||
if c.typ == parser.SelectTypeUnconfigured {
|
for i, case_ := range c.cases {
|
||||||
|
if len(c.conditions) != len(case_.patterns) {
|
||||||
|
evaluator.PropertyErrorf(c.propertyName, "Expected each case to have as many patterns as conditions. conditions: %d, len(cases[%d].patterns): %d", len(c.conditions), i, len(case_.patterns))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(c.conditions) == 0 {
|
||||||
if len(c.cases) == 0 {
|
if len(c.cases) == 0 {
|
||||||
return nil
|
return nil
|
||||||
} else if len(c.cases) != 1 {
|
} else if len(c.cases) == 1 {
|
||||||
panic(fmt.Sprintf("Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases)))
|
return c.cases[0].value
|
||||||
|
} else {
|
||||||
|
evaluator.PropertyErrorf(c.propertyName, "Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
result, ok := c.cases[default_select_branch_name]
|
}
|
||||||
if !ok {
|
values := make([]ConfigurableValue, len(c.conditions))
|
||||||
actual := ""
|
for i, condition := range c.conditions {
|
||||||
for k := range c.cases {
|
values[i] = evaluator.EvaluateConfiguration(condition, c.propertyName)
|
||||||
actual = k
|
}
|
||||||
|
foundMatch := false
|
||||||
|
var result *T
|
||||||
|
for _, case_ := range c.cases {
|
||||||
|
allMatch := true
|
||||||
|
for i, pat := range case_.patterns {
|
||||||
|
if !pat.matchesValueType(values[i]) {
|
||||||
|
evaluator.PropertyErrorf(c.propertyName, "Expected all branches of a select on condition %s to have type %s, found %s", c.conditions[i].String(), values[i].typ.String(), pat.typ.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !pat.matchesValue(values[i]) {
|
||||||
|
allMatch = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
panic(fmt.Sprintf("Expected the single branch of an unconfigured select to be %q, got %q", default_select_branch_name, actual))
|
|
||||||
}
|
}
|
||||||
return result
|
if allMatch && !foundMatch {
|
||||||
}
|
result = case_.value
|
||||||
val, defined := evaluator.EvaluateConfiguration(c.typ, c.propertyName, c.condition)
|
foundMatch = true
|
||||||
if !defined {
|
|
||||||
if result, ok := c.cases[default_select_branch_name]; ok {
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
evaluator.PropertyErrorf(c.propertyName, "%s %q was not defined", c.typ.String(), c.condition)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if val == default_select_branch_name {
|
if foundMatch {
|
||||||
panic("Evaluator cannot return the default branch")
|
|
||||||
}
|
|
||||||
if result, ok := c.cases[val]; ok {
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
if result, ok := c.cases[default_select_branch_name]; ok {
|
evaluator.PropertyErrorf(c.propertyName, "%s had value %s, which was not handled by the select statement", c.conditions, values)
|
||||||
return result
|
|
||||||
}
|
|
||||||
evaluator.PropertyErrorf(c.propertyName, "%s %q had value %q, which was not handled by the select statement", c.typ.String(), c.condition, val)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,17 +415,16 @@ type configurableReflection interface {
|
||||||
// Same as configurableReflection, but since initialize needs to take a pointer
|
// Same as configurableReflection, but since initialize needs to take a pointer
|
||||||
// to a Configurable, it was broken out into a separate interface.
|
// to a Configurable, it was broken out into a separate interface.
|
||||||
type configurablePtrReflection interface {
|
type configurablePtrReflection interface {
|
||||||
initialize(propertyName string, typ parser.SelectType, condition string, cases any)
|
initialize(propertyName string, conditions []ConfigurableCondition, cases any)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ configurableReflection = Configurable[string]{}
|
var _ configurableReflection = Configurable[string]{}
|
||||||
var _ configurablePtrReflection = &Configurable[string]{}
|
var _ configurablePtrReflection = &Configurable[string]{}
|
||||||
|
|
||||||
func (c *Configurable[T]) initialize(propertyName string, typ parser.SelectType, condition string, cases any) {
|
func (c *Configurable[T]) initialize(propertyName string, conditions []ConfigurableCondition, cases any) {
|
||||||
c.propertyName = propertyName
|
c.propertyName = propertyName
|
||||||
c.typ = typ
|
c.conditions = conditions
|
||||||
c.condition = condition
|
c.cases = cases.([]configurableCase[T])
|
||||||
c.cases = cases.(map[string]*T)
|
|
||||||
c.appendWrapper = &appendWrapper[T]{}
|
c.appendWrapper = &appendWrapper[T]{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +441,7 @@ func (c Configurable[T]) isEmpty() bool {
|
||||||
if c.appendWrapper != nil && !c.appendWrapper.append.isEmpty() {
|
if c.appendWrapper != nil && !c.appendWrapper.append.isEmpty() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return c.typ == parser.SelectTypeUnconfigured && len(c.cases) == 0
|
return len(c.cases) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Configurable[T]) configuredType() reflect.Type {
|
func (c Configurable[T]) configuredType() reflect.Type {
|
||||||
|
@ -267,15 +465,17 @@ func (c *Configurable[T]) clone() *Configurable[T] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
casesCopy := make(map[string]*T, len(c.cases))
|
conditionsCopy := make([]ConfigurableCondition, len(c.conditions))
|
||||||
for k, v := range c.cases {
|
copy(conditionsCopy, c.conditions)
|
||||||
casesCopy[k] = copyConfiguredValue(v)
|
|
||||||
|
casesCopy := make([]configurableCase[T], len(c.cases))
|
||||||
|
for i, case_ := range c.cases {
|
||||||
|
casesCopy[i] = case_.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Configurable[T]{
|
return &Configurable[T]{
|
||||||
propertyName: c.propertyName,
|
propertyName: c.propertyName,
|
||||||
typ: c.typ,
|
conditions: conditionsCopy,
|
||||||
condition: c.condition,
|
|
||||||
cases: casesCopy,
|
cases: casesCopy,
|
||||||
appendWrapper: inner,
|
appendWrapper: inner,
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,6 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/blueprint/parser"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type appendPropertyTestCase struct {
|
type appendPropertyTestCase struct {
|
||||||
|
@ -1260,38 +1258,72 @@ func appendPropertiesTestCases() []appendPropertyTestCase {
|
||||||
name: "Append configurable",
|
name: "Append configurable",
|
||||||
dst: &struct{ S Configurable[[]string] }{
|
dst: &struct{ S Configurable[[]string] }{
|
||||||
S: Configurable[[]string]{
|
S: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "foo",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"a": {"1", "2"},
|
"my_namespace",
|
||||||
},
|
"foo",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "a",
|
||||||
|
}},
|
||||||
|
value: &[]string{"1", "2"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
src: &struct{ S Configurable[[]string] }{
|
src: &struct{ S Configurable[[]string] }{
|
||||||
S: Configurable[[]string]{
|
S: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeReleaseVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "bar",
|
FunctionName: "release_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"b": {"3", "4"},
|
"bar",
|
||||||
},
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "b",
|
||||||
|
}},
|
||||||
|
value: &[]string{"3", "4"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
out: &struct{ S Configurable[[]string] }{
|
out: &struct{ S Configurable[[]string] }{
|
||||||
S: Configurable[[]string]{
|
S: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "foo",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"a": {"1", "2"},
|
"my_namespace",
|
||||||
},
|
"foo",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "a",
|
||||||
|
}},
|
||||||
|
value: &[]string{"1", "2"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{
|
appendWrapper: &appendWrapper[[]string]{
|
||||||
append: Configurable[[]string]{
|
append: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeReleaseVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "bar",
|
FunctionName: "release_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"b": {"3", "4"},
|
"bar",
|
||||||
},
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "b",
|
||||||
|
}},
|
||||||
|
value: &[]string{"3", "4"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1303,38 +1335,72 @@ func appendPropertiesTestCases() []appendPropertyTestCase {
|
||||||
order: Prepend,
|
order: Prepend,
|
||||||
dst: &struct{ S Configurable[[]string] }{
|
dst: &struct{ S Configurable[[]string] }{
|
||||||
S: Configurable[[]string]{
|
S: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "foo",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"a": {"1", "2"},
|
"my_namespace",
|
||||||
},
|
"foo",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "a",
|
||||||
|
}},
|
||||||
|
value: &[]string{"1", "2"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
src: &struct{ S Configurable[[]string] }{
|
src: &struct{ S Configurable[[]string] }{
|
||||||
S: Configurable[[]string]{
|
S: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeReleaseVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "bar",
|
FunctionName: "release_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"b": {"3", "4"},
|
"bar",
|
||||||
},
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "b",
|
||||||
|
}},
|
||||||
|
value: &[]string{"3", "4"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
out: &struct{ S Configurable[[]string] }{
|
out: &struct{ S Configurable[[]string] }{
|
||||||
S: Configurable[[]string]{
|
S: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeReleaseVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "bar",
|
FunctionName: "release_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"b": {"3", "4"},
|
"bar",
|
||||||
},
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "b",
|
||||||
|
}},
|
||||||
|
value: &[]string{"3", "4"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{
|
appendWrapper: &appendWrapper[[]string]{
|
||||||
append: Configurable[[]string]{
|
append: Configurable[[]string]{
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "foo",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*[]string{
|
Args: []string{
|
||||||
"a": {"1", "2"},
|
"my_namespace",
|
||||||
},
|
"foo",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[[]string]{{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "a",
|
||||||
|
}},
|
||||||
|
value: &[]string{"1", "2"},
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -356,10 +356,9 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
|
||||||
}
|
}
|
||||||
result := Configurable[string]{
|
result := Configurable[string]{
|
||||||
propertyName: property.Name,
|
propertyName: property.Name,
|
||||||
typ: parser.SelectTypeUnconfigured,
|
cases: []configurableCase[string]{{
|
||||||
cases: map[string]*string{
|
value: &v.Value,
|
||||||
default_select_branch_name: &v.Value,
|
}},
|
||||||
},
|
|
||||||
appendWrapper: &appendWrapper[string]{},
|
appendWrapper: &appendWrapper[string]{},
|
||||||
}
|
}
|
||||||
return reflect.ValueOf(&result), true
|
return reflect.ValueOf(&result), true
|
||||||
|
@ -374,10 +373,9 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
|
||||||
}
|
}
|
||||||
result := Configurable[bool]{
|
result := Configurable[bool]{
|
||||||
propertyName: property.Name,
|
propertyName: property.Name,
|
||||||
typ: parser.SelectTypeUnconfigured,
|
cases: []configurableCase[bool]{{
|
||||||
cases: map[string]*bool{
|
value: &v.Value,
|
||||||
default_select_branch_name: &v.Value,
|
}},
|
||||||
},
|
|
||||||
appendWrapper: &appendWrapper[bool]{},
|
appendWrapper: &appendWrapper[bool]{},
|
||||||
}
|
}
|
||||||
return reflect.ValueOf(&result), true
|
return reflect.ValueOf(&result), true
|
||||||
|
@ -392,9 +390,9 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
|
||||||
}
|
}
|
||||||
switch configuredType.Elem().Kind() {
|
switch configuredType.Elem().Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
var cases map[string]*[]string
|
var value []string
|
||||||
if v.Values != nil {
|
if v.Values != nil {
|
||||||
value := make([]string, 0, len(v.Values))
|
value = make([]string, len(v.Values))
|
||||||
itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos}
|
itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos}
|
||||||
for i, expr := range v.Values {
|
for i, expr := range v.Values {
|
||||||
itemProperty.Name = propertyName + "[" + strconv.Itoa(i) + "]"
|
itemProperty.Name = propertyName + "[" + strconv.Itoa(i) + "]"
|
||||||
|
@ -404,14 +402,14 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
|
||||||
ctx.addError(err)
|
ctx.addError(err)
|
||||||
return reflect.ValueOf(Configurable[[]string]{}), false
|
return reflect.ValueOf(Configurable[[]string]{}), false
|
||||||
}
|
}
|
||||||
value = append(value, exprUnpacked.Interface().(string))
|
value[i] = exprUnpacked.Interface().(string)
|
||||||
}
|
}
|
||||||
cases = map[string]*[]string{default_select_branch_name: &value}
|
|
||||||
}
|
}
|
||||||
result := Configurable[[]string]{
|
result := Configurable[[]string]{
|
||||||
propertyName: property.Name,
|
propertyName: property.Name,
|
||||||
typ: parser.SelectTypeUnconfigured,
|
cases: []configurableCase[[]string]{{
|
||||||
cases: cases,
|
value: &value,
|
||||||
|
}},
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
}
|
}
|
||||||
return reflect.ValueOf(&result), true
|
return reflect.ValueOf(&result), true
|
||||||
|
@ -424,46 +422,81 @@ func (ctx *unpackContext) unpackToConfigurable(propertyName string, property *pa
|
||||||
case *parser.Select:
|
case *parser.Select:
|
||||||
resultPtr := reflect.New(configurableType)
|
resultPtr := reflect.New(configurableType)
|
||||||
result := resultPtr.Elem()
|
result := resultPtr.Elem()
|
||||||
cases := reflect.MakeMapWithSize(reflect.MapOf(reflect.TypeOf(""), reflect.PointerTo(configuredType)), len(v.Cases))
|
conditions := make([]ConfigurableCondition, len(v.Conditions))
|
||||||
|
for i, cond := range v.Conditions {
|
||||||
|
args := make([]string, len(cond.Args))
|
||||||
|
for j, arg := range cond.Args {
|
||||||
|
args[j] = arg.Value
|
||||||
|
}
|
||||||
|
conditions[i] = ConfigurableCondition{
|
||||||
|
FunctionName: cond.FunctionName,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurableCaseType := configurableCaseType(configuredType)
|
||||||
|
cases := reflect.MakeSlice(reflect.SliceOf(configurableCaseType), 0, len(v.Cases))
|
||||||
for i, c := range v.Cases {
|
for i, c := range v.Cases {
|
||||||
p := &parser.Property{
|
p := &parser.Property{
|
||||||
Name: property.Name + "[" + strconv.Itoa(i) + "]",
|
Name: property.Name + "[" + strconv.Itoa(i) + "]",
|
||||||
NamePos: c.ColonPos,
|
NamePos: c.ColonPos,
|
||||||
Value: c.Value,
|
Value: c.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patterns := make([]configurablePattern, len(c.Patterns))
|
||||||
|
for i, pat := range c.Patterns {
|
||||||
|
switch pat := pat.(type) {
|
||||||
|
case *parser.String:
|
||||||
|
if pat.Value == "__soong_conditions_default__" {
|
||||||
|
patterns[i].typ = configurablePatternTypeDefault
|
||||||
|
} else {
|
||||||
|
patterns[i].typ = configurablePatternTypeString
|
||||||
|
patterns[i].stringValue = pat.Value
|
||||||
|
}
|
||||||
|
case *parser.Bool:
|
||||||
|
patterns[i].typ = configurablePatternTypeBool
|
||||||
|
patterns[i].boolValue = pat.Value
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value reflect.Value
|
||||||
// Map the "unset" keyword to a nil pointer in the cases map
|
// Map the "unset" keyword to a nil pointer in the cases map
|
||||||
if _, ok := c.Value.(parser.UnsetProperty); ok {
|
if _, ok := c.Value.(parser.UnsetProperty); ok {
|
||||||
cases.SetMapIndex(reflect.ValueOf(c.Pattern.Value), reflect.Zero(reflect.PointerTo(configuredType)))
|
value = reflect.Zero(reflect.PointerTo(configuredType))
|
||||||
continue
|
} else {
|
||||||
}
|
var err error
|
||||||
switch configuredType.Kind() {
|
switch configuredType.Kind() {
|
||||||
case reflect.String, reflect.Bool:
|
case reflect.String, reflect.Bool:
|
||||||
val, err := propertyToValue(reflect.PointerTo(configuredType), p)
|
value, err = propertyToValue(reflect.PointerTo(configuredType), p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.addError(&UnpackError{
|
ctx.addError(&UnpackError{
|
||||||
err,
|
err,
|
||||||
c.Value.Pos(),
|
c.Value.Pos(),
|
||||||
})
|
})
|
||||||
return reflect.New(configurableType), false
|
return reflect.New(configurableType), false
|
||||||
}
|
}
|
||||||
cases.SetMapIndex(reflect.ValueOf(c.Pattern.Value), val)
|
case reflect.Slice:
|
||||||
case reflect.Slice:
|
if configuredType.Elem().Kind() != reflect.String {
|
||||||
if configuredType.Elem().Kind() != reflect.String {
|
panic("This should be unreachable because ConfigurableElements only accepts slices of strings")
|
||||||
panic("This should be unreachable because ConfigurableElements only accepts slices of strings")
|
}
|
||||||
}
|
value, ok = ctx.unpackToSlice(p.Name, p, reflect.PointerTo(configuredType))
|
||||||
val, ok := ctx.unpackToSlice(p.Name, p, reflect.PointerTo(configuredType))
|
if !ok {
|
||||||
if !ok {
|
return reflect.New(configurableType), false
|
||||||
return reflect.New(configurableType), false
|
}
|
||||||
}
|
default:
|
||||||
cases.SetMapIndex(reflect.ValueOf(c.Pattern.Value), val)
|
panic("This should be unreachable because ConfigurableElements only accepts strings, boools, or slices of strings")
|
||||||
default:
|
}
|
||||||
panic("This should be unreachable because ConfigurableElements only accepts strings, boools, or slices of strings")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case_ := reflect.New(configurableCaseType)
|
||||||
|
case_.Interface().(configurableCaseReflection).initialize(patterns, value.Interface())
|
||||||
|
cases = reflect.Append(cases, case_.Elem())
|
||||||
}
|
}
|
||||||
resultPtr.Interface().(configurablePtrReflection).initialize(
|
resultPtr.Interface().(configurablePtrReflection).initialize(
|
||||||
property.Name,
|
property.Name,
|
||||||
v.Typ,
|
conditions,
|
||||||
v.Condition.Value,
|
|
||||||
cases.Interface(),
|
cases.Interface(),
|
||||||
)
|
)
|
||||||
if v.Append != nil {
|
if v.Append != nil {
|
||||||
|
|
|
@ -734,10 +734,9 @@ var validUnpackTestCases = []struct {
|
||||||
}{
|
}{
|
||||||
Foo: Configurable[string]{
|
Foo: Configurable[string]{
|
||||||
propertyName: "foo",
|
propertyName: "foo",
|
||||||
typ: parser.SelectTypeUnconfigured,
|
cases: []configurableCase[string]{{
|
||||||
cases: map[string]*string{
|
value: StringPtr("bar"),
|
||||||
default_select_branch_name: StringPtr("bar"),
|
}},
|
||||||
},
|
|
||||||
appendWrapper: &appendWrapper[string]{},
|
appendWrapper: &appendWrapper[string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -756,10 +755,9 @@ var validUnpackTestCases = []struct {
|
||||||
}{
|
}{
|
||||||
Foo: Configurable[bool]{
|
Foo: Configurable[bool]{
|
||||||
propertyName: "foo",
|
propertyName: "foo",
|
||||||
typ: parser.SelectTypeUnconfigured,
|
cases: []configurableCase[bool]{{
|
||||||
cases: map[string]*bool{
|
value: BoolPtr(true),
|
||||||
default_select_branch_name: BoolPtr(true),
|
}},
|
||||||
},
|
|
||||||
appendWrapper: &appendWrapper[bool]{},
|
appendWrapper: &appendWrapper[bool]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -778,10 +776,9 @@ var validUnpackTestCases = []struct {
|
||||||
}{
|
}{
|
||||||
Foo: Configurable[[]string]{
|
Foo: Configurable[[]string]{
|
||||||
propertyName: "foo",
|
propertyName: "foo",
|
||||||
typ: parser.SelectTypeUnconfigured,
|
cases: []configurableCase[[]string]{{
|
||||||
cases: map[string]*[]string{
|
value: &[]string{"a", "b"},
|
||||||
default_select_branch_name: {"a", "b"},
|
}},
|
||||||
},
|
|
||||||
appendWrapper: &appendWrapper[[]string]{},
|
appendWrapper: &appendWrapper[[]string]{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -804,12 +801,34 @@ var validUnpackTestCases = []struct {
|
||||||
}{
|
}{
|
||||||
Foo: Configurable[string]{
|
Foo: Configurable[string]{
|
||||||
propertyName: "foo",
|
propertyName: "foo",
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "my_namespace:my_variable",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*string{
|
Args: []string{
|
||||||
"a": StringPtr("a2"),
|
"my_namespace",
|
||||||
"b": StringPtr("b2"),
|
"my_variable",
|
||||||
default_select_branch_name: StringPtr("c2"),
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[string]{
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "a",
|
||||||
|
}},
|
||||||
|
value: StringPtr("a2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "b",
|
||||||
|
}},
|
||||||
|
value: StringPtr("b2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeDefault,
|
||||||
|
}},
|
||||||
|
value: StringPtr("c2"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
appendWrapper: &appendWrapper[string]{},
|
appendWrapper: &appendWrapper[string]{},
|
||||||
},
|
},
|
||||||
|
@ -837,22 +856,66 @@ var validUnpackTestCases = []struct {
|
||||||
}{
|
}{
|
||||||
Foo: Configurable[string]{
|
Foo: Configurable[string]{
|
||||||
propertyName: "foo",
|
propertyName: "foo",
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "my_namespace:my_variable",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*string{
|
Args: []string{
|
||||||
"a": StringPtr("a2"),
|
"my_namespace",
|
||||||
"b": StringPtr("b2"),
|
"my_variable",
|
||||||
default_select_branch_name: StringPtr("c2"),
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[string]{
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "a",
|
||||||
|
}},
|
||||||
|
value: StringPtr("a2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "b",
|
||||||
|
}},
|
||||||
|
value: StringPtr("b2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeDefault,
|
||||||
|
}},
|
||||||
|
value: StringPtr("c2"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
appendWrapper: &appendWrapper[string]{
|
appendWrapper: &appendWrapper[string]{
|
||||||
append: Configurable[string]{
|
append: Configurable[string]{
|
||||||
propertyName: "foo",
|
propertyName: "foo",
|
||||||
typ: parser.SelectTypeSoongConfigVariable,
|
conditions: []ConfigurableCondition{{
|
||||||
condition: "my_namespace:my_2nd_variable",
|
FunctionName: "soong_config_variable",
|
||||||
cases: map[string]*string{
|
Args: []string{
|
||||||
"d": StringPtr("d2"),
|
"my_namespace",
|
||||||
"e": StringPtr("e2"),
|
"my_2nd_variable",
|
||||||
default_select_branch_name: StringPtr("f2"),
|
},
|
||||||
|
}},
|
||||||
|
cases: []configurableCase[string]{
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "d",
|
||||||
|
}},
|
||||||
|
value: StringPtr("d2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeString,
|
||||||
|
stringValue: "e",
|
||||||
|
}},
|
||||||
|
value: StringPtr("e2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: []configurablePattern{{
|
||||||
|
typ: configurablePatternTypeDefault,
|
||||||
|
}},
|
||||||
|
value: StringPtr("f2"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
appendWrapper: &appendWrapper[string]{},
|
appendWrapper: &appendWrapper[string]{},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue