Support license expression in dep check. (#120)

diff --git a/pkg/deps/check.go b/pkg/deps/check.go
index d00a1f2..15bc546 100644
--- a/pkg/deps/check.go
+++ b/pkg/deps/check.go
@@ -28,12 +28,21 @@
 	"github.com/apache/skywalking-eyes/internal/logger"
 )
 
-type compatibilityMatrix struct {
+type CompatibilityMatrix struct {
 	Compatible   []string `yaml:"compatible"`
 	Incompatible []string `yaml:"incompatible"`
 }
 
-var matrices = make(map[string]compatibilityMatrix)
+var matrices = make(map[string]CompatibilityMatrix)
+
+type LicenseOperator int
+
+const (
+	LicenseOperatorNone LicenseOperator = iota
+	LicenseOperatorAND
+	LicenseOperatorOR
+	LicenseOperatorWITH
+)
 
 func init() {
 	dir := "compatibility"
@@ -43,7 +52,7 @@
 	}
 	for _, file := range files {
 		name := file.Name()
-		matrix := compatibilityMatrix{}
+		matrix := CompatibilityMatrix{}
 		if bytes, err := assets.Asset(filepath.Join(dir, name)); err != nil {
 			logger.Log.Fatalln("Failed to read compatibility file:", name, err)
 		} else if err := yaml.Unmarshal(bytes, &matrix); err != nil {
@@ -54,27 +63,78 @@
 }
 
 func Check(mainLicenseSpdxID string, config *ConfigDeps) error {
+	matrix := matrices[mainLicenseSpdxID]
+
 	report := Report{}
 	if err := Resolve(config, &report); err != nil {
 		return nil
 	}
 
-	matrix := matrices[mainLicenseSpdxID]
+	return CheckWithMatrix(mainLicenseSpdxID, &matrix, &report)
+}
+
+func CheckWithMatrix(mainLicenseSpdxID string, matrix *CompatibilityMatrix, report *Report) error {
 	var incompatibleResults []*Result
 	for _, result := range append(report.Resolved, report.Skipped...) {
-		compare := func(list []string) bool {
+		compare := func(list []string, spdxID string) bool {
 			for _, com := range list {
-				if result.LicenseSpdxID == com {
+				if spdxID == com {
 					return true
 				}
 			}
 			return false
 		}
-		if compatible := compare(matrix.Compatible); compatible {
-			continue
+		compareAll := func(spdxIDs []string, compare func(spdxID string) bool) bool {
+			for _, spdxID := range spdxIDs {
+				if !compare(spdxID) {
+					return false
+				}
+			}
+			return true
 		}
-		if incompatible := compare(matrix.Incompatible); incompatible {
-			incompatibleResults = append(incompatibleResults, result)
+		compareAny := func(spdxIDs []string, compare func(spdxID string) bool) bool {
+			for _, spdxID := range spdxIDs {
+				if compare(spdxID) {
+					return true
+				}
+			}
+			return false
+		}
+
+		operator, spdxIDs := parseLicenseExpression(result.LicenseSpdxID)
+
+		switch operator {
+		case LicenseOperatorAND:
+			if compareAll(spdxIDs, func(spdxID string) bool {
+				return compare(matrix.Compatible, spdxID)
+			}) {
+				continue
+			}
+			if compareAny(spdxIDs, func(spdxID string) bool {
+				return compare(matrix.Incompatible, spdxID)
+			}) {
+				incompatibleResults = append(incompatibleResults, result)
+			}
+
+		case LicenseOperatorOR:
+			if compareAny(spdxIDs, func(spdxID string) bool {
+				return compare(matrix.Compatible, spdxID)
+			}) {
+				continue
+			}
+			if compareAll(spdxIDs, func(spdxID string) bool {
+				return compare(matrix.Incompatible, spdxID)
+			}) {
+				incompatibleResults = append(incompatibleResults, result)
+			}
+
+		default:
+			if compatible := compare(matrix.Compatible, spdxIDs[0]); compatible {
+				continue
+			}
+			if incompatible := compare(matrix.Incompatible, spdxIDs[0]); incompatible {
+				incompatibleResults = append(incompatibleResults, result)
+			}
 		}
 	}
 
@@ -88,3 +148,25 @@
 
 	return nil
 }
+
+func parseLicenseExpression(s string) (operator LicenseOperator, spdxIDs []string) {
+	if ss := strings.Split(s, " AND "); len(ss) > 1 {
+		return LicenseOperatorAND, ss
+	}
+	if ss := strings.Split(s, " and "); len(ss) > 1 {
+		return LicenseOperatorAND, ss
+	}
+	if ss := strings.Split(s, " OR "); len(ss) > 1 {
+		return LicenseOperatorOR, ss
+	}
+	if ss := strings.Split(s, " or "); len(ss) > 1 {
+		return LicenseOperatorOR, ss
+	}
+	if ss := strings.Split(s, " WITH "); len(ss) > 1 {
+		return LicenseOperatorWITH, ss
+	}
+	if ss := strings.Split(s, " with "); len(ss) > 1 {
+		return LicenseOperatorWITH, ss
+	}
+	return LicenseOperatorNone, []string{s}
+}
diff --git a/pkg/deps/check_test.go b/pkg/deps/check_test.go
new file mode 100644
index 0000000..2ba66a4
--- /dev/null
+++ b/pkg/deps/check_test.go
@@ -0,0 +1,148 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 deps_test
+
+import (
+	"github.com/apache/skywalking-eyes/pkg/deps"
+	"strings"
+	"testing"
+)
+
+var TestMatrix = deps.CompatibilityMatrix{
+	Compatible: []string{
+		"Apache-2.0",
+		"PHP-3.01",
+		"BSD-3-Clause",
+		"BSD-2-Clause",
+		"PostgreSQL",
+		"EPL-1.0",
+		"ISC",
+	},
+	Incompatible: []string{
+		"Unknown",
+		"LGPL-2.0+",
+		"LGPL-2.0",
+		"LGPL-2.0-only",
+		"LGPL-2.0-or-later",
+		"LGPL-2.1+",
+		"LGPL-2.1",
+		"LGPL-2.1-only",
+		"LGPL-2.1-or-later",
+		"LGPL-3.0+",
+		"LGPL-3.0",
+		"GPL-3.0+",
+		"GPL-3.0",
+		"GPL-2.0+",
+		"GPL-2.0",
+		"GPL-2.0-only",
+		"GPL-2.0-or-later",
+	},
+}
+
+func TestCheckWithMatrix(t *testing.T) {
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "Apache-2.0",
+			},
+		},
+	}); err != nil {
+		t.Errorf("Shouldn't return error")
+	}
+
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "Apache-2.0",
+			},
+			{
+				Dependency:    "Bar",
+				LicenseSpdxID: "LGPL-2.0",
+			},
+		},
+	}); err == nil {
+		t.Errorf("Should return error")
+	} else if !strings.Contains(err.Error(), "License: LGPL-2.0 Dependency: Bar") {
+		t.Errorf("Should return error and contains dependency Bar, now is `%s`", err.Error())
+	}
+
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "Apache-2.0",
+			},
+		},
+		Skipped: []*deps.Result{
+			{
+				Dependency:    "Bar",
+				LicenseSpdxID: "Unknown",
+			},
+		},
+	}); err == nil {
+		t.Errorf("Should return error")
+	} else if !strings.Contains(err.Error(), "License: Unknown Dependency: Bar") {
+		t.Errorf("Should return error and has dependency Bar, now is `%s`", err.Error())
+	}
+
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "Apache-2.0 OR MIT",
+			},
+		},
+	}); err != nil {
+		t.Errorf("Shouldn't return error")
+	}
+
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "GPL-3.0 and GPL-3.0-or-later",
+			},
+		},
+	}); err == nil {
+		t.Errorf("Should return error")
+	}
+
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "LGPL-2.1-only AND MIT AND BSD-2-Clause",
+			},
+		},
+	}); err == nil {
+		t.Errorf("Should return error")
+	}
+
+	if err := deps.CheckWithMatrix("Apache-2.0", &TestMatrix, &deps.Report{
+		Resolved: []*deps.Result{
+			{
+				Dependency:    "Foo",
+				LicenseSpdxID: "GPL-2.0-or-later WITH Bison-exception-2.2",
+			},
+		},
+	}); err == nil {
+		t.Errorf("Should return error")
+	}
+}