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
573pub fn combine_expressions_or(expressions: &[&str], unique: bool) -> Result<String, ParseError> {
578 combine_expressions_with(
579 expressions,
580 unique,
581 LicenseExpression::or,
582 simplify_expression,
583 )
584}
585
586pub fn combine_expressions_or_preserving_structure(
589 expressions: &[&str],
590 unique: bool,
591) -> Result<String, ParseError> {
592 combine_expressions_with(
593 expressions,
594 unique,
595 LicenseExpression::or,
596 simplify_expression_preserving_structure,
597 )
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 fn expression_depth(expr: &LicenseExpression) -> usize {
605 match expr {
606 LicenseExpression::License(_) | LicenseExpression::LicenseRef(_) => 1,
607 LicenseExpression::And { left, right }
608 | LicenseExpression::Or { left, right }
609 | LicenseExpression::With { left, right } => {
610 1 + expression_depth(left).max(expression_depth(right))
611 }
612 }
613 }
614
615 #[test]
616 fn test_simplify_expression_no_change() {
617 let expr = super::super::parse::parse_expression("MIT AND Apache-2.0").unwrap();
618 let simplified = simplify_expression(&expr);
619 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
620 }
621
622 #[test]
623 fn test_simplify_expression_with_duplicates() {
624 let expr = super::super::parse::parse_expression("MIT OR MIT").unwrap();
625 let simplified = simplify_expression(&expr);
626 assert_eq!(expression_to_string(&simplified), "mit");
627 }
628
629 #[test]
630 fn test_simplify_expression_preserving_structure_keeps_distinct_nested_operands() {
631 let expr = super::super::parse::parse_expression("mit AND (apache-2.0 OR mit)").unwrap();
632 let simplified = simplify_expression_preserving_structure(&expr);
633 assert_eq!(
634 expression_to_string(&simplified),
635 "mit AND (apache-2.0 OR mit)"
636 );
637 }
638
639 #[test]
640 fn test_simplify_and_duplicates() {
641 let expr = super::super::parse::parse_expression("crapl-0.1 AND crapl-0.1").unwrap();
642 let simplified = simplify_expression(&expr);
643 assert_eq!(expression_to_string(&simplified), "crapl-0.1");
644 }
645
646 #[test]
647 fn test_simplify_or_duplicates() {
648 let expr = super::super::parse::parse_expression("mit OR mit").unwrap();
649 let simplified = simplify_expression(&expr);
650 assert_eq!(expression_to_string(&simplified), "mit");
651 }
652
653 #[test]
654 fn test_combine_expressions_and_preserving_structure_keeps_distinct_nested_operands() {
655 let result =
656 combine_expressions_and_preserving_structure(&["mit", "apache-2.0 OR mit"], true)
657 .unwrap();
658 assert_eq!(result, "mit AND (apache-2.0 OR mit)");
659 }
660
661 #[test]
662 fn test_simplify_preserves_different_licenses() {
663 let expr = super::super::parse::parse_expression("mit AND apache-2.0").unwrap();
664 let simplified = simplify_expression(&expr);
665 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
666 }
667
668 #[test]
669 fn test_simplify_complex_duplicates() {
670 let expr = super::super::parse::parse_expression(
671 "gpl-2.0-plus AND gpl-2.0-plus AND lgpl-2.0-plus",
672 )
673 .unwrap();
674 let simplified = simplify_expression(&expr);
675 assert_eq!(
676 expression_to_string(&simplified),
677 "gpl-2.0-plus AND lgpl-2.0-plus"
678 );
679 }
680
681 #[test]
682 fn test_simplify_three_duplicates() {
683 let expr =
684 super::super::parse::parse_expression("fsf-free AND fsf-free AND fsf-free").unwrap();
685 let simplified = simplify_expression(&expr);
686 assert_eq!(expression_to_string(&simplified), "fsf-free");
687 }
688
689 #[test]
690 fn test_simplify_with_expression_dedup() {
691 let expr = super::super::parse::parse_expression(
692 "gpl-2.0 WITH classpath-exception-2.0 AND gpl-2.0 WITH classpath-exception-2.0",
693 )
694 .unwrap();
695 let simplified = simplify_expression(&expr);
696 assert_eq!(
697 expression_to_string(&simplified),
698 "gpl-2.0 WITH classpath-exception-2.0"
699 );
700 }
701
702 #[test]
703 fn test_simplify_nested_duplicates() {
704 let expr =
705 super::super::parse::parse_expression("(mit AND apache-2.0) OR (mit AND apache-2.0)")
706 .unwrap();
707 let simplified = simplify_expression(&expr);
708 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
709 }
710
711 #[test]
712 fn test_simplify_sorts_operands_canonically() {
713 let expr =
714 super::super::parse::parse_expression("apache-2.0 AND mit AND apache-2.0").unwrap();
715 let simplified = simplify_expression(&expr);
716 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
717 }
718
719 #[test]
720 fn test_simplify_mit_and_mit_and_apache() {
721 let expr = super::super::parse::parse_expression("mit AND mit AND apache-2.0").unwrap();
722 let simplified = simplify_expression(&expr);
723 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
724 }
725
726 #[test]
727 fn test_simplify_and_absorption() {
728 let expr = super::super::parse::parse_expression("mit AND (mit OR apache-2.0)").unwrap();
729 let simplified = simplify_expression(&expr);
730
731 assert_eq!(expression_to_string(&simplified), "mit");
732 }
733
734 #[test]
735 fn test_simplify_or_absorption() {
736 let expr = super::super::parse::parse_expression("mit OR (mit AND apache-2.0)").unwrap();
737 let simplified = simplify_expression(&expr);
738
739 assert_eq!(expression_to_string(&simplified), "mit");
740 }
741
742 #[test]
743 fn test_simplify_or_subsumption() {
744 let expr = super::super::parse::parse_expression(
745 "(mit AND apache-2.0) OR (mit AND apache-2.0 AND bsd-new)",
746 )
747 .unwrap();
748 let simplified = simplify_expression(&expr);
749
750 assert_eq!(expression_to_string(&simplified), "apache-2.0 AND mit");
751 }
752
753 #[test]
754 fn test_simplify_and_subsumption() {
755 let expr = super::super::parse::parse_expression(
756 "(mit OR apache-2.0) AND (mit OR apache-2.0 OR bsd-new)",
757 )
758 .unwrap();
759 let simplified = simplify_expression(&expr);
760
761 assert_eq!(expression_to_string(&simplified), "apache-2.0 OR mit");
762 }
763
764 #[test]
765 fn test_simplify_and_keeps_gpl_or_later_with_only() {
766 let expr =
767 super::super::parse::parse_expression("gpl-2.0-or-later AND gpl-2.0-only").unwrap();
768 let simplified = simplify_expression(&expr);
769
770 assert_eq!(
771 expression_to_string(&simplified),
772 "gpl-2.0-only AND gpl-2.0-or-later"
773 );
774 }
775
776 #[test]
777 fn test_expression_to_string_simple() {
778 let expr = LicenseExpression::License("mit".to_string());
779 assert_eq!(expression_to_string(&expr), "mit");
780 }
781
782 #[test]
783 fn test_expression_to_string_and() {
784 let expr = LicenseExpression::And {
785 left: Box::new(LicenseExpression::License("mit".to_string())),
786 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
787 };
788 assert_eq!(expression_to_string(&expr), "mit AND apache-2.0");
789 }
790
791 #[test]
792 fn test_expression_to_string_or() {
793 let expr = LicenseExpression::Or {
794 left: Box::new(LicenseExpression::License("mit".to_string())),
795 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
796 };
797 assert_eq!(expression_to_string(&expr), "mit OR apache-2.0");
798 }
799
800 #[test]
801 fn test_expression_to_string_with() {
802 let expr = LicenseExpression::With {
803 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
804 right: Box::new(LicenseExpression::License(
805 "classpath-exception-2.0".to_string(),
806 )),
807 };
808 assert_eq!(
809 expression_to_string(&expr),
810 "gpl-2.0 WITH classpath-exception-2.0"
811 );
812 }
813
814 #[test]
815 fn test_expression_to_string_licenseref() {
816 let expr = LicenseExpression::LicenseRef("licenseref-scancode-custom".to_string());
817 assert_eq!(expression_to_string(&expr), "licenseref-scancode-custom");
818 }
819
820 #[test]
821 fn test_expression_to_string_or_inside_and() {
822 let or_expr = LicenseExpression::Or {
823 left: Box::new(LicenseExpression::License("mit".to_string())),
824 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
825 };
826 let and_expr = LicenseExpression::And {
827 left: Box::new(or_expr),
828 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
829 };
830 assert_eq!(
831 expression_to_string(&and_expr),
832 "(mit OR apache-2.0) AND gpl-2.0"
833 );
834 }
835
836 #[test]
837 fn test_expression_to_string_and_inside_or() {
838 let and_expr = LicenseExpression::And {
839 left: Box::new(LicenseExpression::License("mit".to_string())),
840 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
841 };
842 let or_expr = LicenseExpression::Or {
843 left: Box::new(and_expr),
844 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
845 };
846 assert_eq!(
847 expression_to_string(&or_expr),
848 "(mit AND apache-2.0) OR gpl-2.0"
849 );
850 }
851
852 #[test]
853 fn test_expression_to_string_with_inside_or() {
854 let with_expr = LicenseExpression::With {
855 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
856 right: Box::new(LicenseExpression::License(
857 "classpath-exception-2.0".to_string(),
858 )),
859 };
860 let or_expr = LicenseExpression::Or {
861 left: Box::new(with_expr),
862 right: Box::new(LicenseExpression::License("mit".to_string())),
863 };
864 assert_eq!(
865 expression_to_string(&or_expr),
866 "gpl-2.0 WITH classpath-exception-2.0 OR mit"
867 );
868 }
869
870 #[test]
871 fn test_expression_to_string_with_inside_and() {
872 let with_expr = LicenseExpression::With {
873 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
874 right: Box::new(LicenseExpression::License(
875 "classpath-exception-2.0".to_string(),
876 )),
877 };
878 let and_expr = LicenseExpression::And {
879 left: Box::new(with_expr),
880 right: Box::new(LicenseExpression::License("mit".to_string())),
881 };
882 assert_eq!(
883 expression_to_string(&and_expr),
884 "gpl-2.0 WITH classpath-exception-2.0 AND mit"
885 );
886 }
887
888 #[test]
889 fn test_expression_to_string_nested_or_flattens_same_operator_grouping() {
890 let or_expr = LicenseExpression::Or {
891 left: Box::new(LicenseExpression::Or {
892 left: Box::new(LicenseExpression::License("mit".to_string())),
893 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
894 }),
895 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
896 };
897 assert_eq!(
898 expression_to_string(&or_expr),
899 "mit OR apache-2.0 OR gpl-2.0"
900 );
901 }
902
903 #[test]
904 fn test_expression_to_string_nested_and_flattens_same_operator_grouping() {
905 let and_expr = LicenseExpression::And {
906 left: Box::new(LicenseExpression::And {
907 left: Box::new(LicenseExpression::License("mit".to_string())),
908 right: Box::new(LicenseExpression::License("apache-2.0".to_string())),
909 }),
910 right: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
911 };
912 assert_eq!(
913 expression_to_string(&and_expr),
914 "mit AND apache-2.0 AND gpl-2.0"
915 );
916 }
917
918 #[test]
919 fn test_expression_to_string_roundtrip_or_and() {
920 let input = "(mit OR apache-2.0) AND gpl-2.0";
921 let expr = super::super::parse::parse_expression(input).unwrap();
922 let output = expression_to_string(&expr);
923 assert_eq!(output, "(mit OR apache-2.0) AND gpl-2.0");
924 }
925
926 #[test]
927 fn test_expression_to_string_roundtrip_or_with() {
928 let input = "gpl-2.0 WITH classpath-exception-2.0 OR mit";
929 let expr = super::super::parse::parse_expression(input).unwrap();
930 let output = expression_to_string(&expr);
931 assert_eq!(output, "gpl-2.0 WITH classpath-exception-2.0 OR mit");
932 }
933
934 #[test]
935 fn test_combine_expressions_empty() {
936 let result = combine_expressions_and(&[], true).unwrap();
937 assert_eq!(result, "");
938 }
939
940 #[test]
941 fn test_combine_expressions_single() {
942 let result = combine_expressions_and(&["mit"], true).unwrap();
943 assert_eq!(result, "mit");
944 }
945
946 #[test]
947 fn test_combine_expressions_two_and() {
948 let result = combine_expressions_and(&["mit", "gpl-2.0-plus"], true).unwrap();
949 assert_eq!(result, "gpl-2.0-plus AND mit");
950 }
951
952 #[test]
953 fn test_combine_expressions_two_or() {
954 let result = combine_expressions_or(&["mit", "apache-2.0"], true).unwrap();
955 assert_eq!(result, "apache-2.0 OR mit");
956 }
957
958 #[test]
959 fn test_combine_expressions_multiple_and() {
960 let result = combine_expressions_and(&["mit", "apache-2.0", "gpl-2.0-plus"], true).unwrap();
961 assert_eq!(result, "apache-2.0 AND gpl-2.0-plus AND mit");
962 }
963
964 #[test]
965 fn test_combine_expressions_with_duplicates_unique() {
966 let result = combine_expressions_or(&["mit", "mit", "apache-2.0"], true).unwrap();
967 let expr = super::super::parse::parse_expression(&result).unwrap();
968 let keys = expr.license_keys();
969 assert_eq!(keys.len(), 2);
970 assert!(keys.contains(&"mit".to_string()));
971 assert!(keys.contains(&"apache-2.0".to_string()));
972 }
973
974 #[test]
975 fn test_combine_expressions_with_duplicates_not_unique() {
976 let result = combine_expressions_or(&["mit", "mit", "apache-2.0"], false).unwrap();
977 let expr = super::super::parse::parse_expression(&result).unwrap();
978 assert_eq!(result, "mit OR mit OR apache-2.0");
979 let keys = expr.license_keys();
980 assert_eq!(keys.len(), 2);
981 }
982
983 #[test]
984 fn test_combine_expressions_complex_with_simplification() {
985 let result = combine_expressions_and(&["mit OR apache-2.0", "gpl-2.0-plus"], true).unwrap();
986 assert_eq!(result, "(apache-2.0 OR mit) AND gpl-2.0-plus");
987 let expr = super::super::parse::parse_expression(&result).unwrap();
988 assert!(matches!(expr, LicenseExpression::And { .. }));
989 let keys = expr.license_keys();
990 assert_eq!(keys.len(), 3);
991 }
992
993 #[test]
994 fn test_combine_expressions_parse_error() {
995 let result = combine_expressions_and(&["mit", "@invalid@"], true);
996 assert!(result.is_err());
997 }
998
999 #[test]
1000 fn test_combine_expressions_with_existing_and() {
1001 let result = combine_expressions_and(&["mit AND apache-2.0", "gpl-2.0"], true).unwrap();
1002 assert!(result.contains("mit"));
1003 assert!(result.contains("apache-2.0"));
1004 assert!(result.contains("gpl-2.0"));
1005 }
1006
1007 #[test]
1008 fn test_combine_expressions_with_existing_or() {
1009 let result = combine_expressions_or(&["mit OR apache-2.0", "gpl-2.0"], true).unwrap();
1010 assert!(result.contains("mit"));
1011 assert!(result.contains("apache-2.0"));
1012 assert!(result.contains("gpl-2.0"));
1013 }
1014
1015 #[test]
1016 fn test_expression_to_string_with_no_outer_parens() {
1017 let with_expr = LicenseExpression::With {
1018 left: Box::new(LicenseExpression::License("gpl-2.0-plus".to_string())),
1019 right: Box::new(LicenseExpression::License(
1020 "classpath-exception-2.0".to_string(),
1021 )),
1022 };
1023 assert_eq!(
1024 expression_to_string(&with_expr),
1025 "gpl-2.0-plus WITH classpath-exception-2.0"
1026 );
1027 }
1028
1029 #[test]
1030 fn test_expression_to_string_with_as_right_operand_of_or() {
1031 let with_expr = LicenseExpression::With {
1032 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
1033 right: Box::new(LicenseExpression::License(
1034 "classpath-exception-2.0".to_string(),
1035 )),
1036 };
1037 let or_expr = LicenseExpression::Or {
1038 left: Box::new(LicenseExpression::License("mit".to_string())),
1039 right: Box::new(with_expr),
1040 };
1041 assert_eq!(
1042 expression_to_string(&or_expr),
1043 "mit OR gpl-2.0 WITH classpath-exception-2.0"
1044 );
1045 }
1046
1047 #[test]
1048 fn test_expression_to_string_with_as_right_operand_of_and() {
1049 let with_expr = LicenseExpression::With {
1050 left: Box::new(LicenseExpression::License("gpl-2.0".to_string())),
1051 right: Box::new(LicenseExpression::License(
1052 "classpath-exception-2.0".to_string(),
1053 )),
1054 };
1055 let and_expr = LicenseExpression::And {
1056 left: Box::new(LicenseExpression::License("mit".to_string())),
1057 right: Box::new(with_expr),
1058 };
1059 assert_eq!(
1060 expression_to_string(&and_expr),
1061 "mit AND gpl-2.0 WITH classpath-exception-2.0"
1062 );
1063 }
1064
1065 #[test]
1066 fn test_expression_to_string_complex_precedence() {
1067 let input = "mit OR apache-2.0 AND gpl-2.0";
1068 let expr = super::super::parse::parse_expression(input).unwrap();
1069 assert_eq!(
1070 expression_to_string(&expr),
1071 "mit OR (apache-2.0 AND gpl-2.0)"
1072 );
1073 }
1074
1075 #[test]
1076 fn test_expression_to_string_with_no_outer_parens_in_complex_and() {
1077 let input = "bsd-new AND mit AND gpl-3.0-plus WITH autoconf-simple-exception";
1080 let expr = super::super::parse::parse_expression(input).unwrap();
1081 assert_eq!(
1082 expression_to_string(&expr),
1083 "bsd-new AND mit AND gpl-3.0-plus WITH autoconf-simple-exception"
1084 );
1085 }
1086
1087 #[test]
1088 fn test_combine_expressions_and_flattens_reported_redundant_parentheses() {
1089 let result = combine_expressions_and(
1090 &[
1091 "Apache-2.0",
1092 "BSD-3-Clause",
1093 "GPL-2.0-only",
1094 "LicenseRef-scancode-oracle-openjdk-exception-2.0",
1095 "APSL-1.0",
1096 "APSL-2.0",
1097 ],
1098 true,
1099 )
1100 .unwrap();
1101
1102 assert_eq!(
1103 result,
1104 "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"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_build_expression_from_list_balances_large_and_chains() {
1110 let unique: Vec<_> = (0..1024)
1111 .map(|idx| LicenseExpression::License(format!("license-{idx}")))
1112 .collect();
1113
1114 let result = build_expression_from_list(&unique, true);
1115
1116 assert!(expression_depth(&result) <= 12);
1117 }
1118
1119 #[test]
1120 fn test_build_expression_from_list_balances_large_or_chains() {
1121 let unique: Vec<_> = (0..1024)
1122 .map(|idx| LicenseExpression::License(format!("license-{idx}")))
1123 .collect();
1124
1125 let result = build_expression_from_list(&unique, false);
1126
1127 assert!(expression_depth(&result) <= 12);
1128 }
1129}
1130
1131#[cfg(test)]
1132mod contains_tests {
1133 use super::*;
1134
1135 #[test]
1136 fn test_basic_containment() {
1137 assert!(licensing_contains("mit", "mit"));
1138 assert!(!licensing_contains("mit", "apache"));
1139 }
1140
1141 #[test]
1142 fn test_or_containment() {
1143 assert!(licensing_contains("mit OR apache", "mit"));
1144 assert!(licensing_contains("mit OR apache", "apache"));
1145 assert!(!licensing_contains("mit OR apache", "gpl"));
1146 }
1147
1148 #[test]
1149 fn test_and_containment() {
1150 assert!(licensing_contains("mit AND apache", "mit"));
1151 assert!(licensing_contains("mit AND apache", "apache"));
1152 assert!(!licensing_contains("mit", "mit AND apache"));
1153 }
1154
1155 #[test]
1156 fn test_expression_subset() {
1157 assert!(licensing_contains(
1158 "mit AND apache AND bsd",
1159 "mit AND apache"
1160 ));
1161 assert!(!licensing_contains(
1162 "mit AND apache",
1163 "mit AND apache AND bsd"
1164 ));
1165 assert!(licensing_contains("mit OR apache OR bsd", "mit OR apache"));
1166 assert!(!licensing_contains("mit OR apache", "mit OR apache OR bsd"));
1167 }
1168
1169 #[test]
1170 fn test_order_independence() {
1171 assert!(licensing_contains("mit AND apache", "apache AND mit"));
1172 assert!(licensing_contains("mit OR apache", "apache OR mit"));
1173 }
1174
1175 #[test]
1176 fn test_plus_suffix_no_containment() {
1177 assert!(!licensing_contains("gpl-2.0-plus", "gpl-2.0"));
1178 assert!(!licensing_contains("gpl-2.0", "gpl-2.0-plus"));
1179 }
1180
1181 #[test]
1182 fn test_with_decomposition() {
1183 assert!(licensing_contains(
1184 "gpl-2.0 WITH classpath-exception",
1185 "gpl-2.0"
1186 ));
1187 assert!(licensing_contains(
1188 "gpl-2.0 WITH classpath-exception",
1189 "classpath-exception"
1190 ));
1191 assert!(!licensing_contains(
1192 "gpl-2.0",
1193 "gpl-2.0 WITH classpath-exception"
1194 ));
1195 }
1196
1197 #[test]
1198 fn test_mixed_operators() {
1199 assert!(!licensing_contains("mit OR apache", "mit AND apache"));
1200 assert!(!licensing_contains("mit AND apache", "mit OR apache"));
1201 }
1202
1203 #[test]
1204 fn test_nested_expressions() {
1205 assert!(!licensing_contains("(mit OR apache) AND bsd", "mit"));
1206 assert!(licensing_contains(
1207 "(mit OR apache) AND bsd",
1208 "mit OR apache"
1209 ));
1210 assert!(licensing_contains("(mit OR apache) AND bsd", "bsd"));
1211 }
1212
1213 #[test]
1214 fn test_empty_expressions() {
1215 assert!(!licensing_contains("", "mit"));
1216 assert!(!licensing_contains("mit", ""));
1217 assert!(!licensing_contains("", ""));
1218 assert!(!licensing_contains(" ", "mit"));
1219 }
1220
1221 #[test]
1222 fn test_invalid_expressions() {
1223 assert!(!licensing_contains("mit AND", "mit"));
1224 assert!(!licensing_contains("mit", "AND apache"));
1225 }
1226}