Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8297,6 +8297,27 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.import_records.truncate(snapshot.import_records_len);
}

/// Runs `attempt` with the log off, then undoes everything it did to the parser. `None` means the attempt failed. Stack and memory exhaustion are not properties of the attempt and pass through.
#[inline]
pub(crate) fn speculate<R>(
&mut self,
attempt: impl FnOnce(&mut Self) -> Result<R, crate::Error>,
) -> Result<Option<R>, crate::Error> {
let snapshot = self.parser_snapshot();
self.lexer.is_log_disabled = true;
// Metadata looks up type names, which adds unbound symbols to the module scope.
let emit_decorator_metadata =
core::mem::take(&mut self.options.features.emit_decorator_metadata);
let result = attempt(self);
self.restore_parser_snapshot(snapshot);
self.options.features.emit_decorator_metadata = emit_decorator_metadata;
match result {
Ok(value) => Ok(Some(value)),
Err(err @ (crate::Error::StackOverflow | crate::Error::Alloc(_))) => Err(err),
Err(_) => Ok(None),
}
}

/// When not transpiling we dont use the renamer, so our solution is to generate really
/// hard to collide with variables, instead of actually making things collision free
pub(crate) fn generate_temp_ref(&mut self, default_name: Option<&'a [u8]>) -> Ref {
Expand Down
12 changes: 8 additions & 4 deletions src/js_parser/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
errors: Option<&mut DeferredErrors>,
expr: &mut Expr,
) -> Result<(), Error> {
self.parse_expr_common(level, errors, EFlags::None, expr)
self.parse_expr_common::<false>(level, errors, EFlags::None, expr)
}
#[inline]
pub fn parse_expr(&mut self, level: Level) -> Result<Expr, Error> {
let mut expr = Expr::EMPTY;
self.parse_expr_common(level, None, EFlags::None, &mut expr)?;
self.parse_expr_common::<false>(level, None, EFlags::None, &mut expr)?;
Ok(expr)
}
#[inline]
Expand All @@ -59,9 +59,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
flags: EFlags,
expr: &mut Expr,
) -> Result<(), Error> {
self.parse_expr_common(level, None, flags, expr)
self.parse_expr_common::<false>(level, None, flags, expr)
}
pub(crate) fn parse_expr_common(
/// With `STOP_BEFORE_NAME`, an identifier after the prefix ends the expression, so the caller can read it as a name.
pub(crate) fn parse_expr_common<const STOP_BEFORE_NAME: bool>(
&mut self,
level: Level,
mut errors: Option<&mut DeferredErrors>,
Expand All @@ -75,6 +76,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let had_pure_comment_before =
self.lexer.has_pure_comment_before && !self.options.ignore_dce_annotations;
*expr = self.parse_prefix(level, errors.as_deref_mut(), flags)?;
if STOP_BEFORE_NAME && self.lexer.token == T::TIdentifier {
return Ok(());
}
// `errors` is reborrowed via as_deref_mut for each call site.

// There is no formal spec for "__PURE__" comments but from reverse-
Expand Down
33 changes: 10 additions & 23 deletions src/js_parser/parse/parse_skip_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1612,30 +1612,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
return Ok(self.ts_conditional_arrow_attempts[i] & 1 == 1);
}

let snapshot = self.parser_snapshot();
self.lexer.is_log_disabled = true;
// Metadata looks up type names, which adds unbound symbols to the module scope.
let emit_decorator_metadata =
core::mem::take(&mut self.options.features.emit_decorator_metadata);

let mut data = arrow_data.clone();
let result: Result<(), Error> = (|| {
self.lexer.expect(T::TColon)?;
self.skip_typescript_return_type()?;
self.parse_arrow_body(&mut [], &mut data)?;
// The ":" that pairs with the "?"
self.lexer.expect(T::TColon)?;
Ok(())
})();

self.restore_parser_snapshot(snapshot);
self.options.features.emit_decorator_metadata = emit_decorator_metadata;
let is_arrow_fn = match result {
Ok(()) => true,
// Stack and memory exhaustion are not properties of the attempt
Err(err @ (Error::StackOverflow | Error::Alloc(_))) => return Err(err),
Err(_) => false,
};
let is_arrow_fn = self
.speculate(|p| {
p.lexer.expect(T::TColon)?;
p.skip_typescript_return_type()?;
p.parse_arrow_body(&mut [], &mut data)?;
// The ":" that pairs with the "?"
p.lexer.expect(T::TColon)?;
Ok(())
})?
.is_some();

// Re-search for the insertion point: attempts nested inside this one may
// have added entries of their own.
Expand Down
50 changes: 49 additions & 1 deletion src/js_parser/parse/parse_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use js_lexer::T;
use crate::parser::fs;
use crate::parser::{
AwaitOrYield, DeferredTsDecorators, LexicalDecl, ParseStatementOptions, ParsedPath, Ref,
StatementScope, StmtList,
StatementScope, StmtList, TypeParameterFlag,
};
use crate::typescript;
use bun_ast::{ImportKind, ImportRecordFlags, ImportRecordTag};
Expand Down Expand Up @@ -1738,6 +1738,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

expr = p.parse_async_prefix_expr(async_range, Level::Lowest, EFlags::None)?;
p.parse_suffix(&mut expr, Level::Lowest, None, EFlags::None)?;
} else if Self::IS_TYPESCRIPT_ENABLED
&& is_identifier
&& matches!(name, b"type" | b"interface" | b"namespace" | b"module")
{
expr = Self::parse_ts_decl_keyword_expr(p, name, opts.scope)?;
} else {
let expr_or_let = p.parse_expr_or_let_stmt(opts)?;
match expr_or_let.stmt_or_expr {
Expand Down Expand Up @@ -1781,6 +1786,49 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
))
}

