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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use std::env;
use std::fs;
use std::io;
use std::io::prelude::*;
use std::path::PathBuf;
use std::process as proc;

use crate::runtime::{
    self,
    EvalError,
    EvalResult,
};
use crate::{
    error::ParseError,
    runtime::Parser,
    CallContext,
    Exception,
    Heap,
    Interpretable,
    Interpreted,
    JSResult,
    Program,
    JSON,
};

fn nodejs_eval(call: CallContext, heap: &mut Heap) -> JSResult<Interpreted> {
    let code = call.arg_value(0, heap)?.stringify(heap)?;

    let tmpdir = env::temp_dir().join(NodejsParser::TMPDIRNAME);
    let espath = tmpdir.join("esparse.js");
    let parser = NodejsParser { loc: true, espath };

    let program = parser.parse(&code, heap)?;
    program.interpret(heap)
}

/// [`NodejsParser`] runs Esprima in an external nodejs process, consumes JSON AST.
#[derive(Debug)]
pub struct NodejsParser {
    loc: bool,
    espath: PathBuf,
}

impl NodejsParser {
    const TMPDIRNAME: &'static str = "sljs";
    const ESPRIMA: &'static str = include_str!("../../node_modules/esprima/dist/esprima.js");
    const ESPARSE: &'static str = include_str!("../../node_modules/esprima/bin/esparse.js");
    const NODE: &'static str = if cfg!(target_os = "windows") {
        "node.exe"
    } else {
        "node"
    };

    pub fn works() -> EvalResult<bool> {
        let output = proc::Command::new(Self::NODE).arg("-v").output()?;
        Ok(output.status.success())
    }

    pub fn new() -> Box<Self> {
        Box::new(NodejsParser {
            loc: true,
            espath: PathBuf::default(),
        })
    }

    pub fn without_location(mut self: Box<Self>) -> Box<Self> {
        self.loc = false;
        self
    }

    fn run_esprima(&self, input: &str) -> EvalResult<String> {
        let tmpdir = (self.espath.parent())
            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{:?}", self.espath)))?;

        let mut esparse = proc::Command::new(Self::NODE);
        esparse
            .arg(&self.espath)
            .env("NODE_PATH", tmpdir)
            .stdin(proc::Stdio::piped())
            .stdout(proc::Stdio::piped())
            .stderr(proc::Stdio::piped());
        if self.loc {
            esparse.arg("--loc");
        }
        let mut esparse = esparse.spawn()?;

        {
            let stdin = esparse.stdin.as_mut().expect("stdin");
            stdin.write_all(input.as_bytes())?;
        }

        let esparse_output = esparse.wait_with_output()?;

        let status = esparse_output.status;
        let stdout = String::from_utf8(esparse_output.stdout)?;
        let stderr = core::str::from_utf8(&esparse_output.stderr)?;
        if !status.success() {
            let perr = ParseError::from(stderr);
            return Err(EvalError::from(Exception::from(perr)));
        }
        if !stderr.is_empty() {
            eprintln!("{}", stderr);
        }
        Ok(stdout)
    }
}

impl runtime::Parser for NodejsParser {
    fn load(&mut self, _: &mut Heap) -> EvalResult<()> {
        let tmpdir = env::temp_dir().join(Self::TMPDIRNAME);

        let tmpdir = tmpdir.as_path();
        fs::create_dir_all(&tmpdir)?;

        let esprima_path = tmpdir.join("esprima.js");
        if !esprima_path.exists() {
            fs::File::create(&esprima_path)?.write_all(Self::ESPRIMA.as_bytes())?;
        }

        let espath = tmpdir.join("esparse.js");
        if !espath.exists() {
            fs::File::create(&espath)?.write_all(Self::ESPARSE.as_bytes())?;
        }

        self.espath = espath;
        Ok(())
    }

    fn parse(&self, input: &str, _heap: &mut Heap) -> EvalResult<Program> {
        let stdout = self.run_esprima(input)?;
        let json: JSON = serde_json::from_str(&stdout)?;

        let program = Program::parse_from(&json).map_err(Exception::SyntaxTreeError)?;
        Ok(program)
    }

    fn eval_func(&self) -> crate::HostFn {
        nodejs_eval
    }
}