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
369#[derive(Clone, Copy)]
378enum BooleanOperator {
379 And,
380 Or,
381}
382
383pub fn expression_to_string(expr: &LicenseExpression) -> String {
384 match expr {
385 LicenseExpression::License(key) => key.clone(),
386 LicenseExpression::LicenseRef(key) => key.clone(),
387 LicenseExpression::And { .. } => render_flat_boolean_chain(expr, BooleanOperator::And),
388 LicenseExpression::Or { .. } => render_flat_boolean_chain(expr, BooleanOperator::Or),
389 LicenseExpression::With { left, right } => {
390 let left_str = expression_to_string(left);
391 let right_str = expression_to_string(right);
392 format!("{} WITH {}", left_str, right_str)
393 }
394 }
395}
396
397fn render_flat_boolean_chain(expr: &LicenseExpression, operator: BooleanOperator) -> String {
398 let mut parts = Vec::new();
399 collect_boolean_chain(expr, operator, &mut parts);
400
401 let separator = match operator {
402 BooleanOperator::And => " AND ",
403 BooleanOperator::Or => " OR ",
404 };
405
406 parts
407 .into_iter()
408 .map(|part| render_boolean_operand(part, operator))
409 .collect::<Vec<_>>()
410 .join(separator)
411}
412
413fn collect_boolean_chain<'a>(
414 expr: &'a LicenseExpression,
415 operator: BooleanOperator,
416 parts: &mut Vec<&'a LicenseExpression>,
417) {
418 match (operator, expr) {
419 (BooleanOperator::And, LicenseExpression::And { left, right })
420 | (BooleanOperator::Or, LicenseExpression::Or { left, right }) => {
421 collect_boolean_chain(left, operator, parts);
422 collect_boolean_chain(right, operator, parts);
423 }
424 _ => parts.push(expr),
425 }
426}
427
428fn render_boolean_operand(expr: &LicenseExpression, parent_operator: BooleanOperator) -> String {
429 match expr {
430 LicenseExpression::License(key) => key.clone(),
431 LicenseExpression::LicenseRef(key) => key.clone(),
432 LicenseExpression::And { .. } => match parent_operator {
433 BooleanOperator::And => expression_to_string(expr),
434 BooleanOperator::Or => format!("({})", expression_to_string(expr)),
435 },
436 LicenseExpression::Or { .. } => match parent_operator {
437 BooleanOperator::Or => expression_to_string(expr),
438 BooleanOperator::And => format!("({})", expression_to_string(expr)),
439 },
440 LicenseExpression::With { left, right } => {
441 let left_str = expression_to_string(left);
442 let right_str = expression_to_string(right);
443 format!("{} WITH {}", left_str, right_str)
444 }
445 }
446}
447
448fn combine_expressions_with(
449 expressions: &[&str],
450 unique: bool,
451 combiner: fn(Vec<LicenseExpression>) -> Option<LicenseExpression>,
452) -> Result<String, ParseError> {
453 if expressions.is_empty() {
454 return Ok(String::new());
455 }
456 if expressions.len() == 1 {
457 let parsed = super::parse::parse_expression(expressions[0])?;
458 return Ok(expression_to_string(&if unique {
459 simplify_expression(&parsed)
460 } else {
461 parsed
462 }));
463 }
464
465 let parsed_exprs: Vec<LicenseExpression> = expressions
466 .iter()
467 .map(|e| super::parse::parse_expression(e))
468 .collect::<Result<Vec<_>, _>>()?;
469
470 let combined = combiner(parsed_exprs);
471
472 match combined {
473 Some(expr) => {
474 let final_expr = if unique {
475 simplify_expression(&expr)
476 } else {
477 expr
478 };
479 Ok(expression_to_string(&final_expr))
480 }
481 None => Ok(String::new()),
482 }
483}
484
485pub fn combine_expressions_and(expressions: &[&str], unique: bool) -> Result<String, ParseError> {
490 combine_expressions_with(expressions, unique, LicenseExpression::and)
491}
492
493#[allow(dead_code)]
498pub fn combine_expressions_or(expressions: &[&str], unique: bool) -> Result<String, ParseError> {
499 combine_expressions_with(expressions, unique, LicenseExpression::or)
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_simplify_expression_no_change() {
508 let expr = super::super::parse::parse_expression("MIT AND Apache-2.0").unwrap();
509 let simplified = simplify_expression(&expr);
510 assert_eq!(expression_to_string(&simplified), "mit AND apache-2.0");
511 }
512
513 #[test]
514 fn test_simplify_expression_with_duplicates() {
515 let expr = super::super::parse::parse_expression("MIT OR MIT").unwrap();
516 let simplified = simplify_expression(&expr);
517 assert_eq!(expression_to_string(&simplified), "mit");
518 }
519
520 #[test]
521 fn test_simplify_and_duplicates() {
522 let expr = super::super::parse::parse_expression("crapl-0.1 AND crapl-0.1").unwrap();
523 let simplified = simplify_expression(&expr);
524 assert_eq!(expression_to_string(&simplified), "crapl-0.1");
525 }
526
527 #[test]
528 fn test_simplify_or_duplicates() {
529 let expr = super::super::parse::parse_expression("mit OR mit").unwrap();
530 let simplified = simplify_expression(&expr);
531 assert_eq!(expression_to_string(&simplified), "mit");
532 }
533
534 #[test]
535 fn test_simplify_preserves_different_licenses() {
536 let expr = super::super::parse::parse_expression("mit AND apache-2.0").unwrap();
537 let simplified = simplify_expression(&expr);
538 assert_eq!(expression_to_string(&simplified), "mit AND apache-2.0");
539 }
540
541 #[test]
542 fn test_simplify_complex_duplicates() {
543 let expr = super::super::parse::parse_expression(
544 "gpl-2.0-plus AND gpl-2.0-plus AND lgpl-2.0-plus",
545 )
546 .unwrap();
547 let simplified = simplify_expression(&expr);
548 assert_eq!(
549 expression_to_string(&simplified),
550 "gpl-2.0-plus AND lgpl-2.0-plus"
551 );
552 }
553
554 #[test]
555 fn test_simplify_three_duplicates() {
556 let expr =
557 super::super::parse::parse_expression("fsf-free AND fsf-free AND fsf-free").unwrap();
558 let simplified = simplify_expression(&expr);
559 assert_eq!(expression_to_string(&simplified), "fsf-free");
560 }
561
562 #[test]
563 fn test_simplify_with_expression_dedup() {
564 let expr = super::super::parse::parse_expression(
565 "gpl-2.0 WITH classpath-exception-2.0 AND gpl-2.0 WITH classpath-exception-2.0",
566 )
567 .unwrap();
568 let simplified = simplify_expression(&expr);
569 assert_eq!(
570 expression_to_string(&simplified),
571 "gpl-2.0 WITH classpath-exception-2.0"
572 );
573 }
574
575 #[test]
576 fn test_simplify_nested_duplicates() {
577 let expr =
578 super::super::parse::parse_expression("(mit AND apache-2.0) OR (mit AND apache-2.0)")
579 .unwrap();
580 let simplified = simplify_expression(&expr);
581 assert_eq!(expression_to_string(&simplified), "mit AND apache-2.0");
582 }
583
584 #[test]
585 fn test_simplify_preserves_order() {
586 let expr =
587 super::super::parse::parse_expression("apache-2.0 AND mit AND apache-2.0").unwrap();
588 let simplified = simplify_expression(&expr);
589 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
590 }
591
592 #[test]
593 fn test_simplify_mit_and_mit_and_apache() {
594 let expr = super::super::parse::parse_expression("mit AND mit AND apache-2.0").unwrap();
595 let simplified = simplify_expression(&expr);
596 assert_eq!(expression_to_string(&simplified), "mit AND apache-2.0");
597 }
598
599 #[test]
600 fn test_simplify_and_absorption() {
601 let expr = super::super::parse::parse_expression("mit AND (mit OR apache-2.0)").unwrap();
602 let simplified = simplify_expression(&expr);
603
604 assert_eq!(expression_to_string(&simplified), "mit");
605 }
606
607 #[test]
608 fn test_simplify_or_absorption() {
609 let expr = super::super::parse::parse_expression("mit OR (mit AND apache-2.0)").unwrap();
610 let simplified = simplify_expression(&expr);
611
612 assert_eq!(expression_to_string(&simplified), "mit");
613 }
614
615 #[test]
616 fn test_simplify_or_subsumption() {
617 let expr = super::super::parse::parse_expression(
618 "(mit AND apache-2.0) OR (mit AND apache-2.0 AND bsd-new)",
619 )
620 .unwrap();
621 let simplified = simplify_expression(&expr);
622
623 assert_eq!(expression_to_string(&simplified), "mit AND apache-2.0");
624 }
625
626 #[test]
627 fn test_simplify_and_subsumption() {
628 let expr = super::super::parse::parse_expression(
629 "(mit OR apache-2.0) AND (mit OR apache-2.0 OR bsd-new)",
630 )
631 .unwrap();
632 let simplified = simplify_expression(&expr);
633
634 assert_eq!(expression_to_string(&simplified), "mit OR apache-2.0");
635 }
636
637 #[test]
638 fn test_expression_to_string_simple() {
639 let expr = LicenseExpression::License("mit".to_string());
640 assert_eq!(expression_to_string(&expr), "mit");
641 }
642
643 #[test]
644 fn test_expression_to_string_and() {
645 let expr = LicenseExpression::And {
646 left: Box::new(LicenseExpression::License("mit".to_string())),
647 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
648 };
649 assert_eq!(expression_to_string(&expr), "mit AND apache-2.0");
650 }
651
652 #[test]
653 fn test_expression_to_string_or() {
654 let expr = LicenseExpression::Or {
655 left: Box::new(LicenseExpression::License("mit".to_string())),
656 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
657 };
658 assert_eq!(expression_to_string(&expr), "mit OR apache-2.0");
659 }
660
661 #[test]
662 fn test_expression_to_string_with() {
663 let expr = LicenseExpression::With {
664 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
665 right: Box::new(LicenseExpression::License(
666 "classpath-exception-2.0".to_string(),
667 )),
668 };
669 assert_eq!(
670 expression_to_string(&expr),
671 "gpl-2.0 WITH classpath-exception-2.0"
672 );
673 }
674
675 #[test]
676 fn test_expression_to_string_licenseref() {
677 let expr = LicenseExpression::LicenseRef("licenseref-scancode-custom".to_string());
678 assert_eq!(expression_to_string(&expr), "licenseref-scancode-custom");
679 }
680
681 #[test]
682 fn test_expression_to_string_or_inside_and() {
683 let or_expr = LicenseExpression::Or {
684 left: Box::new(LicenseExpression::License("mit".to_string())),
685 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
686 };
687 let and_expr = LicenseExpression::And {
688 left: Box::new(or_expr),
689 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
690 };
691 assert_eq!(
692 expression_to_string(&and_expr),
693 "(mit OR apache-2.0) AND gpl-2.0"
694 );
695 }
696
697 #[test]
698 fn test_expression_to_string_and_inside_or() {
699 let and_expr = LicenseExpression::And {
700 left: Box::new(LicenseExpression::License("mit".to_string())),
701 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
702 };
703 let or_expr = LicenseExpression::Or {
704 left: Box::new(and_expr),
705 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
706 };
707 assert_eq!(
708 expression_to_string(&or_expr),
709 "(mit AND apache-2.0) OR gpl-2.0"
710 );
711 }
712
713 #[test]
714 fn test_expression_to_string_with_inside_or() {
715 let with_expr = LicenseExpression::With {
716 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
717 right: Box::new(LicenseExpression::License(
718 "classpath-exception-2.0".to_string(),
719 )),
720 };
721 let or_expr = LicenseExpression::Or {
722 left: Box::new(with_expr),
723 right: Box::new(LicenseExpression::License("mit".to_string())),
724 };
725 assert_eq!(
726 expression_to_string(&or_expr),
727 "gpl-2.0 WITH classpath-exception-2.0 OR mit"
728 );
729 }
730
731 #[test]
732 fn test_expression_to_string_with_inside_and() {
733 let with_expr = LicenseExpression::With {
734 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
735 right: Box::new(LicenseExpression::License(
736 "classpath-exception-2.0".to_string(),
737 )),
738 };
739 let and_expr = LicenseExpression::And {
740 left: Box::new(with_expr),
741 right: Box::new(LicenseExpression::License("mit".to_string())),
742 };
743 assert_eq!(
744 expression_to_string(&and_expr),
745 "gpl-2.0 WITH classpath-exception-2.0 AND mit"
746 );
747 }
748
749 #[test]
750 fn test_expression_to_string_nested_or_flattens_same_operator_grouping() {
751 let or_expr = LicenseExpression::Or {
752 left: Box::new(LicenseExpression::Or {
753 left: Box::new(LicenseExpression::License("mit".to_string())),
754 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
755 }),
756 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
757 };
758 assert_eq!(
759 expression_to_string(&or_expr),
760 "mit OR apache-2.0 OR gpl-2.0"
761 );
762 }
763
764 #[test]
765 fn test_expression_to_string_nested_and_flattens_same_operator_grouping() {
766 let and_expr = LicenseExpression::And {
767 left: Box::new(LicenseExpression::And {
768 left: Box::new(LicenseExpression::License("mit".to_string())),
769 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
770 }),
771 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
772 };
773 assert_eq!(
774 expression_to_string(&and_expr),
775 "mit AND apache-2.0 AND gpl-2.0"
776 );
777 }
778
779 #[test]
780 fn test_expression_to_string_roundtrip_or_and() {
781 let input = "(mit OR apache-2.0) AND gpl-2.0";
782 let expr = super::super::parse::parse_expression(input).unwrap();
783 let output = expression_to_string(&expr);
784 assert_eq!(output, "(mit OR apache-2.0) AND gpl-2.0");
785 }
786
787 #[test]
788 fn test_expression_to_string_roundtrip_or_with() {
789 let input = "gpl-2.0 WITH classpath-exception-2.0 OR mit";
790 let expr = super::super::parse::parse_expression(input).unwrap();
791 let output = expression_to_string(&expr);
792 assert_eq!(output, "gpl-2.0 WITH classpath-exception-2.0 OR mit");
793 }
794
795 #[test]
796 fn test_combine_expressions_empty() {
797 let result = combine_expressions_and(&[], true).unwrap();
798 assert_eq!(result, "");
799 }
800
801 #[test]
802 fn test_combine_expressions_single() {
803 let result = combine_expressions_and(&["mit"], true).unwrap();
804 assert_eq!(result, "mit");
805 }
806
807 #[test]
808 fn test_combine_expressions_two_and() {
809 let result = combine_expressions_and(&["mit", "gpl-2.0-plus"], true).unwrap();
810 assert_eq!(result, "mit AND gpl-2.0-plus");
811 }
812
813 #[test]
814 fn test_combine_expressions_two_or() {
815 let result = combine_expressions_or(&["mit", "apache-2.0"], true).unwrap();
816 assert_eq!(result, "mit OR apache-2.0");
817 }
818
819 #[test]
820 fn test_combine_expressions_multiple_and() {
821 let result = combine_expressions_and(&["mit", "apache-2.0", "gpl-2.0-plus"], true).unwrap();
822 assert_eq!(result, "mit AND apache-2.0 AND gpl-2.0-plus");
823 }
824
825 #[test]
826 fn test_combine_expressions_with_duplicates_unique() {
827 let result = combine_expressions_or(&["mit", "mit", "apache-2.0"], true).unwrap();
828 let expr = super::super::parse::parse_expression(&result).unwrap();
829 let keys = expr.license_keys();
830 assert_eq!(keys.len(), 2);
831 assert!(keys.contains(&"mit".to_string()));
832 assert!(keys.contains(&"apache-2.0".to_string()));
833 }
834
835 #[test]
836 fn test_combine_expressions_with_duplicates_not_unique() {
837 let result = combine_expressions_or(&["mit", "mit", "apache-2.0"], false).unwrap();
838 let expr = super::super::parse::parse_expression(&result).unwrap();
839 assert_eq!(result, "mit OR mit OR apache-2.0");
840 let keys = expr.license_keys();
841 assert_eq!(keys.len(), 2);
842 }
843
844 #[test]
845 fn test_combine_expressions_complex_with_simplification() {
846 let result = combine_expressions_and(&["mit OR apache-2.0", "gpl-2.0-plus"], true).unwrap();
847 assert_eq!(result, "(mit OR apache-2.0) AND gpl-2.0-plus");
848 let expr = super::super::parse::parse_expression(&result).unwrap();
849 assert!(matches!(expr, LicenseExpression::And { .. }));
850 let keys = expr.license_keys();
851 assert_eq!(keys.len(), 3);
852 }
853
854 #[test]
855 fn test_combine_expressions_parse_error() {
856 let result = combine_expressions_and(&["mit", "@invalid@"], true);
857 assert!(result.is_err());
858 }
859
860 #[test]
861 fn test_combine_expressions_with_existing_and() {
862 let result = combine_expressions_and(&["mit AND apache-2.0", "gpl-2.0"], true).unwrap();
863 assert!(result.contains("mit"));
864 assert!(result.contains("apache-2.0"));
865 assert!(result.contains("gpl-2.0"));
866 }
867
868 #[test]
869 fn test_combine_expressions_with_existing_or() {
870 let result = combine_expressions_or(&["mit OR apache-2.0", "gpl-2.0"], true).unwrap();
871 assert!(result.contains("mit"));
872 assert!(result.contains("apache-2.0"));
873 assert!(result.contains("gpl-2.0"));
874 }
875
876 #[test]
877 fn test_expression_to_string_with_no_outer_parens() {
878 let with_expr = LicenseExpression::With {
879 left: Box::new(LicenseExpression::License("gpl-2.0-plus".to_string())),
880 right: Box::new(LicenseExpression::License(
881 "classpath-exception-2.0".to_string(),
882 )),
883 };
884 assert_eq!(
885 expression_to_string(&with_expr),
886 "gpl-2.0-plus WITH classpath-exception-2.0"
887 );
888 }
889
890 #[test]
891 fn test_expression_to_string_with_as_right_operand_of_or() {
892 let with_expr = LicenseExpression::With {
893 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
894 right: Box::new(LicenseExpression::License(
895 "classpath-exception-2.0".to_string(),
896 )),
897 };
898 let or_expr = LicenseExpression::Or {
899 left: Box::new(LicenseExpression::License("mit".to_string())),
900 right: Box::new(with_expr),
901 };
902 assert_eq!(
903 expression_to_string(&or_expr),
904 "mit OR gpl-2.0 WITH classpath-exception-2.0"
905 );
906 }
907
908 #[test]
909 fn test_expression_to_string_with_as_right_operand_of_and() {
910 let with_expr = LicenseExpression::With {
911 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
912 right: Box::new(LicenseExpression::License(
913 "classpath-exception-2.0".to_string(),
914 )),
915 };
916 let and_expr = LicenseExpression::And {
917 left: Box::new(LicenseExpression::License("mit".to_string())),
918 right: Box::new(with_expr),
919 };
920 assert_eq!(
921 expression_to_string(&and_expr),
922 "mit AND gpl-2.0 WITH classpath-exception-2.0"
923 );
924 }
925
926 #[test]
927 fn test_expression_to_string_complex_precedence() {
928 let input = "mit OR apache-2.0 AND gpl-2.0";
929 let expr = super::super::parse::parse_expression(input).unwrap();
930 assert_eq!(
931 expression_to_string(&expr),
932 "mit OR (apache-2.0 AND gpl-2.0)"
933 );
934 }
935
936 #[test]
937 fn test_expression_to_string_with_no_outer_parens_in_complex_and() {
938 let input = "bsd-new AND mit AND gpl-3.0-plus WITH autoconf-simple-exception";
941 let expr = super::super::parse::parse_expression(input).unwrap();
942 assert_eq!(
943 expression_to_string(&expr),
944 "bsd-new AND mit AND gpl-3.0-plus WITH autoconf-simple-exception"
945 );
946 }
947
948 #[test]
949 fn test_combine_expressions_and_flattens_reported_redundant_parentheses() {
950 let result = combine_expressions_and(
951 &[
952 "Apache-2.0",
953 "BSD-3-Clause",
954 "GPL-2.0-only",
955 "LicenseRef-scancode-oracle-openjdk-exception-2.0",
956 "APSL-1.0",
957 "APSL-2.0",
958 ],
959 true,
960 )
961 .unwrap();
962
963 assert_eq!(
964 result,
965 "apache-2.0 AND bsd-3-clause AND gpl-2.0-only AND licenseref-scancode-oracle-openjdk-exception-2.0 AND apsl-1.0 AND apsl-2.0"
966 );
967 }
968}
969
970#[cfg(test)]
971mod contains_tests {
972 use super::*;
973
974 #[test]
975 fn test_basic_containment() {
976 assert!(licensing_contains("mit", "mit"));
977 assert!(!licensing_contains("mit", "apache"));
978 }
979
980 #[test]
981 fn test_or_containment() {
982 assert!(licensing_contains("mit OR apache", "mit"));
983 assert!(licensing_contains("mit OR apache", "apache"));
984 assert!(!licensing_contains("mit OR apache", "gpl"));
985 }
986
987 #[test]
988 fn test_and_containment() {
989 assert!(licensing_contains("mit AND apache", "mit"));
990 assert!(licensing_contains("mit AND apache", "apache"));
991 assert!(!licensing_contains("mit", "mit AND apache"));
992 }
993
994 #[test]
995 fn test_expression_subset() {
996 assert!(licensing_contains(
997 "mit AND apache AND bsd",
998 "mit AND apache"
999 ));
1000 assert!(!licensing_contains(
1001 "mit AND apache",
1002 "mit AND apache AND bsd"
1003 ));
1004 assert!(licensing_contains("mit OR apache OR bsd", "mit OR apache"));
1005 assert!(!licensing_contains("mit OR apache", "mit OR apache OR bsd"));
1006 }
1007
1008 #[test]
1009 fn test_order_independence() {
1010 assert!(licensing_contains("mit AND apache", "apache AND mit"));
1011 assert!(licensing_contains("mit OR apache", "apache OR mit"));
1012 }
1013
1014 #[test]
1015 fn test_plus_suffix_no_containment() {
1016 assert!(!licensing_contains("gpl-2.0-plus", "gpl-2.0"));
1017 assert!(!licensing_contains("gpl-2.0", "gpl-2.0-plus"));
1018 }
1019
1020 #[test]
1021 fn test_with_decomposition() {
1022 assert!(licensing_contains(
1023 "gpl-2.0 WITH classpath-exception",
1024 "gpl-2.0"
1025 ));
1026 assert!(licensing_contains(
1027 "gpl-2.0 WITH classpath-exception",
1028 "classpath-exception"
1029 ));
1030 assert!(!licensing_contains(
1031 "gpl-2.0",
1032 "gpl-2.0 WITH classpath-exception"
1033 ));
1034 }
1035
1036 #[test]
1037 fn test_mixed_operators() {
1038 assert!(!licensing_contains("mit OR apache", "mit AND apache"));
1039 assert!(!licensing_contains("mit AND apache", "mit OR apache"));
1040 }
1041
1042 #[test]
1043 fn test_nested_expressions() {
1044 assert!(!licensing_contains("(mit OR apache) AND bsd", "mit"));
1045 assert!(licensing_contains(
1046 "(mit OR apache) AND bsd",
1047 "mit OR apache"
1048 ));
1049 assert!(licensing_contains("(mit OR apache) AND bsd", "bsd"));
1050 }
1051
1052 #[test]
1053 fn test_empty_expressions() {
1054 assert!(!licensing_contains("", "mit"));
1055 assert!(!licensing_contains("mit", ""));
1056 assert!(!licensing_contains("", ""));
1057 assert!(!licensing_contains(" ", "mit"));
1058 }
1059
1060 #[test]
1061 fn test_invalid_expressions() {
1062 assert!(!licensing_contains("mit AND", "mit"));
1063 assert!(!licensing_contains("mit", "AND apache"));
1064 }
1065}