Files
bun.sh/src/semver/Version.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

996 lines
37 KiB
Zig

pub const Version = VersionType(u64);
pub const OldV2Version = VersionType(u32);
pub fn VersionType(comptime IntType: type) type {
return extern struct {
major: IntType = 0,
minor: IntType = 0,
patch: IntType = 0,
_tag_padding: [if (IntType == u32) 4 else 0]u8 = .{0} ** if (IntType == u32) 4 else 0, // [see padding_checker.zig]
tag: Tag = .{},
const This = @This();
pub fn migrate(this: This) VersionType(u64) {
if (comptime IntType != u32) {
@compileError("unexpected IntType");
}
return .{
.major = this.major,
.minor = this.minor,
.patch = this.patch,
._tag_padding = .{},
.tag = .{
.pre = this.tag.pre,
.build = this.tag.build,
},
};
}
/// Assumes that there is only one buffer for all the strings
pub fn sortGt(ctx: []const u8, lhs: This, rhs: This) bool {
return orderFn(ctx, lhs, rhs) == .gt;
}
pub fn orderFn(ctx: []const u8, lhs: This, rhs: This) std.math.Order {
return lhs.order(rhs, ctx, ctx);
}
pub fn isZero(this: This) bool {
return this.patch == 0 and this.minor == 0 and this.major == 0;
}
pub fn parseUTF8(slice: []const u8) ParseResult {
return parse(.{ .buf = slice, .slice = slice });
}
pub fn cloneInto(this: This, slice: []const u8, buf: *[]u8) This {
return .{
.major = this.major,
.minor = this.minor,
.patch = this.patch,
.tag = this.tag.cloneInto(slice, buf),
};
}
pub inline fn len(this: *const This) u32 {
return this.tag.build.len + this.tag.pre.len;
}
pub const Formatter = struct {
version: This,
input: string,
pub fn format(formatter: Formatter, writer: *std.Io.Writer) !void {
const self = formatter.version;
try writer.print("{d}.{d}.{d}", .{ self.major, self.minor, self.patch });
if (self.tag.hasPre()) {
const pre = self.tag.pre.slice(formatter.input);
try writer.writeAll("-");
try writer.writeAll(pre);
}
if (self.tag.hasBuild()) {
const build = self.tag.build.slice(formatter.input);
try writer.writeAll("+");
try writer.writeAll(build);
}
}
};
pub fn fmt(this: This, input: string) Formatter {
return .{ .version = this, .input = input };
}
pub const DiffFormatter = struct {
version: This,
buf: string,
other: This,
other_buf: string,
pub fn format(this: DiffFormatter, writer: *std.Io.Writer) !void {
if (!Output.enable_ansi_colors_stdout) {
// print normally if no colors
const formatter: Formatter = .{ .version = this.version, .input = this.buf };
return Formatter.format(formatter, writer);
}
const diff = this.version.whichVersionIsDifferent(this.other, this.buf, this.other_buf) orelse .none;
switch (diff) {
.major => try writer.print(Output.prettyFmt("<r><b><red>{d}.{d}.{d}", true), .{
this.version.major, this.version.minor, this.version.patch,
}),
.minor => {
if (this.version.major == 0) {
try writer.print(Output.prettyFmt("<d>{d}.<r><b><red>{d}.{d}", true), .{
this.version.major, this.version.minor, this.version.patch,
});
} else {
try writer.print(Output.prettyFmt("<d>{d}.<r><b><yellow>{d}.{d}", true), .{
this.version.major, this.version.minor, this.version.patch,
});
}
},
.patch => {
if (this.version.major == 0 and this.version.minor == 0) {
try writer.print(Output.prettyFmt("<d>{d}.{d}.<r><b><red>{d}", true), .{
this.version.major, this.version.minor, this.version.patch,
});
} else {
try writer.print(Output.prettyFmt("<d>{d}.{d}.<r><b><green>{d}", true), .{
this.version.major, this.version.minor, this.version.patch,
});
}
},
.none, .pre, .build => try writer.print(Output.prettyFmt("<d>{d}.{d}.{d}", true), .{
this.version.major, this.version.minor, this.version.patch,
}),
}
// might be pre or build. loop through all characters, and insert <red> on
// first diff.
var set_color = false;
if (this.version.tag.hasPre()) {
if (this.other.tag.hasPre()) {
const pre = this.version.tag.pre.slice(this.buf);
const other_pre = this.other.tag.pre.slice(this.other_buf);
var first = true;
for (pre, 0..) |c, i| {
if (!set_color and i < other_pre.len and c != other_pre[i]) {
set_color = true;
try writer.writeAll(Output.prettyFmt("<r><b><red>", true));
}
if (first) {
first = false;
try writer.writeByte('-');
}
try writer.writeByte(c);
}
} else {
try writer.print(Output.prettyFmt("<r><b><red>-{f}", true), .{this.version.tag.pre.fmt(this.buf)});
set_color = true;
}
}
if (this.version.tag.hasBuild()) {
if (this.other.tag.hasBuild()) {
const build = this.version.tag.build.slice(this.buf);
const other_build = this.other.tag.build.slice(this.other_buf);
var first = true;
for (build, 0..) |c, i| {
if (!set_color and i < other_build.len and c != other_build[i]) {
set_color = true;
try writer.writeAll(Output.prettyFmt("<r><b><red>", true));
}
if (first) {
first = false;
try writer.writeByte('+');
}
try writer.writeByte(c);
}
} else {
if (!set_color) {
try writer.print(Output.prettyFmt("<r><b><red>+{f}", true), .{this.version.tag.build.fmt(this.buf)});
} else {
try writer.print("+{f}", .{this.version.tag.build.fmt(this.other_buf)});
}
}
}
try writer.writeAll(Output.prettyFmt("<r>", true));
}
};
pub fn diffFmt(this: This, other: This, this_buf: string, other_buf: string) DiffFormatter {
return .{
.version = this,
.buf = this_buf,
.other = other,
.other_buf = other_buf,
};
}
pub const ChangedVersion = enum {
major,
minor,
patch,
pre,
build,
none,
};
pub fn whichVersionIsDifferent(
left: This,
right: This,
left_buf: string,
right_buf: string,
) ?ChangedVersion {
if (left.major != right.major) return .major;
if (left.minor != right.minor) return .minor;
if (left.patch != right.patch) return .patch;
if (left.tag.hasPre() != right.tag.hasPre()) return .pre;
if (!left.tag.hasPre() and !right.tag.hasPre()) return null;
if (left.tag.orderPre(right.tag, left_buf, right_buf) != .eq) return .pre;
if (left.tag.hasBuild() != right.tag.hasBuild()) return .build;
if (!left.tag.hasBuild() and !right.tag.hasBuild()) return null;
return if (left.tag.build.order(&right.tag.build, left_buf, right_buf) != .eq)
.build
else
null;
}
pub fn count(this: *const This, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) void {
if (this.tag.hasPre() and !this.tag.pre.isInline()) builder.count(this.tag.pre.slice(buf));
if (this.tag.hasBuild() and !this.tag.build.isInline()) builder.count(this.tag.build.slice(buf));
}
pub fn append(this: *const This, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) This {
var that = this.*;
if (this.tag.hasPre() and !this.tag.pre.isInline()) that.tag.pre = builder.append(ExternalString, this.tag.pre.slice(buf));
if (this.tag.hasBuild() and !this.tag.build.isInline()) that.tag.build = builder.append(ExternalString, this.tag.build.slice(buf));
return that;
}
pub const Partial = struct {
major: ?IntType = null,
minor: ?IntType = null,
patch: ?IntType = null,
tag: Tag = .{},
pub fn min(this: Partial) This {
return .{
.major = this.major orelse 0,
.minor = this.minor orelse 0,
.patch = this.patch orelse 0,
.tag = this.tag,
};
}
pub fn max(this: Partial) This {
return .{
.major = this.major orelse std.math.maxInt(IntType),
.minor = this.minor orelse std.math.maxInt(IntType),
.patch = this.patch orelse std.math.maxInt(IntType),
.tag = this.tag,
};
}
};
pub fn eql(lhs: This, rhs: This) bool {
return lhs.major == rhs.major and lhs.minor == rhs.minor and lhs.patch == rhs.patch and rhs.tag.eql(lhs.tag);
}
pub const PinnedVersion = enum {
major, // ^
minor, // ~
patch, // =
};
/// Modified version of pnpm's `whichVersionIsPinned`
/// https://github.com/pnpm/pnpm/blob/bc0618cf192a9cafd0ab171a3673e23ed0869bbd/packages/which-version-is-pinned/src/index.ts#L9
///
/// Differences:
/// - It's not used for workspaces
/// - `npm:` is assumed already removed from aliased versions
/// - Invalid input is considered major pinned (important because these strings are coming
/// from package.json)
///
/// The goal of this function is to avoid a complete parse of semver that's unused
pub fn whichVersionIsPinned(input: string) PinnedVersion {
const version = strings.trim(input, &strings.whitespace_chars);
var i: usize = 0;
const pinned: PinnedVersion = pinned: {
for (0..version.len) |j| {
switch (version[j]) {
// newlines & whitespace
' ',
'\t',
'\n',
'\r',
std.ascii.control_code.vt,
std.ascii.control_code.ff,
// version separators
'v',
'=',
=> {},
else => |c| {
i = j;
switch (c) {
'~', '^' => {
i += 1;
for (i..version.len) |k| {
switch (version[k]) {
' ',
'\t',
'\n',
'\r',
std.ascii.control_code.vt,
std.ascii.control_code.ff,
=> {
// `v` and `=` not included.
// `~v==1` would update to `^1.1.0` if versions `1.0.0`, `1.0.1`, `1.1.0`, and `2.0.0` are available
// note that `~` changes to `^`
},
else => {
i = k;
break :pinned if (c == '~') .minor else .major;
},
}
}
// entire version after `~` is whitespace. invalid
return .major;
},
'0'...'9' => break :pinned .patch,
// could be invalid, could also be valid range syntax (>=, ...)
// either way, pin major
else => return .major,
}
},
}
}
// entire semver is whitespace, `v`, and `=`. Invalid
return .major;
};
// `pinned` is `.major`, `.minor`, or `.patch`. Check for each version core number:
// - if major is missing, return `if (pinned == .patch) .major else pinned`
// - if minor is missing, return `if (pinned == .patch) .minor else pinned`
// - if patch is missing, return `pinned`
// - if there's whitespace or non-digit characters between core numbers, return `.major`
// - if the end is reached, return `pinned`
// major
if (i >= version.len or !std.ascii.isDigit(version[i])) return .major;
var d = version[i];
while (std.ascii.isDigit(d)) {
i += 1;
if (i >= version.len) return if (pinned == .patch) .major else pinned;
d = version[i];
}
if (d != '.') return .major;
// minor
i += 1;
if (i >= version.len or !std.ascii.isDigit(version[i])) return .major;
d = version[i];
while (std.ascii.isDigit(d)) {
i += 1;
if (i >= version.len) return if (pinned == .patch) .minor else pinned;
d = version[i];
}
if (d != '.') return .major;
// patch
i += 1;
if (i >= version.len or !std.ascii.isDigit(version[i])) return .major;
d = version[i];
while (std.ascii.isDigit(d)) {
i += 1;
// patch is done and at input end, valid
if (i >= version.len) return pinned;
d = version[i];
}
// Skip remaining valid pre/build tag characters and whitespace.
// Does not validate whitespace used inside pre/build tags.
if (!validPreOrBuildTagCharacter(d) or std.ascii.isWhitespace(d)) return .major;
i += 1;
// at this point the semver is valid so we can return true if it ends
if (i >= version.len) return pinned;
d = version[i];
while (validPreOrBuildTagCharacter(d) and !std.ascii.isWhitespace(d)) {
i += 1;
if (i >= version.len) return pinned;
d = version[i];
}
// We've come across a character that is not valid for tags or is whitespace.
// Trailing whitespace was trimmed so we can assume there's another range
return .major;
}
fn validPreOrBuildTagCharacter(c: u8) bool {
return switch (c) {
'-', '+', '.', 'A'...'Z', 'a'...'z', '0'...'9' => true,
else => false,
};
}
pub fn isTaggedVersionOnly(input: []const u8) bool {
const version = strings.trim(input, &strings.whitespace_chars);
// first needs to be a-z
if (version.len == 0 or !std.ascii.isAlphabetic(version[0])) return false;
for (1..version.len) |i| {
if (!std.ascii.isAlphanumeric(version[i])) return false;
}
return true;
}
pub fn orderWithoutTag(
lhs: This,
rhs: This,
) std.math.Order {
if (lhs.major < rhs.major) return .lt;
if (lhs.major > rhs.major) return .gt;
if (lhs.minor < rhs.minor) return .lt;
if (lhs.minor > rhs.minor) return .gt;
if (lhs.patch < rhs.patch) return .lt;
if (lhs.patch > rhs.patch) return .gt;
if (lhs.tag.hasPre()) {
if (!rhs.tag.hasPre()) return .lt;
} else {
if (rhs.tag.hasPre()) return .gt;
}
return .eq;
}
pub fn order(
lhs: This,
rhs: This,
lhs_buf: []const u8,
rhs_buf: []const u8,
) std.math.Order {
const order_without_tag = orderWithoutTag(lhs, rhs);
if (order_without_tag != .eq) return order_without_tag;
return lhs.tag.order(rhs.tag, lhs_buf, rhs_buf);
}
pub fn orderWithoutBuild(
lhs: This,
rhs: This,
lhs_buf: []const u8,
rhs_buf: []const u8,
) std.math.Order {
const order_without_tag = orderWithoutTag(lhs, rhs);
if (order_without_tag != .eq) return order_without_tag;
return lhs.tag.orderWithoutBuild(rhs.tag, lhs_buf, rhs_buf);
}
pub const Tag = extern struct {
pre: ExternalString = ExternalString{},
build: ExternalString = ExternalString{},
pub fn orderPre(lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order {
const lhs_str = lhs.pre.slice(lhs_buf);
const rhs_str = rhs.pre.slice(rhs_buf);
// 1. split each by '.', iterating through each one looking for integers
// 2. compare as integers, or if not possible compare as string
// 3. whichever is greater is the greater one
//
// 1.0.0-canary.0.0.0.0.0.0 < 1.0.0-canary.0.0.0.0.0.1
var lhs_itr = strings.split(lhs_str, ".");
var rhs_itr = strings.split(rhs_str, ".");
while (true) {
const lhs_part = lhs_itr.next();
const rhs_part = rhs_itr.next();
if (lhs_part == null and rhs_part == null) return .eq;
// if right is null, left is greater than.
if (rhs_part == null) return .gt;
// if left is null, left is less than.
if (lhs_part == null) return .lt;
const lhs_uint: ?IntType = std.fmt.parseUnsigned(IntType, lhs_part.?, 10) catch null;
const rhs_uint: ?IntType = std.fmt.parseUnsigned(IntType, rhs_part.?, 10) catch null;
// a part that doesn't parse as an integer is greater than a part that does
// https://github.com/npm/node-semver/blob/816c7b2cbfcb1986958a290f941eddfd0441139e/internal/identifiers.js#L12
if (lhs_uint != null and rhs_uint == null) return .lt;
if (lhs_uint == null and rhs_uint != null) return .gt;
if (lhs_uint == null and rhs_uint == null) {
switch (strings.order(lhs_part.?, rhs_part.?)) {
.eq => {
// continue to the next part
continue;
},
else => |not_equal| return not_equal,
}
}
switch (std.math.order(lhs_uint.?, rhs_uint.?)) {
.eq => continue,
else => |not_equal| return not_equal,
}
}
unreachable;
}
pub fn order(
lhs: Tag,
rhs: Tag,
lhs_buf: []const u8,
rhs_buf: []const u8,
) std.math.Order {
if (!lhs.pre.isEmpty() and !rhs.pre.isEmpty()) {
return lhs.orderPre(rhs, lhs_buf, rhs_buf);
}
const pre_order = lhs.pre.order(&rhs.pre, lhs_buf, rhs_buf);
if (pre_order != .eq) return pre_order;
return lhs.build.order(&rhs.build, lhs_buf, rhs_buf);
}
pub fn orderWithoutBuild(
lhs: Tag,
rhs: Tag,
lhs_buf: []const u8,
rhs_buf: []const u8,
) std.math.Order {
if (!lhs.pre.isEmpty() and !rhs.pre.isEmpty()) {
return lhs.orderPre(rhs, lhs_buf, rhs_buf);
}
return lhs.pre.order(&rhs.pre, lhs_buf, rhs_buf);
}
pub fn cloneInto(this: Tag, slice: []const u8, buf: *[]u8) Tag {
var pre: String = this.pre.value;
var build: String = this.build.value;
if (this.pre.isInline()) {
pre = this.pre.value;
} else {
const pre_slice = this.pre.slice(slice);
bun.copy(u8, buf.*, pre_slice);
pre = String.init(buf.*, buf.*[0..pre_slice.len]);
buf.* = buf.*[pre_slice.len..];
}
if (this.build.isInline()) {
build = this.build.value;
} else {
const build_slice = this.build.slice(slice);
bun.copy(u8, buf.*, build_slice);
build = String.init(buf.*, buf.*[0..build_slice.len]);
buf.* = buf.*[build_slice.len..];
}
return .{
.pre = .{
.value = pre,
.hash = this.pre.hash,
},
.build = .{
.value = build,
.hash = this.build.hash,
},
};
}
pub inline fn hasPre(this: Tag) bool {
return !this.pre.isEmpty();
}
pub inline fn hasBuild(this: Tag) bool {
return !this.build.isEmpty();
}
pub fn eql(lhs: Tag, rhs: Tag) bool {
return lhs.pre.hash == rhs.pre.hash;
}
pub const TagResult = struct {
tag: Tag = Tag{},
len: u32 = 0,
};
var multi_tag_warn = false;
// TODO: support multiple tags
pub fn parse(sliced_string: SlicedString) TagResult {
return parseWithPreCount(sliced_string, 0);
}
pub fn parseWithPreCount(sliced_string: SlicedString, initial_pre_count: u32) TagResult {
var input = sliced_string.slice;
var build_count: u32 = 0;
var pre_count: u32 = initial_pre_count;
for (input) |c| {
switch (c) {
' ' => break,
'+' => {
build_count += 1;
},
'-' => {
pre_count += 1;
},
else => {},
}
}
if (build_count == 0 and pre_count == 0) {
return TagResult{
.len = 0,
};
}
const State = enum { none, pre, build };
var result = TagResult{};
// Common case: no allocation is necessary.
var state = State.none;
var start: usize = 0;
var i: usize = 0;
while (i < input.len) : (i += 1) {
const c = input[i];
switch (c) {
'+' => {
// qualifier ::= ( '-' pre )? ( '+' build )?
if (state == .pre or state == .none and initial_pre_count > 0) {
result.tag.pre = sliced_string.sub(input[start..i]).external();
}
if (state != .build) {
state = .build;
start = i + 1;
}
},
'-' => {
if (state != .pre) {
state = .pre;
start = i + 1;
}
},
// only continue if character is a valid pre/build tag character
// https://semver.org/#spec-item-9
'a'...'z', 'A'...'Z', '0'...'9', '.' => {},
else => {
switch (state) {
.none => {},
.pre => {
result.tag.pre = sliced_string.sub(input[start..i]).external();
state = State.none;
},
.build => {
result.tag.build = sliced_string.sub(input[start..i]).external();
if (comptime Environment.isDebug) {
assert(!strings.containsChar(result.tag.build.slice(sliced_string.buf), '-'));
}
state = State.none;
},
}
result.len = @truncate(i);
break;
},
}
}
if (state == .none and initial_pre_count > 0) {
state = .pre;
start = 0;
}
switch (state) {
.none => {},
.pre => {
result.tag.pre = sliced_string.sub(input[start..i]).external();
// a pre can contain multiple consecutive tags
// checking for "-" prefix is not enough, as --canary.67e7966.0 is a valid tag
state = State.none;
},
.build => {
// a build can contain multiple consecutive tags
result.tag.build = sliced_string.sub(input[start..i]).external();
state = State.none;
},
}
result.len = @as(u32, @truncate(i));
return result;
}
};
pub const ParseResult = struct {
wildcard: Query.Token.Wildcard = .none,
valid: bool = true,
version: This.Partial = .{},
len: u32 = 0,
};
pub fn parse(sliced_string: SlicedString) ParseResult {
var input = sliced_string.slice;
var result = ParseResult{};
var part_i: u8 = 0;
var part_start_i: usize = 0;
var last_char_i: usize = 0;
if (input.len == 0) {
result.valid = false;
return result;
}
var is_done = false;
var i: usize = 0;
for (0..input.len) |c| {
switch (input[c]) {
// newlines & whitespace
' ',
'\t',
'\n',
'\r',
std.ascii.control_code.vt,
std.ascii.control_code.ff,
// version separators
'v',
'=',
=> {},
else => {
i = c;
break;
},
}
}
if (i == input.len) {
result.valid = false;
return result;
}
// two passes :(
while (i < input.len) {
if (is_done) {
break;
}
switch (input[i]) {
' ' => {
is_done = true;
break;
},
'|', '^', '#', '&', '%', '!' => {
is_done = true;
if (i > 0) {
i -= 1;
}
break;
},
'0'...'9' => {
part_start_i = i;
i += 1;
while (i < input.len and switch (input[i]) {
'0'...'9' => true,
else => false,
}) {
i += 1;
}
last_char_i = i;
switch (part_i) {
0 => {
result.version.major = parseVersionNumber(input[part_start_i..last_char_i]);
part_i = 1;
},
1 => {
result.version.minor = parseVersionNumber(input[part_start_i..last_char_i]);
part_i = 2;
},
2 => {
result.version.patch = parseVersionNumber(input[part_start_i..last_char_i]);
part_i = 3;
},
else => {},
}
if (i < input.len and switch (input[i]) {
// `.` is expected only if there are remaining core version numbers
'.' => part_i != 3,
else => false,
}) {
i += 1;
}
},
'.' => {
result.valid = false;
is_done = true;
break;
},
'-', '+' => {
// Just a plain tag with no version is invalid.
if (part_i < 2 and result.wildcard == .none) {
result.valid = false;
is_done = true;
break;
}
part_start_i = i;
while (i < input.len and switch (input[i]) {
' ' => true,
else => false,
}) {
i += 1;
}
const tag_result = Tag.parse(sliced_string.sub(input[part_start_i..]));
result.version.tag = tag_result.tag;
i += tag_result.len;
break;
},
'x', '*', 'X' => {
part_start_i = i;
i += 1;
while (i < input.len and switch (input[i]) {
'x', '*', 'X' => true,
else => false,
}) {
i += 1;
}
last_char_i = i;
if (i < input.len and switch (input[i]) {
'.' => true,
else => false,
}) {
i += 1;
}
if (result.wildcard == .none) {
switch (part_i) {
0 => {
result.wildcard = Query.Token.Wildcard.major;
part_i = 1;
},
1 => {
result.wildcard = Query.Token.Wildcard.minor;
part_i = 2;
},
2 => {
result.wildcard = Query.Token.Wildcard.patch;
part_i = 3;
},
else => {},
}
}
},
else => |c| {
// Some weirdo npm packages in the wild have a version like "1.0.0rc.1"
// npm just expects that to work...even though it has no "-" qualifier.
if (result.wildcard == .none and part_i >= 2 and switch (c) {
'a'...'z', 'A'...'Z' => true,
else => false,
}) {
part_start_i = i;
const tag_result = Tag.parseWithPreCount(sliced_string.sub(input[part_start_i..]), 1);
result.version.tag = tag_result.tag;
i += tag_result.len;
is_done = true;
last_char_i = i;
break;
}
last_char_i = 0;
result.valid = false;
is_done = true;
break;
},
}
}
if (result.wildcard == .none) {
switch (part_i) {
0 => {
result.wildcard = Query.Token.Wildcard.major;
},
1 => {
result.wildcard = Query.Token.Wildcard.minor;
},
2 => {
result.wildcard = Query.Token.Wildcard.patch;
},
else => {},
}
}
result.len = @as(u32, @intCast(i));
return result;
}
fn parseVersionNumber(input: string) ?IntType {
// max decimal u64 is 18446744073709551615
var bytes: [20]u8 = undefined;
var byte_i: u8 = 0;
assert(input[0] != '.');
for (input) |char| {
switch (char) {
'X', 'x', '*' => return null,
'0'...'9' => {
// out of bounds
if (byte_i + 1 > bytes.len) return null;
bytes[byte_i] = char;
byte_i += 1;
},
' ', '.' => break,
// ignore invalid characters
else => {},
}
}
// If there are no numbers
if (byte_i == 0) return null;
if (comptime Environment.isDebug) {
return std.fmt.parseInt(IntType, bytes[0..byte_i], 10) catch |err| {
Output.prettyErrorln("ERROR {s} parsing version: \"{s}\", bytes: {s}", .{
@errorName(err),
input,
bytes[0..byte_i],
});
return 0;
};
}
return std.fmt.parseInt(IntType, bytes[0..byte_i], 10) catch 0;
}
};
}
const string = []const u8;
const std = @import("std");
const bun = @import("bun");
const Environment = bun.Environment;
const Output = bun.Output;
const assert = bun.assert;
const strings = bun.strings;
const ExternalString = bun.Semver.ExternalString;
const Query = bun.Semver.Query;
const SlicedString = bun.Semver.SlicedString;
const String = bun.Semver.String;