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