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