diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2021-06-21 15:13:25 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-21 15:13:25 -0400 |
commit | 2d2b5625e04a466362c9a4afb05e2f559c4fb4b0 (patch) | |
tree | 09dbb655a649fee6d05e2179be7a9ac7d104523e | |
parent | f9ff981daf6931a01e1516db0b5714e7a94f145b (diff) |
feat(repl): Type stripping in the REPL (#10934)
-rw-r--r-- | cli/tests/integration_tests.rs | 84 | ||||
-rw-r--r-- | cli/tools/repl.rs | 107 |
2 files changed, 157 insertions, 34 deletions
diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index c50d80323..081ea40e5 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -2022,9 +2022,9 @@ mod integration { let mut output = String::new(); master.read_to_string(&mut output).unwrap(); - assert!(output.contains("Unexpected token ')'")); - assert!(output.contains("Unexpected token ']'")); - assert!(output.contains("Unexpected token '}'")); + assert!(output.contains("Unexpected token `)`")); + assert!(output.contains("Unexpected token `]`")); + assert!(output.contains("Unexpected token `}`")); fork.wait().unwrap(); } else { @@ -2232,6 +2232,59 @@ mod integration { } #[test] + fn typescript() { + let (out, err) = util::run_and_collect_output( + true, + "repl", + Some(vec![ + "function add(a: number, b: number) { return a + b }", + "const result: number = add(1, 2) as number;", + "result", + ]), + Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), + false, + ); + assert!(out.ends_with("undefined\nundefined\n3\n")); + assert!(err.is_empty()); + } + + #[test] + fn typescript_declarations() { + let (out, err) = util::run_and_collect_output( + true, + "repl", + Some(vec![ + "namespace Test { export enum Values { A, B, C } }", + "Test.Values.A", + "Test.Values.C", + "interface MyInterface { prop: string; }", + "type MyTypeAlias = string;", + ]), + Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), + false, + ); + assert!(out.ends_with("undefined\n0\n2\nundefined\nundefined\n")); + assert!(err.is_empty()); + } + + #[test] + fn typescript_decorators() { + let (out, err) = util::run_and_collect_output( + true, + "repl", + Some(vec![ + "function dec(target) { target.prototype.test = () => 2; }", + "@dec class Test {}", + "new Test().test()", + ]), + Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), + false, + ); + assert!(out.ends_with("undefined\nundefined\n2\n")); + assert!(err.is_empty()); + } + + #[test] fn eof() { let (out, err) = util::run_and_collect_output( true, @@ -2362,11 +2415,30 @@ mod integration { let (out, err) = util::run_and_collect_output( true, "repl", - Some(vec!["syntax error"]), - None, + Some(vec![ + "syntax error", + "2", // ensure it keeps accepting input after + ]), + Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), + false, + ); + assert!( + out.ends_with("parse error: Expected ';', '}' or <eof> at 1:7\n2\n") + ); + assert!(err.is_empty()); + } + + #[test] + fn syntax_error_jsx() { + // JSX is not supported in the REPL + let (out, err) = util::run_and_collect_output( + true, + "repl", + Some(vec!["const element = <div />;"]), + Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), false, ); - assert!(out.contains("Unexpected identifier")); + assert!(out.contains("Unexpected token `>`")); assert!(err.is_empty()); } diff --git a/cli/tools/repl.rs b/cli/tools/repl.rs index 67e68c0cd..069d3e417 100644 --- a/cli/tools/repl.rs +++ b/cli/tools/repl.rs @@ -1,6 +1,8 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::ast; +use crate::ast::Diagnostic; +use crate::ast::ImportsNotUsedAsValues; use crate::ast::TokenOrComment; use crate::colors; use crate::media_type::MediaType; @@ -187,7 +189,7 @@ impl Validator for EditorHelper { let mut stack: Vec<Token> = Vec::new(); let mut in_template = false; - for item in ast::lex("", ctx.input(), &MediaType::JavaScript) { + for item in ast::lex("", ctx.input(), &MediaType::TypeScript) { if let TokenOrComment::Token(token) = item.inner { match token { Token::BackQuote => in_template = !in_template, @@ -247,7 +249,7 @@ impl Highlighter for EditorHelper { fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { let mut out_line = String::from(line); - for item in ast::lex("", line, &MediaType::JavaScript) { + for item in ast::lex("", line, &MediaType::TypeScript) { // Adding color adds more bytes to the string, // so an offset is needed to stop spans falling out of sync. let offset = out_line.len() - line.len(); @@ -439,7 +441,48 @@ impl ReplSession { self.worker.run_event_loop(false).await } - pub async fn evaluate_line(&mut self, line: &str) -> Result<Value, AnyError> { + pub async fn evaluate_line_and_get_output( + &mut self, + line: &str, + ) -> Result<String, AnyError> { + match self.evaluate_line_with_object_wrapping(line).await { + Ok(evaluate_response) => { + let evaluate_result = evaluate_response.get("result").unwrap(); + let evaluate_exception_details = + evaluate_response.get("exceptionDetails"); + + if evaluate_exception_details.is_some() { + self.set_last_thrown_error(evaluate_result).await?; + } else { + self.set_last_eval_result(evaluate_result).await?; + } + + let value = self.get_eval_value(evaluate_result).await?; + Ok(match evaluate_exception_details { + Some(_) => format!("Uncaught {}", value), + None => value, + }) + } + Err(err) => { + // handle a parsing diagnostic + match err.downcast_ref::<Diagnostic>() { + Some(diagnostic) => Ok(format!( + "{}: {} at {}:{}", + colors::red("parse error"), + diagnostic.message, + diagnostic.location.line, + diagnostic.location.col + )), + None => Err(err), + } + } + } + } + + async fn evaluate_line_with_object_wrapping( + &mut self, + line: &str, + ) -> Result<Value, AnyError> { // It is a bit unexpected that { "foo": "bar" } is interpreted as a block // statement rather than an object literal so we interpret it as an expression statement // to match the behavior found in a typical prompt including browser developer tools. @@ -451,9 +494,7 @@ impl ReplSession { line.to_string() }; - let evaluate_response = self - .evaluate_expression(&format!("'use strict'; void 0;\n{}", &wrapped_line)) - .await?; + let evaluate_response = self.evaluate_ts_expression(&wrapped_line).await?; // If that fails, we retry it without wrapping in parens letting the error bubble up to the // user if it is still an error. @@ -461,9 +502,7 @@ impl ReplSession { if evaluate_response.get("exceptionDetails").is_some() && wrapped_line != line { - self - .evaluate_expression(&format!("'use strict'; void 0;\n{}", &line)) - .await? + self.evaluate_ts_expression(&line).await? } else { evaluate_response }; @@ -471,7 +510,7 @@ impl ReplSession { Ok(evaluate_response) } - pub async fn set_last_thrown_error( + async fn set_last_thrown_error( &mut self, error: &Value, ) -> Result<(), AnyError> { @@ -488,7 +527,7 @@ impl ReplSession { Ok(()) } - pub async fn set_last_eval_result( + async fn set_last_eval_result( &mut self, evaluate_result: &Value, ) -> Result<(), AnyError> { @@ -529,6 +568,34 @@ impl ReplSession { Ok(value.to_string()) } + async fn evaluate_ts_expression( + &mut self, + expression: &str, + ) -> Result<Value, AnyError> { + let parsed_module = + crate::ast::parse("repl.ts", &expression, &crate::MediaType::TypeScript)?; + + let transpiled_src = parsed_module + .transpile(&crate::ast::EmitOptions { + emit_metadata: false, + source_map: false, + inline_source_map: false, + imports_not_used_as_values: ImportsNotUsedAsValues::Preserve, + // JSX is not supported in the REPL + transform_jsx: false, + jsx_factory: "React.createElement".into(), + jsx_fragment_factory: "React.Fragment".into(), + })? + .0; + + self + .evaluate_expression(&format!( + "'use strict'; void 0;\n{}", + transpiled_src + )) + .await + } + async fn evaluate_expression( &mut self, expression: &str, @@ -615,7 +682,7 @@ pub async fn run( .await; match line { Ok(line) => { - let evaluate_response = repl_session.evaluate_line(&line).await?; + let output = repl_session.evaluate_line_and_get_output(&line).await?; // We check for close and break here instead of making it a loop condition to get // consistent behavior in when the user evaluates a call to close(). @@ -623,22 +690,6 @@ pub async fn run( break; } - let evaluate_result = evaluate_response.get("result").unwrap(); - let evaluate_exception_details = - evaluate_response.get("exceptionDetails"); - - if evaluate_exception_details.is_some() { - repl_session.set_last_thrown_error(evaluate_result).await?; - } else { - repl_session.set_last_eval_result(evaluate_result).await?; - } - - let value = repl_session.get_eval_value(evaluate_result).await?; - let output = match evaluate_exception_details { - Some(_) => format!("Uncaught {}", value), - None => value, - }; - println!("{}", output); editor.add_history_entry(line); |