/// The expression that starts a statement led by `type`, `interface`, `namespace` or `module`. It ends before the name of a declaration.
#[cold]
#[inline(never)]
fn parse_ts_decl_keyword_expr(
p: &mut Self,
keyword: &[u8],
scope: StatementScope,
) -> Result<Expr> {
let mut expr = Expr::EMPTY;
p.parse_expr_common::<true>(Level::Lowest, None, EFlags::None, &mut expr)?;
if (p.lexer.is_contextual_keyword(b"as") || p.lexer.is_contextual_keyword(b"satisfies"))
&& !Self::ts_decl_follows_name(p, keyword, scope)?
{
// "type as any;" casts a variable named `type`, as in esbuild. tsc reads a declaration and rejects it (microsoft/TypeScript#58248).
p.parse_suffix(&mut expr, Level::Lowest, None, EFlags::None)?;
}
Ok(expr)
}

/// Looks past the name after `keyword`. Returns whether the rest of a declaration with that name follows ("type as = any").
#[cold]
#[inline(never)]
fn ts_decl_follows_name(p: &mut Self, keyword: &[u8], scope: StatementScope) -> Result<bool> {
let follows = p.speculate(|p| {
p.lexer.next()?;
if matches!(keyword, b"type" | b"interface") {
let _ = p.skip_type_script_type_parameters(
TypeParameterFlag::ALLOW_IN_OUT_VARIANCE_ANNOTATIONS
| TypeParameterFlag::ALLOW_EMPTY_TYPE_PARAMETERS,
)?;
}
Ok(match keyword {
b"type" => p.lexer.token == T::TEquals,
b"interface" => matches!(p.lexer.token, T::TOpenBrace | T::TExtends),
_ => {
scope != StatementScope::Nested
&& matches!(p.lexer.token, T::TOpenBrace | T::TDot)
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
}
})
})?;
Ok(follows == Some(true))
}

/// Cold TS-only statement keywords reached from `parse_stmt_fallthrough` once the
/// leading identifier has been recognised as one of the contextual statement keywords.
/// Returns `Some(stmt)` when the keyword form was consumed; `None` means the caller
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ bun_core::declare_scope!(cache, visible);
/// Version 42: The body of a class with lowered class decorators names the class by the
/// binding that holds what the decorators return.
/// Version 43: A `bun:` specifier that names no builtin is printed as it is written.
const EXPECTED_VERSION: u32 = 43;
/// Version 44: A TypeScript `interface` or `namespace` named `as` or `satisfies` is a declaration, not the statement `interface;`.
const EXPECTED_VERSION: u32 = 44;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
21 changes: 21 additions & 0 deletions test/bundler/esbuild/dce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3510,6 +3510,27 @@ describe("bundler", () => {
stdout: "PASS",
},
});
itBundled("dce/IgnoreAnnotationsCallOfTypeScriptDeclarationKeyword", {
files: {
"/entry.ts": /* ts */ `
import { as, satisfies } from "./names";
function type(n: number) { console.log("type", n); }
/* @__PURE__ */ type(as.one);
/* @__PURE__ */ type(satisfies.two) as any;
console.log("done");
`,
"/names.ts": /* ts */ `
export type as = number;
export namespace as { export const one: as = 1; }
namespace satisfies { export const two = 2; }
export { satisfies };
`,
},
ignoreDCEAnnotations: true,
run: {
stdout: "type 1\ntype 2\ndone",
},
});
itBundled("dce/IgnoreAnnotationsDoesNotApplyToRuntime", {
files: {
"/entry.js": /* js */ `
Expand Down
Loading
Loading