Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/bundler/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ impl JavaScript {
};

let result = match parser.parse() {
Ok(r) => r,
Ok(r) => {
// The parser halts on every logged error.
debug_assert_eq!(temp_log.errors, 0);
r
}
Err(err) => {
// `Parser::parse` consumes `self`, so `parser` is gone in this
// arm. The `&'a mut temp_log` it held is released, so read
Expand Down
186 changes: 96 additions & 90 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub struct Parser<'a> {
pub(crate) source: &'a bun_ast::Source,
pub(crate) define: &'a Define,
pub(crate) bump: &'a Arena,
/// `log.errors` before the priming `lexer.next()` in `init`.
pub(crate) orig_error_count: u32,
}

pub struct Options<'a> {
Expand Down Expand Up @@ -314,6 +316,7 @@ impl<'a> Parser<'a> {
bump: &'a Arena,
) -> Result<Parser<'a>, Error> {
source.check_parseable_len(log, "File")?;
let orig_error_count = log.errors;
let mut lexer = js_lexer::Lexer::init_without_reading(log, source, bump);
// Must be set before the priming `next()` so leading comments are seen.
lexer.track_comments = options.features.minify_identifiers;
Expand All @@ -330,6 +333,7 @@ impl<'a> Parser<'a> {
define,
source,
log: log_ptr,
orig_error_count,
})
}
}
Expand Down Expand Up @@ -741,11 +745,9 @@ impl<'a> Parser<'a> {
source,
define,
bump,
orig_error_count,
} = self;

// `lexer.log` aliases `log`; route through the centralised
// `Lexer::log()` accessor so this site stays safe.
let orig_error_count = lexer.log().errors;
// `P.log` and `Lexer.log` are both `NonNull<Log>` (see P.rs / lexer.rs
// field docs), so handing the same raw pointer to both is defined —
// no `&mut` is materialized.
Expand Down Expand Up @@ -775,6 +777,11 @@ impl<'a> Parser<'a> {
p.lexer.next()?;
}

// The first token may already have logged an error; halt before the early returns below.
if p.log().errors > orig_error_count {
return Err(crate::Error::SyntaxError);
}

// Detect a leading "// @bun" pragma
if p.options.features.dont_bundle_twice {
if let Some(pragma) = Self::has_bun_pragma(&source.contents, !hashbang.is_empty()) {
Expand Down Expand Up @@ -1555,6 +1562,8 @@ impl<'a> Parser<'a> {
p.symbols.as_slice()[p.module_ref.inner_index() as usize].use_count_estimate > 0;

let mut wrap_mode: WrapMode = WrapMode::None;
// Checked after `to_ast`, which marks TypeScript type-only imports unused.
let mut reject_import_statements = false;

if p.is_deoptimized_commonjs() {
exports_kind = js_ast::ExportsKind::Cjs;
Expand All @@ -1565,87 +1574,7 @@ impl<'a> Parser<'a> {
exports_kind = js_ast::ExportsKind::Cjs;
if p.options.features.commonjs_at_runtime {
wrap_mode = WrapMode::BunCommonjs;

let import_record: Option<&ImportRecord> = 'brk: {
for import_record in p.import_records.items() {
if import_record.flags.intersects(
ImportRecordFlags::IS_INTERNAL | ImportRecordFlags::IS_UNUSED,
) {
continue;
}
if import_record.kind == bun_ast::ImportKind::Stmt {
break 'brk Some(import_record);
}
}

None
};

// make it an error to use an import statement with a commonjs exports usage
if let Some(record) = import_record {
// find the usage of the export symbol

let mut notes = BumpVec::<bun_ast::Data>::new_in(p.arena);

notes.push(bun_ast::Data {
text: {
use std::io::Write;
let mut v = Vec::<u8>::new();
let _ = write!(
&mut v,
"Try require({}) instead",
bun_core::fmt::QuotedFormatter {
text: record.path.text
}
);
std::borrow::Cow::Owned(v)
},
..Default::default()
});

if uses_module_ref {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because 'module' was used",
),
..Default::default()
});
}

if uses_exports_ref {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because 'exports' was used",
),
..Default::default()
});
}

if p.has_top_level_return {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because top-level return was used",
),
..Default::default()
});
}

if p.has_with_scope {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because a \"with\" statement is used",
),
..Default::default()
});
}

