Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a99ac620 authored by Bob Badour's avatar Bob Badour
Browse files

compliance package structures for license metadata

package to read, consume, and analyze license metadata and dependency
graph.

Bug: 68860345
Bug: 151177513
Bug: 151953481
Change-Id: I3ebf44e4d5195b9851fd076161049bf82ed76dd2
parent fab626b7
Loading
Loading
Loading
Loading
+44 −0
Original line number Diff line number Diff line
//
// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package {
    default_applicable_licenses: ["Android-Apache-2.0"],
}

bootstrap_go_package {
    name: "compliance-module",
    srcs: [
        "actionset.go",
        "condition.go",
        "conditionset.go",
        "graph.go",
        "readgraph.go",
        "resolution.go",
        "resolutionset.go",
    ],
    testSrcs: [
        "condition_test.go",
        "conditionset_test.go",
        "readgraph_test.go",
        "resolutionset_test.go",
        "test_util.go",
    ],
    deps: [
        "golang-protobuf-proto",
        "golang-protobuf-encoding-prototext",
        "license_metadata_proto",
    ],
    pkgPath: "compliance",
}
+110 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

import (
	"fmt"
	"sort"
	"strings"
)

// actionSet maps `actOn` target nodes to the license conditions the actions resolve.
type actionSet map[*TargetNode]*LicenseConditionSet

// String returns a string representation of the set.
func (as actionSet) String() string {
	var sb strings.Builder
	fmt.Fprintf(&sb, "{")
	osep := ""
	for actsOn, cs := range as {
		cl := cs.AsList()
		sort.Sort(cl)
		fmt.Fprintf(&sb, "%s%s -> %s", osep, actsOn.name, cl.String())
		osep = ", "
	}
	fmt.Fprintf(&sb, "}")
	return sb.String()
}

// byName returns the subset of `as` actions where the condition name is in `names`.
func (as actionSet) byName(names ConditionNames) actionSet {
	result := make(actionSet)
	for actsOn, cs := range as {
		bn := cs.ByName(names)
		if bn.IsEmpty() {
			continue
		}
		result[actsOn] = bn
	}
	return result
}

// byActsOn returns the subset of `as` where `actsOn` is in the `reachable` target node set.
func (as actionSet) byActsOn(reachable *TargetNodeSet) actionSet {
	result := make(actionSet)
	for actsOn, cs := range as {
		if !reachable.Contains(actsOn) || cs.IsEmpty() {
			continue
		}
		result[actsOn] = cs.Copy()
	}
	return result
}

// copy returns another actionSet with the same value as `as`
func (as actionSet) copy() actionSet {
	result := make(actionSet)
	for actsOn, cs := range as {
		if cs.IsEmpty() {
			continue
		}
		result[actsOn] = cs.Copy()
	}
	return result
}

// addSet adds all of the actions of `other` if not already present.
func (as actionSet) addSet(other actionSet) {
	for actsOn, cs := range other {
		as.add(actsOn, cs)
	}
}

// add makes the action on `actsOn` to resolve the conditions in `cs` a member of the set.
func (as actionSet) add(actsOn *TargetNode, cs *LicenseConditionSet) {
	if acs, ok := as[actsOn]; ok {
		acs.AddSet(cs)
	} else {
		as[actsOn] = cs.Copy()
	}
}

// addCondition makes the action on `actsOn` to resolve `lc` a member of the set.
func (as actionSet) addCondition(actsOn *TargetNode, lc LicenseCondition) {
	if _, ok := as[actsOn]; !ok {
		as[actsOn] = newLicenseConditionSet()
	}
	as[actsOn].Add(lc)
}

// isEmpty returns true if no action to resolve a condition exists.
func (as actionSet) isEmpty() bool {
	for _, cs := range as {
		if !cs.IsEmpty() {
			return false
		}
	}
	return true
}
+156 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

import (
	"fmt"
	"strings"
)

// LicenseCondition describes an individual license condition or requirement
// originating at a specific target node. (immutable)
//
// e.g. A module licensed under GPL terms would originate a `restricted` condition.
type LicenseCondition struct {
	name   string
	origin *TargetNode
}

// Name returns the name of the condition. e.g. "restricted" or "notice"
func (lc LicenseCondition) Name() string {
	return lc.name
}

// Origin identifies the TargetNode where the condition originates.
func (lc LicenseCondition) Origin() *TargetNode {
	return lc.origin
}

// asString returns a string representation of a license condition:
// origin+separator+condition.
func (lc LicenseCondition) asString(separator string) string {
	return lc.origin.name + separator + lc.name
}

