codegraph_c/pipeline/
conditionals.rs

1//! Preprocessor conditional evaluation
2//!
3//! This module provides evaluation of preprocessor conditionals to strip dead code
4//! before parsing. This is crucial for handling kernel code which often has
5//! `#if 0` blocks for debugging or commented-out code.
6
7/// Strategy for handling preprocessor conditionals
8#[derive(Debug, Clone, PartialEq)]
9pub enum ConditionalStrategy {
10    /// Keep all conditionals as-is (no modification)
11    KeepAll,
12    /// Strip all preprocessor lines except #include
13    StripAll,
14    /// Evaluate simple conditions (#if 0, #if 1)
15    EvaluateSimple,
16}
17
18/// State machine for tracking conditional nesting
19#[derive(Debug, Clone, PartialEq)]
20enum ConditionalState {
21    /// Normal code, being included
22    Active,
23    /// In a disabled block (#if 0 or false branch)
24    Disabled,
25    /// Seen #else while in disabled state, now active
26    ElseActive,
27    /// Seen #else while in active state, now disabled
28    ElseDisabled,
29}
30
31/// Evaluate preprocessor conditionals in source code
32///
33/// Returns the processed source and the count of blocks stripped.
34pub fn evaluate_conditionals(source: &str, strategy: ConditionalStrategy) -> (String, usize) {
35    match strategy {
36        ConditionalStrategy::KeepAll => (source.to_string(), 0),
37        ConditionalStrategy::StripAll => strip_all_preprocessor(source),
38        ConditionalStrategy::EvaluateSimple => evaluate_simple_conditionals(source),
39    }
40}
41
42/// Strip all preprocessor directives except #include
43fn strip_all_preprocessor(source: &str) -> (String, usize) {
44    let mut result = String::with_capacity(source.len());
45    let mut stripped_count = 0;
46
47    for line in source.lines() {
48        let trimmed = line.trim();
49
50        if trimmed.starts_with('#') {
51            if trimmed.starts_with("#include") {
52                result.push_str(line);
53                result.push('\n');
54            } else {
55                // Replace with empty line to preserve line numbers
56                result.push('\n');
57                stripped_count += 1;
58            }
59        } else {
60            result.push_str(line);
61            result.push('\n');
62        }
63    }
64
65    (result, stripped_count)
66}
67
68/// Evaluate simple conditionals (#if 0, #if 1)
69fn evaluate_simple_conditionals(source: &str) -> (String, usize) {
70    let mut result = String::with_capacity(source.len());
71    let mut stripped_count = 0;
72
73    // Stack to track nested conditionals
74    // Each entry is (state, depth)
75    let mut state_stack: Vec<ConditionalState> = vec![ConditionalState::Active];
76
77    // Track if we're in a multi-line macro (continuation with backslash)
78    let mut in_multiline_define = false;
79    let mut multiline_define_content = String::new();
80
81    let lines: Vec<&str> = source.lines().collect();
82    let mut i = 0;
83
84    while i < lines.len() {
85        let line = lines[i];
86        let trimmed = line.trim();
87
88        // Handle continuation lines from multi-line #define
89        if in_multiline_define {
90            // Check if this line also ends with backslash
91            let continues = line.trim_end().ends_with('\\');
92            // Append content without the trailing backslash
93            let content = if continues {
94                line.trim_end().trim_end_matches('\\').trim_end()
95            } else {
96                line.trim()
97            };
98            // Escape any internal comments to avoid nested comment issues
99            // Replace /* with /+ and */ with +/
100            let escaped_content = content.replace("/*", "/+").replace("*/", "+/");
101            multiline_define_content.push(' ');
102            multiline_define_content.push_str(&escaped_content);
103
104            if !continues {
105                // End of multi-line define - output as single line comment
106                result.push_str("/* ");
107                result.push_str(&multiline_define_content);
108                result.push_str(" */\n");
109                in_multiline_define = false;
110                multiline_define_content.clear();
111            } else {
112                // Still continuing, just emit empty line to preserve line numbers
113                result.push('\n');
114            }
115            i += 1;
116            continue;
117        }
118
119        if let Some(directive) = get_preprocessor_directive(trimmed) {
120            match directive {
121                PreprocessorDirective::If(condition) => {
122                    let current_state = state_stack.last().unwrap_or(&ConditionalState::Active);
123
124                    let new_state = if *current_state == ConditionalState::Active
125                        || *current_state == ConditionalState::ElseActive
126                    {
127                        // Evaluate the condition
128                        if is_false_condition(&condition) {
129                            ConditionalState::Disabled
130                        } else {
131                            ConditionalState::Active
132                        }
133                    } else {
134                        // Parent is disabled, so we're disabled too
135                        ConditionalState::Disabled
136                    };
137
138                    state_stack.push(new_state);
139
140                    // Always emit empty line to preserve line numbers
141                    result.push('\n');
142                    stripped_count += 1;
143                }
144
145                PreprocessorDirective::Ifdef(_) | PreprocessorDirective::Ifndef(_) => {
146                    let current_state = state_stack.last().unwrap_or(&ConditionalState::Active);
147
148                    // For ifdef/ifndef, we can't evaluate without knowing defined macros
149                    // Keep as active if parent is active, otherwise disabled
150                    let new_state = if *current_state == ConditionalState::Active
151                        || *current_state == ConditionalState::ElseActive
152                    {
153                        ConditionalState::Active
154                    } else {
155                        ConditionalState::Disabled
156                    };
157
158                    state_stack.push(new_state);
159                    // Strip the #ifdef/#ifndef line itself (not valid C)
160                    result.push('\n');
161                    stripped_count += 1;
162                }
163
164                PreprocessorDirective::Elif(condition) => {
165                    if let Some(state) = state_stack.last_mut() {
166                        match state {
167                            ConditionalState::Disabled => {
168                                // Previous branch was disabled, check this condition
169                                if !is_false_condition(&condition) {
170                                    *state = ConditionalState::Active;
171                                }
172                            }
173                            ConditionalState::Active => {
174                                // Previous branch was active, so skip this one
175                                *state = ConditionalState::Disabled;
176                            }
177                            ConditionalState::ElseActive | ConditionalState::ElseDisabled => {
178                                // elif after else is an error, but handle gracefully
179                            }
180                        }
181                    }
182                    result.push('\n');
183                    stripped_count += 1;
184                }
185
186                PreprocessorDirective::Else => {
187                    if let Some(state) = state_stack.last_mut() {
188                        *state = match state {
189                            ConditionalState::Active => ConditionalState::ElseDisabled,
190                            ConditionalState::Disabled => ConditionalState::ElseActive,
191                            ConditionalState::ElseActive | ConditionalState::ElseDisabled => {
192                                // Multiple else is an error, but handle gracefully
193                                state.clone()
194                            }
195                        };
196                    }
197                    result.push('\n');
198                    stripped_count += 1;
199                }
200
201                PreprocessorDirective::Endif => {
202                    if state_stack.len() > 1 {
203                        state_stack.pop();
204                    }
205                    result.push('\n');
206                    stripped_count += 1;
207                }
208
209                PreprocessorDirective::Include => {
210                    // Always keep includes
211                    result.push_str(line);
212                    result.push('\n');
213                }
214
215                PreprocessorDirective::Define
216                | PreprocessorDirective::Undef
217                | PreprocessorDirective::Pragma
218                | PreprocessorDirective::Error
219                | PreprocessorDirective::Warning => {
220                    let current_state = state_stack.last().unwrap_or(&ConditionalState::Active);
221                    if *current_state == ConditionalState::Active
222                        || *current_state == ConditionalState::ElseActive
223                    {
224                        // Keep directive but it may cause parsing issues
225                        // Convert to comment to preserve line numbers
226                        // Check if this is a multi-line macro (ends with backslash)
227                        let continues = line.trim_end().ends_with('\\');
228                        if continues {
229                            // Start collecting multi-line define
230                            // Remove trailing backslash from content and escape internal comments
231                            let content = trimmed.trim_end_matches('\\').trim_end();
232                            let escaped = content.replace("/*", "/+").replace("*/", "+/");
233                            multiline_define_content.clear();
234                            multiline_define_content.push_str(&escaped);
235                            in_multiline_define = true;
236                            // Emit empty line to preserve line numbers
237                            result.push('\n');
238                        } else {
239                            // Single-line define - wrap in comment
240                            // Escape any internal comments
241                            let escaped = trimmed.replace("/*", "/+").replace("*/", "+/");
242                            result.push_str("/* ");
243                            result.push_str(&escaped);
244                            result.push_str(" */\n");
245                        }
246                    } else {
247                        result.push('\n');
248                    }
249                    stripped_count += 1;
250                }
251
252                PreprocessorDirective::Other => {
253                    let current_state = state_stack.last().unwrap_or(&ConditionalState::Active);
254                    if *current_state == ConditionalState::Active
255                        || *current_state == ConditionalState::ElseActive
256                    {
257                        result.push_str(line);
258                        result.push('\n');
259                    } else {
260                        result.push('\n');
261                        stripped_count += 1;
262                    }
263                }
264            }
265        } else {
266            // Regular code line
267            let current_state = state_stack.last().unwrap_or(&ConditionalState::Active);
268            if *current_state == ConditionalState::Active
269                || *current_state == ConditionalState::ElseActive
270            {
271                result.push_str(line);
272                result.push('\n');
273            } else {
274                // Emit empty line to preserve line numbers
275                result.push('\n');
276                stripped_count += 1;
277            }
278        }
279        i += 1;
280    }
281
282    (result, stripped_count)
283}
284
285#[derive(Debug, Clone)]
286enum PreprocessorDirective {
287    If(String),
288    Ifdef(#[allow(dead_code)] String),
289    Ifndef(#[allow(dead_code)] String),
290    Elif(String),
291    Else,
292    Endif,
293    Include,
294    Define,
295    Undef,
296    Pragma,
297    Error,
298    Warning,
299    Other,
300}
301
302fn get_preprocessor_directive(trimmed: &str) -> Option<PreprocessorDirective> {
303    if !trimmed.starts_with('#') {
304        return None;
305    }
306
307    let rest = trimmed[1..].trim_start();
308
309    if rest.starts_with("if ") || rest == "if" {
310        let condition = rest.strip_prefix("if").unwrap_or("").trim().to_string();
311        Some(PreprocessorDirective::If(condition))
312    } else if rest.starts_with("ifdef ") || rest.starts_with("ifdef\t") {
313        let name = rest
314            .strip_prefix("ifdef")
315            .unwrap_or("")
316            .split_whitespace()
317            .next()
318            .unwrap_or("")
319            .to_string();
320        Some(PreprocessorDirective::Ifdef(name))
321    } else if rest.starts_with("ifndef ") || rest.starts_with("ifndef\t") {
322        let name = rest
323            .strip_prefix("ifndef")
324            .unwrap_or("")
325            .split_whitespace()
326            .next()
327            .unwrap_or("")
328            .to_string();
329        Some(PreprocessorDirective::Ifndef(name))
330    } else if rest.starts_with("elif ") {
331        let condition = rest.strip_prefix("elif").unwrap_or("").trim().to_string();
332        Some(PreprocessorDirective::Elif(condition))
333    } else if rest == "else" || rest.starts_with("else ") || rest.starts_with("else\t") {
334        Some(PreprocessorDirective::Else)
335    } else if rest == "endif"
336        || rest.starts_with("endif ")
337        || rest.starts_with("endif\t")
338        || rest.starts_with("endif/")
339    {
340        Some(PreprocessorDirective::Endif)
341    } else if rest.starts_with("include") {
342        Some(PreprocessorDirective::Include)
343    } else if rest.starts_with("define") {
344        Some(PreprocessorDirective::Define)
345    } else if rest.starts_with("undef") {
346        Some(PreprocessorDirective::Undef)
347    } else if rest.starts_with("pragma") {
348        Some(PreprocessorDirective::Pragma)
349    } else if rest.starts_with("error") {
350        Some(PreprocessorDirective::Error)
351    } else if rest.starts_with("warning") {
352        Some(PreprocessorDirective::Warning)
353    } else {
354        Some(PreprocessorDirective::Other)
355    }
356}
357
358/// Check if a condition is definitely false
359fn is_false_condition(condition: &str) -> bool {
360    let condition = condition.trim();
361
362    // #if 0 is definitely false
363    if condition == "0" {
364        return true;
365    }
366
367    // #if (0) is also false
368    if condition == "(0)" {
369        return true;
370    }
371
372    // Check for common patterns
373    // !1 is false
374    if condition == "!1" {
375        return true;
376    }
377
378    // defined(NEVER_DEFINED_MACRO) - can't evaluate without context
379    // So we return false (assume it might be true)
380
381    false
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_keep_all_strategy() {
390        let source = r#"
391#if 0
392int x;
393#endif
394int y;
395"#;
396        let (result, count) = evaluate_conditionals(source, ConditionalStrategy::KeepAll);
397        assert_eq!(result, source);
398        assert_eq!(count, 0);
399    }
400
401    #[test]
402    fn test_strip_all_strategy() {
403        let source = r#"#include <stdio.h>
404#define FOO 1
405#if FOO
406int x;
407#endif
408int y;
409"#;
410        let (result, count) = evaluate_conditionals(source, ConditionalStrategy::StripAll);
411
412        // Should keep include
413        assert!(result.contains("#include <stdio.h>"));
414        // Should strip #define
415        assert!(!result.contains("#define"));
416        // Should strip #if and #endif
417        assert!(!result.contains("#if"));
418        assert!(!result.contains("#endif"));
419        // Should keep code
420        assert!(result.contains("int x;"));
421        assert!(result.contains("int y;"));
422        assert!(count > 0);
423    }
424
425    #[test]
426    fn test_evaluate_simple_if_0() {
427        let source = r#"int a;
428#if 0
429int b;
430#endif
431int c;
432"#;
433        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
434
435        assert!(result.contains("int a;"));
436        assert!(!result.contains("int b;"));
437        assert!(result.contains("int c;"));
438    }
439
440    #[test]
441    fn test_evaluate_simple_if_0_else() {
442        let source = r#"int a;
443#if 0
444int b;
445#else
446int c;
447#endif
448int d;
449"#;
450        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
451
452        assert!(result.contains("int a;"));
453        assert!(!result.contains("int b;"));
454        assert!(result.contains("int c;"));
455        assert!(result.contains("int d;"));
456    }
457
458    #[test]
459    fn test_evaluate_nested_conditionals() {
460        let source = r#"int a;
461#if 0
462int b;
463#if 1
464int c;
465#endif
466int d;
467#endif
468int e;
469"#;
470        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
471
472        assert!(result.contains("int a;"));
473        assert!(!result.contains("int b;"));
474        assert!(!result.contains("int c;")); // Nested in #if 0
475        assert!(!result.contains("int d;"));
476        assert!(result.contains("int e;"));
477    }
478
479    #[test]
480    fn test_evaluate_elif() {
481        let source = r#"int a;
482#if 0
483int b;
484#elif 1
485int c;
486#else
487int d;
488#endif
489int e;
490"#;
491        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
492
493        assert!(result.contains("int a;"));
494        assert!(!result.contains("int b;"));
495        // We can't evaluate #elif 1 as definitely true without more context
496        // But it won't be in a #if 0 block
497        assert!(result.contains("int e;"));
498    }
499
500    #[test]
501    fn test_preserve_line_numbers() {
502        let source = "line1\n#if 0\nline3\n#endif\nline5\n";
503        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
504
505        // Count lines
506        let line_count = result.lines().count();
507        let original_line_count = source.lines().count();
508
509        // Line count should be preserved
510        assert_eq!(line_count, original_line_count);
511    }
512
513    #[test]
514    fn test_ifdef_content_kept_directive_stripped() {
515        let source = r#"#ifdef CONFIG_FOO
516int x;
517#endif
518"#;
519        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
520
521        // #ifdef can't be evaluated without knowing what's defined
522        // The code inside should be kept (assume true)
523        assert!(result.contains("int x;"));
524        // But the #ifdef directive itself should be stripped (not valid C)
525        assert!(!result.contains("#ifdef"));
526    }
527
528    #[test]
529    fn test_is_false_condition() {
530        assert!(is_false_condition("0"));
531        assert!(is_false_condition(" 0 "));
532        assert!(is_false_condition("(0)"));
533        assert!(is_false_condition("!1"));
534
535        assert!(!is_false_condition("1"));
536        assert!(!is_false_condition("FOO"));
537        assert!(!is_false_condition("defined(BAR)"));
538    }
539
540    #[test]
541    fn test_complex_kernel_code() {
542        let source = r#"
543#include <linux/module.h>
544
545#if 0
546/* Disabled debug code */
547#define DEBUG 1
548static void debug_print(void) {}
549#endif
550
551MODULE_LICENSE("GPL");
552
553#ifdef CONFIG_DEBUG
554static int debug_level = 1;
555#else
556static int debug_level = 0;
557#endif
558"#;
559
560        let (result, _count) = evaluate_conditionals(source, ConditionalStrategy::EvaluateSimple);
561
562        // Should keep include
563        assert!(result.contains("#include <linux/module.h>"));
564        // Should strip #if 0 block
565        assert!(!result.contains("debug_print"));
566        // Should keep CONFIG_DEBUG block (can't evaluate)
567        assert!(result.contains("debug_level"));
568    }
569}