use hermes_ast::context::{GCLock, NodeRc};
use hermes_ast::node::Node;
use hermes_parser::js::JSParserImpl;
use hermes_parser::lexer::{GrammarContext, JSLexer};
use hermes_parser::ParsedJS;
use hermes_support::diag::{DiagKind, OutputOptions, ResolvedDiagnostic};
use hermes_support::manager::SourceErrorManager;
use hermes_support::render::render_diagnostic;
use crate::dump::sem_dump;
use crate::keywords::Keywords;
use crate::libhermes::LIBHERMES;
use crate::resolve::{resolve_ast, resolve_ast_for_parser};
use crate::sem_context::SemContext;
pub struct ResolvedJS {
sem_ctx: SemContext,
parsed: ParsedJS,
}
impl ResolvedJS {
pub fn with_program<R, F>(&mut self, f: F) -> R
where
F: for<'gc> FnOnce(
&'gc GCLock<'static, '_>,
&'gc Node<'gc>,
&SemContext,
) -> R,
{
let sem_ctx = &self.sem_ctx;
self.parsed.with_program(|gc, root| f(gc, root, sem_ctx))
}
pub fn sem_context(&self) -> &SemContext {
&self.sem_ctx
}
pub fn to_sema_dump(&mut self) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
self.with_program(|gc, root, sem_ctx| {
sem_dump(&mut out, gc, sem_ctx, root);
});
out
}
pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
self.parsed.diagnostics()
}
pub fn error_count(&self) -> u32 {
self.parsed.source_manager().error_count()
}
pub fn source_manager(&self) -> &SourceErrorManager {
self.parsed.source_manager()
}
pub fn into_parsed(self) -> ParsedJS {
let ResolvedJS { sem_ctx, parsed } = self;
drop(sem_ctx);
parsed
}
}
impl std::fmt::Debug for ResolvedJS {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedJS")
.field("functions", &self.sem_ctx.functions_len())
.field("diagnostics", &self.diagnostics().len())
.field("errors", &self.error_count())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct ResolveError {
diagnostics: Vec<ResolvedDiagnostic>,
error_count: u32,
}
impl ResolveError {
pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
&self.diagnostics
}
pub fn error_count(&self) -> u32 {
self.error_count
}
pub fn messages(&self) -> Vec<String> {
let opts = OutputOptions {
show_colors: false,
..OutputOptions::default()
};
self.diagnostics
.iter()
.map(|d| render_diagnostic(d, &opts))
.collect()
}
fn from_resolved(resolved: &ResolvedJS) -> ResolveError {
ResolveError {
diagnostics: resolved.diagnostics().to_vec(),
error_count: resolved.error_count(),
}
}
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let plural = if self.error_count == 1 { "" } else { "s" };
match self.diagnostics.iter().find(|d| d.kind == DiagKind::Error) {
Some(d) => write!(
f,
"{} semantic error{plural}; first at {}:{}:{}: {}",
self.error_count, d.file_name, d.line, d.col, d.message
),
None => write!(f, "{} semantic error{plural}", self.error_count),
}
}
}
impl std::error::Error for ResolveError {}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GlobalDefinitions {
pub file_name: String,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileOptions {
pub std_globals: bool,
pub global_definitions: Vec<GlobalDefinitions>,
}
impl Default for CompileOptions {
fn default() -> Self {
CompileOptions {
std_globals: true,
global_definitions: Vec::new(),
}
}
}
pub fn resolve(parsed: ParsedJS) -> Result<ResolvedJS, ResolveError> {
let resolved = resolve_for_parser(parsed);
if resolved.error_count() == 0 {
Ok(resolved)
} else {
Err(ResolveError::from_resolved(&resolved))
}
}
pub fn resolve_for_parser(parsed: ParsedJS) -> ResolvedJS {
let mut parsed = parsed;
let sem_ctx = parsed.transform_program(|gc, root, sm| {
let mut sem_ctx = SemContext::new(Keywords::new(gc));
let resolved = resolve_ast_for_parser(gc, &mut sem_ctx, sm, root);
(resolved, sem_ctx)
});
ResolvedJS { sem_ctx, parsed }
}
pub fn resolve_for_compile(
parsed: ParsedJS,
options: &CompileOptions,
) -> Result<ResolvedJS, ResolveError> {
let mut parsed = parsed;
let sem_ctx = parsed.transform_program(|gc, root, sm| {
let mut ambient_decls: Vec<NodeRc> = Vec::new();
let mut ambient_ok = true;
if options.std_globals {
match parse_ambient(gc, sm, "<libhermes>", LIBHERMES) {
None => panic!("libhermes must parse: it is a constant"),
Some(program) => ambient_decls.push(program),
}
}
for gd in &options.global_definitions {
match parse_ambient(gc, sm, &gd.file_name, &gd.source) {
None => {
ambient_ok = false;
break;
}
Some(program) => ambient_decls.push(program),
}
}
if !ambient_ok {
return (root, None);
}
let mut sem_ctx = SemContext::new(Keywords::new(gc));
match resolve_ast(gc, &mut sem_ctx, sm, root, &ambient_decls) {
Some(resolved) => (resolved, Some(sem_ctx)),
None => (root, None),
}
});
match sem_ctx {
Some(sem_ctx) => Ok(ResolvedJS { sem_ctx, parsed }),
None => {
Err(ResolveError {
diagnostics: parsed.diagnostics().to_vec(),
error_count: parsed.source_manager().error_count(),
})
}
}
}
fn parse_ambient<'gc>(
gc: &'gc GCLock<'static, '_>,
sm: &mut SourceErrorManager,
file_name: &str,
source: &str,
) -> Option<NodeRc> {
let buf_id = sm.add_buffer(file_name, source);
let program: Option<&'gc Node<'gc>> = {
let lexer = JSLexer::new(
buf_id,
sm,
&gc.ctx().atom_table,
GrammarContext::AllowRegExp,
);
JSParserImpl::new(gc, lexer).parse()
};
program.map(|p| NodeRc::from_node(gc, p))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::DeclId;
use crate::sem_context::DeclKind;
use hermes_ast::node::NodeKind;
use hermes_parser::{parse, ParseFlags};
fn dump(source: &str) -> String {
let parsed = parse(source, ParseFlags::default()).unwrap();
let mut resolved = resolve(parsed).unwrap();
String::from_utf8(resolved.to_sema_dump()).unwrap()
}
fn last_expression_decl(resolved: &mut ResolvedJS) -> Option<DeclId> {
resolved.with_program(|_gc, program, sem| {
let body = match program {
Node::Program(p) => p.body,
_ => panic!("root is not a Program"),
};
let stmt = body.iter().last().expect("empty program");
let expr = match stmt {
Node::ExpressionStatement(e) => e.expression,
_ => panic!("last statement is not an ExpressionStatement"),
};
match expr {
Node::Identifier(id) => sem.get_expression_decl(id),
_ => panic!("not an identifier expression"),
}
})
}
#[test]
fn resolves_a_reference_to_its_declaration() {
let parsed = parse("var x = 1; x;", ParseFlags::default()).unwrap();
let mut resolved = resolve(parsed).unwrap();
let decl = last_expression_decl(&mut resolved).expect("unresolved");
assert_eq!(
resolved.sem_context().decl(decl).kind,
DeclKind::GlobalProperty
);
assert!(resolved.diagnostics().is_empty());
}
#[test]
fn lexical_declarations_are_scoped() {
let src = "let x = 1; { let x = 2; } x;";
let parsed = parse(src, ParseFlags::default()).unwrap();
let mut resolved = resolve(parsed).unwrap();
let decl = last_expression_decl(&mut resolved).expect("unresolved");
assert_eq!(resolved.sem_context().decl(decl).kind, DeclKind::Let);
let dump = String::from_utf8(resolved.to_sema_dump()).unwrap();
assert_eq!(dump.matches("'x' Let").count(), 2, "{dump}");
}
#[test]
fn semantic_errors_are_reported_without_printing() {
let parsed = parse("continue;", ParseFlags::default()).unwrap();
let err = resolve(parsed).unwrap_err();
assert_eq!(err.error_count(), 1);
assert_eq!(err.diagnostics().len() as u32, err.error_count());
assert_eq!(err.diagnostics()[0].kind, DiagKind::Error);
let _: &[crate::ResolvedDiagnostic] = err.diagnostics();
let shown = err.to_string();
assert!(!shown.contains('\n'), "{shown}");
assert!(
shown.starts_with("1 semantic error; first at input:1:"),
"{shown}"
);
assert_eq!(err.messages().len(), 1);
assert!(err.messages()[0].contains('\n'), "{:?}", err.messages()[0]);
}
#[test]
fn parser_path_keeps_the_tree_on_error() {
let parsed = parse("continue; var x;", ParseFlags::default()).unwrap();
let mut resolved = resolve_for_parser(parsed);
assert_eq!(resolved.error_count(), 1);
assert_eq!(
resolved.with_program(|_gc, root, _sem| root.kind()),
NodeKind::Program
);
let dump = String::from_utf8(resolved.to_sema_dump()).unwrap();
assert!(dump.contains("'x' GlobalProperty"), "{dump}");
}
#[test]
fn compile_path_transforms_the_ast_and_the_parser_path_does_not() {
let parsed = parse("var y = 1 + 2;", ParseFlags::default()).unwrap();
let mut compiled = resolve_for_compile(
parsed,
&CompileOptions {
std_globals: false,
..Default::default()
},
)
.unwrap();
let folded = String::from_utf8(compiled.to_sema_dump()).unwrap();
assert!(!folded.contains("BinaryExpression"), "{folded}");
let unfolded = dump("var y = 1 + 2;");
assert!(unfolded.contains("BinaryExpression"), "{unfolded}");
}
#[test]
fn compile_path_fails_where_the_parser_path_does_not() {
let src = "with (o) { x; }";
let parsed = parse(src, ParseFlags::default()).unwrap();
assert_eq!(resolve_for_parser(parsed).error_count(), 0);
let parsed = parse(src, ParseFlags::default()).unwrap();
let err = resolve_for_compile(parsed, &CompileOptions::default())
.expect_err("compile path must reject it");
assert!(err.error_count() > 0);
}
#[test]
fn std_globals_are_ambient_declarations() {
let parsed = parse("print(1);", ParseFlags::default()).unwrap();
let mut with = resolve_for_compile(parsed, &CompileOptions::default())
.unwrap()
.to_sema_dump();
let parsed = parse("print(1);", ParseFlags::default()).unwrap();
let without = resolve_for_compile(
parsed,
&CompileOptions {
std_globals: false,
..Default::default()
},
)
.unwrap()
.to_sema_dump();
assert!(with.len() > without.len());
with.truncate(0);
assert!(String::from_utf8(without)
.unwrap()
.contains("'print' UndeclaredGlobalProperty"));
}
#[test]
fn user_global_definitions_are_declared() {
let parsed = parse("myGlobal;", ParseFlags::default()).unwrap();
let opts = CompileOptions {
std_globals: false,
global_definitions: vec![GlobalDefinitions {
file_name: "<host>".to_string(),
source: "var myGlobal;".to_string(),
}],
};
let mut resolved = resolve_for_compile(parsed, &opts).unwrap();
let decl = last_expression_decl(&mut resolved).expect("unresolved");
assert_eq!(
resolved.sem_context().decl(decl).kind,
DeclKind::UndeclaredGlobalProperty
);
}
#[test]
fn a_broken_global_definition_file_is_an_error() {
let parsed = parse("x;", ParseFlags::default()).unwrap();
let opts = CompileOptions {
std_globals: false,
global_definitions: vec![GlobalDefinitions {
file_name: "<host>".to_string(),
source: "var 1x;".to_string(),
}],
};
let err = resolve_for_compile(parsed, &opts).unwrap_err();
assert!(err.error_count() > 0);
assert!(err.to_string().contains("<host>"), "{err}");
}
#[test]
fn into_parsed_gives_back_the_resolved_tree() {
let parsed = parse("var y = 1 + 2;", ParseFlags::default()).unwrap();
let resolved = resolve_for_compile(
parsed,
&CompileOptions {
std_globals: false,
..Default::default()
},
)
.unwrap();
let json = resolved.into_parsed().to_estree_json(false);
assert!(json.contains(r#""value":3"#), "{json}");
}
#[test]
fn debug_summarizes() {
let parsed = parse("function f() {}", ParseFlags::default()).unwrap();
let resolved = resolve(parsed).unwrap();
let shown = format!("{resolved:?}");
assert!(shown.starts_with("ResolvedJS { functions: 2"), "{shown}");
}
#[test]
fn dump_is_the_hermesc_dump_sema_text() {
let text = dump("var x;");
assert!(text.starts_with("SemContext\n"), "{text}");
assert!(text.ends_with('\n'), "{text}");
}
}