mirror of
https://github.com/oven-sh/bun
synced 2026-02-14 12:51:54 +00:00
Split JS parser into multiple files (#20880)
Splits up js_parser.zig into multiple files. Also changes visitExprInOut to use function calls rather than switch Not ready: - [ ] P.zig is ~70,000 tokens, still needs to get smaller - [x] ~~measure zig build time before & after (is it slower?)~~ no significant impact --------- Co-authored-by: pfgithub <6010774+pfgithub@users.noreply.github.com>
This commit is contained in:
437
src/ast/parseImportExport.zig
Normal file
437
src/ast/parseImportExport.zig
Normal file
@@ -0,0 +1,437 @@
|
||||
pub fn ParseImportExport(
|
||||
comptime parser_feature__typescript: bool,
|
||||
comptime parser_feature__jsx: JSXTransformType,
|
||||
comptime parser_feature__scan_only: bool,
|
||||
) type {
|
||||
return struct {
|
||||
const P = js_parser.NewParser_(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only);
|
||||
const is_typescript_enabled = P.is_typescript_enabled;
|
||||
const only_scan_imports_and_do_not_visit = P.only_scan_imports_and_do_not_visit;
|
||||
|
||||
/// Note: The caller has already parsed the "import" keyword
|
||||
pub fn parseImportExpr(noalias p: *P, loc: logger.Loc, level: Level) anyerror!Expr {
|
||||
// Parse an "import.meta" expression
|
||||
if (p.lexer.token == .t_dot) {
|
||||
p.esm_import_keyword = js_lexer.rangeOfIdentifier(p.source, loc);
|
||||
try p.lexer.next();
|
||||
if (p.lexer.isContextualKeyword("meta")) {
|
||||
try p.lexer.next();
|
||||
p.has_import_meta = true;
|
||||
return p.newExpr(E.ImportMeta{}, loc);
|
||||
} else {
|
||||
try p.lexer.expectedString("\"meta\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (level.gt(.call)) {
|
||||
const r = js_lexer.rangeOfIdentifier(p.source, loc);
|
||||
p.log.addRangeError(p.source, r, "Cannot use an \"import\" expression here without parentheses") catch unreachable;
|
||||
}
|
||||
|
||||
// allow "in" inside call arguments;
|
||||
const old_allow_in = p.allow_in;
|
||||
p.allow_in = true;
|
||||
|
||||
p.lexer.preserve_all_comments_before = true;
|
||||
try p.lexer.expect(.t_open_paren);
|
||||
|
||||
// const comments = try p.lexer.comments_to_preserve_before.toOwnedSlice();
|
||||
p.lexer.comments_to_preserve_before.clearRetainingCapacity();
|
||||
|
||||
p.lexer.preserve_all_comments_before = false;
|
||||
|
||||
const value = try p.parseExpr(.comma);
|
||||
|
||||
var import_options = Expr.empty;
|
||||
if (p.lexer.token == .t_comma) {
|
||||
// "import('./foo.json', )"
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.token != .t_close_paren) {
|
||||
// "import('./foo.json', { assert: { type: 'json' } })"
|
||||
import_options = try p.parseExpr(.comma);
|
||||
|
||||
if (p.lexer.token == .t_comma) {
|
||||
// "import('./foo.json', { assert: { type: 'json' } }, )"
|
||||
try p.lexer.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try p.lexer.expect(.t_close_paren);
|
||||
|
||||
p.allow_in = old_allow_in;
|
||||
|
||||
if (comptime only_scan_imports_and_do_not_visit) {
|
||||
if (value.data == .e_string and value.data.e_string.isUTF8() and value.data.e_string.isPresent()) {
|
||||
const import_record_index = p.addImportRecord(.dynamic, value.loc, value.data.e_string.slice(p.allocator));
|
||||
|
||||
return p.newExpr(E.Import{
|
||||
.expr = value,
|
||||
// .leading_interior_comments = comments,
|
||||
.import_record_index = import_record_index,
|
||||
.options = import_options,
|
||||
}, loc);
|
||||
}
|
||||
}
|
||||
|
||||
// _ = comments; // TODO: leading_interior comments
|
||||
|
||||
return p.newExpr(E.Import{
|
||||
.expr = value,
|
||||
// .leading_interior_comments = comments,
|
||||
.import_record_index = std.math.maxInt(u32),
|
||||
.options = import_options,
|
||||
}, loc);
|
||||
}
|
||||
|
||||
pub fn parseImportClause(
|
||||
p: *P,
|
||||
) !ImportClause {
|
||||
var items = ListManaged(js_ast.ClauseItem).init(p.allocator);
|
||||
try p.lexer.expect(.t_open_brace);
|
||||
var is_single_line = !p.lexer.has_newline_before;
|
||||
// this variable should not exist if we're not in a typescript file
|
||||
var had_type_only_imports = if (comptime is_typescript_enabled)
|
||||
false;
|
||||
|
||||
while (p.lexer.token != .t_close_brace) {
|
||||
// The alias may be a keyword;
|
||||
const isIdentifier = p.lexer.token == .t_identifier;
|
||||
const alias_loc = p.lexer.loc();
|
||||
const alias = try p.parseClauseAlias("import");
|
||||
var name = LocRef{ .loc = alias_loc, .ref = try p.storeNameInRef(alias) };
|
||||
var original_name = alias;
|
||||
try p.lexer.next();
|
||||
|
||||
const probably_type_only_import = if (comptime is_typescript_enabled)
|
||||
strings.eqlComptime(alias, "type") and
|
||||
p.lexer.token != .t_comma and
|
||||
p.lexer.token != .t_close_brace
|
||||
else
|
||||
false;
|
||||
|
||||
// "import { type xx } from 'mod'"
|
||||
// "import { type xx as yy } from 'mod'"
|
||||
// "import { type 'xx' as yy } from 'mod'"
|
||||
// "import { type as } from 'mod'"
|
||||
// "import { type as as } from 'mod'"
|
||||
// "import { type as as as } from 'mod'"
|
||||
if (probably_type_only_import) {
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
original_name = p.lexer.identifier;
|
||||
name = LocRef{ .loc = p.lexer.loc(), .ref = try p.storeNameInRef(original_name) };
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.token == .t_identifier) {
|
||||
|
||||
// "import { type as as as } from 'mod'"
|
||||
// "import { type as as foo } from 'mod'"
|
||||
had_type_only_imports = true;
|
||||
try p.lexer.next();
|
||||
} else {
|
||||
// "import { type as as } from 'mod'"
|
||||
|
||||
try items.append(.{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
});
|
||||
}
|
||||
} else if (p.lexer.token == .t_identifier) {
|
||||
had_type_only_imports = true;
|
||||
|
||||
// "import { type as xxx } from 'mod'"
|
||||
original_name = p.lexer.identifier;
|
||||
name = LocRef{ .loc = p.lexer.loc(), .ref = try p.storeNameInRef(original_name) };
|
||||
try p.lexer.expect(.t_identifier);
|
||||
|
||||
if (isEvalOrArguments(original_name)) {
|
||||
const r = p.source.rangeOfString(name.loc);
|
||||
try p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot use {s} as an identifier here", .{original_name});
|
||||
}
|
||||
|
||||
try items.append(.{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const is_identifier = p.lexer.token == .t_identifier;
|
||||
|
||||
// "import { type xx } from 'mod'"
|
||||
// "import { type xx as yy } from 'mod'"
|
||||
// "import { type if as yy } from 'mod'"
|
||||
// "import { type 'xx' as yy } from 'mod'"
|
||||
_ = try p.parseClauseAlias("import");
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
|
||||
try p.lexer.expect(.t_identifier);
|
||||
} else if (!is_identifier) {
|
||||
// An import where the name is a keyword must have an alias
|
||||
try p.lexer.expectedString("\"as\"");
|
||||
}
|
||||
had_type_only_imports = true;
|
||||
}
|
||||
} else {
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
original_name = p.lexer.identifier;
|
||||
name = LocRef{ .loc = alias_loc, .ref = try p.storeNameInRef(original_name) };
|
||||
try p.lexer.expect(.t_identifier);
|
||||
} else if (!isIdentifier) {
|
||||
// An import where the name is a keyword must have an alias
|
||||
try p.lexer.expectedString("\"as\"");
|
||||
}
|
||||
|
||||
// Reject forbidden names
|
||||
if (isEvalOrArguments(original_name)) {
|
||||
const r = js_lexer.rangeOfIdentifier(p.source, name.loc);
|
||||
try p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot use \"{s}\" as an identifier here", .{original_name});
|
||||
}
|
||||
|
||||
try items.append(js_ast.ClauseItem{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
});
|
||||
}
|
||||
|
||||
if (p.lexer.token != .t_comma) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (p.lexer.has_newline_before) {
|
||||
is_single_line = false;
|
||||
}
|
||||
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.has_newline_before) {
|
||||
is_single_line = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.lexer.has_newline_before) {
|
||||
is_single_line = false;
|
||||
}
|
||||
|
||||
try p.lexer.expect(.t_close_brace);
|
||||
return ImportClause{
|
||||
.items = items.items,
|
||||
.is_single_line = is_single_line,
|
||||
.had_type_only_imports = if (comptime is_typescript_enabled)
|
||||
had_type_only_imports
|
||||
else
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseExportClause(p: *P) !ExportClauseResult {
|
||||
var items = ListManaged(js_ast.ClauseItem).initCapacity(p.allocator, 1) catch unreachable;
|
||||
try p.lexer.expect(.t_open_brace);
|
||||
var is_single_line = !p.lexer.has_newline_before;
|
||||
var first_non_identifier_loc = logger.Loc{ .start = 0 };
|
||||
var had_type_only_exports = false;
|
||||
|
||||
while (p.lexer.token != .t_close_brace) {
|
||||
var alias = try p.parseClauseAlias("export");
|
||||
var alias_loc = p.lexer.loc();
|
||||
|
||||
const name = LocRef{
|
||||
.loc = alias_loc,
|
||||
.ref = p.storeNameInRef(alias) catch unreachable,
|
||||
};
|
||||
const original_name = alias;
|
||||
|
||||
// The name can actually be a keyword if we're really an "export from"
|
||||
// statement. However, we won't know until later. Allow keywords as
|
||||
// identifiers for now and throw an error later if there's no "from".
|
||||
//
|
||||
// // This is fine
|
||||
// export { default } from 'path'
|
||||
//
|
||||
// // This is a syntax error
|
||||
// export { default }
|
||||
//
|
||||
if (p.lexer.token != .t_identifier and first_non_identifier_loc.start == 0) {
|
||||
first_non_identifier_loc = p.lexer.loc();
|
||||
}
|
||||
try p.lexer.next();
|
||||
|
||||
if (comptime is_typescript_enabled) {
|
||||
if (strings.eqlComptime(alias, "type") and p.lexer.token != .t_comma and p.lexer.token != .t_close_brace) {
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
alias = try p.parseClauseAlias("export");
|
||||
alias_loc = p.lexer.loc();
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.token != .t_comma and p.lexer.token != .t_close_brace) {
|
||||
// "export { type as as as }"
|
||||
// "export { type as as foo }"
|
||||
// "export { type as as 'foo' }"
|
||||
_ = p.parseClauseAlias("export") catch "";
|
||||
had_type_only_exports = true;
|
||||
try p.lexer.next();
|
||||
} else {
|
||||
// "export { type as as }"
|
||||
items.append(js_ast.ClauseItem{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
}) catch unreachable;
|
||||
}
|
||||
} else if (p.lexer.token != .t_comma and p.lexer.token != .t_close_brace) {
|
||||
// "export { type as xxx }"
|
||||
// "export { type as 'xxx' }"
|
||||
alias = try p.parseClauseAlias("export");
|
||||
alias_loc = p.lexer.loc();
|
||||
try p.lexer.next();
|
||||
|
||||
items.append(js_ast.ClauseItem{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
}) catch unreachable;
|
||||
} else {
|
||||
had_type_only_exports = true;
|
||||
}
|
||||
} else {
|
||||
// The name can actually be a keyword if we're really an "export from"
|
||||
// statement. However, we won't know until later. Allow keywords as
|
||||
// identifiers for now and throw an error later if there's no "from".
|
||||
//
|
||||
// // This is fine
|
||||
// export { default } from 'path'
|
||||
//
|
||||
// // This is a syntax error
|
||||
// export { default }
|
||||
//
|
||||
if (p.lexer.token != .t_identifier and first_non_identifier_loc.start == 0) {
|
||||
first_non_identifier_loc = p.lexer.loc();
|
||||
}
|
||||
|
||||
// "export { type xx }"
|
||||
// "export { type xx as yy }"
|
||||
// "export { type xx as if }"
|
||||
// "export { type default } from 'path'"
|
||||
// "export { type default as if } from 'path'"
|
||||
// "export { type xx as 'yy' }"
|
||||
// "export { type 'xx' } from 'mod'"
|
||||
_ = p.parseClauseAlias("export") catch "";
|
||||
try p.lexer.next();
|
||||
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
_ = p.parseClauseAlias("export") catch "";
|
||||
try p.lexer.next();
|
||||
}
|
||||
|
||||
had_type_only_exports = true;
|
||||
}
|
||||
} else {
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
alias = try p.parseClauseAlias("export");
|
||||
alias_loc = p.lexer.loc();
|
||||
|
||||
try p.lexer.next();
|
||||
}
|
||||
|
||||
items.append(js_ast.ClauseItem{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
}) catch unreachable;
|
||||
}
|
||||
} else {
|
||||
if (p.lexer.isContextualKeyword("as")) {
|
||||
try p.lexer.next();
|
||||
alias = try p.parseClauseAlias("export");
|
||||
alias_loc = p.lexer.loc();
|
||||
|
||||
try p.lexer.next();
|
||||
}
|
||||
|
||||
items.append(js_ast.ClauseItem{
|
||||
.alias = alias,
|
||||
.alias_loc = alias_loc,
|
||||
.name = name,
|
||||
.original_name = original_name,
|
||||
}) catch unreachable;
|
||||
}
|
||||
|
||||
// we're done if there's no comma
|
||||
if (p.lexer.token != .t_comma) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (p.lexer.has_newline_before) {
|
||||
is_single_line = false;
|
||||
}
|
||||
try p.lexer.next();
|
||||
if (p.lexer.has_newline_before) {
|
||||
is_single_line = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.lexer.has_newline_before) {
|
||||
is_single_line = false;
|
||||
}
|
||||
try p.lexer.expect(.t_close_brace);
|
||||
|
||||
// Throw an error here if we found a keyword earlier and this isn't an
|
||||
// "export from" statement after all
|
||||
if (first_non_identifier_loc.start != 0 and !p.lexer.isContextualKeyword("from")) {
|
||||
const r = js_lexer.rangeOfIdentifier(p.source, first_non_identifier_loc);
|
||||
try p.lexer.addRangeError(r, "Expected identifier but found \"{s}\"", .{p.source.textForRange(r)}, true);
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
return ExportClauseResult{
|
||||
.clauses = items.items,
|
||||
.is_single_line = is_single_line,
|
||||
.had_type_only_exports = had_type_only_exports,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const assert = bun.assert;
|
||||
const js_lexer = bun.js_lexer;
|
||||
const logger = bun.logger;
|
||||
const strings = bun.strings;
|
||||
|
||||
const js_ast = bun.ast;
|
||||
const E = js_ast.E;
|
||||
const Expr = js_ast.Expr;
|
||||
const LocRef = js_ast.LocRef;
|
||||
|
||||
const Op = js_ast.Op;
|
||||
const Level = js_ast.Op.Level;
|
||||
|
||||
const js_parser = bun.js_parser;
|
||||
const ExportClauseResult = js_parser.ExportClauseResult;
|
||||
const ImportClause = js_parser.ImportClause;
|
||||
const JSXTransformType = js_parser.JSXTransformType;
|
||||
const isEvalOrArguments = js_parser.isEvalOrArguments;
|
||||
const options = js_parser.options;
|
||||
|
||||
const std = @import("std");
|
||||
const ListManaged = std.ArrayList;
|
||||
Reference in New Issue
Block a user