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