mod blank;
mod scan;
mod walk;
use std::fmt;
use std::ops::Range;
use tsv_lang::{Error, ErrorKind, ErrorReport};
pub fn strip(source: &str) -> Result<Stripped, StripError> {
strip_one_shot(source, None)
}
pub fn strip_with_goal(source: &str, goal: crate::Goal) -> Result<Stripped, StripError> {
strip_one_shot(source, Some(goal))
}
fn strip_one_shot(source: &str, goal: Option<crate::Goal>) -> Result<Stripped, StripError> {
let arena = bumpalo::Bump::with_capacity(tsv_lang::estimated_ast_arena_capacity(source.len()));
strip_in(source, goal, &arena)
}
pub fn strip_in(
source: &str,
goal: Option<crate::Goal>,
arena: &bumpalo::Bump,
) -> Result<Stripped, StripError> {
let program = crate::__private::parse_with_goal_or_fallback(source, goal, arena)
.map_err(StripError::Parse)?;
let mut walk = walk::Walk::new(source, arena);
walk.program(&program);
let mut edits = walk.finish()?;
let code = if edits.is_empty() {
source.to_owned()
} else {
blank::apply(source, &mut edits)
};
Ok(Stripped { code })
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Stripped {
pub code: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum StripError {
Parse(Error),
#[non_exhaustive]
Unsupported {
construct: UnsupportedConstruct,
error: Error,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnsupportedConstruct {
Enum,
ValueNamespace,
ParameterProperty,
ImportEquals,
ExportEquals,
TypeAssertion,
AssertionGrouping,
}
impl UnsupportedConstruct {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Enum => "enum",
Self::ValueNamespace => "value_namespace",
Self::ParameterProperty => "parameter_property",
Self::ImportEquals => "import_equals",
Self::ExportEquals => "export_equals",
Self::TypeAssertion => "type_assertion",
Self::AssertionGrouping => "assertion_grouping",
}
}
#[must_use]
pub(crate) const fn description(self) -> &'static str {
match self {
Self::Enum => "an enum",
Self::ValueNamespace => "a namespace with a value member",
Self::ParameterProperty => "a parameter property",
Self::ImportEquals => "an 'import =' declaration",
Self::ExportEquals => "an 'export =' assignment",
Self::TypeAssertion => "an angle-bracket type assertion",
Self::AssertionGrouping => "an assertion whose blanking would regroup its expression",
}
}
}
impl StripError {
fn unsupported(construct: UnsupportedConstruct, source: &str, span: Range<usize>) -> Self {
let message = format!("cannot strip {}", construct.description());
let error = Error::refusal(
&message,
source,
span.start,
span.end,
crate::WIRE_COORDINATES,
);
StripError::Unsupported { construct, error }
}
#[must_use]
pub fn error(&self) -> &Error {
match self {
Self::Parse(error) | Self::Unsupported { error, .. } => error,
}
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.error().kind()
}
#[must_use]
pub fn message(&self) -> String {
self.error().message()
}
#[must_use]
pub fn span(&self) -> Option<Range<usize>> {
self.error().span()
}
#[must_use]
pub fn to_report(&self) -> Option<ErrorReport> {
self.error().to_report()
}
}
impl fmt::Display for StripError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.error(), f)
}
}
const _: () = {
const fn assert_auto_traits<
T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe + 'static,
>() {
}
assert_auto_traits::<StripError>();
assert_auto_traits::<Stripped>();
};
impl std::error::Error for StripError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(self.error())
}
}
#[cfg(test)]
mod tests {
use super::{StripError, UnsupportedConstruct, strip};
fn strip_error(source: &str) -> StripError {
match strip(source) {
Err(error) => error,
Ok(stripped) => panic!("expected a strip error, stripped to {:?}", stripped.code),
}
}
#[test]
fn refusal_message_names_the_construct_at_its_point() {
let source = "let \u{1d49c}: T = 1;\nenum E {}\n";
let error = strip_error(source);
assert!(matches!(
error,
StripError::Unsupported {
construct: UnsupportedConstruct::Enum,
..
}
));
assert_eq!(error.to_string(), "cannot strip an enum (2:1)");
assert_eq!(
format!("{error:#}"),
"cannot strip an enum\n2:1 enum E {}\n ^ here"
);
assert_eq!(error.kind(), tsv_lang::ErrorKind::Refusal);
assert_eq!(error.message(), "cannot strip an enum");
let report = error.to_report().expect("a refusal reports");
assert_eq!(report.message, error.message());
assert_eq!(report.frame, "2:1 enum E {}\n ^ here");
assert_eq!((report.start.line, report.start.column), (2, 0));
}
#[test]
fn refusal_span_is_the_refused_bytes() {
let source = "let \u{1d49c}: T = 1;\nenum E {}\n";
let error = strip_error(source);
let enum_start = source.find("enum").expect("the source holds an enum");
assert_eq!(
error.span(),
Some(enum_start..enum_start + "enum E {}".len())
);
assert_eq!(
&source[error.span().expect("a refusal has a span")],
"enum E {}"
);
assert!(std::error::Error::source(&error).is_none());
}
#[test]
fn parse_error_span_and_source_are_the_inner_errors() {
let source = "let \u{1d49c} = ;\n";
let error = strip_error(source);
let StripError::Parse(parse) = &error else {
panic!("expected a parse error: {error:?}");
};
let span = error.span().expect("a syntax error has a point");
assert_eq!(Some(span.clone()), parse.span());
assert!(span.is_empty());
assert_eq!(&source[span.start..], ";\n");
assert!(std::error::Error::source(parse).is_none());
assert!(std::error::Error::source(&error).is_none());
assert_eq!(error.to_string(), parse.to_string());
}
#[test]
fn parse_error_message_is_the_parse_errors_own() {
let source = "let = ;\n";
let error = strip_error(source);
let StripError::Parse(parse) = &error else {
panic!("expected a parse error: {error:?}");
};
assert_eq!(format!("{error:#}"), format!("{parse:#}"));
assert_eq!(error.to_report(), parse.to_report());
assert_eq!(error.message(), parse.message());
assert_eq!(
format!("{error:#}").lines().next(),
Some(error.message().as_str())
);
assert_eq!(
error.to_report().expect("a syntax error reports").message,
error.message()
);
}
fn stripped(source: &str) -> String {
match strip(source) {
Ok(stripped) => stripped.code,
Err(error) => panic!("expected {source:?} to strip: {error:#}"),
}
}
fn parse_errors(source: &str, goal: crate::Goal) -> (String, String) {
let StripError::Parse(stripped) = strip_error(source) else {
panic!("expected a parse error for {source:?}");
};
let arena = bumpalo::Bump::new();
let Err(parsed) = crate::__private::parse_with_goal(source, goal, &arena) else {
panic!("expected {source:?} to fail at {goal:?}");
};
(stripped.to_string(), parsed.to_string())
}
#[test]
fn an_unset_goal_retries_a_failed_module_parse_as_a_script() {
for (source, code) in [
(
"fs.chmodSync(p as string, 0755);\n",
"fs.chmodSync(p , 0755);\n",
),
("with (o as any) { x; }\n", "with (o ) { x; }\n"),
("var await: number = 1;\n", "var await = 1;\n"),
("let n = 08 as number;\n", "let n = 08 ;\n"),
(
"module.exports = function (mode: number = 0644) { return mode; };\n",
"module.exports = function (mode = 0644) { return mode; };\n",
),
(
"var await: any;\nawait\ntype T = 1\n(z)\n",
"var await ;\nawait\n; \n(z)\n",
),
] {
assert_eq!(stripped(source), code, "{source:?}");
}
}
#[test]
fn a_static_block_decorator_blanks_its_type_arguments() {
assert_eq!(
stripped("class A { @a<T>() static {} }\n"),
"class A { @a () static {} }\n"
);
}
#[test]
fn a_with_body_blanked_whole_becomes_an_empty_statement() {
assert_eq!(
stripped("with (o) type T = 1;\nf();\n"),
"with (o) ; \nf();\n"
);
assert_eq!(
stripped("with (o) function f(): void;\nf();\n"),
"with (o) ; \nf();\n"
);
}
#[test]
fn a_failed_retry_reports_the_attempt_the_source_was_written_for() {
let (stripped, script) = parse_errors("with (o) {}\nlet = ;\n", crate::Goal::Script);
assert_eq!(stripped, script, "the sloppy script's typo");
let (stripped, module) =
parse_errors("let x: number = 0755;\nexport {};\n", crate::Goal::Module);
assert_eq!(stripped, module, "the module's legacy octal");
let (stripped, module) = parse_errors(
"import type { A } from 'a';\nlet x: A = 0755;\n",
crate::Goal::Module,
);
assert_eq!(stripped, module, "a type-only import is a goal gate too");
let (stripped, module) = parse_errors(
"\"use strict\"; let x: number = 010;\n",
crate::Goal::Module,
);
assert_eq!(stripped, module, "a strict script");
}
#[test]
fn a_module_valid_source_is_never_read_as_a_script() {
assert_eq!(stripped("await f() as T;\n"), "await f() ;\n");
assert_eq!(
stripped("let x: number = 1;\nexport { x };\n"),
"let x = 1;\nexport { x };\n"
);
}
#[test]
fn a_chain_blanks_every_links_typescript() {
for (source, code) in [
(
"x = new a.b<T>(c satisfies D)!.e;\n",
"x = new a.b (c ) .e;\n",
),
("x = a!.b![\"c\"]!(d!)!;\n", "x = a .b [\"c\"] (d ) ;\n"),
(
"x = a<T>`q`.b<U>`r${c as D}`;\n",
"x = a `q`.b `r${c }`;\n",
),
(
"x = a.b<T>(c)<U>(d).e as F as G;\n",
"x = a.b (c) (d).e ;\n",
),
(
"x = (a as T).b(c!)?.d<E>?.(f as G);\n",
"x = (a ).b(c )?.d ?.(f );\n",
),
("x = f<T>;\n", "x = f ;\n"),
("y = a.b as T\n[1].c\n", "y = a.b; \n[1].c\n"),
(
"class C { p = a.b as T\n[k]: number = 1 }\n",
"class C { p = a.b; \n[k] = 1 }\n",
),
] {
assert_eq!(stripped(source), code, "{source:?}");
}
}
#[test]
fn a_chain_past_the_recursion_blanks_as_the_recursion_does() {
let links = ".z".repeat(20);
for (chain, code) in [
(
"new a.b<T>(c satisfies D)!.e",
"new a.b (c ) .e",
),
("a!.b![\"c\"]!(d!)!", "a .b [\"c\"] (d ) "),
("a<T>`q`.b<U>`r${c as D}`", "a `q`.b `r${c }`"),
(
"(a.b<T>(c)<U>(d).e as F as G)",
"(a.b (c) (d).e )",
),
(
"(a as T).b(c!)?.d<E>?.(f as G)",
"(a ).b(c )?.d ?.(f )",
),
("a[b as K]!.c", "a[b ] .c"),
("(a satisfies T).b", "(a ).b"),
] {
let source = format!("x = {chain}{links};\n");
assert_eq!(
stripped(&source),
format!("x = {code}{links};\n"),
"{source:?}"
);
}
}
#[test]
fn a_chain_assertion_is_refused_before_its_operand_is_walked() {
let source = "x = <T>a + b as U * c;\n";
let error = strip_error(source);
let StripError::Unsupported { construct, .. } = &error else {
panic!("expected a refusal");
};
assert_eq!(*construct, UnsupportedConstruct::AssertionGrouping);
assert_eq!(
&source[error.span().expect("a refusal has a span")],
"<T>a + b as U"
);
}
#[test]
fn a_long_chain_strips_without_a_frame_per_link() {
let source = format!("x = a{}{};\n", ".b!".repeat(1000), " as T".repeat(100));
let stripped = std::thread::Builder::new()
.stack_size(256 << 10)
.spawn(move || {
strip(&source)
.map(|stripped| stripped.code)
.map_err(|error| format!("{error:#}"))
})
.expect("spawn")
.join()
.expect("the strip ran");
let expected = format!("x = a{}{};\n", ".b ".repeat(1000), " ".repeat(500));
assert_eq!(stripped, Ok(expected));
}
#[test]
fn every_construct_has_a_snake_case_name() {
for (construct, name) in [
(UnsupportedConstruct::Enum, "enum"),
(UnsupportedConstruct::ValueNamespace, "value_namespace"),
(
UnsupportedConstruct::ParameterProperty,
"parameter_property",
),
(UnsupportedConstruct::ImportEquals, "import_equals"),
(UnsupportedConstruct::ExportEquals, "export_equals"),
(UnsupportedConstruct::TypeAssertion, "type_assertion"),
(
UnsupportedConstruct::AssertionGrouping,
"assertion_grouping",
),
] {
assert_eq!(construct.as_str(), name);
assert!(!construct.description().is_empty());
}
}
}