p.log().add_range_error_with_notes(
Some(p.source),
record.range,
b"Cannot use import statement with CommonJS-only features".as_slice(),
notes.into_iter().collect::<Vec<_>>().into_boxed_slice(),
);
}
reject_import_statements = true;
}
} else {
match p.options.module_type {
Expand Down Expand Up @@ -2232,12 +2161,89 @@ impl<'a> Parser<'a> {
}
}

Ok(crate::Result::Ast(p.to_ast(
&mut parts,
exports_kind,
wrap_mode,
hashbang,
)?))
let ast = p.to_ast(&mut parts, exports_kind, wrap_mode, hashbang)?;

if reject_import_statements {
// An empty range marks a parser-generated record, like the JSX runtime import.
let import_record: Option<&ImportRecord> =
ast.import_records.as_slice().iter().find(|import_record| {
!import_record
.flags
.intersects(ImportRecordFlags::IS_INTERNAL | ImportRecordFlags::IS_UNUSED)
&& import_record.kind == bun_ast::ImportKind::Stmt
&& !import_record.range.is_empty()
});

if let Some(record) = import_record {
let mut notes = BumpVec::<bun_ast::Data>::new_in(p.arena);

notes.push(bun_ast::Data {
text: {
use std::io::Write;
let mut v = Vec::<u8>::new();
let _ = write!(
&mut v,
"Try require({}) instead",
bun_core::fmt::QuotedFormatter {
text: record.path.text
}
);
std::borrow::Cow::Owned(v)
},
..Default::default()
});

if uses_module_ref {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because 'module' was used",
),
..Default::default()
});
}

if uses_exports_ref {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because 'exports' was used",
),
..Default::default()
});
}

if p.has_top_level_return {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because top-level return was used",
),
..Default::default()
});
}

if p.has_with_scope {
notes.push(bun_ast::Data {
text: std::borrow::Cow::Borrowed(
b"This file is CommonJS because a \"with\" statement is used",
),
..Default::default()
});
}

p.log().add_range_error_with_notes(
Some(p.source),
record.range,
b"Cannot use import statement with CommonJS-only features".as_slice(),
notes.into_iter().collect::<Vec<_>>().into_boxed_slice(),
);
}
}

// If there were errors during to_ast, also halt here
if p.log().errors > orig_error_count {
return Err(crate::Error::SyntaxError);
}

Ok(crate::Result::Ast(ast))
}

// associated fn (was `&self` reading `self.lexer.source.contents`)
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,7 @@ pub fn new_lazy_export_ast_impl<'bump>(
define,
source,
log: log_ptr,
orig_error_count: 0,
};
let result = match parser.to_lazy_export_ast(expr, runtime_api_call, symbols) {
Ok(r) => r,
Expand Down
6 changes: 0 additions & 6 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -926,12 +926,6 @@ impl TranspilerJob {
}
}

// The parser can log errors and still return an AST.
if transpiler.log().errors > 0 {
self.parse_error = Some(crate::CrateError::ParseError);
return;
}

// SAFETY: leaf scalar field read; see `vm` note above. Inlined
// `VirtualMachine::use_isolation_source_provider_cache` to avoid forming
// `&VirtualMachine`.
Expand Down
30 changes: 30 additions & 0 deletions test/cli/run/transpiler-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ describe("transpiler cache", () => {
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("a");
expect(!existsSync(cache_dir)).toBeTrue();
});
test("does not cache a file whose parse logged an error", async () => {
// The parser reports the `import` next to `module.exports` after it has
// built the AST, and the lexer reports `0foo` while the parser is being
// constructed. Nothing may be printed or cached for such a file, or a
// later run would serve the broken output from the cache without the error.
const filler = "\n//" + Buffer.alloc(5 * 1024, "f").toString();
writeFileSync(join(temp_dir, "dep.js"), `export const x = 1;`);
writeFileSync(join(temp_dir, "mixed.js"), `import { x } from "./dep.js";\nmodule.exports = { x };` + filler);
writeFileSync(join(temp_dir, "first.js"), `\\u0030foo = 1;` + filler);
writeFileSync(
join(temp_dir, "main.js"),
`const out = {};
for (const file of ["./mixed.js", "./first.js"]) {
try { await import(file); } catch (e) { out["import " + file] = [e.name, e.message]; }
try { require(file); } catch (e) { out["require " + file] = [e.name, e.message]; }
}
console.log(JSON.stringify(out));`,
);
const mixed = ["BuildMessage", "Cannot use import statement with CommonJS-only features"];
const first = ["BuildMessage", 'Invalid identifier: "0foo"'];
const expected = JSON.stringify({
"import ./mixed.js": mixed,
"require ./mixed.js": mixed,
"import ./first.js": first,
"require ./first.js": first,
});
expect(await bunRun(join(temp_dir, "main.js"), env)).toSpawn(expected);
expect(await bunRun(join(temp_dir, "main.js"), env)).toSpawn(expected);
expect(!existsSync(cache_dir)).toBeTrue();
});
test("it is indeed content addressable", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile(50 * 1024, "1", "b"));
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("b");
Expand Down
Loading
Loading