diff --git a/src/spdx/expressions.clj b/src/spdx/expressions.clj index 59d27b8..1e90ba6 100644 --- a/src/spdx/expressions.clj +++ b/src/spdx/expressions.clj @@ -142,6 +142,69 @@ (def ^:private not-blank? (complement s/blank?)) +(defn- license-ref->string + "Turns map `m` containing a license-ref into a String, returning `nil` if + there isn't one." + [m] + (when (and m (:license-ref m)) + (str (when-let [document-ref (:document-ref m)] (str "DocumentRef-" document-ref ":")) + "LicenseRef-" (:license-ref m)))) + +(defn- addition-ref->string + "Turns map `m` containing an addition-ref into a String, returning `nil` if + there isn't one." + [m] + (when (and m (:addition-ref m)) + (str (when-let [addition-document-ref (:addition-document-ref m)] (str "DocumentRef-" addition-document-ref ":")) + "AdditionRef-" (:addition-ref m)))) + +(defn- license-map->string + "Turns a license map into a (non-readable) string, primarily for the purposes + of comparison." + [m] + (when m + (str (when (:license-id m) (:license-id m)) + (when (:or-later? m) "+") + (when (:license-ref m) (license-ref->string m)) + (when (:license-exception-id m) (str " WITH " (:license-exception-id m))) + (when (:addition-ref m) (str " WITH " (addition-ref->string m)))))) + +(defn- unparse-internal + "Internal implementation of unparse." + [level parse-result] + (when parse-result + (cond + (sequential? parse-result) + (when (pos? (count parse-result)) + (let [op-str (str " " (s/upper-case (name (first parse-result))) " ")] + (str (when (pos? level) "(") + (s/join op-str (map (partial unparse-internal (inc level)) (rest parse-result))) ; Note: naive (stack consuming) recursion + (when (pos? level) ")")))) + (map? parse-result) + (license-map->string parse-result)))) + +(defn unparse + "Turns a valid `parse-result` (i.e. obtained from [[parse]]) back into an + SPDX expression (a `String`), or `nil` if `parse-result` is `nil`. Results + are undefined for invalid parse trees." + [parse-result] + (when-let [result (unparse-internal 0 parse-result)] + (when-not (s/blank? result) + (s/trim result)))) + +(defn- normalise-nested-operators + "Normalises nested operators of the same type." + [type coll] + (loop [result [type] + f (first coll) + r (rest coll)] + (if-not f + (vec result) + (if (and (sequential? f) + (= type (first f))) + (recur (concat result (rest f)) (first r) (rest r)) + (recur (concat result [f]) (first r) (rest r)))))) + (defn- normalise-gpl-id "Normalises a GPL family `license-id` to a tuple (2 element vector) containing the non-deprecated equivalent license id in first position, and (optionally - @@ -187,34 +250,61 @@ [parse-tree] (cond (keyword? parse-tree) parse-tree - (sequential? parse-tree) (some-> (seq (map normalise-gpl-elements parse-tree)) vec) ; Note: naive (stack consuming) recursion (map? parse-tree) (if (contains? gpl-family-ids (:license-id parse-tree)) (normalise-gpl-license-map parse-tree) - parse-tree))) + parse-tree) + (sequential? parse-tree) (some-> (seq (map normalise-gpl-elements parse-tree)) vec))) ; Note: naive (stack consuming) recursion (defn- collapse-redundant-clauses "Collapses redundant clauses in `parse-tree`." [parse-tree] (cond (keyword? parse-tree) parse-tree + (map? parse-tree) parse-tree (sequential? parse-tree) (let [result (some-> (seq (distinct (map collapse-redundant-clauses parse-tree))) vec)] ; Note: naive (stack consuming) recursion (if (= 2 (count result)) (second result) - result)) - (map? parse-tree) parse-tree)) - -(defn- normalise-nested-operators - "Normalises nested operators of the same type." - [type coll] - (loop [result [type] - f (first coll) - r (rest coll)] - (if-not f - (vec result) - (if (and (sequential? f) - (= type (first f))) - (recur (concat result (rest f)) (first r) (rest r)) - (recur (concat result [f]) (first r) (rest r)))))) + result)))) + +(defn- compare-license-maps + "Compares two license maps, as found in a parse tree." + [x y] + ; Todo: consider case-insensitive sorting in future, assuming LicenseRefs & AdditionRefs are _not_ case sensitive (awaiting feedback from spdx-tech on that...) + (compare (license-map->string x) (license-map->string y))) + +(defn- compare-license-sequences + "Compares two license sequences, as found in a parse tree. Comparisons are + based on length - first by number of elements, then, for equi-sized sequences, + by lexicographical length (which is a little hokey, but ensures that 'longest' + sequences go last, for a reasonable definition of 'longest')." + [x y] + (let [result (compare (count x) (count y))] + (if (= 0 result) + (compare (unparse x) (unparse y)) + result))) + +(defn- parse-tree-compare + "sort-by comparator for parse-trees" + [x y] + (cond + (and (keyword? x) (keyword? y)) (compare x y) + (keyword? x) -1 + (keyword? y) 1 + (and (map? x) (map? y)) (compare-license-maps x y) ; Because compare doesn't support maps + (map? x) -1 + (and (sequential? x) (sequential? y)) (compare-license-sequences x y) ; Because compare doesn't support maps (which will be elements inside x and y) + :else 1)) + +(defn- sort-parse-tree + "Sorts the parse tree so that logically equivalent expressions produce the + same parse tree e.g. parsing `Apache-2.0 OR MIT` will produce the same parse + tree as parsing `MIT OR Apache-2.0`." + [parse-tree] + (cond + (keyword? parse-tree) parse-tree + (map? parse-tree) parse-tree + (sequential? parse-tree) (let [result (some-> (seq (map sort-parse-tree parse-tree)) vec)] ; Note: naive (stack consuming) recursion + (some-> (seq (sort-by identity parse-tree-compare result)) vec)))) (defn parse-with-info "As for [[parse]], but returns an [instaparse parse error](https://github.com/Engelberg/instaparse#parse-errors) @@ -224,10 +314,12 @@ ([s] (parse-with-info s nil)) ([^String s {:keys [normalise-gpl-ids? case-sensitive-operators? - collapse-redundant-clauses?] + collapse-redundant-clauses? + sort-licenses?] :or {normalise-gpl-ids? true case-sensitive-operators? false - collapse-redundant-clauses? true}}] + collapse-redundant-clauses? true + sort-licenses? true}}] (when-not (s/blank? s) (let [parser (if case-sensitive-operators? @spdx-license-expression-cs-parser-d @spdx-license-expression-ci-parser-d) result (insta/parse parser s)] @@ -254,7 +346,8 @@ (vec %&))} result) result (if normalise-gpl-ids? (normalise-gpl-elements result) result) - result (if collapse-redundant-clauses? (collapse-redundant-clauses result) result)] + result (if collapse-redundant-clauses? (collapse-redundant-clauses result) result) + result (if sort-licenses? (sort-parse-tree result) result)] result)))))) #_{:clj-kondo/ignore [:unused-binding]} @@ -275,6 +368,12 @@ * `:collapse-redundant-clauses?` (`boolean`, default `true`) - controls whether redundant clauses (e.g. \"Apache-2.0 AND Apache-2.0\") are collapsed during parsing. + * `:sort-licenses?` (`boolean`, default `true`) - controls whether licenses + that appear at the same level in the parse tree are sorted alphabetically. + This means that some parse trees will be identical for different (though + logically identical) inputs, which can be useful in many cases. For example + the parse tree for `Apache-2.0 OR MIT` would be identical to the parse tree + for `MIT OR Apache-2.0`. Notes: @@ -325,56 +424,17 @@ ([s] (parse s nil)) ([s {:keys [normalise-gpl-ids? case-sensitive-operators? - collapse-redundant-clauses?] + collapse-redundant-clauses? + sort-licenses?] :or {normalise-gpl-ids? true case-sensitive-operators? false - collapse-redundant-clauses? true} + collapse-redundant-clauses? true + sort-licenses? true} :as opts}] (when-let [raw-parse-result (parse-with-info s opts)] (when-not (insta/failure? raw-parse-result) raw-parse-result)))) -(defn- unparse-license-ref - "Unparses a license-ref from map `m`, returning `nil` if there isn't one." - [m] - (when (and m (:license-ref m)) - (str (when-let [document-ref (:document-ref m)] (str "DocumentRef-" document-ref ":")) - "LicenseRef-" (:license-ref m)))) - -(defn- unparse-addition-ref - "Unparses an addition-ref from map `m`, returning `nil` if there isn't one." - [m] - (when (and m (:addition-ref m)) - (str (when-let [addition-document-ref (:addition-document-ref m)] (str "DocumentRef-" addition-document-ref ":")) - "AdditionRef-" (:addition-ref m)))) - -(defn- unparse-internal - "Internal implementation of unparse." - [level parse-result] - (when parse-result - (cond - (sequential? parse-result) - (when (pos? (count parse-result)) - (let [op-str (str " " (s/upper-case (name (first parse-result))) " ")] - (str (when (pos? level) "(") - (s/join op-str (map (partial unparse-internal (inc level)) (rest parse-result))) ; Note: naive (stack consuming) recursion - (when (pos? level) ")")))) - (map? parse-result) - (str (:license-id parse-result) - (when (:or-later? parse-result) "+") - (when (:license-ref parse-result) (unparse-license-ref parse-result)) - (when (:license-exception-id parse-result) (str " WITH " (:license-exception-id parse-result))) - (when (:addition-ref parse-result) (str " WITH " (unparse-addition-ref parse-result))))))) - -(defn unparse - "Turns a valid `parse-result` (i.e. obtained from [[parse]]) back into an - SPDX expression (a `String`), or `nil` if `parse-result` is `nil`. Results - are undefined for invalid parse trees." - [parse-result] - (when-let [result (unparse-internal 0 parse-result)] - (when-not (s/blank? result) - (s/trim result)))) - (defn normalise "Normalises an SPDX expression, by running it through [[parse]] then [[unparse]]. Returns `nil` if `s` is not a valid SPDX expression. @@ -444,8 +504,8 @@ (sequential? parse-result) (set (mapcat #(extract-ids % opts) parse-result)) ; Note: naive (stack consuming) recursion (map? parse-result) (set/union (when (:license-id parse-result) #{(str (:license-id parse-result) (when (and include-or-later? (:or-later? parse-result)) "+"))}) (when (:license-exception-id parse-result) #{(:license-exception-id parse-result)}) - (when (:license-ref parse-result) #{(unparse-license-ref parse-result)}) - (when (:addition-ref parse-result) #{(unparse-addition-ref parse-result)})) + (when (:license-ref parse-result) #{(license-ref->string parse-result)}) + (when (:addition-ref parse-result) #{(addition-ref->string parse-result)})) :else nil)))) (defn init! diff --git a/test/spdx/expressions_test.clj b/test/spdx/expressions_test.clj index 517ff08..61c8adc 100644 --- a/test/spdx/expressions_test.clj +++ b/test/spdx/expressions_test.clj @@ -79,8 +79,8 @@ [:or {:license-id "Apache-2.0"} [:and - {:license-id "MIT"} - {:license-id "BSD-2-Clause"}]])) + {:license-id "BSD-2-Clause"} + {:license-id "MIT"}]])) (is (= (parse "Apache-2.0 OR GPL-2.0 WITH Classpath-exception-2.0") [:or {:license-id "Apache-2.0"} @@ -96,36 +96,36 @@ :license-exception-id "Classpath-exception-2.0"}])) (is (= (parse "(Apache-2.0 AND MIT) OR GPL-2.0+ WITH Classpath-exception-2.0 OR DocumentRef-foo:LicenseRef-bar") [:or - [:and {:license-id "Apache-2.0"} {:license-id "MIT"}] + {:license-ref "bar" :document-ref "foo"} {:license-id "GPL-2.0-or-later" :license-exception-id "Classpath-exception-2.0"} - {:license-ref "bar" :document-ref "foo"}])) + [:and {:license-id "Apache-2.0"} {:license-id "MIT"}]])) (is (= (parse "LicenseRef-foo WITH AdditionRef-bar") {:license-ref "foo" :addition-ref "bar"})) (is (= (parse "DocumentRef-foo:LicenseRef-bar WITH DocumentRef-blah:AdditionRef-banana") {:document-ref "foo" :license-ref "bar" :addition-document-ref "blah" :addition-ref "banana"}))) (testing "Expressions that exercise operator precedence" (is (= (parse "GPL-2.0-only AND Apache-2.0 OR MIT") [:or - [:and {:license-id "GPL-2.0-only"} {:license-id "Apache-2.0"}] - {:license-id "MIT"}])) + {:license-id "MIT"} + [:and {:license-id "Apache-2.0"} {:license-id "GPL-2.0-only"}]])) (is (= (parse "GPL-2.0-only OR Apache-2.0 AND MIT") [:or {:license-id "GPL-2.0-only"} [:and {:license-id "Apache-2.0"} {:license-id "MIT"}]])) (is (= (parse "GPL-2.0-only AND Apache-2.0 OR MIT AND BSD-3-Clause") [:or - [:and {:license-id "GPL-2.0-only"} {:license-id "Apache-2.0"}] - [:and {:license-id "MIT"} {:license-id "BSD-3-Clause"}]])) + [:and {:license-id "Apache-2.0"} {:license-id "GPL-2.0-only"}] + [:and {:license-id "BSD-3-Clause"} {:license-id "MIT"}]])) (is (= (parse "GPL-2.0-only OR Apache-2.0 OR MIT OR BSD-3-Clause OR Unlicense") [:or - {:license-id "GPL-2.0-only"} {:license-id "Apache-2.0"} - {:license-id "MIT"} {:license-id "BSD-3-Clause"} + {:license-id "GPL-2.0-only"} + {:license-id "MIT"} {:license-id "Unlicense"}])) (is (= (parse "GPL-2.0-only AND Apache-2.0 AND MIT AND BSD-3-Clause AND Unlicense") [:and - {:license-id "GPL-2.0-only"} {:license-id "Apache-2.0"} - {:license-id "MIT"} {:license-id "BSD-3-Clause"} + {:license-id "GPL-2.0-only"} + {:license-id "MIT"} {:license-id "Unlicense"}]))) (testing "Expressions that exercise GPL identifier normalisation" (is (= (parse "AGPL-1.0-or-later") {:license-id "AGPL-1.0-or-later"})) @@ -138,12 +138,12 @@ {:license-id "GPL-2.0-only" :license-exception-id "Classpath-exception-2.0"})) (is (= (parse "GPL-2.0-with-GCC-exception WITH Classpath-exception-2.0") [:and - {:license-id "GPL-2.0-only" :license-exception-id "GCC-exception-2.0"} - {:license-id "GPL-2.0-only" :license-exception-id "Classpath-exception-2.0"}])) + {:license-id "GPL-2.0-only" :license-exception-id "Classpath-exception-2.0"} + {:license-id "GPL-2.0-only" :license-exception-id "GCC-exception-2.0"}])) (is (= (parse "GPL-2.0-with-GCC-exception+ WITH Classpath-exception-2.0") [:and - {:license-id "GPL-2.0-or-later" :license-exception-id "GCC-exception-2.0"} - {:license-id "GPL-2.0-or-later" :license-exception-id "Classpath-exception-2.0"}]))) + {:license-id "GPL-2.0-or-later" :license-exception-id "Classpath-exception-2.0"} + {:license-id "GPL-2.0-or-later" :license-exception-id "GCC-exception-2.0"}]))) (testing "Expressions that exercise collapsing redundant clauses" (is (= (parse "Apache-2.0 OR Apache-2.0") {:license-id "Apache-2.0"})) (is (= (parse "Apache-2.0 AND Apache-2.0" {:collapse-redundant-clauses? true}) @@ -168,7 +168,12 @@ {:license-id "GPL-2.0-or-later" :license-exception-id "Classpath-exception-2.0"})) (is (= (parse "LicenseRef-foo OR LicenseRef-foo") {:license-ref "foo"})) (is (= (parse "DocumentRef-foo:LicenseRef-bar AND DocumentRef-foo:LicenseRef-bar ") - {:document-ref "foo" :license-ref "bar"})))) + {:document-ref "foo" :license-ref "bar"}))) + (testing "Expressions that exercise sorting of licenses" + (is (= (parse "Apache-2.0 OR MIT") [:or {:license-id "Apache-2.0"} {:license-id "MIT"}])) + (is (= (parse "MIT OR Apache-2.0") [:or {:license-id "Apache-2.0"} {:license-id "MIT"}])) + (is (= (parse "MIT OR Apache-2.0" {:sort-licenses? true}) [:or {:license-id "Apache-2.0"} {:license-id "MIT"}])) + (is (= (parse "MIT OR Apache-2.0" {:sort-licenses? false}) [:or {:license-id "MIT"} {:license-id "Apache-2.0"}])))) (deftest unnormalised-parse-tests (testing "Simple expressions" @@ -197,9 +202,9 @@ :license-exception-id "Classpath-exception-2.0"}])) (is (= (parse "(Apache-2.0 AND MIT) OR GPL-2.0+ WITH Classpath-exception-2.0 OR DocumentRef-foo:LicenseRef-bar" {:normalise-gpl-ids? false}) [:or - [:and {:license-id "Apache-2.0"} {:license-id "MIT"}] + {:license-ref "bar" :document-ref "foo"} {:license-id "GPL-2.0" :or-later? true :license-exception-id "Classpath-exception-2.0"} - {:license-ref "bar" :document-ref "foo"}])) + [:and {:license-id "Apache-2.0"} {:license-id "MIT"}]])) (is (= (parse "GPL-2.0-with-GCC-exception WITH Classpath-exception-2.0" {:normalise-gpl-ids? false}) {:license-id "GPL-2.0-with-GCC-exception" :license-exception-id "Classpath-exception-2.0"})))) @@ -253,7 +258,7 @@ (is (= (unparse (parse "Apache-2.0 OR (GPL-2.0+ WITH Classpath-exception-2.0)")) "Apache-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0")) (is (= (unparse (parse "(Apache-2.0+ AND MIT) OR GPL-2.0+ WITH Classpath-exception-2.0 OR (BSD-2-Clause AND DocumentRef-bar:LicenseRef-foo)")) - "(Apache-2.0+ AND MIT) OR GPL-2.0-or-later WITH Classpath-exception-2.0 OR (BSD-2-Clause AND DocumentRef-bar:LicenseRef-foo)")))) + "GPL-2.0-or-later WITH Classpath-exception-2.0 OR (Apache-2.0+ AND MIT) OR (BSD-2-Clause AND DocumentRef-bar:LicenseRef-foo)")))) ; Note: we keep these short(ish), as the parser is far more extensively exercised by parse-tests and unparse-tests ; Precedence rule tests are only here however, as they're less cumbersome to test using normalise @@ -281,36 +286,36 @@ (is (= (normalise "LicenseRef-foo") "LicenseRef-foo")) (is (= (normalise "DocumentRef-foo:LicenseRef-bar") "DocumentRef-foo:LicenseRef-bar"))) (testing "Compound expressions" - (is (= (normalise "MIT and AGPL-3.0") "MIT AND AGPL-3.0-only")) + (is (= (normalise "MIT and AGPL-3.0") "AGPL-3.0-only AND MIT")) (is (= (normalise "(GPL-2.0 WITH Classpath-exception-2.0)") "GPL-2.0-only WITH Classpath-exception-2.0")) - (is (= (normalise "BSD-2-Clause AND MIT or GPL-2.0+ WITH Classpath-exception-2.0") "(BSD-2-Clause AND MIT) OR GPL-2.0-or-later WITH Classpath-exception-2.0")) - (is (= (normalise "(BSD-2-Clause AND MIT) Or GPL-2.0+ WITH Classpath-exception-2.0") "(BSD-2-Clause AND MIT) OR GPL-2.0-or-later WITH Classpath-exception-2.0")) - (is (= (normalise "GPL-2.0-with-GCC-exception WiTh Classpath-exception-2.0") "GPL-2.0-only WITH GCC-exception-2.0 AND GPL-2.0-only WITH Classpath-exception-2.0")) + (is (= (normalise "BSD-2-Clause AND MIT or GPL-2.0+ WITH Classpath-exception-2.0") "GPL-2.0-or-later WITH Classpath-exception-2.0 OR (BSD-2-Clause AND MIT)")) + (is (= (normalise "(BSD-2-Clause AND MIT) Or GPL-2.0+ WITH Classpath-exception-2.0") "GPL-2.0-or-later WITH Classpath-exception-2.0 OR (BSD-2-Clause AND MIT)")) + (is (= (normalise "GPL-2.0-with-GCC-exception WiTh Classpath-exception-2.0") "GPL-2.0-only WITH Classpath-exception-2.0 AND GPL-2.0-only WITH GCC-exception-2.0")) (is (= (normalise "LicenseRef-foo WITH Classpath-exception-2.0") "LicenseRef-foo WITH Classpath-exception-2.0")) (is (= (normalise "Apache-2.0 WITH AdditionRef-foo") "Apache-2.0 WITH AdditionRef-foo")) (is (= (normalise "LicenseRef-foo with AdditionRef-blah") "LicenseRef-foo WITH AdditionRef-blah")) (is (= (normalise "DocumentRef-foo:LicenseRef-bar wItH DocumentRef-blah:AdditionRef-banana") "DocumentRef-foo:LicenseRef-bar WITH DocumentRef-blah:AdditionRef-banana"))) (testing "Precedence rules" - (is (= (normalise "Apache-2.0 OR (MIT or BSD-3-Clause)") "Apache-2.0 OR MIT OR BSD-3-Clause")) - (is (= (normalise "Apache-2.0 and (MIT AND BSD-3-Clause)") "Apache-2.0 AND MIT AND BSD-3-Clause")) + (is (= (normalise "Apache-2.0 OR (MIT or BSD-3-Clause)") "Apache-2.0 OR BSD-3-Clause OR MIT")) + (is (= (normalise "Apache-2.0 and (MIT AND BSD-3-Clause)") "Apache-2.0 AND BSD-3-Clause AND MIT")) (is (= (normalise "((((((Apache-2.0)))))) AND (MIT and BSD-3-Clause)") - "Apache-2.0 AND MIT AND BSD-3-Clause")) - (is (= (normalise "(Apache-2.0 or MIT) or BSD-3-Clause") "Apache-2.0 OR MIT OR BSD-3-Clause")) - (is (= (normalise "(Apache-2.0 and MIT) and BSD-3-Clause") "Apache-2.0 AND MIT AND BSD-3-Clause")) - (is (= (normalise "Apache-2.0 oR MIT aNd BSD-3-Clause") "Apache-2.0 OR (MIT AND BSD-3-Clause)")) - (is (= (normalise "Apache-2.0 AnD MIT Or BSD-3-Clause") "(Apache-2.0 AND MIT) OR BSD-3-Clause")) + "Apache-2.0 AND BSD-3-Clause AND MIT")) + (is (= (normalise "(Apache-2.0 or MIT) or BSD-3-Clause") "Apache-2.0 OR BSD-3-Clause OR MIT")) + (is (= (normalise "(Apache-2.0 and MIT) and BSD-3-Clause") "Apache-2.0 AND BSD-3-Clause AND MIT")) + (is (= (normalise "Apache-2.0 oR MIT aNd BSD-3-Clause") "Apache-2.0 OR (BSD-3-Clause AND MIT)")) + (is (= (normalise "Apache-2.0 AnD MIT Or BSD-3-Clause") "BSD-3-Clause OR (Apache-2.0 AND MIT)")) (is (= (normalise "Apache-2.0 or MIT and BSD-3-Clause or Unlicense") - "Apache-2.0 OR (MIT AND BSD-3-Clause) OR Unlicense")) + "Apache-2.0 OR Unlicense OR (BSD-3-Clause AND MIT)")) (is (= (normalise "Apache-2.0 AND MIT OR BSD-3-Clause and Unlicense") "(Apache-2.0 AND MIT) OR (BSD-3-Clause AND Unlicense)")) (is (= (normalise "Apache-2.0 OR (MIT and BSD-3-Clause OR Unlicense)") - "Apache-2.0 OR (MIT AND BSD-3-Clause) OR Unlicense")) + "Apache-2.0 OR Unlicense OR (BSD-3-Clause AND MIT)")) (is (= (normalise "mit or bsd-3-clause AND apache-2.0 and beerware OR epl-2.0 and mpl-2.0 OR unlicense and lgpl-3.0 OR wtfpl or glwtpl OR hippocratic-2.1") - "MIT OR (BSD-3-Clause AND Apache-2.0 AND Beerware) OR (EPL-2.0 AND MPL-2.0) OR (Unlicense AND LGPL-3.0-only) OR WTFPL OR GLWTPL OR Hippocratic-2.1")) + "GLWTPL OR Hippocratic-2.1 OR MIT OR WTFPL OR (EPL-2.0 AND MPL-2.0) OR (LGPL-3.0-only AND Unlicense) OR (Apache-2.0 AND BSD-3-Clause AND Beerware)")) (is (= (normalise "MIT or (BSD-3-Clause OR (Apache-2.0 OR (Beerware OR (EPL-2.0 OR (MPL-2.0 OR (Unlicense OR (LGPL-3.0-only OR (WTFPL OR (GLWTPL OR (Hippocratic-2.1))))))))))") - "MIT OR BSD-3-Clause OR Apache-2.0 OR Beerware OR EPL-2.0 OR MPL-2.0 OR Unlicense OR LGPL-3.0-only OR WTFPL OR GLWTPL OR Hippocratic-2.1")) + "Apache-2.0 OR BSD-3-Clause OR Beerware OR EPL-2.0 OR GLWTPL OR Hippocratic-2.1 OR LGPL-3.0-only OR MIT OR MPL-2.0 OR Unlicense OR WTFPL")) (is (= (normalise "MIT and (BSD-3-Clause AND (Apache-2.0 and (Beerware AND (EPL-2.0 and (MPL-2.0 AND (Unlicense and (LGPL-3.0-only AND (WTFPL and (GLWTPL AND (Hippocratic-2.1))))))))))") - "MIT AND BSD-3-Clause AND Apache-2.0 AND Beerware AND EPL-2.0 AND MPL-2.0 AND Unlicense AND LGPL-3.0-only AND WTFPL AND GLWTPL AND Hippocratic-2.1")) + "Apache-2.0 AND BSD-3-Clause AND Beerware AND EPL-2.0 AND GLWTPL AND Hippocratic-2.1 AND LGPL-3.0-only AND MIT AND MPL-2.0 AND Unlicense AND WTFPL")) (is (= (normalise "MIT and (BSD-3-Clause or (Apache-2.0 and (Beerware or (EPL-2.0 and (MPL-2.0 or (Unlicense and (LGPL-3.0-only or (WTFPL and (GLWTPL or Hippocratic-2.1)))))))))") "MIT AND (BSD-3-Clause OR (Apache-2.0 AND (Beerware OR (EPL-2.0 AND (MPL-2.0 OR (Unlicense AND (LGPL-3.0-only OR (WTFPL AND (GLWTPL OR Hippocratic-2.1)))))))))")) (is (= (normalise "MIT OR (BSD-3-Clause AND (Apache-2.0 OR (Beerware AND (EPL-2.0 OR (MPL-2.0 AND (Unlicense OR (LGPL-3.0-only AND (WTFPL OR (GLWTPL AND (Hippocratic-2.1))))))))))") @@ -318,7 +323,9 @@ (testing "Collapsing redundant expressions" (is (= (normalise "Apache-2.0 OR Apache-2.0") "Apache-2.0")) (is (= (normalise "Apache-2.0 OR (Apache-2.0 AND (Apache-2.0 AND Apache-2.0) OR Apache-2.0)") - "Apache-2.0")))) + "Apache-2.0"))) + (testing "Sorting of licenses within the parse tree" + (is (= (normalise "Apache-2.0 OR MIT") (normalise "MIT OR Apache-2.0"))))) ; Note: we keep these short, as the parser is far more extensively exercised by parse-tests (deftest valid?-tests