diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index 37df05cc79dfe..db76870660d4a 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -767,6 +767,22 @@ impl Statement<'_> { | Statement::WhileStatement(_) ) } + + /// Returns the single statement from block statement, or self + pub fn get_one_child(&self) -> Option<&Self> { + if let Statement::BlockStatement(block_stmt) = self { + return (block_stmt.body.len() == 1).then(|| &block_stmt.body[0]); + } + Some(self) + } + + /// Returns the single statement from block statement, or self + pub fn get_one_child_mut(&mut self) -> Option<&mut Self> { + if let Statement::BlockStatement(block_stmt) = self { + return (block_stmt.body.len() == 1).then_some(&mut block_stmt.body[0]); + } + Some(self) + } } impl<'a> FromIn<'a, Expression<'a>> for Statement<'a> { diff --git a/crates/oxc_codegen/examples/codegen.rs b/crates/oxc_codegen/examples/codegen.rs index 5755d891ba311..e941dfcbe34bd 100644 --- a/crates/oxc_codegen/examples/codegen.rs +++ b/crates/oxc_codegen/examples/codegen.rs @@ -3,7 +3,7 @@ use std::path::Path; use oxc_allocator::Allocator; use oxc_codegen::{CodeGenerator, CodegenOptions}; -use oxc_parser::{Parser, ParserReturn}; +use oxc_parser::{ParseOptions, Parser, ParserReturn}; use oxc_span::SourceType; use pico_args::Arguments; @@ -52,7 +52,12 @@ fn parse<'a>( source_text: &'a str, source_type: SourceType, ) -> Option> { - let ret = Parser::new(allocator, source_text, source_type).parse(); + let ret = Parser::new(allocator, source_text, source_type) + .with_options(ParseOptions { + allow_return_outside_function: true, + ..ParseOptions::default() + }) + .parse(); if !ret.errors.is_empty() { for error in ret.errors { println!("{:?}", error.with_source_code(source_text.to_string())); diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index f5cee102b14d3..5a9d864f549f2 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -301,7 +301,12 @@ fn print_if(if_stmt: &IfStatement<'_>, p: &mut Codegen, ctx: Context) { p.print_soft_newline(); } } - stmt => p.print_body(stmt, false, ctx), + stmt => { + p.print_body(stmt, false, ctx); + if if_stmt.alternate.is_some() { + p.print_indent(); + } + } } if let Some(alternate) = if_stmt.alternate.as_ref() { p.print_semicolon_if_needed(); diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index 36d66059d2d89..a05ef1f408bf4 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -92,6 +92,18 @@ fn for_stmt() { ); } +#[test] +fn if_stmt() { + test( + "function f() { if (foo) return foo; else if (bar) return foo; }", + "function f() {\n\tif (foo) return foo;\n\telse if (bar) return foo;\n}\n", + ); + test_minify( + "function f() { if (foo) return foo; else if (bar) return foo; }", + "function f(){if(foo)return foo;else if(bar)return foo}", + ); +} + #[test] fn shorthand() { test("let _ = { x }", "let _ = { x };\n"); diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index a368262908466..ab47066c7de25 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -347,6 +347,27 @@ pub trait ConstantEvaluation<'a> { } None } + BinaryOperator::Instanceof => { + if left.may_have_side_effects() { + return None; + } + + let left_ty = ValueType::from(left); + if left_ty == ValueType::Undetermined { + return None; + } + if left_ty == ValueType::Object { + if let Some(right_ident) = right.get_identifier_reference() { + if right_ident.name == "Object" && self.is_global_reference(right_ident) { + return Some(ConstantValue::Boolean(true)); + } + } + None + } else { + // Non-object types are never instances. + Some(ConstantValue::Boolean(false)) + } + } _ => None, } } diff --git a/crates/oxc_ecmascript/src/to_int_32.rs b/crates/oxc_ecmascript/src/to_int_32.rs index d0a69a48cf16d..468a8c5688c4c 100644 --- a/crates/oxc_ecmascript/src/to_int_32.rs +++ b/crates/oxc_ecmascript/src/to_int_32.rs @@ -56,6 +56,11 @@ impl ToInt32 for f64 { let number = *self; + // NOTE: this also matches with negative zero + if !number.is_finite() || number == 0.0 { + return 0; + } + if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) { let i = number as i32; if f64::from(i) == number { diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index a3b517d547062..4b3571dca696d 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -90,9 +90,11 @@ mod eslint { pub mod no_irregular_whitespace; pub mod no_iterator; pub mod no_label_var; + pub mod no_labels; pub mod no_loss_of_precision; pub mod no_magic_numbers; pub mod no_multi_str; + pub mod no_nested_ternary; pub mod no_new; pub mod no_new_func; pub mod no_new_native_nonconstructor; @@ -536,6 +538,8 @@ oxc_macros::declare_all_lint_rules! { eslint::max_lines, eslint::max_params, eslint::new_cap, + eslint::no_nested_ternary, + eslint::no_labels, eslint::no_restricted_imports, eslint::no_object_constructor, eslint::no_duplicate_imports, diff --git a/crates/oxc_linter/src/rules/eslint/no_labels.rs b/crates/oxc_linter/src/rules/eslint/no_labels.rs new file mode 100644 index 0000000000000..4fb28a7cc410b --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_labels.rs @@ -0,0 +1,272 @@ +use oxc_ast::{ + ast::{LabelIdentifier, Statement}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::NodeId; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_labels_diagnostic(message: &'static str, label_span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(message).with_label(label_span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoLabels { + allow_loop: bool, + allow_switch: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow labeled statements. + /// + /// ### Why is this bad? + /// + /// Labeled statements in JavaScript are used in conjunction with `break` and `continue` to control flow around multiple loops. For example: + /// ```js + /// outer: + /// while (true) { + /// while (true) { + /// break outer; + /// } + /// } + /// ``` + /// The `break outer` statement ensures that this code will not result in an infinite loop because control is returned to the next statement after the `outer` label was applied. If this statement was changed to be just `break`, control would flow back to the outer `while` statement and an infinite loop would result. + /// While convenient in some cases, labels tend to be used only rarely and are frowned upon by some as a remedial form of flow control that is more error prone and harder to understand. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// label: + /// while(true) { + /// // ... + /// } + /// + /// label: + /// while(true) { + /// break label; + /// } + /// + /// label: + /// while(true) { + /// continue label; + /// } + /// + /// label: + /// switch (a) { + /// case 0: + /// break label; + /// } + /// + /// label: + /// { + /// break label; + /// } + /// + /// label: + /// if (a) { + /// break label; + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// var f = { + /// label: "foo" + /// }; + /// + /// while (true) { + /// break; + /// } + /// + /// while (true) { + /// continue; + /// } + /// ``` + /// + /// ### Options + /// + /// The options allow labels with loop or switch statements: + /// * `"allowLoop"` (`boolean`, default is `false`) - If this option was set `true`, this rule ignores labels which are sticking to loop statements. + /// * `"allowSwitch"` (`boolean`, default is `false`) - If this option was set `true`, this rule ignores labels which are sticking to switch statements. + /// + /// Actually labeled statements in JavaScript can be used with other than loop and switch statements. + /// However, this way is ultra rare, not well-known, so this would be confusing developers. + /// + /// #### allowLoop + /// + /// Examples of **correct** code for the `{ "allowLoop": true }` option: + /// ```js + /// label: + /// while (true) { + /// break label; + /// } + /// ``` + /// + /// #### allowSwitch + /// + /// Examples of **correct** code for the `{ "allowSwitch": true }` option: + /// ```js + /// label: + /// switch (a) { + /// case 0: + /// break label; + /// } + /// ``` + NoLabels, + style, +); + +impl Rule for NoLabels { + fn from_configuration(value: serde_json::Value) -> Self { + let allow_loop = value + .get(0) + .and_then(|config| config.get("allowLoop")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + let allow_switch = value + .get(0) + .and_then(|config| config.get("allowSwitch")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + Self { allow_loop, allow_switch } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::LabeledStatement(labeled_stmt) = node.kind() { + if !self.is_allowed(&labeled_stmt.body) { + let label_span = labeled_stmt.label.span; + ctx.diagnostic(no_labels_diagnostic( + "Labeled statement is not allowed", + label_span, + )); + } + } + + if let AstKind::BreakStatement(break_stmt) = node.kind() { + let Some(label) = &break_stmt.label else { return }; + + if !self.is_allowed_in_break_or_continue(label, node.id(), ctx) { + ctx.diagnostic(no_labels_diagnostic( + "Label in break statement is not allowed", + label.span, + )); + } + } + + if let AstKind::ContinueStatement(cont_stmt) = node.kind() { + let Some(label) = &cont_stmt.label else { return }; + + if !self.is_allowed_in_break_or_continue(label, node.id(), ctx) { + ctx.diagnostic(no_labels_diagnostic( + "Label in continue statement is not allowed", + label.span, + )); + } + } + } +} + +impl NoLabels { + fn is_allowed(&self, stmt: &Statement) -> bool { + match stmt { + stmt if stmt.is_iteration_statement() => self.allow_loop, + Statement::SwitchStatement(_) => self.allow_switch, + _ => false, + } + } + + /// Whether the `label` in break/continue statement is allowed. + fn is_allowed_in_break_or_continue<'a>( + &self, + label: &LabelIdentifier<'a>, + stmt_node_id: NodeId, + ctx: &LintContext<'a>, + ) -> bool { + let nodes = ctx.nodes(); + for ancestor_kind in nodes.ancestor_kinds(stmt_node_id) { + if let AstKind::LabeledStatement(labeled_stmt) = ancestor_kind { + if label.name == labeled_stmt.label.name { + return self.is_allowed(&labeled_stmt.body); + } + } + } + false + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("var f = { label: foo ()}", None), + ("while (true) {}", None), + ("while (true) { break; }", None), + ("while (true) { continue; }", None), + ("A: while (a) { break A; }", Some(serde_json::json!([{ "allowLoop": true }]))), + ( + "A: do { if (b) { break A; } } while (a);", + Some(serde_json::json!([{ "allowLoop": true }])), + ), + ( + "A: for (var a in obj) { for (;;) { switch (a) { case 0: continue A; } } }", + Some(serde_json::json!([{ "allowLoop": true }])), + ), + ("A: switch (a) { case 0: break A; }", Some(serde_json::json!([{ "allowSwitch": true }]))), + ]; + + let fail = vec![ + ("label: while(true) {}", None), + ("label: while (true) { break label; }", None), + ("label: while (true) { continue label; }", None), + ("A: var foo = 0;", None), + ("A: break A;", None), + ("A: { if (foo()) { break A; } bar(); };", None), + ("A: if (a) { if (foo()) { break A; } bar(); };", None), + ("A: switch (a) { case 0: break A; default: break; };", None), + ("A: switch (a) { case 0: B: { break A; } default: break; };", None), + ("A: var foo = 0;", Some(serde_json::json!([{ "allowLoop": true }]))), + ("A: break A;", Some(serde_json::json!([{ "allowLoop": true }]))), + ( + "A: { if (foo()) { break A; } bar(); };", + Some(serde_json::json!([{ "allowLoop": true }])), + ), + ( + "A: if (a) { if (foo()) { break A; } bar(); };", + Some(serde_json::json!([{ "allowLoop": true }])), + ), + ( + "A: switch (a) { case 0: break A; default: break; };", + Some(serde_json::json!([{ "allowLoop": true }])), + ), + ("A: var foo = 0;", Some(serde_json::json!([{ "allowSwitch": true }]))), + ("A: break A;", Some(serde_json::json!([{ "allowSwitch": true }]))), + ( + "A: { if (foo()) { break A; } bar(); };", + Some(serde_json::json!([{ "allowSwitch": true }])), + ), + ( + "A: if (a) { if (foo()) { break A; } bar(); };", + Some(serde_json::json!([{ "allowSwitch": true }])), + ), + ("A: while (a) { break A; }", Some(serde_json::json!([{ "allowSwitch": true }]))), + ( + "A: do { if (b) { break A; } } while (a);", + Some(serde_json::json!([{ "allowSwitch": true }])), + ), + ( + "A: for (var a in obj) { for (;;) { switch (a) { case 0: break A; } } }", + Some(serde_json::json!([{ "allowSwitch": true }])), + ), + ]; + + Tester::new(NoLabels::NAME, NoLabels::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/eslint/no_nested_ternary.rs b/crates/oxc_linter/src/rules/eslint/no_nested_ternary.rs new file mode 100644 index 0000000000000..2508793abe4cd --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_nested_ternary.rs @@ -0,0 +1,100 @@ +use oxc_ast::ast::Expression; +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_nested_ternary_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Do not nest ternary expressions.").with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoNestedTernary; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallows nested ternary expressions to improve code readability and maintainability. + /// + /// ### Why is this bad? + /// + /// Nested ternary expressions make code harder to read and understand. They can lead to complex, difficult-to-debug logic. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// const result = condition1 ? (condition2 ? "a" : "b") : "c"; + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// let result; + /// if (condition1) { + /// result = condition2 ? "a" : "b"; + /// } else { + /// result = "c"; + /// } + /// ``` + NoNestedTernary, + style, +); + +impl Rule for NoNestedTernary { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::ConditionalExpression(node) = node.kind() { + if matches!( + node.consequent.get_inner_expression(), + Expression::ConditionalExpression(_) + ) || matches!( + node.alternate.get_inner_expression(), + Expression::ConditionalExpression(_) + ) { + ctx.diagnostic(no_nested_ternary_diagnostic(node.span)); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "foo ? doBar() : doBaz();", + "var foo = bar === baz ? qux : quxx;", + "var result = foo && bar ? baz : qux || quux;", + "var result = foo ? bar : baz === qux;", + "foo ? doSomething(a, b) : doSomethingElse(c, d);", + // Parenthesized Expressions + "var result = (foo ? bar : baz) || qux;", + "var result = (foo ? bar : baz) && qux;", + "var result = foo === bar ? (baz || qux) : quux;", + "var result = (foo ? bar : baz) ? qux : quux;", + // TypeScript + "var result = foo! ? bar : baz;", + "var result = foo ? bar! : baz;", + "var result = (foo as boolean) ? bar : baz;", + "var result = foo ? (bar as string) : baz;", + ]; + + let fail = vec![ + "foo ? bar : baz === qux ? quxx : foobar;", + "foo ? baz === qux ? quxx : foobar : bar;", + // Parenthesized Expressions + "var result = foo ? (bar ? baz : qux) : quux;", + "var result = foo ? (bar === baz ? qux : quux) : foobar;", + "doSomething(foo ? bar : baz ? qux : quux);", + // Comment + "var result = foo /* comment */ ? bar : baz ? qux : quux;", + // TypeScript + "var result = foo! ? bar : baz! ? qux : quux;", + "var result = foo ? bar! : (baz! ? qux : quux);", + "var result = (foo as boolean) ? bar : (baz as string) ? qux : quux;", + "var result = foo ? (bar as string) : (baz as number ? qux : quux);", + ]; + + Tester::new(NoNestedTernary::NAME, NoNestedTernary::CATEGORY, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs b/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs index 607643b279a45..0c29635f85a5b 100644 --- a/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs +++ b/crates/oxc_linter/src/rules/typescript/consistent_type_imports.rs @@ -1517,6 +1517,14 @@ fn test() { // ", // None, // ), + ( + "import { Bar } from './bar'; +export type { Baz } from './baz'; + +export class Foo extends Bar {} +", + None, + ), ]; let fail = vec![ diff --git a/crates/oxc_linter/src/snapshots/eslint_no_labels.snap b/crates/oxc_linter/src/snapshots/eslint_no_labels.snap new file mode 100644 index 0000000000000..b51a31830ffa7 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/eslint_no_labels.snap @@ -0,0 +1,237 @@ +--- +source: crates/oxc_linter/src/tester.rs +snapshot_kind: text +--- + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ label: while(true) {} + · ───── + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ label: while (true) { break label; } + · ───── + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:29] + 1 │ label: while (true) { break label; } + · ───── + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ label: while (true) { continue label; } + · ───── + ╰──── + + ⚠ eslint(no-labels): Label in continue statement is not allowed + ╭─[no_labels.tsx:1:32] + 1 │ label: while (true) { continue label; } + · ───── + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: var foo = 0; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: break A; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:10] + 1 │ A: break A; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:25] + 1 │ A: { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: if (a) { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:32] + 1 │ A: if (a) { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: switch (a) { case 0: break A; default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:31] + 1 │ A: switch (a) { case 0: break A; default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: switch (a) { case 0: B: { break A; } default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:25] + 1 │ A: switch (a) { case 0: B: { break A; } default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:36] + 1 │ A: switch (a) { case 0: B: { break A; } default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: var foo = 0; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: break A; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:10] + 1 │ A: break A; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:25] + 1 │ A: { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: if (a) { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:32] + 1 │ A: if (a) { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: switch (a) { case 0: break A; default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:31] + 1 │ A: switch (a) { case 0: break A; default: break; }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: var foo = 0; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: break A; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:10] + 1 │ A: break A; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:25] + 1 │ A: { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: if (a) { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:32] + 1 │ A: if (a) { if (foo()) { break A; } bar(); }; + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: while (a) { break A; } + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:22] + 1 │ A: while (a) { break A; } + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: do { if (b) { break A; } } while (a); + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:24] + 1 │ A: do { if (b) { break A; } } while (a); + · ─ + ╰──── + + ⚠ eslint(no-labels): Labeled statement is not allowed + ╭─[no_labels.tsx:1:1] + 1 │ A: for (var a in obj) { for (;;) { switch (a) { case 0: break A; } } } + · ─ + ╰──── + + ⚠ eslint(no-labels): Label in break statement is not allowed + ╭─[no_labels.tsx:1:63] + 1 │ A: for (var a in obj) { for (;;) { switch (a) { case 0: break A; } } } + · ─ + ╰──── diff --git a/crates/oxc_linter/src/snapshots/eslint_no_nested_ternary.snap b/crates/oxc_linter/src/snapshots/eslint_no_nested_ternary.snap new file mode 100644 index 0000000000000..9bdafa63bd9d2 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/eslint_no_nested_ternary.snap @@ -0,0 +1,63 @@ +--- +source: crates/oxc_linter/src/tester.rs +snapshot_kind: text +--- + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:1] + 1 │ foo ? bar : baz === qux ? quxx : foobar; + · ─────────────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:1] + 1 │ foo ? baz === qux ? quxx : foobar : bar; + · ─────────────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = foo ? (bar ? baz : qux) : quux; + · ────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = foo ? (bar === baz ? qux : quux) : foobar; + · ───────────────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:13] + 1 │ doSomething(foo ? bar : baz ? qux : quux); + · ──────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = foo /* comment */ ? bar : baz ? qux : quux; + · ────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = foo! ? bar : baz! ? qux : quux; + · ────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = foo ? bar! : (baz! ? qux : quux); + · ──────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = (foo as boolean) ? bar : (baz as string) ? qux : quux; + · ───────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-nested-ternary): Do not nest ternary expressions. + ╭─[no_nested_ternary.tsx:1:14] + 1 │ var result = foo ? (bar as string) : (baz as number ? qux : quux); + · ──────────────────────────────────────────────────── + ╰──── diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index d92458fd6724d..459265a14fe03 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -190,6 +190,10 @@ impl<'a> Traverse<'a> for LatePeepholeOptimizations { fn exit_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { self.x4_peephole_substitute_alternate_syntax.exit_call_expression(expr, ctx); } + + fn exit_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { + self.x4_peephole_substitute_alternate_syntax.exit_property_key(key, ctx); + } } // See `createPeepholeOptimizationsPass` diff --git a/crates/oxc_minifier/src/ast_passes/normalize.rs b/crates/oxc_minifier/src/ast_passes/normalize.rs index cb25270742053..964e2c548d63e 100644 --- a/crates/oxc_minifier/src/ast_passes/normalize.rs +++ b/crates/oxc_minifier/src/ast_passes/normalize.rs @@ -2,7 +2,7 @@ use oxc_ast::ast::*; use oxc_syntax::scope::ScopeFlags; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; -use crate::CompressorPass; +use crate::{node_util::Ctx, CompressorPass}; /// Normalize AST /// @@ -25,6 +25,12 @@ impl<'a> Traverse<'a> for Normalize { Self::convert_while_to_for(stmt, ctx); } } + + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if let Expression::Identifier(_) = expr { + Self::convert_infinity_into_number(expr, ctx); + } + } } impl<'a> Normalize { @@ -45,6 +51,21 @@ impl<'a> Normalize { ); *stmt = Statement::ForStatement(for_stmt); } + + fn convert_infinity_into_number(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let ctx = Ctx(ctx); + match expr { + Expression::Identifier(ident) if ctx.is_identifier_infinity(ident) => { + *expr = ctx.ast.expression_numeric_literal( + ident.span, + f64::INFINITY, + None, + NumberBase::Decimal, + ); + } + _ => {} + } + } } #[cfg(test)] diff --git a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs index 82f7e50cc766e..d0e55587a4aa3 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -241,14 +241,15 @@ impl<'a, 'b> PeepholeFoldConstants { BinaryOperator::ShiftLeft | BinaryOperator::ShiftRight | BinaryOperator::ShiftRightZeroFill - | BinaryOperator::Addition | BinaryOperator::Subtraction | BinaryOperator::Division | BinaryOperator::Remainder | BinaryOperator::Multiplication - | BinaryOperator::Exponential => { + | BinaryOperator::Exponential + | BinaryOperator::Instanceof => { ctx.eval_binary_expression(e).map(|v| ctx.value_to_expr(e.span, v)) } + BinaryOperator::Addition => Self::try_fold_add(e, ctx), BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => { if let Some(v) = ctx.eval_binary_expression(e) { return Some(ctx.value_to_expr(e.span, v)); @@ -260,6 +261,27 @@ impl<'a, 'b> PeepholeFoldConstants { } } + // Simplified version of `tryFoldAdd` from closure compiler. + fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: Ctx<'a, 'b>) -> Option> { + if let Some(v) = ctx.eval_binary_expression(e) { + return Some(ctx.value_to_expr(e.span, v)); + } + debug_assert_eq!(e.operator, BinaryOperator::Addition); + // a + 'b' + 'c' -> a + 'bc' + if let Expression::BinaryExpression(left_binary_expr) = &mut e.left { + if let Expression::StringLiteral(left_str) = &left_binary_expr.right { + if let Expression::StringLiteral(right_str) = &e.right { + let span = Span::new(left_str.span.start, right_str.span.end); + let value = left_str.value.to_string() + right_str.value.as_str(); + let right = ctx.ast.expression_string_literal(span, value, None); + let left = ctx.ast.move_expression(&mut left_binary_expr.left); + return Some(ctx.ast.expression_binary(e.span, left, e.operator, right)); + } + } + } + None + } + fn try_fold_left_child_op( e: &mut BinaryExpression<'a>, ctx: Ctx<'a, '_>, @@ -1318,7 +1340,7 @@ mod test { } #[test] - fn test_fold_bit_shift() { + fn test_fold_bit_shifts() { test("x = 1 << 0", "x=1"); test("x = -1 << 0", "x=-1"); test("x = 1 << 1", "x=2"); @@ -1366,6 +1388,49 @@ mod test { test("8589934593 >>> 0", "1"); } + #[test] + fn test_string_add() { + test("x = 'a' + 'bc'", "x = 'abc'"); + test("x = 'a' + 5", "x = 'a5'"); + test("x = 5 + 'a'", "x = '5a'"); + // test("x = 'a' + 5n", "x = 'a5n'"); + // test("x = 5n + 'a'", "x = '5na'"); + test("x = 'a' + ''", "x = 'a'"); + test("x = 'a' + foo()", "x = 'a'+foo()"); + test("x = foo() + 'a' + 'b'", "x = foo()+'ab'"); + test("x = (foo() + 'a') + 'b'", "x = foo()+'ab'"); // believe it! + test("x = foo() + 'a' + 'b' + 'cd' + bar()", "x = foo()+'abcd'+bar()"); + test("x = foo() + 2 + 'b'", "x = foo()+2+\"b\""); // don't fold! + + // test("x = foo() + 'a' + 2", "x = foo()+\"a2\""); + test("x = '' + null", "x = 'null'"); + test("x = true + '' + false", "x = 'truefalse'"); + // test("x = '' + []", "x = ''"); + // test("x = foo() + 'a' + 1 + 1", "x = foo() + 'a11'"); + test("x = 1 + 1 + 'a'", "x = '2a'"); + test("x = 1 + 1 + 'a'", "x = '2a'"); + test("x = 'a' + (1 + 1)", "x = 'a2'"); + // test("x = '_' + p1 + '_' + ('' + p2)", "x = '_' + p1 + '_' + p2"); + // test("x = 'a' + ('_' + 1 + 1)", "x = 'a_11'"); + // test("x = 'a' + ('_' + 1) + 1", "x = 'a_11'"); + // test("x = 1 + (p1 + '_') + ('' + p2)", "x = 1 + (p1 + '_') + p2"); + // test("x = 1 + p1 + '_' + ('' + p2)", "x = 1 + p1 + '_' + p2"); + // test("x = 1 + 'a' + p1", "x = '1a' + p1"); + // test("x = (p1 + (p2 + 'a')) + 'b'", "x = (p1 + (p2 + 'ab'))"); + // test("'a' + ('b' + p1) + 1", "'ab' + p1 + 1"); + // test("x = 'a' + ('b' + p1 + 'c')", "x = 'ab' + (p1 + 'c')"); + test_same("x = 'a' + (4 + p1 + 'a')"); + test_same("x = p1 / 3 + 4"); + test_same("foo() + 3 + 'a' + foo()"); + test_same("x = 'a' + ('b' + p1 + p2)"); + test_same("x = 1 + ('a' + p1)"); + test_same("x = p1 + '' + p2"); + test_same("x = 'a' + (1 + p1)"); + test_same("x = (p2 + 'a') + (1 + p1)"); + test_same("x = (p2 + 'a') + (1 + p1 + p2)"); + test_same("x = (p2 + 'a') + (1 + (p1 + p2))"); + } + #[test] fn test_fold_arithmetic() { test("x = 10 + 20", "x = 30"); @@ -1439,11 +1504,50 @@ mod test { test("(+x & 1) & 2", "+x & 0"); } + #[test] + fn test_fold_instance_of() { + // Non object types are never instances of anything. + test("64 instanceof Object", "false"); + test("64 instanceof Number", "false"); + test("'' instanceof Object", "false"); + test("'' instanceof String", "false"); + test("true instanceof Object", "false"); + test("true instanceof Boolean", "false"); + test("!0 instanceof Object", "false"); + test("!0 instanceof Boolean", "false"); + test("false instanceof Object", "false"); + test("null instanceof Object", "false"); + test("undefined instanceof Object", "false"); + test("NaN instanceof Object", "false"); + test("Infinity instanceof Object", "false"); + + // Array and object literals are known to be objects. + test("[] instanceof Object", "true"); + test("({}) instanceof Object", "true"); + + // These cases is foldable, but no handled currently. + test_same("new Foo() instanceof Object"); + // These would require type information to fold. + test_same("[] instanceof Foo"); + test_same("({}) instanceof Foo"); + + test("(function() {}) instanceof Object", "true"); + + // An unknown value should never be folded. + test_same("x instanceof Foo"); + } + + #[test] + fn test_fold_instance_of_additional() { + test("(typeof {}) instanceof Object", "false"); + test("(+{}) instanceof Number", "false"); + } + #[test] fn test_fold_left_child_op() { - test_same("x & infinity & 2"); // FIXME: want x & 0 - test_same("x - infinity - 2"); // FIXME: want "x-infinity" - test_same("x - 1 + infinity"); + test("x & Infinity & 2", "x & 0"); + test_same("x - Infinity - 2"); // FIXME: want "x-Infinity" + test_same("x - 1 + Infinity"); test_same("x - 2 + 1"); test_same("x - 2 + 3"); test_same("1 + x - 2 + 1"); diff --git a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs index a4d504c418b7b..f3b4e8593bd3a 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -1,5 +1,6 @@ use oxc_allocator::Vec; use oxc_ast::ast::*; +use oxc_span::GetSpan; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; use crate::CompressorPass; @@ -89,7 +90,9 @@ impl<'a> PeepholeMinimizeConditions { ctx: &mut TraverseCtx<'a>, ) -> Option> { let Statement::IfStatement(if_stmt) = stmt else { unreachable!() }; - match &if_stmt.alternate { + let then_branch = &if_stmt.consequent; + let else_branch = &if_stmt.alternate; + match else_branch { None => { if Self::is_foldable_express_block(&if_stmt.consequent) { let right = Self::get_block_expression(&mut if_stmt.consequent, ctx); @@ -116,11 +119,31 @@ impl<'a> PeepholeMinimizeConditions { ); return Some(ctx.ast.statement_expression(if_stmt.span, logical_expr)); } + } else { + // `if (x) if (y) z` -> `if (x && y) z` + if let Some(Statement::IfStatement(then_if_stmt)) = then_branch.get_one_child() + { + if then_if_stmt.alternate.is_none() { + let and_left = ctx.ast.move_expression(&mut if_stmt.test); + let Statement::IfStatement(mut then_if_stmt) = + ctx.ast.move_statement(&mut if_stmt.consequent) + else { + unreachable!() + }; + let and_right = ctx.ast.move_expression(&mut then_if_stmt.test); + then_if_stmt.test = ctx.ast.expression_logical( + and_left.span(), + and_left, + LogicalOperator::And, + and_right, + ); + return Some(Statement::IfStatement(then_if_stmt)); + } + } } } Some(else_branch) => { - let then_branch_is_expression_block = - Self::is_foldable_express_block(&if_stmt.consequent); + let then_branch_is_expression_block = Self::is_foldable_express_block(then_branch); let else_branch_is_expression_block = Self::is_foldable_express_block(else_branch); // `if(foo) bar else baz` -> `foo ? bar : baz` if then_branch_is_expression_block && else_branch_is_expression_block { @@ -184,37 +207,18 @@ impl<'a> PeepholeMinimizeConditions { } fn is_foldable_express_block(stmt: &Statement<'a>) -> bool { - match stmt { - Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => { - matches!(&block_stmt.body[0], Statement::ExpressionStatement(_)) - } - Statement::ExpressionStatement(_) => true, - _ => false, - } + matches!(stmt.get_one_child(), Some(Statement::ExpressionStatement(_))) } fn get_block_expression(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { - match stmt { - Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => { - if let Statement::ExpressionStatement(s) = &mut block_stmt.body[0] { - ctx.ast.move_expression(&mut s.expression) - } else { - unreachable!() - } - } - Statement::ExpressionStatement(s) => ctx.ast.move_expression(&mut s.expression), - _ => unreachable!(), - } + let Some(Statement::ExpressionStatement(s)) = stmt.get_one_child_mut() else { + unreachable!() + }; + ctx.ast.move_expression(&mut s.expression) } fn is_return_block(stmt: &Statement<'a>) -> bool { - match stmt { - Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => { - matches!(block_stmt.body[0], Statement::ReturnStatement(_)) - } - Statement::ReturnStatement(_) => true, - _ => false, - } + matches!(stmt.get_one_child(), Some(Statement::ReturnStatement(_))) } fn is_return_expression(stmt: &Statement<'a>) -> bool { @@ -235,17 +239,8 @@ impl<'a> PeepholeMinimizeConditions { stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>, ) -> Expression<'a> { - match stmt { - Statement::BlockStatement(block_stmt) if block_stmt.body.len() == 1 => { - if let Statement::ReturnStatement(_) = &mut block_stmt.body[0] { - Self::take_return_argument(stmt, ctx) - } else { - unreachable!() - } - } - Statement::ReturnStatement(_) => Self::take_return_argument(stmt, ctx), - _ => unreachable!(), - } + let Some(stmt) = stmt.get_one_child_mut() else { unreachable!() }; + Self::take_return_argument(stmt, ctx) } fn take_return_argument(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { @@ -323,13 +318,13 @@ mod test { // fold("if(x){do{foo()}while(y)}else bar()", "if(x){do foo();while(y)}else bar()"); // Play with nested IFs - // fold("function f(){if(x){if(y)foo()}}", "function f(){x && (y && foo())}"); - // fold("function f(){if(x){if(y)foo();else bar()}}", "function f(){x&&(y?foo():bar())}"); - // fold("function f(){if(x){if(y)foo()}else bar()}", "function f(){x?y&&foo():bar()}"); - // fold( - // "function f(){if(x){if(y)foo();else bar()}else{baz()}}", - // "function f(){x?y?foo():bar():baz()}", - // ); + fold("function f(){if(x){if(y)foo()}}", "function f(){x && (y && foo())}"); + fold("function f(){if(x){if(y)foo();else bar()}}", "function f(){x&&(y?foo():bar())}"); + fold("function f(){if(x){if(y)foo()}else bar()}", "function f(){x?y&&foo():bar()}"); + fold( + "function f(){if(x){if(y)foo();else bar()}else{baz()}}", + "function f(){x?y?foo():bar():baz()}", + ); // fold("if(e1){while(e2){if(e3){foo()}}}else{bar()}", "if(e1)while(e2)e3&&foo();else bar()"); @@ -1281,8 +1276,11 @@ mod test { #[test] fn test_coercion_substitution_while() { // enableTypeCheck(); - test_same("var x = {}; while (x != null) throw 'a';"); - test_same("var x = 1; while (x != 0) throw 'a';"); + test( + "var x = {}; while (x != null) throw 'a';", + "var x = {}; for (;x != null;) throw 'a';", + ); + test("var x = 1; while (x != 0) throw 'a';", "var x = 1; for (;x != 0;) throw 'a';"); } #[test] diff --git a/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs index b80a526a509de..18d23ed4b9353 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_remove_dead_code.rs @@ -25,9 +25,9 @@ impl<'a> CompressorPass<'a> for PeepholeRemoveDeadCode { impl<'a> Traverse<'a> for PeepholeRemoveDeadCode { fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - self.compress_block(stmt, Ctx(ctx)); let ctx = Ctx(ctx); if let Some(new_stmt) = match stmt { + Statement::BlockStatement(block_stmt) => Self::try_optimize_block(block_stmt, ctx), Statement::IfStatement(if_stmt) => self.try_fold_if(if_stmt, ctx), Statement::ForStatement(for_stmt) => self.try_fold_for(for_stmt, ctx), Statement::ExpressionStatement(expr_stmt) => { @@ -126,28 +126,27 @@ impl<'a, 'b> PeepholeRemoveDeadCode { /// Remove block from single line blocks /// `{ block } -> block` - fn compress_block(&mut self, stmt: &mut Statement<'a>, ctx: Ctx<'a, 'b>) { - if let Statement::BlockStatement(block) = stmt { - // Avoid compressing `if (x) { var x = 1 }` to `if (x) var x = 1` due to different - // semantics according to AnnexB, which lead to different semantics. - if block.body.len() == 1 && !block.body[0].is_declaration() { - *stmt = block.body.remove(0); - self.compress_block(stmt, ctx); - self.changed = true; - return; - } - if block.body.len() == 0 - && (ctx.parent().is_while_statement() - || ctx.parent().is_for_statement() - || ctx.parent().is_for_in_statement() - || ctx.parent().is_for_of_statement() - || ctx.parent().is_block_statement() - || ctx.parent().is_program()) - { - // Remove the block if it is empty and the parent is a block statement. - *stmt = ctx.ast.statement_empty(SPAN); - } + fn try_optimize_block( + stmt: &mut BlockStatement<'a>, + ctx: Ctx<'a, 'b>, + ) -> Option> { + // Avoid compressing `if (x) { var x = 1 }` to `if (x) var x = 1` due to different + // semantics according to AnnexB, which lead to different semantics. + if stmt.body.len() == 1 && !stmt.body[0].is_declaration() { + return Some(stmt.body.remove(0)); + } + if stmt.body.len() == 0 + && (ctx.parent().is_while_statement() + || ctx.parent().is_for_statement() + || ctx.parent().is_for_in_statement() + || ctx.parent().is_for_of_statement() + || ctx.parent().is_block_statement() + || ctx.parent().is_program()) + { + // Remove the block if it is empty and the parent is a block statement. + return Some(ctx.ast.statement_empty(stmt.span)); } + None } fn try_fold_if( @@ -457,7 +456,7 @@ mod test { // Cases to test for empty block. // fold("while(x()){x}", "while(x());"); - fold("while(x()){x()}", "while(x())x()"); + fold("while(x()){x()}", "for(;x();)x()"); // fold("for(x=0;x<100;x++){x}", "for(x=0;x<100;x++);"); // fold("for(x in y){x}", "for(x in y);"); // fold("for (x of y) {x}", "for(x of y);"); diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index 692b2b06517ba..83ddc1af628a6 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -77,6 +77,10 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { self.in_define_export = false; } + fn exit_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { + self.try_compress_property_key(key, ctx); + } + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { let ctx = Ctx(ctx); @@ -687,6 +691,39 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax { fn empty_array_literal(ctx: Ctx<'a, 'b>) -> Expression<'a> { Self::array_literal(ctx.ast.vec(), ctx) } + + // https://github.com/swc-project/swc/blob/4e2dae558f60a9f5c6d2eac860743e6c0b2ec562/crates/swc_ecma_minifier/src/compress/pure/properties.rs + #[allow(clippy::cast_lossless)] + fn try_compress_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { + use oxc_syntax::identifier::is_identifier_name; + let PropertyKey::StringLiteral(s) = key else { return }; + if match ctx.parent() { + Ancestor::ObjectPropertyKey(key) => *key.computed(), + Ancestor::BindingPropertyKey(key) => *key.computed(), + Ancestor::MethodDefinitionKey(key) => *key.computed(), + Ancestor::PropertyDefinitionKey(key) => *key.computed(), + Ancestor::AccessorPropertyKey(key) => *key.computed(), + _ => true, + } { + return; + } + if is_identifier_name(&s.value) { + self.changed = true; + *key = PropertyKey::StaticIdentifier( + ctx.ast.alloc_identifier_name(s.span, s.value.clone()), + ); + } else if (!s.value.starts_with('0') && !s.value.starts_with('+')) || s.value.len() <= 1 { + if let Ok(value) = s.value.parse::() { + self.changed = true; + *key = PropertyKey::NumericLiteral(ctx.ast.alloc_numeric_literal( + s.span, + value as f64, + None, + NumberBase::Decimal, + )); + } + } + } } /// Port from @@ -1137,4 +1174,10 @@ mod test { test("typeof foo !== `number`", "typeof foo != 'number'"); test("`number` !== typeof foo", "'number' != typeof foo"); } + + #[test] + fn test_object_key() { + test("({ '0': _, 'a': _ })", "({ 0: _, a: _ })"); + test_same("({ '1.1': _, '😊': _, 'a.a': _ })"); + } } diff --git a/crates/oxc_minifier/src/ast_passes/statement_fusion.rs b/crates/oxc_minifier/src/ast_passes/statement_fusion.rs index 017af1aa98481..4d563d94789dc 100644 --- a/crates/oxc_minifier/src/ast_passes/statement_fusion.rs +++ b/crates/oxc_minifier/src/ast_passes/statement_fusion.rs @@ -285,10 +285,10 @@ mod test { #[test] fn fuse_into_label() { - // fuse("a;b;c;label:for(x in y){}", "label:for(x in a,b,c,y){}"); - // fuse("a;b;c;label:for(;g;){}", "label:for(a,b,c;g;){}"); - // fuse("a;b;c;l1:l2:l3:for(;g;){}", "l1:l2:l3:for(a,b,c;g;){}"); - fuse_same("a;b;c;label:while(true){}"); + fuse("a;b;c;label:for(x in y){}", "label:for(x in a,b,c,y){}"); + fuse("a;b;c;label:for(;g;){}", "label:for(a,b,c;g;){}"); + fuse("a;b;c;l1:l2:l3:for(;g;){}", "l1:l2:l3:for(a,b,c;g;){}"); + fuse("a;b;c;label:while(true){}", "label:for(a,b,c;true;){}"); } #[test] @@ -304,7 +304,7 @@ mod test { #[test] fn no_fuse_into_while() { - fuse_same("a;b;c;while(x){}"); + fuse("a;b;c;while(x){}", "for(a,b,c;x;){}"); } #[test] diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs index a3c62f6d08480..cf662b884bc52 100644 --- a/crates/oxc_minifier/src/node_util/mod.rs +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -64,4 +64,11 @@ impl<'a> Ctx<'a, '_> { } false } + + pub fn is_identifier_infinity(self, ident: &IdentifierReference) -> bool { + if ident.name == "Infinity" && ident.is_global_reference(self.symbols()) { + return true; + } + false + } } diff --git a/crates/oxc_minifier/src/tester.rs b/crates/oxc_minifier/src/tester.rs index 0ae6dcaa875d0..25e438531028a 100644 --- a/crates/oxc_minifier/src/tester.rs +++ b/crates/oxc_minifier/src/tester.rs @@ -6,7 +6,7 @@ use oxc_span::SourceType; use oxc_traverse::ReusableTraverseCtx; use crate::{ - ast_passes::{CompressorPass, RemoveSyntax}, + ast_passes::{CompressorPass, Normalize, RemoveSyntax}, CompressOptions, }; @@ -45,6 +45,7 @@ fn run<'a, P: CompressorPass<'a>>( SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree(); let mut ctx = ReusableTraverseCtx::new(scopes, symbols, allocator); RemoveSyntax::new(CompressOptions::all_false()).build(&mut program, &mut ctx); + Normalize::new().build(&mut program, &mut ctx); pass.build(&mut program, &mut ctx); } diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 9503630f0f2ff..7a98ecb01f443 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -1861,25 +1861,27 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> { self.visit_declaration(declaration); } - for specifier in &it.specifiers { - // `export type { a }` or `export { type a }` -> `a` is a type reference - if it.export_kind.is_type() || specifier.export_kind.is_type() { - self.current_reference_flags = ReferenceFlags::Type; - } else { - // If the export specifier is not a explicit type export, we consider it as a potential - // type and value reference. If it references to a value in the end, we would delete the - // `ReferenceFlags::Type` flag in `fn resolve_references_for_current_scope`. - self.current_reference_flags = ReferenceFlags::Read | ReferenceFlags::Type; - } - self.visit_export_specifier(specifier); - } - if let Some(source) = &it.source { self.visit_string_literal(source); + self.visit_export_specifiers(&it.specifiers); + } else { + for specifier in &it.specifiers { + // `export type { a }` or `export { type a }` -> `a` is a type reference + if it.export_kind.is_type() || specifier.export_kind.is_type() { + self.current_reference_flags = ReferenceFlags::Type; + } else { + // If the export specifier is not a explicit type export, we consider it as a potential + // type and value reference. If it references to a value in the end, we would delete the + // `ReferenceFlags::Type` flag in `fn resolve_references_for_current_scope`. + self.current_reference_flags = ReferenceFlags::Read | ReferenceFlags::Type; + } + self.visit_export_specifier(specifier); + } } if let Some(with_clause) = &it.with_clause { self.visit_with_clause(with_clause); } + self.leave_node(kind); } diff --git a/crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.snap b/crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.snap new file mode 100644 index 0000000000000..d45c6096c3b79 --- /dev/null +++ b/crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.snap @@ -0,0 +1,43 @@ +--- +source: crates/oxc_semantic/tests/main.rs +input_file: crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.ts +--- +[ + { + "children": [ + { + "children": [], + "flags": "ScopeFlags(StrictMode)", + "id": 1, + "node": "Class(Foo)", + "symbols": [] + } + ], + "flags": "ScopeFlags(StrictMode | Top)", + "id": 0, + "node": "Program", + "symbols": [ + { + "flags": "SymbolFlags(Import)", + "id": 0, + "name": "Bar", + "node": "ImportSpecifier(Bar)", + "references": [ + { + "flags": "ReferenceFlags(Read)", + "id": 0, + "name": "Bar", + "node_id": 17 + } + ] + }, + { + "flags": "SymbolFlags(Class)", + "id": 1, + "name": "Foo", + "node": "Class(Foo)", + "references": [] + } + ] + } +] diff --git a/crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.ts b/crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.ts new file mode 100644 index 0000000000000..8a0d8a108d6da --- /dev/null +++ b/crates/oxc_semantic/tests/fixtures/oxc/ts/issue-7879.ts @@ -0,0 +1,4 @@ +import { Bar } from "./bar"; +export type { Baz } from "./baz"; + +export class Foo extends Bar {} diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index b944c7e70f5c0..0d863febd2b80 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,27 +1,27 @@ | Oxc | ESBuild | Oxc | ESBuild | Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- -72.14 kB | 23.77 kB | 23.70 kB | 8.62 kB | 8.54 kB | react.development.js +72.14 kB | 23.74 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js 173.90 kB | 60.22 kB | 59.82 kB | 19.49 kB | 19.33 kB | moment.js -287.63 kB | 90.77 kB | 90.07 kB | 32.23 kB | 31.95 kB | jquery.js +287.63 kB | 90.61 kB | 90.07 kB | 32.19 kB | 31.95 kB | jquery.js -342.15 kB | 119.00 kB | 118.14 kB | 44.65 kB | 44.37 kB | vue.js +342.15 kB | 118.76 kB | 118.14 kB | 44.54 kB | 44.37 kB | vue.js -544.10 kB | 72.54 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js +544.10 kB | 72.04 kB | 72.48 kB | 26.18 kB | 26.20 kB | lodash.js -555.77 kB | 274.27 kB | 270.13 kB | 91.27 kB | 90.80 kB | d3.js +555.77 kB | 273.90 kB | 270.13 kB | 91.19 kB | 90.80 kB | d3.js -1.01 MB | 461.18 kB | 458.89 kB | 126.93 kB | 126.71 kB | bundle.min.js +1.01 MB | 461.13 kB | 458.89 kB | 126.91 kB | 126.71 kB | bundle.min.js -1.25 MB | 657.26 kB | 646.76 kB | 164.24 kB | 163.73 kB | three.js +1.25 MB | 656.81 kB | 646.76 kB | 164.16 kB | 163.73 kB | three.js -2.14 MB | 735.73 kB | 724.14 kB | 181.11 kB | 181.07 kB | victory.js +2.14 MB | 735.33 kB | 724.14 kB | 180.99 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 332.37 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 332.27 kB | 331.56 kB | echarts.js -6.69 MB | 2.38 MB | 2.31 MB | 495.35 kB | 488.28 kB | antd.js +6.69 MB | 2.36 MB | 2.31 MB | 495.04 kB | 488.28 kB | antd.js -10.95 MB | 3.52 MB | 3.49 MB | 911.00 kB | 915.50 kB | typescript.js +10.95 MB | 3.51 MB | 3.49 MB | 910.93 kB | 915.50 kB | typescript.js