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