// ConditionList implements introspection methods to arrays of LicenseCondition.
type ConditionList []LicenseCondition


// ConditionList orders arrays of LicenseCondition by Origin and Name.

// Len returns the length of the list.
func (l ConditionList) Len() int      { return len(l) }

// Swap rearranges 2 elements in the list so each occupies the other's former position.
func (l ConditionList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }

// Less returns true when the `i`th element is lexicographically less than tht `j`th element.
func (l ConditionList) Less(i, j int) bool {
	if l[i].origin.name == l[j].origin.name {
		return l[i].name < l[j].name
	}
	return l[i].origin.name < l[j].origin.name
}

// String returns a string representation of the set.
func (cl ConditionList) String() string {
	var sb strings.Builder
	fmt.Fprintf(&sb, "[")
	sep := ""
	for _, lc := range cl {
		fmt.Fprintf(&sb, "%s%s:%s", sep, lc.origin.name, lc.name)
		sep = ", "
	}
	fmt.Fprintf(&sb, "]")
	return sb.String()
}

// HasByName returns true if the list contains any condition matching `name`.
func (cl ConditionList) HasByName(name ConditionNames) bool {
	for _, lc := range cl {
		if name.Contains(lc.name) {
			return true
		}
	}
	return false
}

// ByName returns the sublist of conditions that match `name`.
func (cl ConditionList) ByName(name ConditionNames) ConditionList {
	result := make(ConditionList, 0, cl.CountByName(name))
	for _, lc := range cl {
		if name.Contains(lc.name) {
			result = append(result, lc)
		}
	}
	return result
}

// CountByName returns the size of the sublist of conditions that match `name`.
func (cl ConditionList) CountByName(name ConditionNames) int {
	size := 0
	for _, lc := range cl {
		if name.Contains(lc.name) {
			size++
		}
	}
	return size
}

// HasByOrigin returns true if the list contains any condition originating at `origin`.
func (cl ConditionList) HasByOrigin(origin *TargetNode) bool {
	for _, lc := range cl {
		if lc.origin.name == origin.name {
			return true
		}
	}
	return false
}

// ByOrigin returns the sublist of conditions that originate at `origin`.
func (cl ConditionList) ByOrigin(origin *TargetNode) ConditionList {
	result := make(ConditionList, 0, cl.CountByOrigin(origin))
	for _, lc := range cl {
		if lc.origin.name == origin.name {
			result = append(result, lc)
		}
	}
	return result
}

// CountByOrigin returns the size of the sublist of conditions that originate at `origin`.
func (cl ConditionList) CountByOrigin(origin *TargetNode) int {
	size := 0
	for _, lc := range cl {
		if lc.origin.name == origin.name {
			size++
		}
	}
	return size
}

// ConditionNames implements the Contains predicate for slices of condition
// name strings.
type ConditionNames []string

// Contains returns true if the name matches one of the ConditionNames.
func (cn ConditionNames) Contains(name string) bool {
	for _, cname := range cn {
		if cname == name {
			return true
		}
	}
	return false
}
+218 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

import (
	"sort"
	"strings"
	"testing"
)

func TestConditionNames(t *testing.T) {
	impliesShare := ConditionNames([]string{"restricted", "reciprocal"})

	if impliesShare.Contains("notice") {
		t.Errorf("impliesShare.Contains(\"notice\") got true, want false")
	}

	if !impliesShare.Contains("restricted") {
		t.Errorf("impliesShare.Contains(\"restricted\") got false, want true")
	}

	if !impliesShare.Contains("reciprocal") {
		t.Errorf("impliesShare.Contains(\"reciprocal\") got false, want true")
	}

	if impliesShare.Contains("") {
		t.Errorf("impliesShare.Contains(\"\") got true, want false")
	}
}

