This commit is contained in:
Jarred Sumner
2021-05-16 23:25:12 -07:00
parent d8b1d29656
commit 9ccb4dd082
17 changed files with 3748 additions and 509 deletions

58
.vscode/launch.json vendored
View File

@@ -21,14 +21,30 @@
// "--resolve=disable",
// "--cwd",
// "${workspaceFolder}",
// "src/test/fixtures/cannot-assign-to-import-bug.js",
// "/Users/jarredsumner/Code/esdev/src/test/fixtures/exports-bug.js",
// "-o",
// "out"
// ],
// "cwd": "${workspaceFolder}",
// "console": "internalConsole"
// }
// {
// "type": "lldb",
// "request": "launch",
// "name": "Dev Launch",
// "program": "${workspaceFolder}/build/debug/macos-x86_64/esdev",
// "preLaunchTask": "build",
// "args": [
// "--resolve=disable",
// "--cwd",
// "/Users/jarredsumner/Code/esdev/src/test/fixtures/",
// "/Users/jarredsumner/Code/esdev/src/test/fixtures/symbols-bug.js",
// "-o",
// "out"
// ],
// "cwd": "${workspaceFolder}",
// "console": "internalConsole"
// }
{
"type": "lldb",
"request": "launch",
@@ -38,8 +54,8 @@
"args": [
"--resolve=dev",
"--cwd",
"./src/api/demo",
"pages/index.js",
"/Users/jarredsumner/Builds/esbuild/bench/three/src/",
"./entry.js",
"-o",
"out"
],
@@ -50,6 +66,40 @@
// "type": "lldb",
// "request": "launch",
// "name": "Dev Launch",
// "program": "${workspaceFolder}/build/debug/macos-x86_64/esdev",
// "preLaunchTask": "build",
// "args": [
// "--resolve=dev",
// "--cwd",
// "/Users/jarredsumner/Builds/esbuild/bench/three/src/",
// "./entry.js",
// "-o",
// "out"
// ],
// "cwd": "${workspaceFolder}",
// "console": "internalConsole"
// }
// {
// "type": "lldb",
// "request": "launch",
// "name": "Dev Launch",
// "program": "${workspaceFolder}/build/debug/macos-x86_64/esdev",
// "preLaunchTask": "build",
// "args": [
// "--resolve=dev",
// "--cwd",
// "./src/api/demo",
// "pages/index.js",
// "-o",
// "out"
// ],
// "cwd": "${workspaceFolder}",
// "console": "internalConsole"
// }
// {
// "type": "lldb",
// "request": "launch",
// "name": "Dev Launch",
// "program": "${workspaceFolder}/build/bin/debug/esdev",
// "preLaunchTask": "build",
// "args": [

304
src/allocators.zig Normal file
View File

@@ -0,0 +1,304 @@
const std = @import("std");
const Wyhash = std.hash.Wyhash;
const FixedBufferAllocator = std.heap.FixedBufferAllocator;
// https://en.wikipedia.org/wiki/.bss#BSS_in_C
pub fn BSSSectionAllocator(comptime size: usize) type {
return struct {
var backing_buf: [size]u8 = undefined;
var fixed_buffer_allocator = FixedBufferAllocator.init(&backing_buf);
var buf_allocator = &fixed_buffer_allocator.allocator;
const Allocator = std.mem.Allocator;
const Self = @This();
allocator: Allocator,
fallback_allocator: *Allocator,
is_overflowed: bool = false,
pub fn get(self: *Self) *Allocator {
return &self.allocator;
}
pub fn init(fallback_allocator: *Allocator) Self {
return Self{ .fallback_allocator = fallback_allocator, .allocator = Allocator{
.allocFn = BSSSectionAllocator(size).alloc,
.resizeFn = BSSSectionAllocator(size).resize,
} };
}
pub fn alloc(
allocator: *Allocator,
len: usize,
ptr_align: u29,
len_align: u29,
return_address: usize,
) error{OutOfMemory}![]u8 {
const self = @fieldParentPtr(Self, "allocator", allocator);
return buf_allocator.allocFn(buf_allocator, len, ptr_align, len_align, return_address) catch |err| {
self.is_overflowed = true;
return self.fallback_allocator.allocFn(self.fallback_allocator, len, ptr_align, len_align, return_address);
};
}
pub fn resize(
allocator: *Allocator,
buf: []u8,
buf_align: u29,
new_len: usize,
len_align: u29,
return_address: usize,
) error{OutOfMemory}!usize {
const self = @fieldParentPtr(Self, "allocator", allocator);
if (fixed_buffer_allocator.ownsPtr(buf.ptr)) {
return fixed_buffer_allocator.allocator.resizeFn(&fixed_buffer_allocator.allocator, buf, buf_align, new_len, len_align, return_address);
} else {
return self.fallback_allocator.resizeFn(self.fallback_allocator, buf, buf_align, new_len, len_align, return_address);
}
}
};
}
const HashKeyType = u64;
const IndexMap = std.HashMapUnmanaged(HashKeyType, u32, hash_hashFn, hash_eqlFn, 80);
pub const Result = struct {
hash: HashKeyType,
index: u32,
status: ItemStatus,
pub fn hasCheckedIfExists(r: *Result) bool {
return r.status != .unknown;
}
};
const Seed = 999;
pub const NotFound = std.math.maxInt(u32);
pub const Unassigned = NotFound - 1;
pub fn hash_hashFn(key: HashKeyType) HashKeyType {
return key;
}
pub fn hash_eqlFn(a: HashKeyType, b: HashKeyType) bool {
return a == b;
}
pub const ItemStatus = packed enum(u3) {
unknown,
exists,
not_found,
};
const hasDeinit = std.meta.trait.hasFn("deinit")(ValueType);
pub fn BSSMap(comptime ValueType: type, comptime count: anytype, store_keys: bool, estimated_key_length: usize) type {
const max_index = count - 1;
const BSSMapType = struct {
pub var backing_buf: [count]ValueType = undefined;
pub var backing_buf_used: u16 = 0;
const Allocator = std.mem.Allocator;
const Self = @This();
// const HashTableAllocator = BSSSectionAllocator(@bitSizeOf(HashKeyType) * count * 2);
index: IndexMap,
overflow_list: std.ArrayListUnmanaged(ValueType),
allocator: *Allocator,
pub var instance: Self = undefined;
pub fn init(allocator: *std.mem.Allocator) *Self {
instance = Self{
.index = IndexMap{},
.allocator = allocator,
.overflow_list = std.ArrayListUnmanaged(ValueType){},
};
return &instance;
}
pub fn isOverflowing() bool {
return backing_buf_used >= @as(u16, count);
}
pub fn getOrPut(self: *Self, key: []const u8) !Result {
const _key = Wyhash.hash(Seed, key);
var index = try self.index.getOrPut(self.allocator, _key);
if (index.found_existing) {
return Result{
.hash = _key,
.index = index.entry.value,
.status = switch (index.entry.value) {
NotFound => .not_found,
Unassigned => .unknown,
else => .exists,
},
};
}
index.entry.value = Unassigned;
return Result{
.hash = _key,
.index = Unassigned,
.status = .unknown,
};
}
pub fn get(self: *const Self, key: []const u8) ?*ValueType {
const _key = Wyhash.hash(Seed, key);
const index = self.index.get(_key) orelse return null;
return self.atIndex(index);
}
pub fn markNotFound(self: *Self, result: Result) void {
self.index.put(self.allocator, result.hash, NotFound) catch unreachable;
}
pub fn atIndex(self: *const Self, index: u32) ?*ValueType {
return switch (index) {
NotFound, Unassigned => null,
0...max_index => &backing_buf[index],
else => &self.overflow_list.items[index - count],
};
}
pub fn put(self: *Self, result: *Result, value: ValueType) !*ValueType {
var index: u32 = @intCast(u32, backing_buf_used + 1);
if (index >= max_index) {
const real_index = self.overflow_list.items.len;
index += @truncate(u32, real_index);
try self.overflow_list.append(self.allocator, value);
result.index = index;
self.index.putAssumeCapacity(result.hash, index);
return &self.overflow_list.items[real_index];
} else {
backing_buf_used += 1;
backing_buf[index] = value;
result.index = index;
self.index.putAssumeCapacity(result.hash, index);
if (backing_buf_used >= max_index - 1) {
self.overflow_list = try @TypeOf(self.overflow_list).initCapacity(self.allocator, count);
}
return &backing_buf[index];
}
}
pub fn remove(self: *Self, key: string) u32 {
const _key = Wyhash.hash(Seed, key);
const index = self.index.get(_key) orelse return;
switch (index) {
Unassigned => {
self.index.remove(_key);
},
NotFound => {
self.index.remove(_key);
},
0...max_index => {
if (hasDeinit(ValueType)) {
backing_buf[index].deinit();
}
backing_buf[index] = undefined;
},
else => {
const i = index - count;
if (hasDeinit(ValueType)) {
self.overflow_list.items[i].deinit();
}
self.overflow_list.items[index - count] = undefined;
},
}
return index;
}
};
if (!store_keys) {
return BSSMapType;
}
return struct {
map: *BSSMapType,
const Self = @This();
pub var instance: Self = undefined;
var key_list_buffer: [count * estimated_key_length]u8 = undefined;
var key_list_buffer_used: usize = 0;
var key_list_slices: [count][]u8 = undefined;
var key_list_overflow: std.ArrayListUnmanaged([]u8) = undefined;
pub fn init(allocator: *std.mem.Allocator) *Self {
instance = Self{
.map = BSSMapType.init(allocator),
};
return &instance;
}
pub fn isOverflowing() bool {
return instance.map.backing_buf_used >= count;
}
pub fn getOrPut(self: *Self, key: []const u8) !Result {
return try self.map.getOrPut(key);
}
pub fn get(self: *Self, key: []const u8) ?*ValueType {
return @call(.{ .modifier = .always_inline }, BSSMapType.get, .{ self.map, key });
}
pub fn atIndex(self: *Self, index: u32) ?*ValueType {
return @call(.{ .modifier = .always_inline }, BSSMapType.atIndex, .{ self.map, index });
}
pub fn keyAtIndex(self: *Self, index: u32) ?[]const u8 {
return switch (index) {
Unassigned, NotFound => null,
0...max_index => {
return key_list_slices[index];
},
else => {
return key_list_overflow.items[index - count];
},
};
}
pub fn put(self: *Self, key: anytype, comptime store_key: bool, result: *Result, value: ValueType) !*ValueType {
var ptr = try self.map.put(result, value);
if (store_key) {
try self.putKey(key, result);
}
return ptr;
}
pub fn putKey(self: *Self, key: anytype, result: *Result) !void {
if (key_list_buffer_used + key.len < key_list_buffer.len) {
const start = key_list_buffer_used;
key_list_buffer_used += key.len;
var slice = key_list_buffer[start..key_list_buffer_used];
std.mem.copy(u8, slice, key);
if (result.index < count) {
key_list_slices[result.index] = slice;
} else {
try key_list_overflow.append(self.map.allocator, slice);
}
} else if (result.index > key_list_overflow.items.len) {
try key_list_overflow.append(self.map.allocator, try self.map.allocator.dupe(u8, key));
} else {
const real_index = result.index - count;
if (key_list_overflow.items[real_index].len > 0) {
self.map.allocator.free(key_list_overflow.items[real_index]);
}
key_list_overflow.items[real_index] = try self.map.allocator.dupe(u8, key);
}
}
pub fn markNotFound(self: *Self, result: Result) void {
self.map.markNotFound(result);
}
// For now, don't free the keys.
pub fn remove(self: *Self, key: string) u32 {
return self.map.remove(key);
}
};
}

View File

@@ -34,7 +34,7 @@ pub const Ref = packed struct {
.source_index = std.math.maxInt(Ref.Int),
};
pub fn toInt(int: anytype) Int {
return std.math.cast(Ref.Int, int) catch 0;
return std.math.lossyCast(Ref.Int, int);
}
pub fn isNull(self: *const Ref) bool {
return self.source_index == std.math.maxInt(Ref.Int) and self.inner_index == std.math.maxInt(Ref.Int);

View File

@@ -265,7 +265,6 @@ pub const Bundler = struct {
try msg.writeFormat(std.io.getStdOut().writer());
}
}
switch (bundler.options.resolve_mode) {
.lazy, .dev, .bundle => {
while (bundler.resolve_queue.readItem()) |item| {

View File

@@ -19,7 +19,7 @@ pub const Cache = struct {
pub fn init(allocator: *std.mem.Allocator) Set {
return Set{
.js = JavaScript{},
.js = JavaScript.init(allocator),
.fs = Fs{
.mutex = Mutex.init(),
.entries = std.StringHashMap(Fs.Entry).init(allocator),
@@ -130,13 +130,14 @@ pub const Cache = struct {
};
pub const JavaScript = struct {
pub const Entry = struct {
ast: js_ast.Ast,
source: logger.Source,
ok: bool,
msgs: []logger.Msg,
};
mutex: Mutex,
entries: std.StringHashMap(Result),
pub const Result = js_ast.Result;
pub fn init(allocator: *std.mem.Allocator) JavaScript {
return JavaScript{ .mutex = Mutex.init(), .entries = std.StringHashMap(Result).init(allocator) };
}
// For now, we're not going to cache JavaScript ASTs.
// It's probably only relevant when bundling for production.
pub fn parse(
@@ -147,19 +148,31 @@ pub const Cache = struct {
log: *logger.Log,
source: *const logger.Source,
) anyerror!?js_ast.Ast {
cache.mutex.lock();
defer cache.mutex.unlock();
var get_or_put_result = try cache.entries.getOrPut(source.key_path.text);
if (get_or_put_result.found_existing) {
return if (get_or_put_result.entry.value.ok) get_or_put_result.entry.value.ast else null;
}
var temp_log = logger.Log.init(allocator);
var parser = js_parser.Parser.init(opts, &temp_log, source, defines, allocator) catch |err| {
temp_log.appendTo(log) catch {};
get_or_put_result.entry.value = Result{ .ast = undefined, .ok = false };
return null;
};
const result = parser.parse() catch |err| {
get_or_put_result.entry.value = parser.parse() catch |err| {
get_or_put_result.entry.value = Result{ .ast = undefined, .ok = false };
temp_log.appendTo(log) catch {};
return null;
};
temp_log.appendTo(log) catch {};
return if (result.ok) result.ast else null;
return if (get_or_put_result.entry.value.ok) get_or_put_result.entry.value.ast else null;
}
};

View File

@@ -6,11 +6,13 @@ const expect = std.testing.expect;
const Mutex = sync.Mutex;
const Semaphore = sync.Semaphore;
const resolvePath = @import("./resolver/resolve_path.zig").resolvePath;
const path_handler = @import("./resolver/resolve_path.zig");
const allocators = @import("./allocators.zig");
// pub const FilesystemImplementation = @import("fs_impl.zig");
threadlocal var scratch_lookup_buffer = [_]u8{0} ** 255;
threadlocal var scratch_lookup_buffer: [256]u8 = undefined;
pub const FileSystem = struct {
allocator: *std.mem.Allocator,
@@ -41,6 +43,14 @@ pub const FileSystem = struct {
dir: string,
data: EntryMap,
pub fn updateDir(i: *DirEntry, dir: string) void {
var iter = i.data.iterator();
i.dir = dir;
while (iter.next()) |entry| {
entry.value.dir = dir;
}
}
pub fn empty(dir: string, allocator: *std.mem.Allocator) DirEntry {
return DirEntry{ .dir = dir, .data = EntryMap.init(allocator) };
}
@@ -154,10 +164,26 @@ pub const FileSystem = struct {
// pub fn readDir(fs: *FileSystemEntry, path: string) ?[]string {
// }
pub fn normalize(f: *@This(), str: string) string {
return @call(.{ .modifier = .always_inline }, path_handler.normalizeAndJoin, .{ f.top_level_dir, .auto, str });
}
pub fn join(f: *@This(), parts: anytype) string {
return @call(.{ .modifier = .always_inline }, path_handler.normalizeAndJoinString, .{
f.top_level_dir,
parts,
.auto,
});
}
pub fn joinAlloc(f: *@This(), allocator: *std.mem.Allocator, parts: anytype) !string {
const joined = f.join(parts);
return try allocator.dupe(u8, joined);
}
pub const RealFS = struct {
entries_mutex: Mutex = Mutex.init(),
entries: std.StringHashMap(EntriesOption),
entries: *EntriesOption.Map,
allocator: *std.mem.Allocator,
do_not_cache_entries: bool = false,
limiter: Limiter,
@@ -166,7 +192,7 @@ pub const FileSystem = struct {
pub fn init(allocator: *std.mem.Allocator, enable_watcher: bool) RealFS {
return RealFS{
.entries = std.StringHashMap(EntriesOption).init(allocator),
.entries = EntriesOption.Map.init(allocator),
.allocator = allocator,
.limiter = Limiter.init(allocator),
.watcher = if (enable_watcher) std.StringHashMap(WatchData).init(allocator) else null,
@@ -276,6 +302,11 @@ pub const FileSystem = struct {
entries,
err,
};
// This custom map implementation:
// - Preallocates a fixed amount of directory name space
// - Doesn't store directory names which don't exist.
pub const Map = allocators.BSSMap(EntriesOption, 1024, true, 128);
};
// Limit the number of files open simultaneously to avoid ulimit issues
@@ -305,15 +336,20 @@ pub const FileSystem = struct {
}
};
fn readdir(fs: *RealFS, _dir: string) !DirEntry {
pub fn openDir(fs: *RealFS, unsafe_dir_string: string) std.fs.File.OpenError!std.fs.Dir {
return try std.fs.openDirAbsolute(unsafe_dir_string, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true });
}
fn readdir(
fs: *RealFS,
_dir: string,
handle: std.fs.Dir,
) !DirEntry {
fs.limiter.before();
defer fs.limiter.after();
var handle = try std.fs.openDirAbsolute(_dir, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true });
defer handle.close();
var iter: std.fs.Dir.Iterator = handle.iterate();
var dir = DirEntry{ .data = DirEntry.EntryMap.init(fs.allocator), .dir = _dir };
var dir = DirEntry.init("", fs.allocator);
errdefer dir.deinit();
while (try iter.next()) |_entry| {
const entry: std.fs.Dir.Entry = _entry;
@@ -342,7 +378,7 @@ pub const FileSystem = struct {
var entry_ptr = try fs.allocator.create(Entry);
entry_ptr.* = Entry{
.base = name,
.dir = _dir,
.dir = "",
.mutex = Mutex.init(),
// Call "stat" lazily for performance. The "@material-ui/icons" package
// contains a directory with over 11,000 entries in it and running "stat"
@@ -356,12 +392,11 @@ pub const FileSystem = struct {
try dir.data.put(name, entry_ptr);
}
// Copy at the bottom here so in the event of an error, we don't deinit the dir string.
dir.dir = _dir;
return dir;
}
fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !void {
fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !*EntriesOption {
if (fs.watcher) |*watcher| {
fs.watcher_mutex.lock();
defer fs.watcher_mutex.unlock();
@@ -371,27 +406,52 @@ pub const FileSystem = struct {
if (!fs.do_not_cache_entries) {
fs.entries_mutex.lock();
defer fs.entries_mutex.unlock();
try fs.entries.put(dir, EntriesOption{
var get_or_put_result = try fs.entries.getOrPut(dir);
var opt = try fs.entries.put(null, false, &get_or_put_result, EntriesOption{
.err = DirEntry.Err{ .original_err = err, .canonical_error = err },
});
return opt;
}
temp_entries_option = EntriesOption{
.err = DirEntry.Err{ .original_err = err, .canonical_error = err },
};
return &temp_entries_option;
}
pub fn readDirectory(fs: *RealFS, dir: string) !EntriesOption {
threadlocal var temp_entries_option: EntriesOption = undefined;
pub fn readDirectory(fs: *RealFS, dir: string, _handle: ?std.fs.Dir, recursive: bool) !*EntriesOption {
var cache_result: ?allocators.Result = null;
if (!fs.do_not_cache_entries) {
fs.entries_mutex.lock();
defer fs.entries_mutex.unlock();
// First, check the cache
if (fs.entries.get(dir)) |_dir| {
return _dir;
cache_result = try fs.entries.getOrPut(dir);
if (cache_result.?.hasCheckedIfExists()) {
if (fs.entries.atIndex(cache_result.?.index)) |cached_result| {
return cached_result;
}
}
}
var handle = _handle orelse try fs.openDir(dir);
defer {
if (_handle == null) {
handle.close();
}
}
// Cache miss: read the directory entries
const entries = fs.readdir(dir) catch |err| {
_ = fs.readDirectoryError(dir, err) catch {};
return err;
const entries = fs.readdir(
dir,
handle,
) catch |err| {
return fs.readDirectoryError(dir, err) catch unreachable;
};
if (fs.watcher) |*watcher| {
@@ -409,16 +469,23 @@ pub const FileSystem = struct {
WatchData{ .dir_entries = names, .state = .dir_has_entries },
);
}
fs.entries_mutex.lock();
defer fs.entries_mutex.unlock();
const result = EntriesOption{
.entries = entries,
};
if (!fs.do_not_cache_entries) {
try fs.entries.put(dir, result);
fs.entries_mutex.lock();
defer fs.entries_mutex.unlock();
const result = EntriesOption{
.entries = entries,
};
var entries_ptr = try fs.entries.put(dir, true, &cache_result.?, result);
const dir_key = fs.entries.keyAtIndex(cache_result.?.index) orelse unreachable;
entries_ptr.entries.updateDir(dir_key);
return entries_ptr;
}
return result;
temp_entries_option = EntriesOption{ .entries = entries };
temp_entries_option.entries.updateDir(try fs.allocator.dupe(u8, dir));
return &temp_entries_option;
}
fn readFileError(fs: *RealFS, path: string, err: anyerror) void {
@@ -622,6 +689,7 @@ pub const PathName = struct {
};
threadlocal var normalize_buf: [1024]u8 = undefined;
threadlocal var join_buf: [1024]u8 = undefined;
pub const Path = struct {
pretty: string,
@@ -634,35 +702,6 @@ pub const Path = struct {
return try std.fmt.allocPrint(allocator, "{s}://{s}", .{ p.namespace, p.text });
}
// for now, assume you won't try to normalize a path longer than 1024 chars
pub fn normalize(str: string, allocator: *std.mem.Allocator) string {
if (str.len == 0 or (str.len == 1 and str[0] == ' ')) return ".";
if (resolvePath(&normalize_buf, str)) |out| {
return allocator.dupe(u8, out) catch unreachable;
}
return str;
}
// for now, assume you won't try to normalize a path longer than 1024 chars
pub fn normalizeNoAlloc(str: string, comptime remap_windows_paths: bool) string {
if (str.len == 0 or (str.len == 1 and (str[0] == ' ' or str[0] == '\\'))) return ".";
if (remap_windows_paths) {
std.mem.copy(u8, &normalize_buf, str);
var i: usize = 0;
while (i < str.len) : (i += 1) {
if (str[i] == '\\') {
normalize_buf[i] = '/';
}
}
}
if (resolvePath(&normalize_buf, str)) |out| {
return out;
}
return str;
}
pub fn init(text: string) Path {
return Path{ .pretty = text, .text = text, .namespace = "file", .name = PathName.init(text) };
}

View File

@@ -275,7 +275,6 @@ pub const Lexer = struct {
}
// Reset string literal
lexer.string_literal = &([_]u16{});
lexer.string_literal_slice = lexer.source.contents[lexer.start + 1 .. lexer.end - suffixLen];
lexer.string_literal_is_ascii = !needs_slow_path;
lexer.string_literal_buffer.shrinkRetainingCapacity(0);
@@ -283,8 +282,6 @@ pub const Lexer = struct {
lexer.string_literal_buffer.ensureTotalCapacity(lexer.string_literal_slice.len) catch unreachable;
var slice = lexer.string_literal_buffer.allocatedSlice();
lexer.string_literal_buffer.items = slice[0..strings.toUTF16Buf(lexer.string_literal_slice, slice)];
lexer.string_literal = lexer.string_literal_buffer.items;
lexer.string_literal_slice = &[_]u8{};
}
if (quote == '\'' and lexer.json_options != null) {
@@ -483,6 +480,7 @@ pub const Lexer = struct {
if (lexer.code_point == '\\') {
try lexer.scanIdentifierWithEscapes();
lexer.token = T.t_private_identifier;
// lexer.Identifier, lexer.Token = lexer.scanIdentifierWithEscapes(normalIdentifier);
} else {
lexer.token = T.t_private_identifier;
@@ -766,7 +764,6 @@ pub const Lexer = struct {
lexer.token = .t_slash_equals;
},
'/' => {
try lexer.step();
singleLineComment: while (true) {
try lexer.step();
switch (lexer.code_point) {
@@ -1440,14 +1437,12 @@ pub const Lexer = struct {
lexer.token = .t_string_literal;
lexer.string_literal_slice = lexer.source.contents[lexer.start + 1 .. lexer.end - 1];
lexer.string_literal.len = lexer.string_literal_slice.len;
lexer.string_literal_is_ascii = !needs_decode;
lexer.string_literal_buffer.shrinkRetainingCapacity(0);
lexer.string_literal_buffer.clearRetainingCapacity();
if (needs_decode) {
lexer.string_literal_buffer.ensureTotalCapacity(lexer.string_literal_slice.len) catch unreachable;
try lexer.decodeJSXEntities(lexer.string_literal_slice, &lexer.string_literal_buffer);
lexer.string_literal = lexer.string_literal_buffer.items;
lexer.string_literal_slice = &([_]u8{0});
}
}

View File

@@ -533,25 +533,27 @@ pub const SideEffects = enum {
equality.ok = equality.equal;
},
.e_undefined => |l| {
equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_undefined;
equality.ok = equality.equal;
equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_undefined;
equality.equal = equality.ok;
},
.e_boolean => |l| {
equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_boolean;
equality.equal = l.value == right.e_boolean.value;
equality.equal = equality.ok and l.value == right.e_boolean.value;
},
.e_number => |l| {
equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_number;
equality.equal = l.value == right.e_number.value;
equality.equal = equality.ok and l.value == right.e_number.value;
},
.e_big_int => |l| {
equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_big_int;
equality.equal = strings.eql(l.value, right.e_big_int.value);
equality.equal = equality.ok and strings.eql(l.value, right.e_big_int.value);
},
.e_string => |l| {
equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_string;
const r = right.e_string;
equality.equal = r.eql(E.String, l);
if (equality.ok) {
const r = right.e_string;
equality.equal = r.eql(E.String, l);
}
},
else => {},
}
@@ -790,7 +792,7 @@ pub const SideEffects = enum {
return Result{ .ok = true, .value = !strings.eqlComptime(e.value, "0"), .side_effects = .no_side_effects };
},
.e_string => |e| {
return Result{ .ok = true, .value = e.value.len > 0, .side_effects = .no_side_effects };
return Result{ .ok = true, .value = std.math.max(e.value.len, e.utf8.len) > 0, .side_effects = .no_side_effects };
},
.e_function, .e_arrow, .e_reg_exp => {
return Result{ .ok = true, .value = true, .side_effects = .no_side_effects };
@@ -1790,7 +1792,7 @@ pub const P = struct {
}
const str = arg.data.e_string;
const import_record_index = p.addImportRecord(.dynamic, arg.loc, p.lexer.utf16ToString(str.value));
const import_record_index = p.addImportRecord(.dynamic, arg.loc, str.string(p.allocator) catch unreachable);
p.import_records.items[import_record_index].handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target;
p.import_records_for_current_part.append(import_record_index) catch unreachable;
return p.e(E.Import{
@@ -1892,13 +1894,14 @@ pub const P = struct {
}
pub fn findSymbol(p: *P, loc: logger.Loc, name: string) !FindSymbolResult {
var ref: Ref = Ref{};
var ref: Ref = undefined;
var declare_loc: logger.Loc = undefined;
var is_inside_with_scope = false;
var did_forbid_argumen = false;
var scope = p.current_scope;
var _scope: ?*Scope = p.current_scope;
var did_match = false;
while (true) {
while (_scope) |scope| : (_scope = _scope.?.parent) {
// Track if we're inside a "with" statement body
if (scope.kind == .with) {
@@ -1916,19 +1919,17 @@ pub const P = struct {
if (scope.members.get(name)) |member| {
ref = member.ref;
declare_loc = member.loc;
did_match = true;
break;
}
}
if (scope.parent) |parent| {
scope = parent;
} else {
// Allocate an "unbound" symbol
p.checkForNonBMPCodePoint(loc, name);
ref = try p.newSymbol(.unbound, name);
declare_loc = loc;
try p.module_scope.members.put(name, js_ast.Scope.Member{ .ref = ref, .loc = logger.Loc.Empty });
break;
}
if (!did_match) {
// Allocate an "unbound" symbol
p.checkForNonBMPCodePoint(loc, name);
ref = p.newSymbol(.unbound, name) catch unreachable;
declare_loc = loc;
p.module_scope.members.put(name, js_ast.Scope.Member{ .ref = ref, .loc = logger.Loc.Empty }) catch unreachable;
}
// If we had to pass through a "with" statement body to get to the symbol
@@ -1997,9 +1998,12 @@ pub const P = struct {
// code regions since those will be culled.
if (!p.is_control_flow_dead) {
p.symbols.items[ref.inner_index].use_count_estimate += 1;
var use = p.symbol_uses.get(ref) orelse Symbol.Use{};
use.count_estimate += 1;
p.symbol_uses.put(ref, use) catch unreachable;
var result = p.symbol_uses.getOrPut(ref) catch unreachable;
if (!result.found_existing) {
result.entry.value = Symbol.Use{ .count_estimate = 1 };
} else {
result.entry.value.count_estimate += 1;
}
}
// The correctness of TypeScript-to-JavaScript conversion relies on accurate
@@ -2283,8 +2287,59 @@ pub const P = struct {
if (!symbol.isHoisted()) {
continue :nextMember;
}
// Check for collisions that would prevent to hoisting "var" symbols up to the enclosing function scope
var __scope = scope.parent;
while (__scope) |_scope| {
// Variable declarations hoisted past a "with" statement may actually end
// up overwriting a property on the target of the "with" statement instead
// of initializing the variable. We must not rename them or we risk
// causing a behavior change.
//
// var obj = { foo: 1 }
// with (obj) { var foo = 2 }
// assert(foo === undefined)
// assert(obj.foo === 2)
//
if (_scope.kind == .with) {
symbol.must_not_be_renamed = true;
}
if (_scope.members.getEntry(symbol.original_name)) |existing_member_entry| {
const existing_member = existing_member_entry.value;
const existing_symbol: Symbol = p.symbols.items[existing_member.ref.inner_index];
// We can hoist the symbol from the child scope into the symbol in
// this scope if:
//
// - The symbol is unbound (i.e. a global variable access)
// - The symbol is also another hoisted variable
// - The symbol is a function of any kind and we're in a function or module scope
//
// Is this unbound (i.e. a global access) or also hoisted?
if (existing_symbol.kind == .unbound or existing_symbol.kind == .hoisted or
(Symbol.isKindFunction(existing_symbol.kind) and (_scope.kind == .entry or _scope.kind == .function_body)))
{
// Silently merge this symbol into the existing symbol
symbol.link = existing_member.ref;
_scope.members.put(symbol.original_name, existing_member) catch unreachable;
continue :nextMember;
}
}
if (_scope.kindStopsHoisting()) {
_scope.members.put(symbol.original_name, res.value) catch unreachable;
break;
}
__scope = _scope.parent;
}
}
}
for (scope.children.items) |_item, i| {
p.hoistSymbols(scope.children.items[i]);
}
}
pub fn nextScopeInOrderForVisitPass(p: *P) ScopeOrder {
@@ -2737,7 +2792,7 @@ pub const P = struct {
}
var parseStmtOpts = ParseStatementOptions{};
p.declareBinding(.hoisted, arg, &parseStmtOpts) catch unreachable;
p.declareBinding(.hoisted, &arg, &parseStmtOpts) catch unreachable;
var default_value: ?ExprNodeIndex = null;
if (!func.flags.has_rest_arg and p.lexer.token == .t_equals) {
@@ -3523,7 +3578,7 @@ pub const P = struct {
// jarred: TIL!
if (p.lexer.token != .t_open_brace) {
try p.lexer.expect(.t_open_paren);
const value = try p.parseBinding();
var value = try p.parseBinding();
// Skip over types
if (p.options.ts and p.lexer.token == .t_colon) {
@@ -3542,7 +3597,7 @@ pub const P = struct {
else => {},
}
stmtOpts = ParseStatementOptions{};
try p.declareBinding(kind, value, &stmtOpts);
try p.declareBinding(kind, &value, &stmtOpts);
binding = value;
}
@@ -4533,6 +4588,8 @@ pub const P = struct {
const name = p.lexer.identifier;
const loc = p.lexer.loc();
const e_str = p.lexer.toEString();
if (!p.lexer.isIdentifierOrKeyword()) {
try p.lexer.expect(.t_identifier);
}
@@ -4541,7 +4598,7 @@ pub const P = struct {
const ref = p.storeNameInRef(name) catch unreachable;
key = p.e(p.lexer.toEString(), loc);
key = p.e(e_str, loc);
if (p.lexer.token != .t_colon and p.lexer.token != .t_open_paren) {
const value = p.b(B.Identifier{ .ref = ref }, loc);
@@ -4590,7 +4647,7 @@ pub const P = struct {
var value: ?js_ast.Expr = null;
var local = try p.parseBinding();
p.declareBinding(kind, local, opts) catch unreachable;
p.declareBinding(kind, &local, opts) catch unreachable;
// Skip over types
if (p.options.ts) {
@@ -5097,9 +5154,9 @@ pub const P = struct {
try p.lexer.expect(T.t_equals_greater_than);
for (args) |arg| {
for (args) |*arg| {
var opts = ParseStatementOptions{};
try p.declareBinding(Symbol.Kind.hoisted, arg.binding, &opts);
try p.declareBinding(Symbol.Kind.hoisted, &arg.binding, &opts);
}
// The ability to call "super()" is inherited by arrow functions
@@ -5125,7 +5182,7 @@ pub const P = struct {
return E.Arrow{ .args = args, .prefer_expr = true, .body = G.FnBody{ .loc = arrow_loc, .stmts = stmts } };
}
pub fn declareBinding(p: *P, kind: Symbol.Kind, binding: BindingNodeIndex, opts: *ParseStatementOptions) !void {
pub fn declareBinding(p: *P, kind: Symbol.Kind, binding: *BindingNodeIndex, opts: *ParseStatementOptions) !void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |bind| {
@@ -5135,15 +5192,14 @@ pub const P = struct {
},
.b_array => |bind| {
for (bind.items) |item| {
p.declareBinding(kind, item.binding, opts) catch unreachable;
for (bind.items) |item, i| {
p.declareBinding(kind, &bind.items[i].binding, opts) catch unreachable;
}
},
.b_object => |bind| {
for (bind.properties) |*prop| {
const value = prop.value;
p.declareBinding(kind, value, opts) catch unreachable;
p.declareBinding(kind, &prop.value, opts) catch unreachable;
}
},
@@ -5571,7 +5627,7 @@ pub const P = struct {
}
}
key = p.e(p.lexer.toEString(), name_range.loc);
key = p.e(E.String{ .utf8 = name }, name_range.loc);
// Parse a shorthand property
if (!opts.is_class and kind == .normal and p.lexer.token != .t_colon and p.lexer.token != .t_open_paren and p.lexer.token != .t_less_than and !opts.is_generator and !js_lexer.Keywords.has(name)) {
@@ -5691,7 +5747,7 @@ pub const P = struct {
if (opts.is_class and !is_computed) {
switch (key.data) {
.e_string => |str| {
if (!opts.is_static and strings.eqlUtf16("constructor", str.value)) {
if (!opts.is_static and str.eql(string, "constructor")) {
if (kind == .get) {
p.log.addRangeError(p.source, key_range, "Class constructor cannot be a getter") catch unreachable;
} else if (kind == .set) {
@@ -5703,7 +5759,7 @@ pub const P = struct {
} else {
is_constructor = true;
}
} else if (opts.is_static and strings.eqlUtf16("prototype", str.value)) {
} else if (opts.is_static and str.eql(string, "prototype")) {
p.log.addRangeError(p.source, key_range, "Invalid static method name \"prototype\"") catch unreachable;
}
},
@@ -5894,7 +5950,7 @@ pub const P = struct {
if (opts.ts_decorators.len > 0) {
switch ((property.key orelse p.panic("Internal error: Expected property {s} to have a key.", .{property})).data) {
.e_string => |str| {
if (strings.eqlUtf16("constructor", str.value)) {
if (str.eql(string, "constructor")) {
p.log.addError(p.source, first_decorator_loc, "TypeScript does not allow decorators on class constructors") catch unreachable;
}
},
@@ -8586,7 +8642,7 @@ pub const P = struct {
in.assign_target,
is_delete_target,
e_.target,
if (e_.index.data.e_string.isUTF8()) p.lexer.utf16ToString(e_.index.data.e_string.value) else e_.index.data.e_string.utf8,
e_.index.data.e_string.string(p.allocator) catch unreachable,
e_.index.loc,
is_call_target,
)) |val| {
@@ -8598,7 +8654,7 @@ pub const P = struct {
// though this is a run-time error, we make it a compile-time error when
// bundling because scope hoisting means these will no longer be run-time
// errors.
if ((in.assign_target != .none or is_delete_target) and @as(Expr.Tag, e_.target.data) == .e_identifier) {
if ((in.assign_target != .none or is_delete_target) and @as(Expr.Tag, e_.target.data) == .e_identifier and p.symbols.items[e_.target.data.e_identifier.ref.inner_index].kind == .import) {
const r = js_lexer.rangeOfIdentifier(p.source, e_.target.loc);
p.log.addRangeErrorFmt(
p.source,
@@ -8818,14 +8874,10 @@ pub const P = struct {
var has_spread = false;
var has_proto = false;
var i: usize = 0;
while (i < e_.properties.len) : (i += 1) {
var property = e_.properties[i];
for (e_.properties) |*property, i| {
if (property.kind != .spread) {
const key = p.visitExpr(property.key orelse Global.panic("Expected property key", .{}));
e_.properties[i].key = key;
property.key = p.visitExpr(property.key orelse Global.panic("Expected property key", .{}));
const key = property.key.?;
// Forbid duplicate "__proto__" properties according to the specification
if (!property.flags.is_computed and !property.flags.was_shorthand and !property.flags.is_method and in.assign_target == .none and key.data.isStringValue() and strings.eqlComptime(
// __proto__ is utf8, assume it lives in refs
@@ -8873,9 +8925,6 @@ pub const P = struct {
}
}
}
// TODO: can we avoid htis copy
e_.properties[i] = property;
}
},
.e_import => |e_| {
@@ -9208,7 +9257,7 @@ pub const P = struct {
for (ex.properties) |property| {
// The key must still be evaluated if it's computed or a spread
if (property.kind == .spread or property.flags.is_computed) {
if (property.kind == .spread or property.flags.is_computed or property.flags.is_spread) {
return false;
}
@@ -9554,9 +9603,12 @@ pub const P = struct {
if (data.label) |*label| {
const name = p.loadNameFromRef(label.ref orelse p.panic("Expected label to have a ref", .{}));
const res = p.findLabelSymbol(label.loc, name);
label.ref = res.ref;
} else if (p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) {
if (res.found) {
label.ref = res.ref;
} else {
data.label = null;
}
} else if (!p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) {
const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc);
p.log.addRangeError(p.source, r, "Cannot use \"break\" here") catch unreachable;
}
@@ -10289,9 +10341,7 @@ pub const P = struct {
}
},
.b_object => |bind| {
var i: usize = 0;
while (i < bind.properties.len) : (i += 1) {
var property = bind.properties[i];
for (bind.properties) |*property| {
if (!property.flags.is_spread) {
property.key = p.visitExpr(property.key);
}
@@ -10312,7 +10362,6 @@ pub const P = struct {
else => {},
}
}
bind.properties[i] = property;
}
},
else => {
@@ -10377,19 +10426,17 @@ pub const P = struct {
var _scope: ?*Scope = p.current_scope;
while (_scope) |scope| : (_scope = scope.parent) {
var label_ref = scope.label_ref orelse continue;
if (!scope.kindStopsHoisting() or (scope.kind != .label) or !strings.eql(name, p.symbols.items[label_ref.inner_index].original_name)) {
continue;
while (_scope != null and !_scope.?.kindStopsHoisting()) : (_scope = _scope.?.parent.?) {
const scope = _scope orelse unreachable;
const label_ref = scope.label_ref orelse continue;
if (scope.kind == .label and strings.eql(name, p.symbols.items[label_ref.inner_index].original_name)) {
// Track how many times we've referenced this symbol
p.recordUsage(label_ref);
res.ref = label_ref;
res.is_loop = scope.label_stmt_is_loop;
res.found = true;
return res;
}
// Track how many times we've referenced this symbol
p.recordUsage(label_ref);
res.ref = label_ref;
res.is_loop = scope.label_stmt_is_loop;
res.found = true;
break;
}
const r = js_lexer.rangeOfIdentifier(p.source, loc);
@@ -10471,12 +10518,7 @@ pub const P = struct {
if (is_private) {} else if (!property.flags.is_method and !property.flags.is_computed) {
if (property.key) |key| {
if (@as(Expr.Tag, key.data) == .e_string) {
const str = key.data.e_string;
if (str.isUTF8()) {
name_to_keep = p.lexer.utf16ToString(key.data.e_string.value);
} else {
name_to_keep = str.utf8;
}
name_to_keep = key.data.e_string.string(p.allocator) catch unreachable;
}
}
}
@@ -10869,8 +10911,8 @@ pub const P = struct {
// with no statements
while (i < parts.len) : (i += 1) {
var part = parts[i];
_ = p.import_records_for_current_part.toOwnedSlice();
_ = p.declared_symbols.toOwnedSlice();
p.import_records_for_current_part.shrinkRetainingCapacity(0);
p.declared_symbols.shrinkRetainingCapacity(0);
var result = try ImportScanner.scan(p, part.stmts);
kept_import_equals = kept_import_equals or result.kept_import_equals;
@@ -10898,6 +10940,22 @@ pub const P = struct {
}
parts = parts[0..parts_end];
// Do a second pass for exported items now that imported items are filled out
for (parts) |part| {
for (part.stmts) |stmt| {
switch (stmt.data) {
.s_export_clause => |clause| {
for (clause.items) |item| {
if (p.named_imports.getEntry(item.name.ref.?)) |_import| {
_import.value.is_exported = true;
}
}
},
else => {},
}
}
}
// Analyze cross-part dependencies for tree shaking and code splitting
{

View File

@@ -1229,8 +1229,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type {
p.options.indent += 1;
}
var i: usize = 0;
while (i < e.properties.len) : (i += 1) {
for (e.properties) |property, i| {
if (i != 0) {
p.print(",");
if (e.is_single_line) {
@@ -1242,7 +1241,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type {
p.printNewline();
p.printIndent();
}
p.printProperty(e.properties[i]);
p.printProperty(property);
}
if (!e.is_single_line) {
@@ -1642,6 +1641,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type {
p.printExpr(item.value.?, .comma, ExprFlag.None());
return;
}
const _key = item.key orelse unreachable;
if (item.flags.is_static) {
p.print("static");
@@ -1686,7 +1686,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type {
if (item.flags.is_computed) {
p.print("[");
p.printExpr(item.key.?, .comma, ExprFlag.None());
p.printExpr(_key, .comma, ExprFlag.None());
p.print("]");
if (item.value) |val| {
@@ -1711,12 +1711,12 @@ pub fn NewPrinter(comptime ascii_only: bool) type {
return;
}
switch (item.key.?.data) {
switch (_key.data) {
.e_private_identifier => |key| {
p.printSymbol(key.ref);
},
.e_string => |key| {
p.addSourceMapping(item.key.?.loc);
p.addSourceMapping(_key.loc);
if (key.isUTF8()) {
p.printSpaceBeforeIdentifier();
p.printIdentifier(key.utf8);
@@ -1786,14 +1786,21 @@ pub fn NewPrinter(comptime ascii_only: bool) type {
}
}
} else {
const c = p.bestQuoteCharForString(key.value, false);
p.print(c);
p.printQuotedUTF16(key.value, c);
p.print(c);
if (key.isUTF8()) {
const c = p.bestQuoteCharForString(key.utf8, false);
p.print(c);
p.printIdentifier(key.utf8);
p.print(c);
} else {
const c = p.bestQuoteCharForString(key.value, false);
p.print(c);
p.printQuotedUTF16(key.value, c);
p.print(c);
}
}
},
else => {
p.printExpr(item.key.?, .lowest, ExprFlag{});
p.printExpr(_key, .lowest, ExprFlag{});
},
}

View File

@@ -93,17 +93,7 @@ fn JSONLikeParser(opts: js_lexer.JSONOptions) type {
return p.e(E.Null{}, loc);
},
.t_string_literal => {
var str: E.String = undefined;
if (p.lexer.string_literal_is_ascii) {
str = E.String{
.utf8 = p.lexer.string_literal_slice,
};
} else {
const value = p.lexer.stringLiteralUTF16();
str = E.String{
.value = value,
};
}
var str: E.String = p.lexer.toEString();
try p.lexer.next();
return p.e(str, loc);

View File

@@ -50,7 +50,10 @@ pub const PackageJSON = struct {
errdefer r.allocator.free(package_json_path);
const entry = r.caches.fs.readFile(r.fs, input_path) catch |err| {
r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable;
if (err != error.IsDir) {
r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable;
}
return null;
};
@@ -146,7 +149,7 @@ pub const PackageJSON = struct {
// import of "foo", but that's actually not a bug. Or arguably it's a
// bug in Browserify but we have to replicate this bug because packages
// do this in the wild.
const key = fs.Path.normalize(_key_str, r.allocator);
const key = r.allocator.dupe(u8, r.fs.normalize(_key_str)) catch unreachable;
switch (value.data) {
.e_string => |str| {

View File

@@ -1,83 +1,498 @@
// https://github.com/MasterQ32/ftz/blob/3183b582211f8e38c1c3363c56753026ca45c11f/src/main.zig#L431-L509
// Thanks, Felix! We should get this into std perhaps.
const tester = @import("../test/tester.zig");
const std = @import("std");
/// Resolves a unix-like path and removes all "." and ".." from it. Will not escape the root and can be used to sanitize inputs.
pub fn resolvePath(buffer: []u8, src_path: []const u8) ?[]u8 {
var end: usize = 0;
buffer[0] = '.';
threadlocal var parser_join_input_buffer: [1024]u8 = undefined;
threadlocal var parser_buffer: [1024]u8 = undefined;
var iter = std.mem.tokenize(src_path, "/");
while (iter.next()) |segment| {
if (end >= buffer.len) break;
// This function is based on Node.js' path.normalize function.
// https://github.com/nodejs/node/blob/36bb31be5f0b85a0f6cbcb36b64feb3a12c60984/lib/path.js#L66
pub fn normalizeStringGeneric(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime separator: u8, comptime isPathSeparator: anytype, lastIndexOfSeparator: anytype) []u8 {
var i: usize = 0;
var last_segment_length: i32 = 0;
var last_slash: i32 = -1;
var dots: i32 = 0;
var code: u8 = 0;
if (std.mem.eql(u8, segment, ".")) {
continue;
} else if (std.mem.eql(u8, segment, "..")) {
while (true) {
if (end == 0)
break;
if (buffer[end] == '/') {
break;
}
end -= 1;
}
var written_len: usize = 0;
const stop_len = str.len;
while (i <= stop_len) : (i += 1) {
if (i < stop_len) {
code = str[i];
} else if (@call(std.builtin.CallOptions{ .modifier = .always_inline }, isPathSeparator, .{code})) {
break;
} else {
if (end + segment.len + 1 > buffer.len)
return null;
code = separator;
}
const start = end;
buffer[end] = '/';
end += segment.len + 1;
std.mem.copy(u8, buffer[start + 1 .. end], segment);
if (@call(std.builtin.CallOptions{ .modifier = .always_inline }, isPathSeparator, .{code})) {
if (last_slash == @intCast(i32, i) - 1 or dots == 1) {
// NOOP
} else if (dots == 2) {
if (written_len < 2 or last_segment_length != 2 or buf[written_len - 1] != '.' or buf[written_len - 2] != '.') {
if (written_len > 2) {
if (lastIndexOfSeparator(buf[0..written_len])) |last_slash_index| {
written_len = last_slash_index;
last_segment_length = @intCast(i32, written_len - 1 - (lastIndexOfSeparator(buf[0..written_len]) orelse 0));
} else {
written_len = 0;
}
last_slash = @intCast(i32, i);
dots = 0;
continue;
} else if (written_len != 0) {
written_len = 0;
last_segment_length = 0;
last_slash = @intCast(i32, i);
dots = 0;
continue;
}
if (allow_above_root) {
if (written_len > 0) {
buf[written_len] = separator;
written_len += 1;
}
buf[written_len] = '.';
written_len += 1;
buf[written_len] = '.';
written_len += 1;
last_segment_length = 2;
}
}
} else {
if (written_len > 0) {
buf[written_len] = separator;
written_len += 1;
}
const slice = str[@intCast(usize, @intCast(usize, last_slash + 1))..i];
std.mem.copy(u8, buf[written_len .. written_len + slice.len], slice);
written_len += slice.len;
last_segment_length = @intCast(i32, i) - last_slash - 1;
}
last_slash = @intCast(i32, i);
dots = 0;
} else if (code == '.' and dots != -1) {
dots += 1;
} else {
dots = -1;
}
}
const result = if (end == 0)
buffer[0 .. end + 1]
else
buffer[0..end];
return buf[0..written_len];
}
if (std.mem.eql(u8, result, src_path)) {
return null;
pub const Platform = enum {
auto,
loose,
windows,
posix,
pub fn isSeparator(comptime _platform: Platform, char: u8) bool {
const platform = _platform.resolve();
switch (platform) {
.auto => unreachable,
.loose => {
return isSepAny(char);
},
.windows => {
return isSepWin32(char);
},
.posix => {
return isSepPosix(char);
},
}
}
return result;
pub fn leadingSeparatorIndex(comptime _platform: Platform, path: anytype) ?usize {
switch (_platform.resolve()) {
.windows => {
if (path.len < 1)
return null;
if (path[0] == '/')
return 0;
if (path[0] == '\\')
return 0;
if (path.len < 3)
return null;
// C:\
// C:/
if (path[0] >= 'A' and path[0] <= 'Z' and path[1] == ':') {
if (path[2] == '/')
return 2;
if (path[2] == '\\')
return 2;
}
return null;
},
.posix => {
if (path.len > 0 and path[0] == '/') {
return 0;
} else {
return null;
}
},
else => {
return leadingSeparatorIndex(.windows, path) orelse leadingSeparatorIndex(.posix, path);
},
}
}
pub fn resolve(comptime _platform: Platform) Platform {
if (_platform == .auto) {
switch (std.Target.current.os.tag) {
.windows => {
return .windows;
},
.freestanding, .emscripten, .other => {
return .loose;
},
else => {
return .posix;
},
}
}
return _platform;
}
};
pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptime _platform: Platform) []u8 {
return normalizeStringBuf(str, &parser_buffer, allow_above_root, _platform);
}
fn testResolve(expected: []const u8, input: []const u8) !void {
var buffer: [1024]u8 = undefined;
pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime _platform: Platform) []u8 {
comptime const platform = _platform.resolve();
const actual = try resolvePath(&buffer, input);
std.testing.expectEqualStrings(expected, actual);
switch (platform) {
.auto => unreachable,
.windows => {
return normalizeStringWindowsBuf(str, buf, allow_above_root);
},
.posix => {
return normalizeStringPosixBuf(str, buf, allow_above_root);
},
.loose => {
return normalizeStringLooseBuf(str, buf, allow_above_root);
},
}
}
test "resolvePath" {
try testResolve("/", "");
try testResolve("/", "/");
try testResolve("/", "////////////");
try testResolve("/a", "a");
try testResolve("/a", "/a");
try testResolve("/a", "////////////a");
try testResolve("/a", "////////////a///");
try testResolve("/a/b/c/d", "/a/b/c/d");
try testResolve("/a/b/d", "/a/b/c/../d");
try testResolve("/", "..");
try testResolve("/", "/..");
try testResolve("/", "/../../../..");
try testResolve("/a/b/c", "a/b/c/");
try testResolve("/new/date.txt", "/new/../../new/date.txt");
pub fn normalizeStringAlloc(allocator: *std.mem.Allocator, str: []const u8, comptime allow_above_root: bool, comptime _platform: Platform) ![]const u8 {
return try allocator.dupe(u8, normalizeString(str, allow_above_root, _platform));
}
test "resolvePath overflow" {
var buf: [1]u8 = undefined;
std.testing.expectEqualStrings("/", try resolvePath(&buf, "/"));
std.testing.expectError(error.BufferTooSmall, resolvePath(&buf, "a")); // will resolve to "/a"
pub fn normalizeAndJoin2(_cwd: []const u8, comptime _platform: Platform, part: anytype, part2: anytype) []const u8 {
const parts = [_][]const u8{ part, part2 };
const slice = normalizeAndJoinString(_cwd, &parts, _platform);
return slice;
}
pub fn normalizeAndJoin(_cwd: []const u8, comptime _platform: Platform, part: anytype) []const u8 {
const parts = [_][]const u8{
part,
};
const slice = normalizeAndJoinString(_cwd, &parts, _platform);
return slice;
}
// Convert parts of potentially invalid file paths into a single valid filpeath
// without querying the filesystem
// This is the equivalent of
pub fn normalizeAndJoinString(_cwd: []const u8, parts: anytype, comptime _platform: Platform) []const u8 {
return normalizeAndJoinStringBuf(_cwd, &parser_join_input_buffer, parts, _platform);
}
pub fn normalizeAndJoinStringBuf(_cwd: []const u8, buf: []u8, parts: anytype, comptime _platform: Platform) []const u8 {
if (parts.len == 0) {
return _cwd;
}
if ((_platform == .loose or _platform == .posix) and parts.len == 1 and parts[0].len == 1 and parts[0] == std.fs.path.sep_posix) {
return "/";
}
var cwd = _cwd;
var out: usize = 0;
// When parts[0] is absolute, we treat that as, effectively, the cwd
var ignore_cwd = cwd.len == 0;
// Windows leading separators can be a lot of things...
// So we need to do this instead of just checking the first char.
var leading_separator: []const u8 = "";
if (_platform.leadingSeparatorIndex(parts[0])) |leading_separator_i| {
leading_separator = parts[0][0 .. leading_separator_i + 1];
ignore_cwd = true;
}
if (!ignore_cwd) {
leading_separator = cwd[0 .. 1 + (_platform.leadingSeparatorIndex(_cwd) orelse unreachable)]; // cwd must be absolute
cwd = _cwd[leading_separator.len..cwd.len];
out = cwd.len;
std.debug.assert(out < buf.len);
std.mem.copy(u8, buf[0..out], cwd);
}
for (parts) |part, i| {
// This never returns leading separators.
var normalized_part = normalizeString(part, true, _platform);
if (normalized_part.len == 0) {
continue;
}
switch (_platform.resolve()) {
.windows => {
buf[out] = std.fs.path.sep_windows;
},
else => {
buf[out] = std.fs.path.sep_posix;
},
}
out += 1;
const start = out;
out += normalized_part.len;
std.debug.assert(out < buf.len);
std.mem.copy(u8, buf[start..out], normalized_part);
}
// One last normalization, to remove any ../ added
const result = normalizeStringBuf(buf[0..out], parser_buffer[leading_separator.len..parser_buffer.len], false, _platform);
std.mem.copy(u8, buf[0..leading_separator.len], leading_separator);
std.mem.copy(u8, buf[leading_separator.len .. result.len + leading_separator.len], result);
return buf[0 .. result.len + leading_separator.len];
}
pub fn isSepPosix(char: u8) bool {
return char == std.fs.path.sep_posix;
}
pub fn isSepWin32(char: u8) bool {
return char == std.fs.path.sep_windows;
}
pub fn isSepAny(char: u8) bool {
return @call(.{ .modifier = .always_inline }, isSepPosix, .{char}) or @call(.{ .modifier = .always_inline }, isSepWin32, .{char});
}
pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize {
return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_windows);
}
pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize {
return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_posix);
}
pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize {
return std.mem.lastIndexOfAny(u8, slice, "/\\");
}
pub fn normalizeStringPosix(str: []const u8, comptime allow_above_root: bool) []u8 {
return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_posix, isSepPosix, lastIndexOfSeparatorPosix);
}
pub fn normalizeStringPosixBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 {
return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_posix, isSepPosix, lastIndexOfSeparatorPosix);
}
pub fn normalizeStringWindows(str: []const u8, comptime allow_above_root: bool) []u8 {
return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_windows, isSepWin32, lastIndexOfSeparatorWindows);
}
pub fn normalizeStringWindowsBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 {
return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_windows, isSepWin32, lastIndexOfSeparatorWindows);
}
pub fn normalizeStringLoose(str: []const u8, comptime allow_above_root: bool) []u8 {
return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_posix, isSepAny, lastIndexOfSeparatorLoose);
}
pub fn normalizeStringLooseBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 {
return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_posix, isSepAny, lastIndexOfSeparatorLoose);
}
test "normalizeAndJoinStringPosix" {
var t = tester.Tester.t(std.heap.c_allocator);
defer t.report(@src());
const string = []const u8;
const cwd = "/Users/jarredsumner/Code/app";
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/bar/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .posix),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .posix),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .posix),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "././././foo", "././././bar././././", "../file.js" }, .posix),
@src(),
);
_ = t.expect(
"/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .posix),
@src(),
);
_ = t.expect(
"/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .posix),
@src(),
);
_ = t.expect(
"/Code/app/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .posix),
@src(),
);
}
test "normalizeAndJoinStringLoose" {
var t = tester.Tester.t(std.heap.c_allocator);
defer t.report(@src());
const string = []const u8;
const cwd = "/Users/jarredsumner/Code/app";
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/bar/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "././././foo", "././././bar././././", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Code/app/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/bar/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Users/jarredsumner/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ ".\\.\\.\\.\\foo", "././././bar././././", "..\\file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Code/app/foo/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .loose),
@src(),
);
_ = t.expect(
"/Code/app/file.js",
normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .loose),
@src(),
);
}
test "normalizeStringPosix" {
var t = tester.Tester.t(std.heap.c_allocator);
defer t.report(@src());
// Don't mess up strings that
_ = t.expect("foo/bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar.txt", true, .posix), @src());
_ = t.expect("foo/bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar.txt", false, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", true, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", false, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/././foo/././././././bar/../bar/../bar", true, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", false, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar//////", false, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/////foo/bar//////", false, .posix), @src());
_ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/////foo/bar", false, .posix), @src());
_ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "/////", false, .posix), @src());
_ = t.expect("..", try normalizeStringAlloc(std.heap.c_allocator, "../boom/../", true, .posix), @src());
_ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "./", true, .posix), @src());
}
test "normalizeStringWindows" {
var t = tester.Tester.t(std.heap.c_allocator);
defer t.report(@src());
// Don't mess up strings that
_ = t.expect("foo\\bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar.txt", true, .windows), @src());
_ = t.expect("foo\\bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar.txt", false, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", true, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", false, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\.\\.\\foo\\.\\.\\.\\.\\.\\.\\bar\\..\\bar\\..\\bar", true, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", false, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar\\\\\\\\\\\\", false, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\foo\\bar\\\\\\\\\\\\", false, .windows), @src());
_ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\foo\\bar", false, .windows), @src());
_ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\", false, .windows), @src());
_ = t.expect("..", try normalizeStringAlloc(std.heap.c_allocator, "..\\boom\\..\\", true, .windows), @src());
_ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, ".\\", true, .windows), @src());
}

View File

@@ -15,59 +15,7 @@ const hash_map_v2 = @import("../hash_map_v2.zig");
const Mutex = sync.Mutex;
const StringBoolMap = std.StringHashMap(bool);
// https://en.wikipedia.org/wiki/.bss#BSS_in_C
pub fn BSSSectionAllocator(comptime size: usize) type {
const FixedBufferAllocator = std.heap.FixedBufferAllocator;
return struct {
var backing_buf: [size]u8 = undefined;
var fixed_buffer_allocator = FixedBufferAllocator.init(&backing_buf);
var buf_allocator = &fixed_buffer_allocator.allocator;
const Allocator = std.mem.Allocator;
const Self = @This();
allocator: Allocator,
fallback_allocator: *Allocator,
pub fn get(self: *Self) *Allocator {
return &self.allocator;
}
pub fn init(fallback_allocator: *Allocator) Self {
return Self{ .fallback_allocator = fallback_allocator, .allocator = Allocator{
.allocFn = BSSSectionAllocator(size).alloc,
.resizeFn = BSSSectionAllocator(size).resize,
} };
}
pub fn alloc(
allocator: *Allocator,
len: usize,
ptr_align: u29,
len_align: u29,
return_address: usize,
) error{OutOfMemory}![]u8 {
const self = @fieldParentPtr(Self, "allocator", allocator);
return buf_allocator.allocFn(buf_allocator, len, ptr_align, len_align, return_address) catch
return self.fallback_allocator.allocFn(self.fallback_allocator, len, ptr_align, len_align, return_address);
}
pub fn resize(
allocator: *Allocator,
buf: []u8,
buf_align: u29,
new_len: usize,
len_align: u29,
return_address: usize,
) error{OutOfMemory}!usize {
const self = @fieldParentPtr(Self, "allocator", allocator);
if (fixed_buffer_allocator.ownsPtr(buf.ptr)) {
return fixed_buffer_allocator.allocator.resizeFn(&fixed_buffer_allocator.allocator, buf, buf_align, new_len, len_align, return_address);
} else {
return self.fallback_allocator.resizeFn(self.fallback_allocator, buf, buf_align, new_len, len_align, return_address);
}
}
};
}
const allocators = @import("../allocators.zig");
const Path = Fs.Path;
@@ -84,11 +32,11 @@ pub const DirInfo = struct {
// These objects are immutable, so we can just point to the parent directory
// and avoid having to lock the cache again
parent: Index = HashMap.NotFound,
parent: Index = allocators.NotFound,
// A pointer to the enclosing dirInfo with a valid "browser" field in
// package.json. We need this to remap paths after they have been resolved.
enclosing_browser_scope: Index = HashMap.NotFound,
enclosing_browser_scope: Index = allocators.NotFound,
abs_path: string = "",
entries: Fs.FileSystem.DirEntry = undefined,
@@ -98,14 +46,10 @@ pub const DirInfo = struct {
abs_real_path: string = "", // If non-empty, this is the real absolute path resolving any symlinks
pub fn getParent(i: *DirInfo) ?*DirInfo {
if (i.parent == HashMap.NotFound) return null;
std.debug.assert(i.parent < HashMap.instance._data.len);
return &HashMap.instance._data.items(.value)[i.parent];
return HashMap.instance.atIndex(i.parent);
}
pub fn getEnclosingBrowserScope(i: *DirInfo) ?*DirInfo {
if (i.enclosing_browser_scope == HashMap.NotFound) return null;
std.debug.assert(i.enclosing_browser_scope < HashMap.instance._data.len);
return &HashMap.instance._data.items(.value)[i.enclosing_browser_scope];
return HashMap.instance.atIndex(i.enclosing_browser_scope);
}
// Goal: Really fast, low allocation directory map exploiting cache locality where we don't worry about lifetimes much.
@@ -113,98 +57,7 @@ pub const DirInfo = struct {
// 2. Don't expect a provided key to exist after it's queried
// 3. Store whether a directory has been queried and whether that query was successful.
// 4. Allocate onto the https://en.wikipedia.org/wiki/.bss#BSS_in_C instead of the heap, so we can avoid memory leaks
pub const HashMap = struct {
// In a small next.js app with few additional dependencies, there are 191 directories in the node_modules folder
// fd . -d 9999 -L -t d --no-ignore | wc -l
const PreallocatedCount = 256;
const StringAllocatorSize = 128 * PreallocatedCount;
const FallbackStringAllocator = BSSSectionAllocator(StringAllocatorSize);
const FallbackAllocatorSize = @divExact(@bitSizeOf(Entry), 8) * PreallocatedCount;
const FallbackAllocator = BSSSectionAllocator(FallbackAllocatorSize);
const BackingHashMap = std.AutoHashMapUnmanaged(u64, Index);
pub const Entry = struct {
key: string,
value: DirInfo,
};
string_allocator: FallbackStringAllocator,
fallback_allocator: FallbackAllocator,
allocator: *std.mem.Allocator,
_data: std.MultiArrayList(Entry),
hash_map: BackingHashMap,
const Seed = 999;
pub const NotFound: Index = std.math.maxInt(Index);
var instance: HashMap = undefined;
pub fn at(d: *HashMap, index: Index) *DirInfo {
return &d._data.items(.value)[index];
}
pub const Result = struct {
index: Index = NotFound,
hash: u64 = 0,
status: Status = Status.unknown,
pub const Status = enum { unknown, not_found, exists };
};
// pub fn get(d: *HashMap, key: string) Result {
// const _key = Wyhash.hash(Seed, key);
// const index = d.hash_map.get(_key) orelse return Result{};
// return d._data.items(.value)[index];
// }
pub fn getOrPut(
d: *HashMap,
key: string,
) Result {
const _key = Wyhash.hash(Seed, key);
const index = d.hash_map.get(_key) orelse return Result{
.index = std.math.maxInt(u32),
.status = .unknown,
.hash = _key,
};
if (index == NotFound) {
return Result{ .index = NotFound, .status = .not_found, .hash = _key };
}
return Result{ .index = index, .status = .exists, .hash = _key };
}
pub fn put(d: *HashMap, hash: u64, key: string, value: DirInfo) *DirInfo {
const entry = Entry{
.value = value,
.key = d.string_allocator.get().dupe(u8, key) catch unreachable,
};
const index = d._data.len;
d._data.append(d.fallback_allocator.get(), entry) catch unreachable;
d.hash_map.put(d.allocator, hash, @intCast(DirInfo.Index, index)) catch unreachable;
return &d._data.items(.value)[index];
}
pub fn init(allocator: *std.mem.Allocator) *HashMap {
var list = std.MultiArrayList(Entry){};
instance = HashMap{
._data = undefined,
.string_allocator = FallbackStringAllocator.init(allocator),
.allocator = allocator,
.hash_map = BackingHashMap{},
.fallback_allocator = FallbackAllocator.init(allocator),
};
list.ensureTotalCapacity(instance.allocator, PreallocatedCount) catch unreachable;
instance._data = list;
return &instance;
}
pub fn markNotFound(d: *HashMap, hash: u64) void {
d.hash_map.put(d.allocator, hash, NotFound) catch unreachable;
}
pub fn deinit(i: *HashMap) void {
i._data.deinit(i.allocator);
i.hash_map.deinit(i.allocator);
}
};
pub const HashMap = allocators.BSSMap(DirInfo, 1024, true, 128);
};
pub const TemporaryBuffer = struct {
pub threadlocal var ExtensionPathBuf = std.mem.zeroes([512]u8);
@@ -578,7 +431,7 @@ pub const Resolver = struct {
if (check_relative) {
const parts = [_]string{ source_dir, import_path };
const abs_path = std.fs.path.join(r.allocator, &parts) catch unreachable;
const abs_path = r.fs.join(&parts);
if (r.opts.external.abs_paths.count() > 0 and r.opts.external.abs_paths.exists(abs_path)) {
// If the string literal in the source text is an absolute path and has
@@ -760,7 +613,7 @@ pub const Resolver = struct {
// Try looking up the path relative to the base URL
if (tsconfig.base_url) |base| {
const paths = [_]string{ base, import_path };
const abs = std.fs.path.join(r.allocator, &paths) catch unreachable;
const abs = r.fs.join(paths);
if (r.loadAsFileOrDirectory(abs, kind)) |res| {
return res;
@@ -775,7 +628,7 @@ pub const Resolver = struct {
// don't ever want to search for "node_modules/node_modules"
if (dir_info.has_node_modules) {
var _paths = [_]string{ dir_info.abs_path, "node_modules", import_path };
const abs_path = std.fs.path.join(r.allocator, &_paths) catch unreachable;
const abs_path = r.fs.join(&_paths);
if (r.debug_logs) |*debug| {
debug.addNoteFmt("Checking for a package in the directory \"{s}\"", .{abs_path}) catch {};
}
@@ -802,7 +655,7 @@ pub const Resolver = struct {
return r.loadNodeModules(import_path, kind, source_dir_info);
} else {
const paths = [_]string{ source_dir_info.abs_path, import_path };
var resolved = std.fs.path.join(r.allocator, &paths) catch unreachable;
var resolved = r.fs.join(&paths);
return r.loadAsFileOrDirectory(resolved, kind);
}
}
@@ -825,7 +678,7 @@ pub const Resolver = struct {
// // // Skip "node_modules" folders
// // if (!strings.eql(std.fs.path.basename(current), "node_modules")) {
// // var paths1 = [_]string{ current, "node_modules", extends };
// // var join1 = std.fs.path.join(ctx.r.allocator, &paths1) catch unreachable;
// // var join1 = r.fs.joinAlloc(ctx.r.allocator, &paths1) catch unreachable;
// // const res = ctx.r.parseTSConfig(join1, ctx.visited) catch |err| {
// // if (err == error.ENOENT) {
// // continue;
@@ -857,14 +710,14 @@ pub const Resolver = struct {
// this might leak
if (!std.fs.path.isAbsolute(base)) {
const paths = [_]string{ file_dir, base };
result.base_url = std.fs.path.join(r.allocator, &paths) catch unreachable;
result.base_url = r.fs.joinAlloc(r.allocator, &paths) catch unreachable;
}
}
if (result.paths.count() > 0 and (result.base_url_for_paths.len == 0 or !std.fs.path.isAbsolute(result.base_url_for_paths))) {
// this might leak
const paths = [_]string{ file_dir, result.base_url.? };
result.base_url_for_paths = std.fs.path.join(r.allocator, &paths) catch unreachable;
result.base_url_for_paths = r.fs.joinAlloc(r.allocator, &paths) catch unreachable;
}
return result;
@@ -888,10 +741,13 @@ pub const Resolver = struct {
}
fn dirInfoCached(r: *Resolver, path: string) !?*DirInfo {
var dir_info_entry = r.dir_cache.getOrPut(
path,
);
var dir_info_entry = try r.dir_cache.getOrPut(path);
var ptr = try r.dirInfoCachedGetOrPut(path, &dir_info_entry);
return ptr;
}
fn dirInfoCachedGetOrPut(r: *Resolver, path: string, dir_info_entry: *allocators.Result) !?*DirInfo {
switch (dir_info_entry.status) {
.unknown => {
return try r.dirInfoUncached(path, dir_info_entry);
@@ -900,7 +756,7 @@ pub const Resolver = struct {
return null;
},
.exists => {
return r.dir_cache.at(dir_info_entry.index);
return r.dir_cache.atIndex(dir_info_entry.index);
},
}
// if (__entry.found_existing) {
@@ -953,7 +809,7 @@ pub const Resolver = struct {
if (!std.fs.path.isAbsolute(absolute_original_path)) {
const parts = [_]string{ abs_base_url, original_path };
absolute_original_path = std.fs.path.join(r.allocator, &parts) catch unreachable;
absolute_original_path = r.fs.joinAlloc(r.allocator, &parts) catch unreachable;
was_alloc = true;
}
@@ -1032,12 +888,12 @@ pub const Resolver = struct {
const region = TemporaryBuffer.TSConfigMatchPathBuf[0..total_length];
// Load the original path relative to the "baseUrl" from tsconfig.json
var absolute_original_path = region;
var absolute_original_path: string = region;
var did_allocate = false;
if (!std.fs.path.isAbsolute(region)) {
const paths = [_]string{ abs_base_url, original_path };
absolute_original_path = std.fs.path.join(r.allocator, &paths) catch unreachable;
var paths = [_]string{ abs_base_url, original_path };
absolute_original_path = r.fs.joinAlloc(r.allocator, &paths) catch unreachable;
did_allocate = true;
} else {
absolute_original_path = std.mem.dupe(r.allocator, u8, region) catch unreachable;
@@ -1059,7 +915,7 @@ pub const Resolver = struct {
pub fn checkBrowserMap(r: *Resolver, pkg: *PackageJSON, input_path: string) ?string {
// Normalize the path so we can compare against it without getting confused by "./"
var cleaned = Path.normalizeNoAlloc(input_path, true);
var cleaned = r.fs.normalize(input_path);
const original_cleaned = cleaned;
if (cleaned.len == 1 and cleaned[0] == '.') {
@@ -1132,7 +988,7 @@ pub const Resolver = struct {
// Is the path disabled?
if (remap.len == 0) {
const paths = [_]string{ path, field_rel_path };
const new_path = std.fs.path.join(r.allocator, &paths) catch unreachable;
const new_path = r.fs.joinAlloc(r.allocator, &paths) catch unreachable;
var _path = Path.init(new_path);
_path.is_disabled = true;
return MatchResult{
@@ -1147,7 +1003,7 @@ pub const Resolver = struct {
}
}
const _paths = [_]string{ field_rel_path, path };
const field_abs_path = std.fs.path.join(r.allocator, &_paths) catch unreachable;
const field_abs_path = r.fs.joinAlloc(r.allocator, &_paths) catch unreachable;
const field_dir_info = (r.dirInfoCached(field_abs_path) catch null) orelse {
r.allocator.free(field_abs_path);
@@ -1171,7 +1027,7 @@ pub const Resolver = struct {
if (dir_info.entries.get(base)) |lookup| {
if (lookup.entry.kind(rfs) == .file) {
const parts = [_]string{ path, base };
const out_buf = std.fs.path.join(r.allocator, &parts) catch unreachable;
const out_buf = r.fs.joinAlloc(r.allocator, &parts) catch unreachable;
if (r.debug_logs) |*debug| {
debug.addNoteFmt("Found file: \"{s}\"", .{out_buf}) catch unreachable;
}
@@ -1197,7 +1053,7 @@ pub const Resolver = struct {
// This doesn't really make sense to me.
if (remap.len == 0) {
const paths = [_]string{ path, field_rel_path };
const new_path = std.fs.path.join(r.allocator, &paths) catch unreachable;
const new_path = r.fs.joinAlloc(r.allocator, &paths) catch unreachable;
var _path = Path.init(new_path);
_path.is_disabled = true;
return MatchResult{
@@ -1208,7 +1064,7 @@ pub const Resolver = struct {
}
const new_paths = [_]string{ path, remap };
const remapped_abs = std.fs.path.join(r.allocator, &new_paths) catch unreachable;
const remapped_abs = r.fs.joinAlloc(r.allocator, &new_paths) catch unreachable;
// Is this a file
if (r.loadAsFile(remapped_abs, extension_order)) |file_result| {
@@ -1347,13 +1203,13 @@ pub const Resolver = struct {
}
}
// Read the directory entries once to minimize locking
const dir_path = std.fs.path.dirname(path) orelse unreachable; // Expected path to be a file.
const dir_entry: Fs.FileSystem.RealFS.EntriesOption = r.fs.fs.readDirectory(dir_path) catch {
const dir_path = std.fs.path.dirname(path) orelse "/";
const dir_entry: *Fs.FileSystem.RealFS.EntriesOption = rfs.readDirectory(dir_path, null, false) catch {
return null;
};
if (@as(Fs.FileSystem.RealFS.EntriesOption.Tag, dir_entry) == .err) {
if (@as(Fs.FileSystem.RealFS.EntriesOption.Tag, dir_entry.*) == .err) {
if (dir_entry.err.original_err != error.ENOENT) {
r.log.addErrorFmt(
null,
@@ -1383,8 +1239,9 @@ pub const Resolver = struct {
if (r.debug_logs) |*debug| {
debug.addNoteFmt("Found file \"{s}\" ", .{base}) catch {};
}
return LoadResult{ .path = path, .diff_case = query.diff_case };
const abs_path_parts = [_]string{ query.entry.dir, query.entry.base };
const abs_path = r.fs.joinAlloc(r.allocator, &abs_path_parts) catch unreachable;
return LoadResult{ .path = abs_path, .diff_case = query.diff_case };
}
}
@@ -1467,74 +1324,96 @@ pub const Resolver = struct {
return null;
}
fn dirInfoUncached(r: *Resolver, path: string, result: DirInfo.HashMap.Result) anyerror!?*DirInfo {
fn dirInfoUncached(r: *Resolver, unsafe_path: string, result: *allocators.Result) anyerror!?*DirInfo {
var rfs: *Fs.FileSystem.RealFS = &r.fs.fs;
var parent: ?*DirInfo = null;
const parent_dir = std.fs.path.dirname(path) orelse {
r.dir_cache.markNotFound(result.hash);
return null;
var is_root = false;
const parent_dir = (std.fs.path.dirname(unsafe_path) orelse parent_dir_handle: {
is_root = true;
break :parent_dir_handle "/";
});
var parent_result: allocators.Result = allocators.Result{
.hash = std.math.maxInt(u64),
.index = allocators.NotFound,
.status = .unknown,
};
if (!is_root and !strings.eql(parent_dir, unsafe_path)) {
parent = r.dirInfoCached(parent_dir) catch null;
var parent_result: DirInfo.HashMap.Result = undefined;
if (parent_dir.len > 1 and !strings.eql(parent_dir, path)) {
parent = (try r.dirInfoCached(parent_dir)) orelse {
r.dir_cache.markNotFound(result.hash);
return null;
};
parent_result = r.dir_cache.getOrPut(parent_dir);
if (parent != null) {
parent_result = try r.dir_cache.getOrPut(parent_dir);
}
}
var entries: Fs.FileSystem.DirEntry = Fs.FileSystem.DirEntry.empty(unsafe_path, r.allocator);
// List the directories
var _entries = try rfs.readDirectory(path);
var entries: @TypeOf(_entries.entries) = undefined;
if (std.meta.activeTag(_entries) == .err) {
// Just pretend this directory is empty if we can't access it. This is the
// case on Unix for directories that only have the execute permission bit
// set. It means we will just pass through the empty directory and
// continue to check the directories above it, which is now node behaves.
switch (_entries.err.original_err) {
error.EACCESS => {
entries = Fs.FileSystem.DirEntry.empty(path, r.allocator);
},
if (!is_root) {
var _entries: *Fs.FileSystem.RealFS.EntriesOption = undefined;
// Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves
// as if there is nothing there at all instead of causing an error due to
// the directory actually being a file. This is a workaround for situations
// where people try to import from a path containing a file as a parent
// directory. The "pnpm" package manager generates a faulty "NODE_PATH"
// list which contains such paths and treating them as missing means we just
// ignore them during path resolution.
error.ENOENT,
error.ENOTDIR,
=> {},
else => {
const pretty = r.prettyPath(Path.init(path));
r.log.addErrorFmt(
null,
logger.Loc{},
r.allocator,
"Cannot read directory \"{s}\": {s}",
.{
pretty,
@errorName(_entries.err.original_err),
},
) catch {};
r.dir_cache.markNotFound(result.hash);
return null;
},
_entries = try rfs.readDirectory(unsafe_path, null, true);
if (std.meta.activeTag(_entries.*) == .err) {
// Just pretend this directory is empty if we can't access it. This is the
// case on Unix for directories that only have the execute permission bit
// set. It means we will just pass through the empty directory and
// continue to check the directories above it, which is now node behaves.
switch (_entries.err.original_err) {
error.EACCESS => {
entries = Fs.FileSystem.DirEntry.empty(unsafe_path, r.allocator);
},
// Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves
// as if there is nothing there at all instead of causing an error due to
// the directory actually being a file. This is a workaround for situations
// where people try to import from a path containing a file as a parent
// directory. The "pnpm" package manager generates a faulty "NODE_PATH"
// list which contains such paths and treating them as missing means we just
// ignore them during path resolution.
error.ENOENT,
error.ENOTDIR,
error.IsDir,
=> {
entries = Fs.FileSystem.DirEntry.empty(unsafe_path, r.allocator);
},
else => {
const pretty = r.prettyPath(Path.init(unsafe_path));
result.status = .not_found;
r.log.addErrorFmt(
null,
logger.Loc{},
r.allocator,
"Cannot read directory \"{s}\": {s}",
.{
pretty,
@errorName(_entries.err.original_err),
},
) catch {};
r.dir_cache.markNotFound(result.*);
return null;
},
}
} else {
entries = _entries.entries;
}
} else {
entries = _entries.entries;
}
var info = DirInfo{
.abs_path = path,
.parent = if (parent != null) parent_result.index else DirInfo.HashMap.NotFound,
.entries = entries,
var info = dir_info_getter: {
var _info = DirInfo{
.abs_path = "",
.parent = parent_result.index,
.entries = entries,
};
result.status = .exists;
var __info = try r.dir_cache.put(unsafe_path, true, result, _info);
__info.abs_path = r.dir_cache.keyAtIndex(result.index).?;
break :dir_info_getter __info;
};
const path = info.abs_path;
// A "node_modules" directory isn't allowed to directly contain another "node_modules" directory
var base = std.fs.path.basename(path);
if (!strings.eqlComptime(base, "node_modules")) {
@@ -1544,29 +1423,31 @@ pub const Resolver = struct {
}
}
// Propagate the browser scope into child directories
if (parent) |parent_info| {
info.enclosing_browser_scope = parent_info.enclosing_browser_scope;
if (parent_result.status != .unknown) {
// Propagate the browser scope into child directories
if (parent) |parent_info| {
info.enclosing_browser_scope = parent_info.enclosing_browser_scope;
// Make sure "absRealPath" is the real path of the directory (resolving any symlinks)
if (!r.opts.preserve_symlinks) {
if (parent_info.entries.get(base)) |lookup| {
const entry = lookup.entry;
// Make sure "absRealPath" is the real path of the directory (resolving any symlinks)
if (!r.opts.preserve_symlinks) {
if (parent_info.entries.get(base)) |lookup| {
const entry = lookup.entry;
var symlink = entry.symlink(rfs);
if (symlink.len > 0) {
if (r.debug_logs) |*logs| {
try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable);
var symlink = entry.symlink(rfs);
if (symlink.len > 0) {
if (r.debug_logs) |*logs| {
try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable);
}
info.abs_real_path = symlink;
} else if (parent_info.abs_real_path.len > 0) {
// this might leak a little i'm not sure
const parts = [_]string{ parent_info.abs_real_path, base };
symlink = r.fs.joinAlloc(r.allocator, &parts) catch unreachable;
if (r.debug_logs) |*logs| {
try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable);
}
info.abs_real_path = symlink;
}
info.abs_real_path = symlink;
} else if (parent_info.abs_real_path.len > 0) {
// this might leak a little i'm not sure
const parts = [_]string{ parent_info.abs_real_path, base };
symlink = std.fs.path.join(r.allocator, &parts) catch unreachable;
if (r.debug_logs) |*logs| {
try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable);
}
info.abs_real_path = symlink;
}
}
}
@@ -1580,8 +1461,7 @@ pub const Resolver = struct {
if (info.package_json) |pkg| {
if (pkg.browser_map.count() > 0) {
// it has not been written yet, so we reserve the next index
info.enclosing_browser_scope = @intCast(DirInfo.Index, DirInfo.HashMap.instance._data.len);
info.enclosing_browser_scope = result.index;
}
if (r.debug_logs) |*logs| {
@@ -1601,7 +1481,7 @@ pub const Resolver = struct {
const entry = lookup.entry;
if (entry.kind(rfs) == .file) {
const parts = [_]string{ path, "tsconfig.json" };
tsconfig_path = try std.fs.path.join(r.allocator, &parts);
tsconfig_path = try r.fs.joinAlloc(r.allocator, &parts);
}
}
if (tsconfig_path == null) {
@@ -1609,7 +1489,7 @@ pub const Resolver = struct {
const entry = lookup.entry;
if (entry.kind(rfs) == .file) {
const parts = [_]string{ path, "jsconfig.json" };
tsconfig_path = try std.fs.path.join(r.allocator, &parts);
tsconfig_path = try r.fs.joinAlloc(r.allocator, &parts);
}
}
}
@@ -1625,7 +1505,7 @@ pub const Resolver = struct {
if (err == error.ENOENT) {
r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot find tsconfig file \"{s}\"", .{pretty}) catch unreachable;
} else if (err != error.ParseErrorAlreadyLogged) {
} else if (err != error.ParseErrorAlreadyLogged and err != error.IsDir) {
r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ pretty, @errorName(err) }) catch unreachable;
}
break :brk null;
@@ -1637,7 +1517,6 @@ pub const Resolver = struct {
info.tsconfig_json = parent.?.tsconfig_json;
}
info.entries = entries;
return r.dir_cache.put(result.hash, path, info);
return info;
}
};

2342
src/test/fixtures/exports-bug.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
Object.assign(Interpolant.prototype, {
evaluate: function (t) {
var pp = this.parameterPositions,
i1 = this._cachedIndex,
t1 = pp[i1],
t0 = pp[i1 - 1];
validate_interval: {
seek: {
var right;
linear_scan: {
//- See http://jsperf.com/comparison-to-undefined/3
//- slower code:
//-
//- if ( t >= t1 || t1 === undefined ) {
forward_scan: if (!(t < t1)) {
for (var giveUpAt = i1 + 2; ; ) {
if (t1 === undefined) {
if (t < t0) break forward_scan;
// after end
i1 = pp.length;
this._cachedIndex = i1;
return this.afterEnd_(i1 - 1, t, t0);
}
if (i1 === giveUpAt) break; // this loop
t0 = t1;
t1 = pp[++i1];
if (t < t1) {
// we have arrived at the sought interval
break seek;
}
}
// prepare binary search on the right side of the index
right = pp.length;
break linear_scan;
}
//- slower code:
//- if ( t < t0 || t0 === undefined ) {
if (!(t >= t0)) {
// looping?
var t1global = pp[1];
if (t < t1global) {
i1 = 2; // + 1, using the scan for the details
t0 = t1global;
}
// linear reverse scan
for (var giveUpAt = i1 - 2; ; ) {
if (t0 === undefined) {
// before start
this._cachedIndex = 0;
return this.beforeStart_(0, t, t1);
}
if (i1 === giveUpAt) break; // this loop
t1 = t0;
t0 = pp[--i1 - 1];
if (t >= t0) {
// we have arrived at the sought interval
break seek;
}
}
// prepare binary search on the left side of the index
right = i1;
i1 = 0;
break linear_scan;
}
// the interval is valid
break validate_interval;
} // linear scan
// binary search
while (i1 < right) {
var mid = (i1 + right) >>> 1;
if (t < pp[mid]) {
right = mid;
} else {
i1 = mid + 1;
}
}
t1 = pp[i1];
t0 = pp[i1 - 1];
// check boundary cases, again
if (t0 === undefined) {
this._cachedIndex = 0;
return this.beforeStart_(0, t, t1);
}
if (t1 === undefined) {
i1 = pp.length;
this._cachedIndex = i1;
return this.afterEnd_(i1 - 1, t0, t);
}
} // seek
this._cachedIndex = i1;
this.intervalChanged_(i1, t0, t1);
} // validate_interval
return this.interpolate_(i1, t0, t, t1);
},
});

16
src/test/fixtures/symbols-bug.js vendored Normal file
View File

@@ -0,0 +1,16 @@
var boom = {
a: 2,
b: "4",
c: "6",
d: "8",
e: "10",
f: 12,
g: 14,
}["15"];
const foo = "bacon";
const james = "not-bacon";
const lordy = "sammy";
const boop = {
hey: { foo },
};

View File

@@ -66,6 +66,10 @@ pub const Tester = struct {
}
pub fn evaluate_outcome(self: *const @This()) Outcome {
if (self.expected.len > self.result.len) {
return .fail;
}
for (self.expected) |char, i| {
if (char != self.result[i]) {
return Outcome.fail;