Repository: alecthomas/kong
Pull request #443 (2024-08-08 12:14:32)

Feature: Add check for overlapping xor and and groups
This adds a check to make sure that xor and and groups are not overlapping with more than one as promised in #442 


Patch:
diff --git a/README.md b/README.md
index 0d576c6..994da42 100644
--- a/README.md
+++ b/README.md
@@ -577,7 +577,7 @@ Both can coexist with standard Tag parsing.
 | `enum:"X,Y,..."`     | Set of valid values allowed for this flag. An enum field must be `required` or have a valid `default`.                                                                                                                                                                                                                         |
 | `group:"X"`          | Logical group for a flag or command.                                                                                                                                                                                                                                                                                           |
 | `xor:"X,Y,..."`      | Exclusive OR groups for flags. Only one flag in the group can be used which is restricted within the same command. When combined with `required`, at least one of the `xor` group will be required.                                                                                                                            |
-| `and:"X,Y,..."`      | Exclusive AND groups for flags. All flags in the group must be used in the same command. When combined with `required`, all flags in the group will be required.                                                                                                                                                               |
+| `and:"X,Y,..."`      | AND groups for flags. All flags in the group must be used in the same command. When combined with `required`, all flags in the group will be required.                                                                                                                                                                         |
 | `prefix:"X"`         | Prefix for all sub-flags.                                                                                                                                                                                                                                                                                                      |
 | `envprefix:"X"`      | Envar prefix for all sub-flags.                                                                                                                                                                                                                                                                                                |
 | `set:"K=V"`          | Set a variable for expansion by child elements. Multiples can occur.                                                                                                                                                                                                                                                           |
diff --git a/_examples/server/go.mod b/_examples/server/go.mod
index 690e3bb..b923839 100644
--- a/_examples/server/go.mod
+++ b/_examples/server/go.mod
@@ -12,5 +12,5 @@ require (
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/kr/pty v1.1.8
 	github.com/mattn/go-isatty v0.0.12 // indirect
-	golang.org/x/crypto v0.26.0
+	golang.org/x/crypto v0.27.0
 )
diff --git a/_examples/server/go.sum b/_examples/server/go.sum
index b0fd281..fd8f6d6 100644
--- a/_examples/server/go.sum
+++ b/_examples/server/go.sum
@@ -74,6 +74,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
 golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
 golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
+golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -126,6 +128,8 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
 golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -149,6 +153,8 @@ golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
 golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
 golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
 golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
+golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -161,6 +167,7 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/bin/.go-1.23.0.pkg b/bin/.go-1.23.1.pkg
similarity index 100%
rename from bin/.go-1.23.0.pkg
rename to bin/.go-1.23.1.pkg
diff --git a/bin/go b/bin/go
index 4602254..2cf16c6 120000
--- a/bin/go
+++ b/bin/go
@@ -1,1 +1,1 @@
-.go-1.23.0.pkg
\ No newline at end of file
+.go-1.23.1.pkg
\ No newline at end of file
diff --git a/bin/gofmt b/bin/gofmt
index 4602254..2cf16c6 120000
--- a/bin/gofmt
+++ b/bin/gofmt
@@ -1,1 +1,1 @@
-.go-1.23.0.pkg
\ No newline at end of file
+.go-1.23.1.pkg
\ No newline at end of file
diff --git a/kong.go b/kong.go
index 19ff88e..10235d0 100644
--- a/kong.go
+++ b/kong.go
@@ -167,9 +167,42 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
 
 	k.bindings.add(k.vars)
 
+	if err = checkOverlappingXorAnd(k); err != nil {
+		return nil, err
+	}
+
 	return k, nil
 }
 
+func checkOverlappingXorAnd(k *Kong) error {
+	xorGroups := map[string][]string{}
+	andGroups := map[string][]string{}
+	for _, flag := range k.Model.Node.Flags {
+		for _, xor := range flag.Xor {
+			xorGroups[xor] = append(xorGroups[xor], flag.Name)
+		}
+		for _, and := range flag.And {
+			andGroups[and] = append(andGroups[and], flag.Name)
+		}
+	}
+	for xor, xorSet := range xorGroups {
+		for and, andSet := range andGroups {
+			overlappingEntries := []string{}
+			for _, xorTag := range xorSet {
+				for _, andTag := range andSet {
+					if xorTag == andTag {
+						overlappingEntries = append(overlappingEntries, xorTag)
+					}
+				}
+			}
+			if len(overlappingEntries) > 1 {
+				return fmt.Errorf("invalid xor and combination, %s and %s overlap with more than one: %s", xor, and, overlappingEntries)
+			}
+		}
+	}
+	return nil
+}
+
 type varStack []Vars
 
 func (v *varStack) head() Vars { return (*v)[len(*v)-1] }


Test patch:
diff --git a/kong_test.go b/kong_test.go
index 2ac9cbd..c148962 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -1026,6 +1026,16 @@ func TestXorAnd(t *testing.T) {
 	assert.EqualError(t, err, "--hello and --one can't be used together, --hello and --two must be used together")
 }
 
+func TestOverLappingXorAnd(t *testing.T) {
+	var cli struct {
+		Hello bool   `xor:"one" and:"two"`
+		One   bool   `xor:"one" and:"two"`
+		Two   string `xor:"one" and:"two"`
+	}
+	_, err := kong.New(&cli)
+	assert.EqualError(t, err, "invalid xor and combination, one and two overlap with more than one: [hello one two]")
+}
+
 func TestXorRequired(t *testing.T) {
 	var cli struct {
 		One   bool `xor:"one,two" required:""`