func TestConditionList(t *testing.T) {
	tests := []struct {
		name       string
		conditions map[string][]string
		byName     map[string][]string
		byOrigin   map[string][]string
	}{
		{
			name: "noticeonly",
			conditions: map[string][]string{
				"notice": []string{"bin1", "lib1"},
			},
			byName: map[string][]string{
				"notice":     []string{"bin1", "lib1"},
				"restricted": []string{},
			},
			byOrigin: map[string][]string{
				"bin1": []string{"notice"},
				"lib1": []string{"notice"},
				"bin2": []string{},
				"lib2": []string{},
			},
		},
		{
			name:       "empty",
			conditions: map[string][]string{},
			byName: map[string][]string{
				"notice":     []string{},
				"restricted": []string{},
			},
			byOrigin: map[string][]string{
				"bin1": []string{},
				"lib1": []string{},
				"bin2": []string{},
				"lib2": []string{},
			},
		},
		{
			name: "everything",
			conditions: map[string][]string{
				"notice":            []string{"bin1", "bin2", "lib1", "lib2"},
				"reciprocal":        []string{"bin1", "bin2", "lib1", "lib2"},
				"restricted":        []string{"bin1", "bin2", "lib1", "lib2"},
				"by_exception_only": []string{"bin1", "bin2", "lib1", "lib2"},
			},
			byName: map[string][]string{
				"permissive":        []string{},
				"notice":            []string{"bin1", "bin2", "lib1", "lib2"},
				"reciprocal":        []string{"bin1", "bin2", "lib1", "lib2"},
				"restricted":        []string{"bin1", "bin2", "lib1", "lib2"},
				"by_exception_only": []string{"bin1", "bin2", "lib1", "lib2"},
			},
			byOrigin: map[string][]string{
				"bin1":  []string{"notice", "reciprocal", "restricted", "by_exception_only"},
				"bin2":  []string{"notice", "reciprocal", "restricted", "by_exception_only"},
				"lib1":  []string{"notice", "reciprocal", "restricted", "by_exception_only"},
				"lib2":  []string{"notice", "reciprocal", "restricted", "by_exception_only"},
				"other": []string{},
			},
		},
		{
			name: "allbutoneeach",
			conditions: map[string][]string{
				"notice":            []string{"bin2", "lib1", "lib2"},
				"reciprocal":        []string{"bin1", "lib1", "lib2"},
				"restricted":        []string{"bin1", "bin2", "lib2"},
				"by_exception_only": []string{"bin1", "bin2", "lib1"},
			},
			byName: map[string][]string{
				"permissive":        []string{},
				"notice":            []string{"bin2", "lib1", "lib2"},
				"reciprocal":        []string{"bin1", "lib1", "lib2"},
				"restricted":        []string{"bin1", "bin2", "lib2"},
				"by_exception_only": []string{"bin1", "bin2", "lib1"},
			},
			byOrigin: map[string][]string{
				"bin1":  []string{"reciprocal", "restricted", "by_exception_only"},
				"bin2":  []string{"notice", "restricted", "by_exception_only"},
				"lib1":  []string{"notice", "reciprocal", "by_exception_only"},
				"lib2":  []string{"notice", "reciprocal", "restricted"},
				"other": []string{},
			},
		},
		{
			name: "oneeach",
			conditions: map[string][]string{
				"notice":            []string{"bin1"},
				"reciprocal":        []string{"bin2"},
				"restricted":        []string{"lib1"},
				"by_exception_only": []string{"lib2"},
			},
			byName: map[string][]string{
				"permissive":        []string{},
				"notice":            []string{"bin1"},
				"reciprocal":        []string{"bin2"},
				"restricted":        []string{"lib1"},
				"by_exception_only": []string{"lib2"},
			},
			byOrigin: map[string][]string{
				"bin1":  []string{"notice"},
				"bin2":  []string{"reciprocal"},
				"lib1":  []string{"restricted"},
				"lib2":  []string{"by_exception_only"},
				"other": []string{},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lg := newLicenseGraph()
			cl := toConditionList(lg, tt.conditions)
			for names, expected := range tt.byName {
				name := ConditionNames(strings.Split(names, ":"))
				if cl.HasByName(name) {
					if len(expected) == 0 {
						t.Errorf("unexpected ConditionList.HasByName(%q): got true, want false", name)
					}
				} else {
					if len(expected) != 0 {
						t.Errorf("unexpected ConditionList.HasByName(%q): got false, want true", name)
					}
				}
				if len(expected) != cl.CountByName(name) {
					t.Errorf("unexpected ConditionList.CountByName(%q): got %d, want %d", name, cl.CountByName(name), len(expected))
				}
				byName := cl.ByName(name)
				if len(expected) != len(byName) {
					t.Errorf("unexpected ConditionList.ByName(%q): got %v, want %v", name, byName, expected)
				} else {
					sort.Strings(expected)
					actual := make([]string, 0, len(byName))
					for _, lc := range byName {
						actual = append(actual, lc.Origin().Name())
					}
					sort.Strings(actual)
					for i := 0; i < len(expected); i++ {
						if expected[i] != actual[i] {
							t.Errorf("unexpected ConditionList.ByName(%q) index %d in %v: got %s, want %s", name, i, actual, actual[i], expected[i])
						}
					}
				}
			}
			for origin, expected := range tt.byOrigin {
				onode := newTestNode(lg, origin)
				if cl.HasByOrigin(onode) {
					if len(expected) == 0 {
						t.Errorf("unexpected ConditionList.HasByOrigin(%q): got true, want false", origin)
					}
				} else {
					if len(expected) != 0 {
						t.Errorf("unexpected ConditionList.HasByOrigin(%q): got false, want true", origin)
					}
				}
				if len(expected) != cl.CountByOrigin(onode) {
					t.Errorf("unexpected ConditionList.CountByOrigin(%q): got %d, want %d", origin, cl.CountByOrigin(onode), len(expected))
				}
				byOrigin := cl.ByOrigin(onode)
				if len(expected) != len(byOrigin) {
					t.Errorf("unexpected ConditionList.ByOrigin(%q): got %v, want %v", origin, byOrigin, expected)
				} else {
					sort.Strings(expected)
					actual := make([]string, 0, len(byOrigin))
					for _, lc := range byOrigin {
						actual = append(actual, lc.Name())
					}
					sort.Strings(actual)
					for i := 0; i < len(expected); i++ {
						if expected[i] != actual[i] {
							t.Errorf("unexpected ConditionList.ByOrigin(%q) index %d in %v: got %s, want %s", origin, i, actual, actual[i], expected[i])
						}
					}
				}
			}
		})
	}
}
+269 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

