aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/util/error_handler.rs
blob: dcea940e94dae8d98ebaf8be217375780c2d4979 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! ref: composer/src/Composer/Util/ErrorHandler.php

use crate::io::IOInterface;
use crate::io::IOInterfaceImmutable;
use shirabe_php_shim::{
    E_ALL, E_DEPRECATED, E_USER_DEPRECATED, E_USER_WARNING, E_WARNING, ErrorException, PHP_EOL,
    STDERR, debug_backtrace, error_reporting, filter_var_boolean, fwrite, ini_get,
    set_error_handler,
};
use std::cell::Cell;

// PHP keeps `$io` / `$hasShownDeprecationNotice` as process-global statics. Composer runs
// single-threaded, so thread-locals on the main thread reproduce that faithfully while letting
// us hold the same shared (`Rc<RefCell<dyn IOInterface>>`) IO instance the application uses.
thread_local! {
    static IO: std::cell::RefCell<Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>> = const { std::cell::RefCell::new(None) };
    static HAS_SHOWN_DEPRECATION_NOTICE: Cell<i64> = const { Cell::new(0) };
}

pub struct ErrorHandler;

impl ErrorHandler {
    pub fn handle(
        level: i64,
        message: String,
        file: String,
        line: i64,
    ) -> Result<bool, ErrorException> {
        let is_deprecation_notice = level == E_DEPRECATED || level == E_USER_DEPRECATED;

        // error code is not included in error_reporting
        if !is_deprecation_notice && 0 == (error_reporting(None) & level) {
            return Ok(true);
        }

        let mut message = message;

        let xdebug_scream = ini_get("xdebug.scream").unwrap_or_default();
        if filter_var_boolean(&xdebug_scream) {
            message += "\n\nWarning: You have xdebug.scream enabled, the warning above may be\na legitimately suppressed error that you were not supposed to see.";
        }

        if !is_deprecation_notice {
            // ignore some newly introduced warnings in new php versions until dependencies
            // can be fixed as we do not want to abort execution for those
            if (level == E_WARNING || level == E_USER_WARNING)
                && message.contains(
                    "should either be used or intentionally ignored by casting it as (void)",
                )
            {
                Self::output_warning(
                    &format!(
                        "Ignored new PHP warning but it should be reported and fixed: {} in {}:{}",
                        message, file, line
                    ),
                    true,
                );
                return Ok(true);
            }

            return Err(ErrorException::new(message, 0, level, file, line, None));
        }

        let io = IO.with(|cell| cell.borrow().clone());
        if let Some(io) = io {
            let has_shown = HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.get());
            if has_shown > 0 && !io.is_verbose() {
                if has_shown == 1 {
                    io.write_error("<warning>More deprecation notices were hidden, run again with `-v` to show them.</warning>");
                    HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.set(2));
                }
                return Ok(true);
            }
            HAS_SHOWN_DEPRECATION_NOTICE.with(|c| c.set(1));
            Self::output_warning(
                &format!("Deprecation Notice: {} in {}:{}", message, file, line),
                false,
            );
        }

        Ok(true)
    }

    pub fn register(io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>) {
        set_error_handler(|level, message, file, line| {
            Self::handle(level, message.to_string(), file.to_string(), line).unwrap_or(true)
        });
        error_reporting(Some(E_ALL));
        IO.with(|cell| *cell.borrow_mut() = io);
    }

    fn output_warning(message: &str, output_even_without_io: bool) {
        let io = IO.with(|cell| cell.borrow().clone());
        if let Some(io) = io {
            io.write_error(&format!("<warning>{}</warning>", message));
            if io.is_verbose() {
                io.write_error("<warning>Stack trace:</warning>");
                let frames: Vec<String> = debug_backtrace()
                    .into_iter()
                    .skip(2)
                    .filter_map(|frame| {
                        let line = frame.get("line").and_then(|v| v.as_int());
                        let file = frame
                            .get("file")
                            .and_then(|v| v.as_string())
                            .map(|s| s.to_string());
                        if let (Some(line), Some(file)) = (line, file) {
                            Some(format!("<warning> {}:{}</warning>", file, line))
                        } else {
                            None
                        }
                    })
                    .collect();
                for frame_str in frames {
                    io.write_error(&frame_str);
                }
            }
            return;
        }

        if output_even_without_io {
            fwrite(&STDERR, format!("Warning: {}{}", message, PHP_EOL), None);
        }
    }
}