boa_cli 0.22.0

Boa is a Javascript lexer, parser and compiler written in Rust. Currently, it has support for some of the language.
use boa_engine::{ast::scope::Scope, interner::Interner};
use boa_parser::Source;
use colored::{Color, Colorize};
use phf::{Set, phf_set};
use regex::{Captures, Regex, Replacer};
use rustyline::{
    Completer, Helper, Hinter,
    error::ReadlineError,
    highlight::{CmdKind, Highlighter},
    validate::{ValidationContext, ValidationResult, Validator},
};
use std::borrow::Cow::{self, Borrowed};

const STRING_COLOR: Color = Color::Green;
const KEYWORD_COLOR: Color = Color::Yellow;
const PROPERTY_COLOR: Color = Color::Magenta;
const OPERATOR_COLOR: Color = Color::TrueColor {
    r: 214,
    g: 95,
    b: 26,
};
const UNDEFINED_COLOR: Color = Color::TrueColor {
    r: 100,
    g: 100,
    b: 100,
};
const NUMBER_COLOR: Color = Color::TrueColor {
    r: 26,
    g: 214,
    b: 175,
};
const IDENTIFIER_COLOR: Color = Color::TrueColor {
    r: 26,
    g: 160,
    b: 214,
};

const READLINE_COLOR: Color = Color::Cyan;

#[allow(clippy::upper_case_acronyms, clippy::redundant_pub_crate)]
#[derive(Completer, Helper, Hinter)]
pub(crate) struct RLHelper {
    strict: bool,
    highlighter: LineHighlighter,
    colored_prompt: String,
}

impl RLHelper {
    pub(crate) fn new(prompt: &str, strict: bool) -> Self {
        Self {
            highlighter: LineHighlighter::new(),
            strict,
            colored_prompt: prompt.color(READLINE_COLOR).bold().to_string(),
        }
    }
}

impl Validator for RLHelper {
    fn validate(
        &self,
        context: &mut ValidationContext<'_>,
    ) -> Result<ValidationResult, ReadlineError> {
        // Dot commands are handled by the REPL loop, not the JS parser.
        if context.input().trim_start().starts_with('.') {
            return Ok(ValidationResult::Valid(None));
        }

        let mut parser = boa_parser::Parser::new(Source::from_bytes(context.input()));
        if self.strict {
            parser.set_strict();
        }

        match parser.parse_script(&Scope::new_global(), &mut Interner::new()) {
            // Skip scope analysis errors, those can be caused by not having the
            // context's Scope to resolve variables.
            Ok(_) | Err(boa_parser::Error::ScopeAnalysis { .. }) => {
                Ok(ValidationResult::Valid(None))
            }
            Err(boa_parser::Error::AbruptEnd) => Ok(ValidationResult::Incomplete),
            Err(err) => Ok(ValidationResult::Invalid(Some(format!(
                "\n{} {}",
                "Uncaught SyntaxError".red(),
                err.to_string().red()
            )))),
        }
    }

    fn validate_while_typing(&self) -> bool {
        false
    }
}

impl Highlighter for RLHelper {
    fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
        self.highlighter.highlight(line, pos)
    }

    // Must match signature of Highlighter::highlight_prompt, can't elide lifetimes.
    #[allow(single_use_lifetimes)]
    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
        &'s self,
        prompt: &'p str,
        default: bool,
    ) -> Cow<'b, str> {
        if default {
            Borrowed(&self.colored_prompt)
        } else {
            Borrowed(prompt)
        }
    }

    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
        hint.into()
    }

    fn highlight_candidate<'c>(
        &self,
        candidate: &'c str,
        _completion: rustyline::CompletionType,
    ) -> Cow<'c, str> {
        self.highlighter.highlight(candidate, 0)
    }

    fn highlight_char(&self, line: &str, _: usize, _: CmdKind) -> bool {
        !line.is_empty()
    }
}

static KEYWORDS: Set<&'static str> = phf_set! {
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "default",
    "delete",
    "do",
    "else",
    "export",
    "extends",
    "finally",
    "for",
    "function",
    "if",
    "import",
    "instanceof",
    "new",
    "return",
    "super",
    "switch",
    "this",
    "throw",
    "try",
    "typeof",
    "var",
    "void",
    "while",
    "with",
    "yield",
    "await",
    "enum",
    "let",
};

struct LineHighlighter {
    regex: Regex,
}

impl LineHighlighter {
    fn new() -> Self {
        // Precompiles the regex to avoid creating it again after every highlight
        let regex = Regex::new(
            r#"(?x)
            (?P<identifier>\b[$_\p{ID_Start}][$_\p{ID_Continue}\u{200C}\u{200D}]*\b) |
            (?P<string_double_quote>"([^"\\]|\\.)*") |
            (?P<string_single_quote>'([^'\\]|\\.)*') |
            (?P<template_literal>`([^`\\]|\\.)*`) |
            (?P<op>[+\-/*%~^!&|=<>;:]) |
            (?P<number>0[bB][01](_?[01])*n?|0[oO][0-7](_?[0-7])*n?|0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?|(([0-9](_?[0-9])*\.([0-9](_?[0-9])*)?)|(([0-9](_?[0-9])*)?\.[0-9](_?[0-9])*)|([0-9](_?[0-9])*))([eE][+-]?[0-9](_?[0-9])*)?n?)"#,
        ).expect("could not compile regular expression");

        Self { regex }
    }
}

impl Highlighter for LineHighlighter {
    fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> {
        use std::fmt::Write;

        struct Colorizer;

        impl Replacer for Colorizer {
            // Changing to map_or_else moves the handling of "identifier" after all other kinds,
            // which reads worse than this version.
            #[allow(clippy::option_if_let_else)]
            fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
                let colored = if let Some(cap) = caps.name("identifier") {
                    let cap = cap.as_str();

                    let colored = match cap {
                        "true" | "false" | "null" | "Infinity" | "globalThis" => {
                            cap.color(PROPERTY_COLOR)
                        }
                        "undefined" => cap.color(UNDEFINED_COLOR),
                        identifier if KEYWORDS.contains(identifier) => {
                            cap.color(KEYWORD_COLOR).bold()
                        }
                        _ => cap.color(IDENTIFIER_COLOR),
                    };

                    Some(colored)
                } else if let Some(cap) = caps
                    .name("string_double_quote")
                    .or_else(|| caps.name("string_single_quote"))
                    .or_else(|| caps.name("template_literal"))
                {
                    Some(cap.as_str().color(STRING_COLOR))
                } else if let Some(cap) = caps.name("op") {
                    Some(cap.as_str().color(OPERATOR_COLOR))
                } else {
                    caps.name("number")
                        .map(|cap| cap.as_str().color(NUMBER_COLOR))
                };

                if let Some(colored) = colored {
                    write!(dst, "{colored}").expect("could not append data to dst");
                } else {
                    dst.push_str(&caps[0]);
                }
            }
        }
        self.regex.replace_all(line, Colorizer)
    }
}