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