import (
	"fmt"
)

// NewLicenseConditionSet creates a new instance or variable of *LicenseConditionSet.
func NewLicenseConditionSet(conditions ...LicenseCondition) *LicenseConditionSet {
	cs := newLicenseConditionSet()
	cs.Add(conditions...)
	return cs
}

// LicenseConditionSet describes a mutable set of immutable license conditions.
type LicenseConditionSet struct {
	// conditions describes the set of license conditions i.e. (condition name, origin target) pairs
	// by mapping condition name -> origin target -> true.
	conditions map[string]map[*TargetNode]bool
}

// Add makes all `conditions` members of the set if they were not previously.
func (cs *LicenseConditionSet) Add(conditions ...LicenseCondition) {
	if len(conditions) == 0 {
		return
	}
	for _, lc := range conditions {
		if _, ok := cs.conditions[lc.name]; !ok {
			cs.conditions[lc.name] = make(map[*TargetNode]bool)
		}
		cs.conditions[lc.name][lc.origin] = true
	}
}

// AddSet makes all elements of `conditions` members of the set if they were not previously.
func (cs *LicenseConditionSet) AddSet(other *LicenseConditionSet) {
	if len(other.conditions) == 0 {
		return
	}
	for name, origins := range other.conditions {
		if len(origins) == 0 {
			continue
		}
		if _, ok := cs.conditions[name]; !ok {
			cs.conditions[name] = make(map[*TargetNode]bool)
		}
		for origin := range origins {
			cs.conditions[name][origin] = other.conditions[name][origin]
		}
	}
}

// ByName returns a list of the conditions in the set matching `names`.
func (cs *LicenseConditionSet) ByName(names ...ConditionNames) *LicenseConditionSet {
	other := newLicenseConditionSet()
	for _, cn := range names {
		for _, name := range cn {
			if origins, ok := cs.conditions[name]; ok {
				other.conditions[name] = make(map[*TargetNode]bool)
				for origin := range origins {
					other.conditions[name][origin] = true
				}
			}
		}
	}
	return other
}

// HasAnyByName returns true if the set contains any conditions matching `names` originating at any target.
func (cs *LicenseConditionSet) HasAnyByName(names ...ConditionNames) bool {
	for _, cn := range names {
		for _, name := range cn {
			if origins, ok := cs.conditions[name]; ok {
				if len(origins) > 0 {
					return true
				}
			}
		}
	}
	return false
}

// CountByName returns the number of conditions matching `names` originating at any target.
func (cs *LicenseConditionSet) CountByName(names ...ConditionNames) int {
	size := 0
	for _, cn := range names {
		for _, name := range cn {
			if origins, ok := cs.conditions[name]; ok {
				size += len(origins)
			}
		}
	}
	return size
}

// ByOrigin returns all of the conditions that originate at `origin` regardless of name.
func (cs *LicenseConditionSet) ByOrigin(origin *TargetNode) *LicenseConditionSet {
	other := newLicenseConditionSet()
	for name, origins := range cs.conditions {
		if _, ok := origins[origin]; ok {
			other.conditions[name] = make(map[*TargetNode]bool)
			other.conditions[name][origin] = true
		}
	}
	return other
}

