1use std::collections::HashSet;
7
8use super::{LicenseExpression, ParseError};
9
10pub fn simplify_expression(expr: &LicenseExpression) -> LicenseExpression {
19 canonicalize_expression(expr, true)
20}
21
22pub fn simplify_expression_preserving_structure(expr: &LicenseExpression) -> LicenseExpression {
28 canonicalize_expression(expr, false)
29}
30
31fn canonicalize_expression(
32 expr: &LicenseExpression,
33 prune_subsumed_operands_enabled: bool,
34) -> LicenseExpression {
35 match expr {
36 LicenseExpression::License(key) => LicenseExpression::License(key.clone()),
37 LicenseExpression::LicenseRef(key) => LicenseExpression::LicenseRef(key.clone()),
38 LicenseExpression::With { left, right } => LicenseExpression::With {
39 left: Box::new(canonicalize_expression(
40 left,
41 prune_subsumed_operands_enabled,
42 )),
43 right: Box::new(canonicalize_expression(
44 right,
45 prune_subsumed_operands_enabled,
46 )),
47 },
48 LicenseExpression::And { .. } => {
49 let mut unique = Vec::new();
50 let mut seen = HashSet::new();
51 collect_unique_and(
52 expr,
53 &mut unique,
54 &mut seen,
55 prune_subsumed_operands_enabled,
56 );
57 if prune_subsumed_operands_enabled {
58 prune_subsumed_operands(&mut unique, true);
59 sort_operands_canonically(&mut unique);
60 }
61 build_expression_from_list(&unique, true)
62 }
63 LicenseExpression::Or { .. } => {
64 let mut unique = Vec::new();
65 let mut seen = HashSet::new();
66 collect_unique_or(
67 expr,
68 &mut unique,
69 &mut seen,
70 prune_subsumed_operands_enabled,
71 );
72 if prune_subsumed_operands_enabled {
73 prune_subsumed_operands(&mut unique, false);
74 sort_operands_canonically(&mut unique);
75 }
76 build_expression_from_list(&unique, false)
77 }
78 }
79}
80
81fn sort_operands_canonically(operands: &mut [LicenseExpression]) {
82 operands.sort_by_cached_key(canonical_operand_sort_key);
83}
84
85fn canonical_operand_sort_key(expr: &LicenseExpression) -> String {
86 expression_to_string(expr).to_ascii_lowercase()
87}
88
89fn prune_subsumed_operands(operands: &mut Vec<LicenseExpression>, outer_is_and: bool) {
90 let inner_is_and = !outer_is_and;
91 let pruned: Vec<LicenseExpression> = operands
92 .iter()
93 .enumerate()
94 .filter(|(candidate_idx, candidate)| {
95 !operands.iter().enumerate().any(|(other_idx, other)| {
96 candidate_idx != &other_idx && operand_subsumes(other, candidate, inner_is_and)
97 })
98 })
99 .map(|(_, operand)| operand.clone())
100 .collect();
101
102 *operands = pruned;
103}
104
105fn operand_subsumes(
106 other: &LicenseExpression,
107 candidate: &LicenseExpression,
108 inner_is_and: bool,
109) -> bool {
110 let other_args = get_flat_args(other);
111 let candidate_args = get_flat_args(candidate);
112
113 if other_args.len() >= candidate_args.len() {
114 return false;
115 }
116
117 let relevant_operator = matches!(other, LicenseExpression::And { .. })
118 || matches!(other, LicenseExpression::Or { .. })
119 || matches!(candidate, LicenseExpression::And { .. })
120 || matches!(candidate, LicenseExpression::Or { .. });
121
122 if !relevant_operator {
123 return false;
124 }
125
126 let operator_matches = if inner_is_and {
127 matches!(candidate, LicenseExpression::And { .. })
128 || matches!(other, LicenseExpression::And { .. })
129 } else {
130 matches!(candidate, LicenseExpression::Or { .. })
131 || matches!(other, LicenseExpression::Or { .. })
132 };
133
134 if !operator_matches {
135 return false;
136 }
137
138 other_args.iter().all(|other_arg| {
139 candidate_args
140 .iter()
141 .any(|arg| expressions_equal(arg, other_arg))
142 })
143}
144
145fn collect_unique_and(
146 expr: &LicenseExpression,
147 unique: &mut Vec<LicenseExpression>,
148 seen: &mut HashSet<String>,
149 prune_subsumed_operands_enabled: bool,
150) {
151 match expr {
152 LicenseExpression::And { left, right } => {
153 collect_unique_and(left, unique, seen, prune_subsumed_operands_enabled);
154 collect_unique_and(right, unique, seen, prune_subsumed_operands_enabled);
155 }
156 LicenseExpression::Or { .. } => {
157 let simplified = canonicalize_expression(expr, prune_subsumed_operands_enabled);
158 let key = expression_to_string(&simplified);
159 if !seen.contains(&key) {
160 seen.insert(key);
161 unique.push(simplified);
162 }
163 }
164 LicenseExpression::With { left, right } => {
165 let simplified = LicenseExpression::With {
166 left: Box::new(canonicalize_expression(
167 left,
168 prune_subsumed_operands_enabled,
169 )),
170 right: Box::new(canonicalize_expression(
171 right,
172 prune_subsumed_operands_enabled,
173 )),
174 };
175 let key = expression_to_string(&simplified);
176 if !seen.contains(&key) {
177 seen.insert(key);
178 unique.push(simplified);
179 }
180 }
181 LicenseExpression::License(key) => {
182 if !seen.contains(key) {
183 seen.insert(key.clone());
184 unique.push(LicenseExpression::License(key.clone()));
185 }
186 }
187 LicenseExpression::LicenseRef(key) => {
188 if !seen.contains(key) {
189 seen.insert(key.clone());
190 unique.push(LicenseExpression::LicenseRef(key.clone()));
191 }
192 }
193 }
194}
195
196fn collect_unique_or(
197 expr: &LicenseExpression,
198 unique: &mut Vec<LicenseExpression>,
199 seen: &mut HashSet<String>,
200 prune_subsumed_operands_enabled: bool,
201) {
202 match expr {
203 LicenseExpression::Or { left, right } => {
204 collect_unique_or(left, unique, seen, prune_subsumed_operands_enabled);
205 collect_unique_or(right, unique, seen, prune_subsumed_operands_enabled);
206 }
207 LicenseExpression::And { .. } => {
208 let simplified = canonicalize_expression(expr, prune_subsumed_operands_enabled);
209 let key = expression_to_string(&simplified);
210 if !seen.contains(&key) {
211 seen.insert(key);
212 unique.push(simplified);
213 }
214 }
215 LicenseExpression::With { left, right } => {
216 let simplified = LicenseExpression::With {
217 left: Box::new(canonicalize_expression(
218 left,
219 prune_subsumed_operands_enabled,
220 )),
221 right: Box::new(canonicalize_expression(
222 right,
223 prune_subsumed_operands_enabled,
224 )),
225 };
226 let key = expression_to_string(&simplified);
227 if !seen.contains(&key) {
228 seen.insert(key);
229 unique.push(simplified);
230 }
231 }
232 LicenseExpression::License(key) => {
233 if !seen.contains(key) {
234 seen.insert(key.clone());
235 unique.push(LicenseExpression::License(key.clone()));
236 }
237 }
238 LicenseExpression::LicenseRef(key) => {
239 if !seen.contains(key) {
240 seen.insert(key.clone());
241 unique.push(LicenseExpression::LicenseRef(key.clone()));
242 }
243 }
244 }
245}
246
247fn build_expression_from_list(unique: &[LicenseExpression], is_and: bool) -> LicenseExpression {
248 match unique.len() {
249 0 => panic!("build_expression_from_list called with empty list"),
250 1 => unique[0].clone(),
251 _ => {
252 let mut iter = unique.iter();
253 let mut result = iter.next().unwrap().clone();
254 for expr in iter {
255 result = if is_and {
256 LicenseExpression::And {
257 left: Box::new(result),
258 right: Box::new(expr.clone()),
259 }
260 } else {
261 LicenseExpression::Or {
262 left: Box::new(result),
263 right: Box::new(expr.clone()),
264 }
265 };
266 }
267 result
268 }
269 }
270}
271
272fn get_flat_args(expr: &LicenseExpression) -> Vec<LicenseExpression> {
273 match expr {
274 LicenseExpression::And { left, right } => {
275 let mut args = Vec::new();
276 collect_flat_and_args(left, &mut args);
277 collect_flat_and_args(right, &mut args);
278 args
279 }
280 LicenseExpression::Or { left, right } => {
281 let mut args = Vec::new();
282 collect_flat_or_args(left, &mut args);
283 collect_flat_or_args(right, &mut args);
284 args
285 }
286 _ => vec![expr.clone()],
287 }
288}
289
290fn collect_flat_and_args(expr: &LicenseExpression, args: &mut Vec<LicenseExpression>) {
291 match expr {
292 LicenseExpression::And { left, right } => {
293 collect_flat_and_args(left, args);
294 collect_flat_and_args(right, args);
295 }
296 _ => args.push(expr.clone()),
297 }
298}
299
300fn collect_flat_or_args(expr: &LicenseExpression, args: &mut Vec<LicenseExpression>) {
301 match expr {
302 LicenseExpression::Or { left, right } => {
303 collect_flat_or_args(left, args);
304 collect_flat_or_args(right, args);
305 }
306 _ => args.push(expr.clone()),
307 }
308}
309
310fn decompose_expr(expr: &LicenseExpression) -> Vec<LicenseExpression> {
311 match expr {
312 LicenseExpression::With { left, right } => {
313 let mut parts = decompose_expr(left);
314 parts.extend(decompose_expr(right));
315 parts
316 }
317 _ => vec![expr.clone()],
318 }
319}
320
321fn expressions_equal(a: &LicenseExpression, b: &LicenseExpression) -> bool {
322 match (a, b) {
323 (LicenseExpression::License(ka), LicenseExpression::License(kb)) => ka == kb,
324 (LicenseExpression::LicenseRef(ka), LicenseExpression::LicenseRef(kb)) => ka == kb,
325 (
326 LicenseExpression::With {
327 left: l1,
328 right: r1,
329 },
330 LicenseExpression::With {
331 left: l2,
332 right: r2,
333 },
334 ) => expressions_equal(l1, l2) && expressions_equal(r1, r2),
335 (LicenseExpression::And { .. }, LicenseExpression::And { .. }) => {
336 let args_a = get_flat_args(a);
337 let args_b = get_flat_args(b);
338 args_a.len() == args_b.len()
339 && args_b
340 .iter()
341 .all(|b_arg| args_a.iter().any(|a_arg| expressions_equal(a_arg, b_arg)))
342 }
343 (LicenseExpression::Or { .. }, LicenseExpression::Or { .. }) => {
344 let args_a = get_flat_args(a);
345 let args_b = get_flat_args(b);
346 args_a.len() == args_b.len()
347 && args_b
348 .iter()
349 .all(|b_arg| args_a.iter().any(|a_arg| expressions_equal(a_arg, b_arg)))
350 }
351 _ => false,
352 }
353}
354
355fn expr_in_args(expr: &LicenseExpression, args: &[LicenseExpression]) -> bool {
356 if args.iter().any(|a| expressions_equal(a, expr)) {
357 return true;
358 }
359 let decomposed = decompose_expr(expr);
360 if decomposed.len() == 1 {
361 return false;
362 }
363 decomposed
364 .iter()
365 .any(|d| args.iter().any(|a| expressions_equal(a, d)))
366}
367
368pub fn licensing_contains(container: &str, contained: &str) -> bool {
369 let container = container.trim();
370 let contained = contained.trim();
371 if container.is_empty() || contained.is_empty() {
372 return false;
373 }
374
375 if container == contained {
376 return true;
377 }
378
379 let Ok(parsed_container) = super::parse::parse_expression(container) else {
380 return false;
381 };
382 let Ok(parsed_contained) = super::parse::parse_expression(contained) else {
383 return false;
384 };
385
386 let simplified_container = simplify_expression(&parsed_container);
387 let simplified_contained = simplify_expression(&parsed_contained);
388
389 match (&simplified_container, &simplified_contained) {
390 (LicenseExpression::And { .. }, LicenseExpression::And { .. })
391 | (LicenseExpression::Or { .. }, LicenseExpression::Or { .. }) => {
392 let container_args = get_flat_args(&simplified_container);
393 let contained_args = get_flat_args(&simplified_contained);
394 contained_args
395 .iter()
396 .all(|c| container_args.iter().any(|ca| expressions_equal(ca, c)))
397 }
398 (
399 LicenseExpression::And { .. } | LicenseExpression::Or { .. },
400 LicenseExpression::License(_) | LicenseExpression::LicenseRef(_),
401 ) => {
402 let container_args = get_flat_args(&simplified_container);
403 expr_in_args(&simplified_contained, &container_args)
404 }
405 (LicenseExpression::And { .. } | LicenseExpression::Or { .. }, _) => {
406 let container_args = get_flat_args(&simplified_container);
407 container_args
408 .iter()
409 .any(|ca| expressions_equal(ca, &simplified_contained))
410 }
411 (
412 LicenseExpression::With { .. },
413 LicenseExpression::License(_) | LicenseExpression::LicenseRef(_),
414 ) => {
415 let decomposed = decompose_expr(&simplified_container);
416 decomposed
417 .iter()
418 .any(|d| expressions_equal(d, &simplified_contained))
419 }
420 (
421 LicenseExpression::License(_) | LicenseExpression::LicenseRef(_),
422 LicenseExpression::And { .. }
423 | LicenseExpression::Or { .. }
424 | LicenseExpression::With { .. },
425 ) => false,
426 (LicenseExpression::License(k1), LicenseExpression::License(k2)) => k1 == k2,
427 (LicenseExpression::LicenseRef(k1), LicenseExpression::LicenseRef(k2)) => k1 == k2,
428 _ => false,
429 }
430}
431
432#[derive(Clone, Copy)]
441enum BooleanOperator {
442 And,
443 Or,
444}
445
446pub fn expression_to_string(expr: &LicenseExpression) -> String {
447 match expr {
448 LicenseExpression::License(key) => key.clone(),
449 LicenseExpression::LicenseRef(key) => key.clone(),
450 LicenseExpression::And { .. } => render_flat_boolean_chain(expr, BooleanOperator::And),
451 LicenseExpression::Or { .. } => render_flat_boolean_chain(expr, BooleanOperator::Or),
452 LicenseExpression::With { left, right } => {
453 let left_str = expression_to_string(left);
454 let right_str = expression_to_string(right);
455 format!("{} WITH {}", left_str, right_str)
456 }
457 }
458}
459
460fn render_flat_boolean_chain(expr: &LicenseExpression, operator: BooleanOperator) -> String {
461 let mut parts = Vec::new();
462 collect_boolean_chain(expr, operator, &mut parts);
463
464 let separator = match operator {
465 BooleanOperator::And => " AND ",
466 BooleanOperator::Or => " OR ",
467 };
468
469 parts
470 .into_iter()
471 .map(|part| render_boolean_operand(part, operator))
472 .collect::<Vec<_>>()
473 .join(separator)
474}
475
476fn collect_boolean_chain<'a>(
477 expr: &'a LicenseExpression,
478 operator: BooleanOperator,
479 parts: &mut Vec<&'a LicenseExpression>,
480) {
481 match (operator, expr) {
482 (BooleanOperator::And, LicenseExpression::And { left, right })
483 | (BooleanOperator::Or, LicenseExpression::Or { left, right }) => {
484 collect_boolean_chain(left, operator, parts);
485 collect_boolean_chain(right, operator, parts);
486 }
487 _ => parts.push(expr),
488 }
489}
490
491fn render_boolean_operand(expr: &LicenseExpression, parent_operator: BooleanOperator) -> String {
492 match expr {
493 LicenseExpression::License(key) => key.clone(),
494 LicenseExpression::LicenseRef(key) => key.clone(),
495 LicenseExpression::And { .. } => match parent_operator {
496 BooleanOperator::And => expression_to_string(expr),
497 BooleanOperator::Or => format!("({})", expression_to_string(expr)),
498 },
499 LicenseExpression::Or { .. } => match parent_operator {
500 BooleanOperator::Or => expression_to_string(expr),
501 BooleanOperator::And => format!("({})", expression_to_string(expr)),
502 },
503 LicenseExpression::With { left, right } => {
504 let left_str = expression_to_string(left);
505 let right_str = expression_to_string(right);
506 format!("{} WITH {}", left_str, right_str)
507 }
508 }
509}
510
511fn combine_expressions_with(
512 expressions: &[&str],
513 unique: bool,
514 combiner: fn(Vec<LicenseExpression>) -> Option<LicenseExpression>,
515 simplifier: fn(&LicenseExpression) -> LicenseExpression,
516) -> Result<String, ParseError> {
517 if expressions.is_empty() {
518 return Ok(String::new());
519 }
520 if expressions.len() == 1 {
521 let parsed = super::parse::parse_expression(expressions[0])?;
522 return Ok(expression_to_string(&if unique {
523 simplifier(&parsed)
524 } else {
525 parsed
526 }));
527 }
528
529 let parsed_exprs: Vec<LicenseExpression> = expressions
530 .iter()
531 .map(|e| super::parse::parse_expression(e))
532 .collect::<Result<Vec<_>, _>>()?;
533
534 let combined = combiner(parsed_exprs);
535
536 match combined {
537 Some(expr) => {
538 let final_expr = if unique { simplifier(&expr) } else { expr };
539 Ok(expression_to_string(&final_expr))
540 }
541 None => Ok(String::new()),
542 }
543}
544
545pub fn combine_expressions_and(expressions: &[&str], unique: bool) -> Result<String, ParseError> {
550 combine_expressions_with(
551 expressions,
552 unique,
553 LicenseExpression::and,
554 simplify_expression,
555 )
556}
557
558pub fn combine_expressions_and_preserving_structure(
561 expressions: &[&str],
562 unique: bool,
563) -> Result<String, ParseError> {
564 combine_expressions_with(
565 expressions,
566 unique,
567 LicenseExpression::and,
568 simplify_expression_preserving_structure,
569 )
570}
571
572#[allow(dead_code)]
577pub fn combine_expressions_or(expressions: &[&str], unique: bool) -> Result<String, ParseError> {
578 combine_expressions_with(
579 expressions,
580 unique,
581 LicenseExpression::or,
582 simplify_expression,
583 )
584}
585
586pub fn combine_expressions_or_preserving_structure(
589 expressions: &[&str],
590 unique: bool,
591) -> Result<String, ParseError> {
592 combine_expressions_with(
593 expressions,
594 unique,
595 LicenseExpression::or,
596 simplify_expression_preserving_structure,
597 )
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_simplify_expression_no_change() {
606 let expr = super::super::parse::parse_expression("MIT AND Apache-2.0").unwrap();
607 let simplified = simplify_expression(&expr);
608 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
609 }
610
611 #[test]
612 fn test_simplify_expression_with_duplicates() {
613 let expr = super::super::parse::parse_expression("MIT OR MIT").unwrap();
614 let simplified = simplify_expression(&expr);
615 assert_eq!(expression_to_string(&simplified), "mit");
616 }
617
618 #[test]
619 fn test_simplify_expression_preserving_structure_keeps_distinct_nested_operands() {
620 let expr = super::super::parse::parse_expression("mit AND (apache-2.0 OR mit)").unwrap();
621 let simplified = simplify_expression_preserving_structure(&expr);
622 assert_eq!(
623 expression_to_string(&simplified),
624 "mit AND (apache-2.0 OR mit)"
625 );
626 }
627
628 #[test]
629 fn test_simplify_and_duplicates() {
630 let expr = super::super::parse::parse_expression("crapl-0.1 AND crapl-0.1").unwrap();
631 let simplified = simplify_expression(&expr);
632 assert_eq!(expression_to_string(&simplified), "crapl-0.1");
633 }
634
635 #[test]
636 fn test_simplify_or_duplicates() {
637 let expr = super::super::parse::parse_expression("mit OR mit").unwrap();
638 let simplified = simplify_expression(&expr);
639 assert_eq!(expression_to_string(&simplified), "mit");
640 }
641
642 #[test]
643 fn test_combine_expressions_and_preserving_structure_keeps_distinct_nested_operands() {
644 let result =
645 combine_expressions_and_preserving_structure(&["mit", "apache-2.0 OR mit"], true)
646 .unwrap();
647 assert_eq!(result, "mit AND (apache-2.0 OR mit)");
648 }
649
650 #[test]
651 fn test_simplify_preserves_different_licenses() {
652 let expr = super::super::parse::parse_expression("mit AND apache-2.0").unwrap();
653 let simplified = simplify_expression(&expr);
654 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
655 }
656
657 #[test]
658 fn test_simplify_complex_duplicates() {
659 let expr = super::super::parse::parse_expression(
660 "gpl-2.0-plus AND gpl-2.0-plus AND lgpl-2.0-plus",
661 )
662 .unwrap();
663 let simplified = simplify_expression(&expr);
664 assert_eq!(
665 expression_to_string(&simplified),
666 "gpl-2.0-plus AND lgpl-2.0-plus"
667 );
668 }
669
670 #[test]
671 fn test_simplify_three_duplicates() {
672 let expr =
673 super::super::parse::parse_expression("fsf-free AND fsf-free AND fsf-free").unwrap();
674 let simplified = simplify_expression(&expr);
675 assert_eq!(expression_to_string(&simplified), "fsf-free");
676 }
677
678 #[test]
679 fn test_simplify_with_expression_dedup() {
680 let expr = super::super::parse::parse_expression(
681 "gpl-2.0 WITH classpath-exception-2.0 AND gpl-2.0 WITH classpath-exception-2.0",
682 )
683 .unwrap();
684 let simplified = simplify_expression(&expr);
685 assert_eq!(
686 expression_to_string(&simplified),
687 "gpl-2.0 WITH classpath-exception-2.0"
688 );
689 }
690
691 #[test]
692 fn test_simplify_nested_duplicates() {
693 let expr =
694 super::super::parse::parse_expression("(mit AND apache-2.0) OR (mit AND apache-2.0)")
695 .unwrap();
696 let simplified = simplify_expression(&expr);
697 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
698 }
699
700 #[test]
701 fn test_simplify_sorts_operands_canonically() {
702 let expr =
703 super::super::parse::parse_expression("apache-2.0 AND mit AND apache-2.0").unwrap();
704 let simplified = simplify_expression(&expr);
705 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
706 }
707
708 #[test]
709 fn test_simplify_mit_and_mit_and_apache() {
710 let expr = super::super::parse::parse_expression("mit AND mit AND apache-2.0").unwrap();
711 let simplified = simplify_expression(&expr);
712 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
713 }
714
715 #[test]
716 fn test_simplify_and_absorption() {
717 let expr = super::super::parse::parse_expression("mit AND (mit OR apache-2.0)").unwrap();
718 let simplified = simplify_expression(&expr);
719
720 assert_eq!(expression_to_string(&simplified), "mit");
721 }
722
723 #[test]
724 fn test_simplify_or_absorption() {
725 let expr = super::super::parse::parse_expression("mit OR (mit AND apache-2.0)").unwrap();
726 let simplified = simplify_expression(&expr);
727
728 assert_eq!(expression_to_string(&simplified), "mit");
729 }
730
731 #[test]
732 fn test_simplify_or_subsumption() {
733 let expr = super::super::parse::parse_expression(
734 "(mit AND apache-2.0) OR (mit AND apache-2.0 AND bsd-new)",
735 )
736 .unwrap();
737 let simplified = simplify_expression(&expr);
738
739 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
740 }
741
742 #[test]
743 fn test_simplify_and_subsumption() {
744 let expr = super::super::parse::parse_expression(
745 "(mit OR apache-2.0) AND (mit OR apache-2.0 OR bsd-new)",
746 )
747 .unwrap();
748 let simplified = simplify_expression(&expr);
749
750 assert_eq!(expression_to_string(&simplified), "apache-2.0 OR mit");
751 }
752
753 #[test]
754 fn test_simplify_and_keeps_gpl_or_later_with_only() {
755 let expr =
756 super::super::parse::parse_expression("gpl-2.0-or-later AND gpl-2.0-only").unwrap();
757 let simplified = simplify_expression(&expr);
758
759 assert_eq!(
760 expression_to_string(&simplified),
761 "gpl-2.0-only AND gpl-2.0-or-later"
762 );
763 }
764
765 #[test]
766 fn test_expression_to_string_simple() {
767 let expr = LicenseExpression::License("mit".to_string());
768 assert_eq!(expression_to_string(&expr), "mit");
769 }
770
771 #[test]
772 fn test_expression_to_string_and() {
773 let expr = LicenseExpression::And {
774 left: Box::new(LicenseExpression::License("mit".to_string())),
775 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
776 };
777 assert_eq!(expression_to_string(&expr), "mit AND apache-2.0");
778 }
779
780 #[test]
781 fn test_expression_to_string_or() {
782 let expr = LicenseExpression::Or {
783 left: Box::new(LicenseExpression::License("mit".to_string())),
784 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
785 };
786 assert_eq!(expression_to_string(&expr), "mit OR apache-2.0");
787 }
788
789 #[test]
790 fn test_expression_to_string_with() {
791 let expr = LicenseExpression::With {
792 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
793 right: Box::new(LicenseExpression::License(
794 "classpath-exception-2.0".to_string(),
795 )),
796 };
797 assert_eq!(
798 expression_to_string(&expr),
799 "gpl-2.0 WITH classpath-exception-2.0"
800 );
801 }
802
803 #[test]
804 fn test_expression_to_string_licenseref() {
805 let expr = LicenseExpression::LicenseRef("licenseref-scancode-custom".to_string());
806 assert_eq!(expression_to_string(&expr), "licenseref-scancode-custom");
807 }
808
809 #[test]
810 fn test_expression_to_string_or_inside_and() {
811 let or_expr = LicenseExpression::Or {
812 left: Box::new(LicenseExpression::License("mit".to_string())),
813 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
814 };
815 let and_expr = LicenseExpression::And {
816 left: Box::new(or_expr),
817 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
818 };
819 assert_eq!(
820 expression_to_string(&and_expr),
821 "(mit OR apache-2.0) AND gpl-2.0"
822 );
823 }
824
825 #[test]
826 fn test_expression_to_string_and_inside_or() {
827 let and_expr = LicenseExpression::And {
828 left: Box::new(LicenseExpression::License("mit".to_string())),
829 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
830 };
831 let or_expr = LicenseExpression::Or {
832 left: Box::new(and_expr),
833 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
834 };
835 assert_eq!(
836 expression_to_string(&or_expr),
837 "(mit AND apache-2.0) OR gpl-2.0"
838 );
839 }
840
841 #[test]
842 fn test_expression_to_string_with_inside_or() {
843 let with_expr = LicenseExpression::With {
844 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
845 right: Box::new(LicenseExpression::License(
846 "classpath-exception-2.0".to_string(),
847 )),
848 };
849 let or_expr = LicenseExpression::Or {
850 left: Box::new(with_expr),
851 right: Box::new(LicenseExpression::License("mit".to_string())),
852 };
853 assert_eq!(
854 expression_to_string(&or_expr),
855 "gpl-2.0 WITH classpath-exception-2.0 OR mit"
856 );
857 }
858
859 #[test]
860 fn test_expression_to_string_with_inside_and() {
861 let with_expr = LicenseExpression::With {
862 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
863 right: Box::new(LicenseExpression::License(
864 "classpath-exception-2.0".to_string(),
865 )),
866 };
867 let and_expr = LicenseExpression::And {
868 left: Box::new(with_expr),
869 right: Box::new(LicenseExpression::License("mit".to_string())),
870 };
871 assert_eq!(
872 expression_to_string(&and_expr),
873 "gpl-2.0 WITH classpath-exception-2.0 AND mit"
874 );
875 }
876
877 #[test]
878 fn test_expression_to_string_nested_or_flattens_same_operator_grouping() {
879 let or_expr = LicenseExpression::Or {
880 left: Box::new(LicenseExpression::Or {
881 left: Box::new(LicenseExpression::License("mit".to_string())),
882 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
883 }),
884 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
885 };
886 assert_eq!(
887 expression_to_string(&or_expr),
888 "mit OR apache-2.0 OR gpl-2.0"
889 );
890 }
891
892 #[test]
893 fn test_expression_to_string_nested_and_flattens_same_operator_grouping() {
894 let and_expr = LicenseExpression::And {
895 left: Box::new(LicenseExpression::And {
896 left: Box::new(LicenseExpression::License("mit".to_string())),
897 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
898 }),
899 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
900 };
901 assert_eq!(
902 expression_to_string(&and_expr),
903 "mit AND apache-2.0 AND gpl-2.0"
904 );
905 }
906
907 #[test]
908 fn test_expression_to_string_roundtrip_or_and() {
909 let input = "(mit OR apache-2.0) AND gpl-2.0";
910 let expr = super::super::parse::parse_expression(input).unwrap();
911 let output = expression_to_string(&expr);
912 assert_eq!(output, "(mit OR apache-2.0) AND gpl-2.0");
913 }
914
915 #[test]
916 fn test_expression_to_string_roundtrip_or_with() {
917 let input = "gpl-2.0 WITH classpath-exception-2.0 OR mit";
918 let expr = super::super::parse::parse_expression(input).unwrap();
919 let output = expression_to_string(&expr);
920 assert_eq!(output, "gpl-2.0 WITH classpath-exception-2.0 OR mit");
921 }
922
923 #[test]
924 fn test_combine_expressions_empty() {
925 let result = combine_expressions_and(&[], true).unwrap();
926 assert_eq!(result, "");
927 }
928
929 #[test]
930 fn test_combine_expressions_single() {
931 let result = combine_expressions_and(&["mit"], true).unwrap();
932 assert_eq!(result, "mit");
933 }
934
935 #[test]
936 fn test_combine_expressions_two_and() {
937 let result = combine_expressions_and(&["mit", "gpl-2.0-plus"], true).unwrap();
938 assert_eq!(result, "gpl-2.0-plus AND mit");
939 }
940
941 #[test]
942 fn test_combine_expressions_two_or() {
943 let result = combine_expressions_or(&["mit", "apache-2.0"], true).unwrap();
944 assert_eq!(result, "apache-2.0 OR mit");
945 }
946
947 #[test]
948 fn test_combine_expressions_multiple_and() {
949 let result = combine_expressions_and(&["mit", "apache-2.0", "gpl-2.0-plus"], true).unwrap();
950 assert_eq!(result, "apache-2.0 AND gpl-2.0-plus AND mit");
951 }
952
953 #[test]
954 fn test_combine_expressions_with_duplicates_unique() {
955 let result = combine_expressions_or(&["mit", "mit", "apache-2.0"], true).unwrap();
956 let expr = super::super::parse::parse_expression(&result).unwrap();
957 let keys = expr.license_keys();
958 assert_eq!(keys.len(), 2);
959 assert!(keys.contains(&"mit".to_string()));
960 assert!(keys.contains(&"apache-2.0".to_string()));
961 }
962
963 #[test]
964 fn test_combine_expressions_with_duplicates_not_unique() {
965 let result = combine_expressions_or(&["mit", "mit", "apache-2.0"], false).unwrap();
966 let expr = super::super::parse::parse_expression(&result).unwrap();
967 assert_eq!(result, "mit OR mit OR apache-2.0");
968 let keys = expr.license_keys();
969 assert_eq!(keys.len(), 2);
970 }
971
972 #[test]
973 fn test_combine_expressions_complex_with_simplification() {
974 let result = combine_expressions_and(&["mit OR apache-2.0", "gpl-2.0-plus"], true).unwrap();
975 assert_eq!(result, "(apache-2.0 OR mit) AND gpl-2.0-plus");
976 let expr = super::super::parse::parse_expression(&result).unwrap();
977 assert!(matches!(expr, LicenseExpression::And { .. }));
978 let keys = expr.license_keys();
979 assert_eq!(keys.len(), 3);
980 }
981
982 #[test]
983 fn test_combine_expressions_parse_error() {
984 let result = combine_expressions_and(&["mit", "@invalid@"], true);
985 assert!(result.is_err());
986 }
987
988 #[test]
989 fn test_combine_expressions_with_existing_and() {
990 let result = combine_expressions_and(&["mit AND apache-2.0", "gpl-2.0"], true).unwrap();
991 assert!(result.contains("mit"));
992 assert!(result.contains("apache-2.0"));
993 assert!(result.contains("gpl-2.0"));
994 }
995
996 #[test]
997 fn test_combine_expressions_with_existing_or() {
998 let result = combine_expressions_or(&["mit OR apache-2.0", "gpl-2.0"], true).unwrap();
999 assert!(result.contains("mit"));
1000 assert!(result.contains("apache-2.0"));
1001 assert!(result.contains("gpl-2.0"));
1002 }
1003
1004 #[test]
1005 fn test_expression_to_string_with_no_outer_parens() {
1006 let with_expr = LicenseExpression::With {
1007 left: Box::new(LicenseExpression::License("gpl-2.0-plus".to_string())),
1008 right: Box::new(LicenseExpression::License(
1009 "classpath-exception-2.0".to_string(),
1010 )),
1011 };
1012 assert_eq!(
1013 expression_to_string(&with_expr),
1014 "gpl-2.0-plus WITH classpath-exception-2.0"
1015 );
1016 }
1017
1018 #[test]
1019 fn test_expression_to_string_with_as_right_operand_of_or() {
1020 let with_expr = LicenseExpression::With {
1021 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
1022 right: Box::new(LicenseExpression::License(
1023 "classpath-exception-2.0".to_string(),
1024 )),
1025 };
1026 let or_expr = LicenseExpression::Or {
1027 left: Box::new(LicenseExpression::License("mit".to_string())),
1028 right: Box::new(with_expr),
1029 };
1030 assert_eq!(
1031 expression_to_string(&or_expr),
1032 "mit OR gpl-2.0 WITH classpath-exception-2.0"
1033 );
1034 }
1035
1036 #[test]
1037 fn test_expression_to_string_with_as_right_operand_of_and() {
1038 let with_expr = LicenseExpression::With {
1039 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
1040 right: Box::new(LicenseExpression::License(
1041 "classpath-exception-2.0".to_string(),
1042 )),
1043 };
1044 let and_expr = LicenseExpression::And {
1045 left: Box::new(LicenseExpression::License("mit".to_string())),
1046 right: Box::new(with_expr),
1047 };
1048 assert_eq!(
1049 expression_to_string(&and_expr),
1050 "mit AND gpl-2.0 WITH classpath-exception-2.0"
1051 );
1052 }
1053
1054 #[test]
1055 fn test_expression_to_string_complex_precedence() {
1056 let input = "mit OR apache-2.0 AND gpl-2.0";
1057 let expr = super::super::parse::parse_expression(input).unwrap();
1058 assert_eq!(
1059 expression_to_string(&expr),
1060 "mit OR (apache-2.0 AND gpl-2.0)"
1061 );
1062 }
1063
1064 #[test]
1065 fn test_expression_to_string_with_no_outer_parens_in_complex_and() {
1066 let input = "bsd-new AND mit AND gpl-3.0-plus WITH autoconf-simple-exception";
1069 let expr = super::super::parse::parse_expression(input).unwrap();
1070 assert_eq!(
1071 expression_to_string(&expr),
1072 "bsd-new AND mit AND gpl-3.0-plus WITH autoconf-simple-exception"
1073 );
1074 }
1075
1076 #[test]
1077 fn test_combine_expressions_and_flattens_reported_redundant_parentheses() {
1078 let result = combine_expressions_and(
1079 &[
1080 "Apache-2.0",
1081 "BSD-3-Clause",
1082 "GPL-2.0-only",
1083 "LicenseRef-scancode-oracle-openjdk-exception-2.0",
1084 "APSL-1.0",
1085 "APSL-2.0",
1086 ],
1087 true,
1088 )
1089 .unwrap();
1090
1091 assert_eq!(
1092 result,
1093 "apache-2.0 AND apsl-1.0 AND apsl-2.0 AND bsd-3-clause AND gpl-2.0-only AND licenseref-scancode-oracle-openjdk-exception-2.0"
1094 );
1095 }
1096}
1097
1098#[cfg(test)]
1099mod contains_tests {
1100 use super::*;
1101
1102 #[test]
1103 fn test_basic_containment() {
1104 assert!(licensing_contains("mit", "mit"));
1105 assert!(!licensing_contains("mit", "apache"));
1106 }
1107
1108 #[test]
1109 fn test_or_containment() {
1110 assert!(licensing_contains("mit OR apache", "mit"));
1111 assert!(licensing_contains("mit OR apache", "apache"));
1112 assert!(!licensing_contains("mit OR apache", "gpl"));
1113 }
1114
1115 #[test]
1116 fn test_and_containment() {
1117 assert!(licensing_contains("mit AND apache", "mit"));
1118 assert!(licensing_contains("mit AND apache", "apache"));
1119 assert!(!licensing_contains("mit", "mit AND apache"));
1120 }
1121
1122 #[test]
1123 fn test_expression_subset() {
1124 assert!(licensing_contains(
1125 "mit AND apache AND bsd",
1126 "mit AND apache"
1127 ));
1128 assert!(!licensing_contains(
1129 "mit AND apache",
1130 "mit AND apache AND bsd"
1131 ));
1132 assert!(licensing_contains("mit OR apache OR bsd", "mit OR apache"));
1133 assert!(!licensing_contains("mit OR apache", "mit OR apache OR bsd"));
1134 }
1135
1136 #[test]
1137 fn test_order_independence() {
1138 assert!(licensing_contains("mit AND apache", "apache AND mit"));
1139 assert!(licensing_contains("mit OR apache", "apache OR mit"));
1140 }
1141
1142 #[test]
1143 fn test_plus_suffix_no_containment() {
1144 assert!(!licensing_contains("gpl-2.0-plus", "gpl-2.0"));
1145 assert!(!licensing_contains("gpl-2.0", "gpl-2.0-plus"));
1146 }
1147
1148 #[test]
1149 fn test_with_decomposition() {
1150 assert!(licensing_contains(
1151 "gpl-2.0 WITH classpath-exception",
1152 "gpl-2.0"
1153 ));
1154 assert!(licensing_contains(
1155 "gpl-2.0 WITH classpath-exception",
1156 "classpath-exception"
1157 ));
1158 assert!(!licensing_contains(
1159 "gpl-2.0",
1160 "gpl-2.0 WITH classpath-exception"
1161 ));
1162 }
1163
1164 #[test]
1165 fn test_mixed_operators() {
1166 assert!(!licensing_contains("mit OR apache", "mit AND apache"));
1167 assert!(!licensing_contains("mit AND apache", "mit OR apache"));
1168 }
1169
1170 #[test]
1171 fn test_nested_expressions() {
1172 assert!(!licensing_contains("(mit OR apache) AND bsd", "mit"));
1173 assert!(licensing_contains(
1174 "(mit OR apache) AND bsd",
1175 "mit OR apache"
1176 ));
1177 assert!(licensing_contains("(mit OR apache) AND bsd", "bsd"));
1178 }
1179
1180 #[test]
1181 fn test_empty_expressions() {
1182 assert!(!licensing_contains("", "mit"));
1183 assert!(!licensing_contains("mit", ""));
1184 assert!(!licensing_contains("", ""));
1185 assert!(!licensing_contains(" ", "mit"));
1186 }
1187
1188 #[test]
1189 fn test_invalid_expressions() {
1190 assert!(!licensing_contains("mit AND", "mit"));
1191 assert!(!licensing_contains("mit", "AND apache"));
1192 }
1193}