// HasAnyByOrigin returns true if the set contains any conditions originating at `origin` regardless of condition name.
func (cs *LicenseConditionSet) HasAnyByOrigin(origin *TargetNode) bool {
	for _, origins := range cs.conditions {
		if _, ok := origins[origin]; ok {
			return true
		}
	}
	return false
}

// CountByOrigin returns the number of conditions originating at `origin` regardless of condition name.
func (cs *LicenseConditionSet) CountByOrigin(origin *TargetNode) int {
	size := 0
	for _, origins := range cs.conditions {
		if _, ok := origins[origin]; ok {
			size++
		}
	}
	return size
}

// AsList returns a list of all the conditions in the set.
func (cs *LicenseConditionSet) AsList() ConditionList {
	result := make(ConditionList, 0, cs.Count())
	for name, origins := range cs.conditions {
		for origin := range origins {
			result = append(result, LicenseCondition{name, origin})
		}
	}
	return result
}

// Count returns the number of conditions in the set.
func (cs *LicenseConditionSet) Count() int {
	size := 0
	for _, origins := range cs.conditions {
		size += len(origins)
	}
	return size
}

// Copy creates a new LicenseCondition variable with the same value.
func (cs *LicenseConditionSet) Copy() *LicenseConditionSet {
	other := newLicenseConditionSet()
	for name := range cs.conditions {
		other.conditions[name] = make(map[*TargetNode]bool)
		for origin := range cs.conditions[name] {
			other.conditions[name][origin] = cs.conditions[name][origin]
		}
	}
	return other
}

// HasCondition returns true if the set contains any condition matching both `names` and `origin`.
func (cs *LicenseConditionSet) HasCondition(names ConditionNames, origin *TargetNode) bool {
	for _, name := range names {
		if origins, ok := cs.conditions[name]; ok {
			_, isPresent := origins[origin]
			if isPresent {
				return true
			}
		}
	}
	return false
}

// IsEmpty returns true when the set of conditions contains zero elements.
func (cs *LicenseConditionSet) IsEmpty() bool {
	for _, origins := range cs.conditions {
		if 0 < len(origins) {
			return false
		}
	}
	return true
}

// RemoveAllByName changes the set to delete all conditions matching `names`.
func (cs *LicenseConditionSet) RemoveAllByName(names ...ConditionNames) {
	for _, cn := range names {
		for _, name := range cn {
			delete(cs.conditions, name)
		}
	}
}

// Remove changes the set to delete `conditions`.
func (cs *LicenseConditionSet) Remove(conditions ...LicenseCondition) {
	for _, lc := range conditions {
		if _, isPresent := cs.conditions[lc.name]; !isPresent {
			panic(fmt.Errorf("attempt to remove non-existent condition: %q", lc.asString(":")))
		}
		if _, isPresent := cs.conditions[lc.name][lc.origin]; !isPresent {
			panic(fmt.Errorf("attempt to remove non-existent origin: %q", lc.asString(":")))
		}
		delete(cs.conditions[lc.name], lc.origin)
	}
}

// removeSet changes the set to delete all conditions also present in `other`.
func (cs *LicenseConditionSet) RemoveSet(other *LicenseConditionSet) {
	for name, origins := range other.conditions {
		if _, isPresent := cs.conditions[name]; !isPresent {
			continue
		}
		for origin := range origins {
			delete(cs.conditions[name], origin)
		}
	}
}

// compliance-only LicenseConditionSet methods

// newLicenseConditionSet constructs a set of `conditions`.
func newLicenseConditionSet() *LicenseConditionSet {
	return &LicenseConditionSet{make(map[string]map[*TargetNode]bool)}
}

// add changes the set to include each element of `conditions` originating at `origin`.
func (cs *LicenseConditionSet) add(origin *TargetNode, conditions ...string) {
	for _, name := range conditions {
		if _, ok := cs.conditions[name]; !ok {
			cs.conditions[name] = make(map[*TargetNode]bool)
		}
		cs.conditions[name][origin] = true
	}
}

// asStringList returns the conditions in the set as `separator`-separated (origin, condition-name) pair strings.
func (cs *LicenseConditionSet) asStringList(separator string) []string {
	result := make([]string, 0, cs.Count())
	for name, origins := range cs.conditions {
		for origin := range origins {
			result = append(result, origin.name+separator+name)
		}
	}
	return result
}

// conditionNamesArray implements a `contains` predicate for arrays of ConditionNames
type conditionNamesArray []ConditionNames

func (cn conditionNamesArray) contains(name string) bool {
	for _, names := range cn {
		if names.Contains(name) {
			return true
		}
	}
	return false
}
Loading