From 52b8c43b8f2246d361718066dd2904941ac334e3 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 11 May 2026 06:15:55 -0500 Subject: [PATCH 1/5] Update for Zig 0.16 --- README.md | 4 +- build.zig | 12 +- build.zig.zon | 9 +- build_readme.zig | 15 +- mise.toml | 2 + src/Walk.zig | 20 ++- src/build_runner_0.16.zig | 210 +++++++++++++++++++++++++++ src/main.zig | 198 +++++++++++++------------ src/templates/build.zig.template | 6 - src/templates/build.zig.zon.template | 8 +- 10 files changed, 342 insertions(+), 142 deletions(-) create mode 100644 mise.toml create mode 100644 src/build_runner_0.16.zig diff --git a/README.md b/README.md index 2352885..1891589 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ Commands: ## Project Initialization -`zigdoc @init` scaffolds a minimal Zig project with `AGENTS.md` -plus `build.zig` and `build.zig.zon` configured for `ziglint`. +`zigdoc @init` scaffolds a minimal Zig 0.16 project with `AGENTS.md`, +`build.zig`, and `build.zig.zon`. ```bash mkdir my-project && cd my-project diff --git a/build.zig b/build.zig index 3940ecc..1a28f18 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const ziglint = @import("ziglint"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); @@ -19,7 +18,7 @@ pub fn build(b: *std.Build) void { // Generate README.md as part of the default build const readme_run = b.addRunArtifact(exe); readme_run.addArg("--help"); - const help_output = readme_run.captureStdOut(); + const help_output = readme_run.captureStdOut(.{}); const gen_readme = b.addExecutable(.{ .name = "gen_readme", @@ -66,13 +65,4 @@ pub fn build(b: *std.Build) void { }); fmt_step.dependOn(&fmt_check.step); test_step.dependOn(fmt_step); - - const lint_step = b.step("lint", "Run ziglint"); - const ziglint_dep = b.dependency("ziglint", .{ .optimize = .ReleaseFast }); - lint_step.dependOn(ziglint.addLint( - b, - ziglint_dep, - &.{ b.path("src"), b.path("build.zig"), b.path("build_readme.zig") }, - )); - test_step.dependOn(lint_step); } diff --git a/build.zig.zon b/build.zig.zon index c522f3f..2f83ddb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,13 +2,8 @@ .name = .zigdoc, .version = "0.0.0", .fingerprint = 0x50a871f441c1a7ed, - .minimum_zig_version = "0.15.1", - .dependencies = .{ - .ziglint = .{ - .url = "https://github.com/rockorager/ziglint/archive/refs/tags/v0.5.2.tar.gz", - .hash = "ziglint-0.5.2-t0bwL2FwBQC5i-ifhKKv2ls5jGXHShJpuNhFVhiU-Tt-", - }, - }, + .minimum_zig_version = "0.16.0", + .dependencies = .{}, .paths = .{ "build.zig", "build.zig.zon", diff --git a/build_readme.zig b/build_readme.zig index 03ab8ab..2cf1df6 100644 --- a/build_readme.zig +++ b/build_readme.zig @@ -1,10 +1,9 @@ const std = @import("std"); -pub fn main() !void { - const allocator = std.heap.page_allocator; +pub fn main(init: std.process.Init) !void { + const allocator = init.gpa; - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const args = try init.minimal.args.toSlice(init.arena.allocator()); if (args.len != 3) { std.debug.print("Usage: {s} \n", .{args[0]}); @@ -14,7 +13,7 @@ pub fn main() !void { const help_file = args[1]; const output_file = args[2]; - const help_content = try std.fs.cwd().readFileAlloc(allocator, help_file, 1024 * 1024); + const help_content = try std.Io.Dir.cwd().readFileAlloc(init.io, help_file, allocator, .limited(1024 * 1024)); defer allocator.free(help_content); const readme = try std.fmt.allocPrint(allocator, @@ -35,8 +34,8 @@ pub fn main() !void { \\ \\## Project Initialization \\ - \\`zigdoc @init` scaffolds a minimal Zig project with `AGENTS.md` - \\plus `build.zig` and `build.zig.zon` configured for `ziglint`. + \\`zigdoc @init` scaffolds a minimal Zig 0.16 project with `AGENTS.md`, + \\`build.zig`, and `build.zig.zon`. \\ \\```bash \\mkdir my-project && cd my-project @@ -66,5 +65,5 @@ pub fn main() !void { , .{help_content}); defer allocator.free(readme); - try std.fs.cwd().writeFile(.{ .sub_path = output_file, .data = readme }); + try std.Io.Dir.cwd().writeFile(init.io, .{ .sub_path = output_file, .data = readme }); } diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..d9786bf --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +zig = "0.16" diff --git a/src/Walk.zig b/src/Walk.zig index 4d4f06f..c1451f2 100644 --- a/src/Walk.zig +++ b/src/Walk.zig @@ -6,9 +6,11 @@ const Ast = std.zig.Ast; const assert = std.debug.assert; const log = std.log; var gpa: std.mem.Allocator = undefined; +var io: std.Io = undefined; -pub fn init(allocator: std.mem.Allocator) void { +pub fn init(allocator: std.mem.Allocator, io_arg: std.Io) void { gpa = allocator; + io = io_arg; } const Oom = error{OutOfMemory}; @@ -338,7 +340,7 @@ pub const File = struct { } const resolved_path = if (std.fs.path.isAbsolute(file_path)) - std.fs.realpathAlloc(gpa, file_path) catch file_path + std.Io.Dir.realPathFileAbsoluteAlloc(io, file_path, gpa) catch file_path else blk: { const base_path = file_index.path(); break :blk std.fs.path.resolve(gpa, &.{ @@ -360,10 +362,11 @@ pub const File = struct { node, ); } else { - const import_content = std.fs.cwd().readFileAlloc( - gpa, + const import_content = std.Io.Dir.cwd().readFileAlloc( + io, resolved_path, - 10 * 1024 * 1024, + gpa, + .limited(10 * 1024 * 1024), ) catch |err| { log.warn("import target '{s}' could not be read: {}", .{ resolved_path, err }); return .{ .global_const = node }; @@ -431,7 +434,10 @@ pub fn addFile(file_name: []const u8, bytes: []u8) !File.Index { const ast = try parse(file_name, bytes); assert(ast.errors.len == 0); - const normalized_path = std.fs.realpathAlloc(gpa, file_name) catch file_name; + const normalized_path = if (std.fs.path.isAbsolute(file_name)) + std.Io.Dir.realPathFileAbsoluteAlloc(io, file_name, gpa) catch file_name + else + std.Io.Dir.cwd().realPathFileAlloc(io, file_name, gpa) catch file_name; // Check if this file already exists to avoid duplicate entries if (files.getIndex(normalized_path)) |existing_index| { @@ -844,8 +850,6 @@ fn expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index) try expr(w, scope, parent_decl, full.ast.template); }, - .asm_legacy => {}, - .builtin_call_two, .builtin_call_two_comma, .builtin_call, diff --git a/src/build_runner_0.16.zig b/src/build_runner_0.16.zig new file mode 100644 index 0000000..69a92c7 --- /dev/null +++ b/src/build_runner_0.16.zig @@ -0,0 +1,210 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const process = std.process; + +pub const root = @import("@build"); +pub const dependencies = @import("@dependencies"); + +pub const std_options: std.Options = .{ + .side_channels_mitigations = .none, + .http_disable_tls = true, +}; + +pub fn main(init: process.Init.Minimal) !void { + var debug_gpa_state: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_gpa_state.deinit(); + const gpa = debug_gpa_state.allocator(); + + var threaded: std.Io.Threaded = .init(gpa, .{ + .environ = init.environ, + .argv0 = .init(init.args), + }); + defer threaded.deinit(); + const io = threaded.io(); + + var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer arena_instance.deinit(); + const arena = arena_instance.allocator(); + + const args = try init.args.toSlice(arena); + + // skip my own exe name + var arg_idx: usize = 1; + + const zig_exe = args[arg_idx]; + arg_idx += 1; + const zig_lib_dir = args[arg_idx]; + arg_idx += 1; + const build_root = args[arg_idx]; + arg_idx += 1; + const cache_root = args[arg_idx]; + arg_idx += 1; + const global_cache_root = args[arg_idx]; + arg_idx += 1; + + const cwd: std.Io.Dir = .cwd(); + + const zig_lib_directory: std.Build.Cache.Directory = .{ + .path = zig_lib_dir, + .handle = try cwd.openDir(io, zig_lib_dir, .{}), + }; + + const build_root_directory: std.Build.Cache.Directory = .{ + .path = build_root, + .handle = try cwd.openDir(io, build_root, .{}), + }; + + const local_cache_directory: std.Build.Cache.Directory = .{ + .path = cache_root, + .handle = try cwd.createDirPathOpen(io, cache_root, .{}), + }; + + const global_cache_directory: std.Build.Cache.Directory = .{ + .path = global_cache_root, + .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}), + }; + + var graph: std.Build.Graph = .{ + .io = io, + .arena = arena, + .cache = .{ + .io = io, + .gpa = gpa, + .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), + .cwd = try process.currentPathAlloc(io, arena), + }, + .zig_exe = zig_exe, + .environ_map = try init.environ.createMap(arena), + .global_cache_root = global_cache_directory, + .zig_lib_directory = zig_lib_directory, + .host = .{ + .query = .{}, + .result = try std.zig.system.resolveTargetQuery(io, .{}), + }, + .time_report = false, + }; + + graph.cache.addPrefix(.{ .path = null, .handle = cwd }); + graph.cache.addPrefix(build_root_directory); + graph.cache.addPrefix(local_cache_directory); + graph.cache.addPrefix(global_cache_directory); + graph.cache.hash.addBytes(builtin.zig_version_string); + + const builder = try std.Build.create( + &graph, + build_root_directory, + local_cache_directory, + dependencies.root_deps, + ); + + // Initialize install_path - required before calling build() + builder.resolveInstallPrefix(null, .{}); + + // Call the user's build() function + try builder.runBuild(root); + + // NOW WE HAVE THE BUILD GRAPH! + // Instead of executing it, let's dump information about it + + // Use a separate allocator for our module collection to avoid interfering with build graph + const our_allocator = std.heap.page_allocator; + + var stdout: std.Io.Writer.Allocating = .init(arena); + defer stdout.deinit(); + + // Collect all modules - from builder.modules and compile steps + var all_modules = std.StringHashMap(*std.Build.Module).init(our_allocator); + + // Add global modules + var global_iter = builder.modules.iterator(); + while (global_iter.next()) |entry| { + try all_modules.put(entry.key_ptr.*, entry.value_ptr.*); + } + + // Walk compile steps to find their root modules and imports + var visited_steps = std.AutoHashMap(*std.Build.Step, void).init(our_allocator); + var step_iter = builder.top_level_steps.iterator(); + while (step_iter.next()) |entry| { + const tls = entry.value_ptr.*; + try collectStepModules(&all_modules, &tls.step, &visited_steps); + } + + // Output in JSON format + try stdout.writer.writeAll("{\n"); + try stdout.writer.writeAll(" \"modules\": {\n"); + + var module_iter = all_modules.iterator(); + var first_module = true; + while (module_iter.next()) |mod_entry| { + const import_name = mod_entry.key_ptr.*; + const module = mod_entry.value_ptr.*; + const root_source = if (module.root_source_file) |rsf| blk: { + // Skip generated files - they don't have a real path yet + if (rsf == .generated) break :blk null; + break :blk rsf.getPath2(builder, null); + } else null; + + if (root_source) |root_path| { + if (!first_module) try stdout.writer.writeAll(",\n"); + first_module = false; + + try stdout.writer.print(" \"{s}\": {{\n", .{import_name}); + try stdout.writer.print(" \"root\": \"{s}\"", .{root_path}); + + if (module.import_table.count() > 0) { + try stdout.writer.writeAll(",\n \"imports\": {\n"); + var dep_iter = module.import_table.iterator(); + var first_dep = true; + while (dep_iter.next()) |dep| { + const dep_name = dep.key_ptr.*; + const dep_module = dep.value_ptr.*; + const dep_root = if (dep_module.root_source_file) |rsf| blk: { + if (rsf == .generated) break :blk null; + break :blk rsf.getPath2(builder, null); + } else null; + if (dep_root) |droot| { + if (!first_dep) try stdout.writer.writeAll(",\n"); + first_dep = false; + try stdout.writer.print(" \"{s}\": \"{s}\"", .{ dep_name, droot }); + } + } + try stdout.writer.writeAll("\n }\n"); + } else { + try stdout.writer.writeAll("\n"); + } + + try stdout.writer.writeAll(" }"); + } + } + + try stdout.writer.writeAll("\n }\n"); + try stdout.writer.writeAll("}\n"); + + try std.Io.File.stdout().writeStreamingAll(io, stdout.written()); +} + +fn collectStepModules( + modules: *std.StringHashMap(*std.Build.Module), + step: *std.Build.Step, + visited: *std.AutoHashMap(*std.Build.Step, void), +) !void { + // Avoid infinite recursion on circular dependencies + if (visited.contains(step)) return; + try visited.put(step, {}); + + // Check if this is a compile step + if (step.cast(std.Build.Step.Compile)) |compile_step| { + // Add imports from this compile step's root module + var iter = compile_step.root_module.import_table.iterator(); + while (iter.next()) |entry| { + const import_name = entry.key_ptr.*; + const module = entry.value_ptr.*; + try modules.put(import_name, module); + } + } + + // Recursively check dependencies + for (step.dependencies.items) |dep_step| { + try collectStepModules(modules, dep_step, visited); + } +} diff --git a/src/main.zig b/src/main.zig index 2516f64..5d24ef5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,10 +1,10 @@ const std = @import("std"); -const builtin = @import("builtin"); const Walk = @import("Walk.zig"); const log = std.log.scoped(.zigdoc); const build_runner_0_14 = @embedFile("build_runner_0.14.zig"); const build_runner_0_15 = @embedFile("build_runner_0.15.zig"); +const build_runner_0_16 = @embedFile("build_runner_0.16.zig"); const template_build_zig = @embedFile("templates/build.zig.template"); const template_main_zig = @embedFile("templates/main.zig.template"); @@ -12,70 +12,60 @@ const template_build_zig_zon = @embedFile("templates/build.zig.zon.template"); const template_agents_md = @embedFile("templates/AGENTS.md.template"); const template_gitignore = @embedFile("templates/.gitignore.template"); -pub fn main() !void { - var debug_allocator: std.heap.DebugAllocator(.{}) = .init; - const gpa, const is_debug = gpa: { - break :gpa switch (builtin.mode) { - .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true }, - .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false }, - }; - }; - defer if (is_debug) { - _ = debug_allocator.deinit(); - }; +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const arena = init.arena; + const io = init.io; - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - - var args = try std.process.argsWithAllocator(arena.allocator()); + var args = try init.minimal.args.iterateAllocator(gpa); defer args.deinit(); _ = args.skip(); // skip program name const symbol = args.next(); if (symbol == null) { - try printUsage(); + try printUsage(io); return; } if (std.mem.eql(u8, symbol.?, "--help") or std.mem.eql(u8, symbol.?, "-h")) { - try printUsage(); + try printUsage(io); return; } if (std.mem.eql(u8, symbol.?, "--dump-imports")) { - try dumpImports(&arena); + try dumpImports(arena, io); return; } if (std.mem.eql(u8, symbol.?, "@init")) { - try initProject(arena.allocator()); + try initProject(arena.allocator(), io); return; } - Walk.init(arena.allocator()); + Walk.init(arena.allocator(), io); Walk.Decl.init(arena.allocator()); - const std_dir_path = try getStdDir(&arena); + const std_dir_path = try getStdDir(arena, io); // Only parse std library if the symbol starts with "std" if (std.mem.startsWith(u8, symbol.?, "std")) { - try walkStdLib(&arena, std_dir_path); + try walkStdLib(arena, io, std_dir_path); // Register std/std.zig as the "std" module for @import("std") const std_file_index = Walk.files.getIndex("std/std.zig") orelse return error.StdNotFound; try Walk.modules.put(arena.allocator(), "std", @enumFromInt(std_file_index)); } else { // For non-std symbols, process build.zig to get imported modules - try processBuildZig(&arena); + try processBuildZig(arena, io); } - try printDocs(arena.allocator(), symbol.?, std_dir_path); + try printDocs(arena.allocator(), io, symbol.?, std_dir_path); } -fn printUsage() !void { +fn printUsage(io: std.Io) !void { var stdout_buf: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buf); try stdout_writer.interface.writeAll( \\Usage: zigdoc [options] \\ @@ -99,42 +89,41 @@ fn printUsage() !void { \\ @init Initialize a new Zig project with AGENTS.md \\ ); - try stdout_writer.interface.flush(); + try stdout_writer.flush(); } -fn initProject(allocator: std.mem.Allocator) !void { - const cwd = std.fs.cwd(); +fn initProject(allocator: std.mem.Allocator, io: std.Io) !void { + const cwd = std.Io.Dir.cwd(); // Check if project already exists - if (cwd.access("build.zig", .{})) |_| { + if (cwd.access(io, "build.zig", .{})) |_| { std.debug.print("Error: build.zig already exists\n", .{}); return error.ProjectExists; } else |_| {} // Get project name from current directory - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const cwd_path = try cwd.realpath(".", &path_buf); + var path_buf: [std.Io.Dir.max_path_bytes]u8 = undefined; + const cwd_path = path_buf[0..try std.process.currentPath(io, &path_buf)]; const name = std.fs.path.basename(cwd_path); // Create src directory - try cwd.makeDir("src"); + try cwd.createDir(io, "src", .default_dir); // Write files with substitutions - try cwd.writeFile(.{ + try cwd.writeFile(io, .{ .sub_path = "build.zig", .data = try substitute(allocator, template_build_zig, name), }); - try cwd.writeFile(.{ + try cwd.writeFile(io, .{ .sub_path = "build.zig.zon", .data = try substitute(allocator, template_build_zig_zon, name), }); - try cwd.writeFile(.{ .sub_path = "src/main.zig", .data = template_main_zig }); - try cwd.writeFile(.{ .sub_path = "AGENTS.md", .data = template_agents_md }); - try cwd.writeFile(.{ .sub_path = ".gitignore", .data = template_gitignore }); + try cwd.writeFile(io, .{ .sub_path = "src/main.zig", .data = template_main_zig }); + try cwd.writeFile(io, .{ .sub_path = "AGENTS.md", .data = template_agents_md }); + try cwd.writeFile(io, .{ .sub_path = ".gitignore", .data = template_gitignore }); // Run zig build to get suggested fingerprint from error message - const result = std.process.Child.run(.{ - .allocator = allocator, + const result = std.process.run(allocator, io, .{ .argv = &.{ "zig", "build" }, }) catch { std.debug.print("Initialized Zig project '{s}' (run 'zig build' to generate fingerprint)\n", .{name}); @@ -148,7 +137,7 @@ fn initProject(allocator: std.mem.Allocator) !void { const fingerprint = result.stderr[fp_start..fp_end]; // Read current build.zig.zon and insert fingerprint - const zon_content = try cwd.readFileAlloc(allocator, "build.zig.zon", 64 * 1024); + const zon_content = try cwd.readFileAlloc(io, "build.zig.zon", allocator, .limited(64 * 1024)); const new_zon = try std.mem.replaceOwned( u8, allocator, @@ -156,30 +145,47 @@ fn initProject(allocator: std.mem.Allocator) !void { ".version = \"0.0.0\",", try std.fmt.allocPrint(allocator, ".version = \"0.0.0\",\n .fingerprint = {s},", .{fingerprint}), ); - try cwd.writeFile(.{ .sub_path = "build.zig.zon", .data = new_zon }); + try cwd.writeFile(io, .{ .sub_path = "build.zig.zon", .data = new_zon }); } std.debug.print("Initialized Zig project '{s}'\n", .{name}); } fn substitute(allocator: std.mem.Allocator, template: []const u8, name: []const u8) ![]const u8 { - const sanitized = try std.mem.replaceOwned(u8, allocator, name, "-", "_"); + const sanitized = try sanitizeName(allocator, name); return std.mem.replaceOwned(u8, allocator, template, "{{name}}", sanitized); } -fn dumpImports(arena: *std.heap.ArenaAllocator) !void { +fn sanitizeName(allocator: std.mem.Allocator, name: []const u8) ![]const u8 { + const needs_prefix = name.len == 0 or std.ascii.isDigit(name[0]); + const result = try allocator.alloc(u8, name.len + @intFromBool(needs_prefix)); + var index: usize = 0; + + if (needs_prefix) { + result[index] = '_'; + index += 1; + } + + for (name) |c| { + result[index] = if (std.ascii.isAlphanumeric(c) or c == '_') c else '_'; + index += 1; + } + + return result; +} + +fn dumpImports(arena: *std.heap.ArenaAllocator, io: std.Io) !void { // Check if build.zig exists - std.fs.cwd().access("build.zig", .{}) catch { + std.Io.Dir.cwd().access(io, "build.zig", .{}) catch { std.debug.print("No build.zig found in current directory\n", .{}); return error.NoBuildZig; }; // Setup the build runner - try setupBuildRunner(arena); + try setupBuildRunner(arena, io); // Run zig build with our custom runner - const result = try std.process.Child.run(.{ - .allocator = arena.allocator(), + const result = try std.process.run(arena.allocator(), io, .{ .argv = &[_][]const u8{ "zig", "build", @@ -188,29 +194,32 @@ fn dumpImports(arena: *std.heap.ArenaAllocator) !void { }, }); - if (result.term.Exited != 0) { + if (!childExitedSuccessfully(result.term)) { std.debug.print("Error running build runner:\n{s}\n", .{result.stderr}); return error.BuildRunnerFailed; } // Print the JSON output directly - var stdout_buf: [4096]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); - try stdout_writer.interface.writeAll(result.stdout); - try stdout_writer.interface.flush(); + try std.Io.File.stdout().writeStreamingAll(io, result.stdout); } const ZigEnv = struct { std_dir: []const u8, }; -fn getZigVersion(arena: *std.heap.ArenaAllocator) !std.SemanticVersion { - const version_result = try std.process.Child.run(.{ - .allocator = arena.allocator(), +fn childExitedSuccessfully(term: std.process.Child.Term) bool { + return switch (term) { + .exited => |code| code == 0, + else => false, + }; +} + +fn getZigVersion(arena: *std.heap.ArenaAllocator, io: std.Io) !std.SemanticVersion { + const version_result = try std.process.run(arena.allocator(), io, .{ .argv = &[_][]const u8{ "zig", "version" }, }); - if (version_result.term.Exited != 0) { + if (!childExitedSuccessfully(version_result.term)) { return error.ZigVersionFailed; } @@ -218,40 +227,40 @@ fn getZigVersion(arena: *std.heap.ArenaAllocator) !std.SemanticVersion { return std.SemanticVersion.parse(version_str); } -fn setupBuildRunner(arena: *std.heap.ArenaAllocator) !void { - const version = try getZigVersion(arena); +fn setupBuildRunner(arena: *std.heap.ArenaAllocator, io: std.Io) !void { + const version = try getZigVersion(arena, io); - const runner_src = switch (version.minor) { + const runner_src = if (version.major == 0) switch (version.minor) { 14 => build_runner_0_14, 15 => build_runner_0_15, + 16 => build_runner_0_16, else => return error.UnsupportedZigVersion, - }; + } else return error.UnsupportedZigVersion; - std.fs.cwd().makeDir(".zig-cache") catch |err| switch (err) { + std.Io.Dir.cwd().createDir(io, ".zig-cache", .default_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; const runner_path = ".zig-cache/zigdoc_build_runner.zig"; - try std.fs.cwd().writeFile(.{ + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = runner_path, .data = runner_src, }); } -fn processBuildZig(arena: *std.heap.ArenaAllocator) !void { +fn processBuildZig(arena: *std.heap.ArenaAllocator, io: std.Io) !void { // Check if build.zig exists - std.fs.cwd().access("build.zig", .{}) catch { + std.Io.Dir.cwd().access(io, "build.zig", .{}) catch { // No build.zig, nothing to do return; }; // Setup the build runner - try setupBuildRunner(arena); + try setupBuildRunner(arena, io); // Run zig build with our custom runner - const result = try std.process.Child.run(.{ - .allocator = arena.allocator(), + const result = try std.process.run(arena.allocator(), io, .{ .argv = &[_][]const u8{ "zig", "build", @@ -260,16 +269,16 @@ fn processBuildZig(arena: *std.heap.ArenaAllocator) !void { }, }); - if (result.term.Exited != 0) { + if (!childExitedSuccessfully(result.term)) { log.err("Failed to analyze build.zig", .{}); return; } // Parse the output to extract module information - try parseBuildOutput(arena.allocator(), result.stdout); + try parseBuildOutput(arena.allocator(), io, result.stdout); } -fn parseBuildOutput(allocator: std.mem.Allocator, output: []const u8) !void { +fn parseBuildOutput(allocator: std.mem.Allocator, io: std.Io, output: []const u8) !void { const parsed = try std.json.parseFromSlice(std.json.Value, allocator, output, .{}); defer parsed.deinit(); @@ -287,10 +296,11 @@ fn parseBuildOutput(allocator: std.mem.Allocator, output: []const u8) !void { if (!std.mem.endsWith(u8, root_path, ".zig")) continue; // Read and add the module file - const file_content = std.fs.cwd().readFileAlloc( - allocator, + const file_content = std.Io.Dir.cwd().readFileAlloc( + io, root_path, - 10 * 1024 * 1024, + allocator, + .limited(10 * 1024 * 1024), ) catch |err| { std.debug.print("Failed to read module {s}: {}\n", .{ module_name, err }); continue; @@ -310,10 +320,11 @@ fn parseBuildOutput(allocator: std.mem.Allocator, output: []const u8) !void { if (!std.mem.endsWith(u8, import_path, ".zig")) continue; // Read and add the imported file - const import_content = std.fs.cwd().readFileAlloc( - allocator, + const import_content = std.Io.Dir.cwd().readFileAlloc( + io, import_path, - 10 * 1024 * 1024, + allocator, + .limited(10 * 1024 * 1024), ) catch |err| { std.debug.print("Failed to read import {s}: {}\n", .{ import_name, err }); continue; @@ -326,17 +337,16 @@ fn parseBuildOutput(allocator: std.mem.Allocator, output: []const u8) !void { } } -fn getStdDir(arena: *std.heap.ArenaAllocator) ![]const u8 { - const version = try getZigVersion(arena); +fn getStdDir(arena: *std.heap.ArenaAllocator, io: std.Io) ![]const u8 { + const version = try getZigVersion(arena, io); const is_pre_0_15 = version.order(.{ .major = 0, .minor = 15, .patch = 0 }) == .lt; - const result = try std.process.Child.run(.{ - .allocator = arena.allocator(), + const result = try std.process.run(arena.allocator(), io, .{ .argv = &[_][]const u8{ "zig", "env" }, }); - if (result.term.Exited != 0) { + if (!childExitedSuccessfully(result.term)) { return error.ZigEnvFailed; } @@ -351,7 +361,7 @@ fn getStdDir(arena: *std.heap.ArenaAllocator) ![]const u8 { ); return parsed.value.std_dir; } else { - const parsed = try std.zon.parse.fromSlice( + const parsed = try std.zon.parse.fromSliceAlloc( ZigEnv, arena.allocator(), stdout, @@ -362,26 +372,26 @@ fn getStdDir(arena: *std.heap.ArenaAllocator) ![]const u8 { } } -fn walkStdLib(arena: *std.heap.ArenaAllocator, std_dir_path: []const u8) !void { +fn walkStdLib(arena: *std.heap.ArenaAllocator, io: std.Io, std_dir_path: []const u8) !void { const allocator = arena.allocator(); - var dir = try std.fs.openDirAbsolute(std_dir_path, .{ .iterate = true }); - defer dir.close(); + var dir = try std.Io.Dir.openDirAbsolute(io, std_dir_path, .{ .iterate = true }); + defer dir.close(io); var walker = try dir.walk(allocator); defer walker.deinit(); - while (try walker.next()) |entry| { + while (try walker.next(io)) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.basename, ".zig")) continue; if (std.mem.endsWith(u8, entry.basename, "test.zig")) continue; const file_content = try entry.dir.readFileAllocOptions( - allocator, + io, entry.basename, - 10 * 1024 * 1024, + allocator, + .limited(10 * 1024 * 1024), + .of(u8), null, - @enumFromInt(0), - 0, ); const file_name = try std.fmt.allocPrint(allocator, "std/{s}", .{entry.path}); @@ -539,9 +549,9 @@ fn printDeclInfo( } } -fn printDocs(allocator: std.mem.Allocator, symbol: []const u8, std_dir_path: []const u8) !void { +fn printDocs(allocator: std.mem.Allocator, io: std.Io, symbol: []const u8, std_dir_path: []const u8) !void { var stdout_buf: [4096]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buf); const stdout = &stdout_writer.interface; // Try hierarchical resolution first (e.g., "zeit.timezone.Posix") diff --git a/src/templates/build.zig.template b/src/templates/build.zig.template index 338a987..c6fd540 100644 --- a/src/templates/build.zig.template +++ b/src/templates/build.zig.template @@ -1,5 +1,4 @@ const std = @import("std"); -const ziglint = @import("ziglint"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); @@ -39,9 +38,4 @@ pub fn build(b: *std.Build) void { const fmt_check = b.addFmt(.{ .paths = &.{ "src", "build.zig", "build.zig.zon" }, .check = true }); fmt_step.dependOn(&fmt_check.step); test_step.dependOn(fmt_step); - - const lint_step = b.step("lint", "Run ziglint"); - const ziglint_dep = b.dependency("ziglint", .{ .optimize = .ReleaseFast }); - lint_step.dependOn(ziglint.addLint(b, ziglint_dep, &.{ b.path("src"), b.path("build.zig") })); - test_step.dependOn(lint_step); } diff --git a/src/templates/build.zig.zon.template b/src/templates/build.zig.zon.template index 7483d9f..eebcbf3 100644 --- a/src/templates/build.zig.zon.template +++ b/src/templates/build.zig.zon.template @@ -1,12 +1,8 @@ .{ .name = .{{name}}, .version = "0.0.0", - .dependencies = .{ - .ziglint = .{ - .url = "https://github.com/rockorager/ziglint/archive/refs/tags/v0.5.2.tar.gz", - .hash = "ziglint-0.5.2-t0bwL2FwBQC5i-ifhKKv2ls5jGXHShJpuNhFVhiU-Tt-", - }, - }, + .minimum_zig_version = "0.16.0", + .dependencies = .{}, .paths = .{ "build.zig", "build.zig.zon", From 55447798ecbe14b2699b98b000f663289e5c0dc8 Mon Sep 17 00:00:00 2001 From: Evgenii Tretiakov Date: Fri, 12 Jun 2026 16:27:31 +0200 Subject: [PATCH 2/5] fix: complete zig 0.16 quality gates --- build.zig | 10 +++ src/Walk.zig | 65 ++++++++++++++++-- src/main.zig | 23 ++++++- src/templates/AGENTS.md.template | 8 +-- src/templates/main.zig.template | 7 +- src/test_symbol_resolution.zig | 110 +++++++++++-------------------- 6 files changed, 141 insertions(+), 82 deletions(-) diff --git a/build.zig b/build.zig index ccbef23..664931d 100644 --- a/build.zig +++ b/build.zig @@ -66,9 +66,19 @@ pub fn build(b: *std.Build) void { }); const run_build_readme_tests = b.addRunArtifact(build_readme_tests); + const symbol_resolution_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/test_symbol_resolution.zig"), + .target = target, + .optimize = optimize, + }), + }); + const run_symbol_resolution_tests = b.addRunArtifact(symbol_resolution_tests); + const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_exe_tests.step); test_step.dependOn(&run_build_readme_tests.step); + test_step.dependOn(&run_symbol_resolution_tests.step); const fmt_step = b.step("fmt", "Check code formatting"); const fmt_check = b.addFmt(.{ diff --git a/src/Walk.zig b/src/Walk.zig index 380c49a..15c5a9d 100644 --- a/src/Walk.zig +++ b/src/Walk.zig @@ -17,9 +17,21 @@ const Oom = error{OutOfMemory}; pub const Decl = @import("Decl.zig"); -pub var files: std.array_hash_map.String(File) = .empty; +pub var files: std.StringArrayHashMapUnmanaged(File) = .empty; pub var decls: std.ArrayList(Decl) = .empty; -pub var modules: std.array_hash_map.String(File.Index) = .empty; +pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .empty; + +pub fn deinit() void { + for (files.values()) |*file| { + file.deinit(); + } + files.deinit(gpa); + decls.deinit(gpa); + modules.deinit(gpa); + files = .empty; + decls = .empty; + modules = .empty; +} file: File.Index, @@ -69,6 +81,23 @@ pub const File = struct { /// struct/union/enum/opaque decl node => its namespace scope /// local var decl node => its local variable scope scopes: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, *Scope) = .empty, + top_scope: ?*Scope = null, + + fn deinit(file: *File) void { + file.ast.deinit(gpa); + file.ident_decls.deinit(gpa); + file.token_parents.deinit(gpa); + file.node_decls.deinit(gpa); + file.doctests.deinit(gpa); + for (file.scopes.values()) |scope| { + scope.destroy(); + } + if (file.top_scope) |scope| { + scope.destroy(); + } + file.scopes.deinit(gpa); + file.* = undefined; + } pub fn lookupToken(file: *File, token: Ast.TokenIndex) Decl.Index { const decl_node = file.ident_decls.get(token) orelse return .none; @@ -452,6 +481,7 @@ pub fn addFile(file_name: []const u8, bytes: []u8) !File.Index { }; const scope = try gpa.create(Scope); scope.* = .{ .tag = .top }; + file_index.get().top_scope = scope; const decl_index = try file_index.addDecl(.root, .none); try structDecl(&w, scope, decl_index, .root, ast.containerDeclRoot()); @@ -521,11 +551,38 @@ pub const Scope = struct { const Namespace = struct { base: Scope = .{ .tag = .namespace }, parent: *Scope, - names: std.array_hash_map.String(Ast.Node.Index) = .empty, - doctests: std.array_hash_map.String(Ast.Node.Index) = .empty, + names: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .empty, + doctests: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .empty, decl_index: Decl.Index, + + fn deinit(namespace: *Namespace) void { + namespace.names.deinit(gpa); + namespace.doctests.deinit(gpa); + namespace.* = undefined; + } }; + fn destroy(self: *Scope) void { + switch (self.tag) { + .top => { + self.* = undefined; + gpa.destroy(self); + }, + .local => { + const local: *Local = @alignCast(@fieldParentPtr("base", self)); + self.* = undefined; + local.* = undefined; + gpa.destroy(local); + }, + .namespace => { + const namespace: *Namespace = @alignCast(@fieldParentPtr("base", self)); + namespace.deinit(); + self.* = undefined; + gpa.destroy(namespace); + }, + } + } + fn getNamespaceDecl(start_scope: *Scope) Decl.Index { var it: *Scope = start_scope; while (true) switch (it.tag) { diff --git a/src/main.zig b/src/main.zig index 52c1789..7bc31e6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,7 +46,13 @@ pub fn main(init: std.process.Init) !void { Walk.init(arena.allocator(), io); Walk.Decl.init(arena.allocator()); - const std_dir_path = try getStdDir(io, arena); + const std_dir_path = getStdDir(io, arena) catch |err| switch (err) { + error.ZigNotFound => { + try printZigNotFound(io); + std.process.exit(1); + }, + else => return err, + }; // Only parse std library if the symbol starts with "std" if (std.mem.startsWith(u8, symbol.?, "std")) { @@ -92,6 +98,14 @@ fn printUsage(io: std.Io) !void { try stdout_writer.flush(); } +fn printZigNotFound(io: std.Io) !void { + try std.Io.File.stderr().writeStreamingAll(io, + \\Error: 'zig' command not found. + \\Please ensure Zig is installed and available in your PATH. + \\ + ); +} + fn initProject(allocator: std.mem.Allocator, io: std.Io) !void { const cwd = std.Io.Dir.cwd(); @@ -215,9 +229,12 @@ fn childExitedSuccessfully(term: std.process.Child.Term) bool { } fn getZigVersion(io: std.Io, arena: *std.heap.ArenaAllocator) !std.SemanticVersion { - const version_result = try std.process.run(arena.allocator(), io, .{ + const version_result = std.process.run(arena.allocator(), io, .{ .argv = &[_][]const u8{ "zig", "version" }, - }); + }) catch |err| switch (err) { + error.FileNotFound => return error.ZigNotFound, + else => return err, + }; if (!childExitedSuccessfully(version_result.term)) { return error.ZigVersionFailed; diff --git a/src/templates/AGENTS.md.template b/src/templates/AGENTS.md.template index 12641ee..8b026da 100644 --- a/src/templates/AGENTS.md.template +++ b/src/templates/AGENTS.md.template @@ -30,9 +30,9 @@ try map.put(allocator, "key", 42); **stdout/stderr writer:** ```zig var buf: [4096]u8 = undefined; -var writer = std.fs.File.stdout().writer(&buf); -defer writer.interface.flush() catch {}; +var writer = std.Io.File.stdout().writer(io, &buf); try writer.interface.print("hello {s}\n", .{"world"}); +try writer.end(); ``` **build.zig executable:** @@ -50,8 +50,8 @@ b.addExecutable(.{ **JSON writing:** ```zig var buf: [4096]u8 = undefined; -var writer = std.fs.File.stdout().writer(&buf); -defer writer.interface.flush() catch {}; +var writer = std.Io.File.stdout().writer(io, &buf); +defer writer.end() catch {}; var jw: std.json.Stringify = .{ .writer = &writer.interface, diff --git a/src/templates/main.zig.template b/src/templates/main.zig.template index 9b1e935..820e8ce 100644 --- a/src/templates/main.zig.template +++ b/src/templates/main.zig.template @@ -1,3 +1,8 @@ //! Application entry point. -pub fn main() void {} +const std = @import("std"); + +pub fn main(init: std.process.Init) !u8 { + try std.Io.File.stdout().writeStreamingAll(init.io, "hello\n"); + return 0; +} diff --git a/src/test_symbol_resolution.zig b/src/test_symbol_resolution.zig index 78a1558..13635fd 100644 --- a/src/test_symbol_resolution.zig +++ b/src/test_symbol_resolution.zig @@ -67,14 +67,11 @@ fn resolveHierarchical(allocator: std.mem.Allocator, symbol: []const u8) !?*Decl } test "simple public member resolution" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = "pub const A = 1;\n"; const file_idx = try addTestFile(allocator, "mod.zig", source); @@ -86,14 +83,11 @@ test "simple public member resolution" { } test "nested struct member resolution" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub const S = struct { @@ -110,14 +104,11 @@ test "nested struct member resolution" { } test "alias chain resolution" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub const A = struct { @@ -135,14 +126,11 @@ test "alias chain resolution" { } test "private member not resolved" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = "const hidden = 1;\n"; const file_idx = try addTestFile(allocator, "mod.zig", source); @@ -153,14 +141,11 @@ test "private member not resolved" { } test "non-existent symbol" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = "pub const A = 1;\n"; const file_idx = try addTestFile(allocator, "mod.zig", source); @@ -171,14 +156,11 @@ test "non-existent symbol" { } test "categorize struct with fields as container" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub const S = struct { @@ -196,14 +178,11 @@ test "categorize struct with fields as container" { } test "categorize struct with only consts as namespace" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub const S = struct { @@ -221,14 +200,11 @@ test "categorize struct with only consts as namespace" { } test "type function categorization" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub fn MyType() type { @@ -246,14 +222,11 @@ test "type function categorization" { } test "regular function categorization" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub fn myFunc() i32 { @@ -271,14 +244,11 @@ test "regular function categorization" { } test "deep alias chain within limit" { - const allocator = testing.allocator; + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); try setupTest(allocator); - defer { - for (Walk.files.values()) |*file| file.ast.deinit(allocator); - Walk.files.deinit(allocator); - Walk.decls.deinit(allocator); - Walk.modules.deinit(allocator); - } + defer Walk.deinit(); const source = \\pub const A = struct { pub const X = 1; }; From 8b72ad42b28f262a8cde2757912f68edb5128c82 Mon Sep 17 00:00:00 2001 From: Evgenii Tretiakov Date: Fri, 12 Jun 2026 18:59:53 +0200 Subject: [PATCH 3/5] =?UTF-8?q?fix(zigdoc):=20address=20PR=20#2=20review?= =?UTF-8?q?=20=E2=80=94=20escape=20JSON=20paths,=20propagate=20build-runne?= =?UTF-8?q?r=20failures,=20plug=20initProject=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHY: Review on #2 (Codex P2 ×2, Copilot ×1) flagged correctness + resource issues that let malformed input or build failures masquerade as missing docs. WHAT: - build_runner_0.16.zig: encode module/import names and root paths via std.json.Stringify.encodeJsonString instead of raw "{s}" interpolation, so Windows backslash paths (C:\...) no longer emit invalid JSON that breaks parseBuildOutput. - main.zig processBuildZig: return error.BuildRunnerFailed (with stderr) on non-zero build-runner exit instead of silently continuing with zero modules. - main.zig initProject: cap run() stdout/stderr at .limited(1MiB) and free the caller-owned buffers (was leaking on success path). - Walk.zig: migrate StringArrayHashMapUnmanaged -> array_hash_map.String (0.16). - build.zig.zon: repoint ziglint dep ref fix/0.16-build-compat -> main. IMPACT: zigdoc module discovery on Windows and on failing builds; no public API change. VALIDATION: mise x zig@0.16.0 zig build test --summary all => 13/13 steps, 15/15 tests, zig fmt --check clean, ziglint self-lint gate clean. --- build.zig.zon | 2 +- src/Walk.zig | 4 ++-- src/build_runner_0.16.zig | 15 ++++++++++++--- src/main.zig | 12 ++++++++++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index c5077ed..16f8d94 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,7 +5,7 @@ .minimum_zig_version = "0.16.0", .dependencies = .{ .ziglint = .{ - .url = "git+https://github.com/EugOT/ziglint.git?ref=fix/0.16-build-compat#40f26f1283468032ec993c93fca921bd3c106adb", + .url = "git+https://github.com/EugOT/ziglint.git?ref=main#e5612280f345fcd808f9a2562a1b5964b45b9b42", .hash = "ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq", }, }, diff --git a/src/Walk.zig b/src/Walk.zig index 15c5a9d..3186933 100644 --- a/src/Walk.zig +++ b/src/Walk.zig @@ -17,9 +17,9 @@ const Oom = error{OutOfMemory}; pub const Decl = @import("Decl.zig"); -pub var files: std.StringArrayHashMapUnmanaged(File) = .empty; +pub var files: std.array_hash_map.String(File) = .empty; pub var decls: std.ArrayList(Decl) = .empty; -pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .empty; +pub var modules: std.array_hash_map.String(File.Index) = .empty; pub fn deinit() void { for (files.values()) |*file| { diff --git a/src/build_runner_0.16.zig b/src/build_runner_0.16.zig index 69a92c7..db475c0 100644 --- a/src/build_runner_0.16.zig +++ b/src/build_runner_0.16.zig @@ -148,8 +148,14 @@ pub fn main(init: process.Init.Minimal) !void { if (!first_module) try stdout.writer.writeAll(",\n"); first_module = false; - try stdout.writer.print(" \"{s}\": {{\n", .{import_name}); - try stdout.writer.print(" \"root\": \"{s}\"", .{root_path}); + // Module names and paths may contain characters that must be + // escaped to keep the output valid JSON (e.g. backslashes on + // Windows paths, or quotes). Encode them with the JSON encoder + // rather than printing raw with "{s}". + try stdout.writer.writeAll(" "); + try std.json.Stringify.encodeJsonString(import_name, .{}, &stdout.writer); + try stdout.writer.writeAll(": {\n \"root\": "); + try std.json.Stringify.encodeJsonString(root_path, .{}, &stdout.writer); if (module.import_table.count() > 0) { try stdout.writer.writeAll(",\n \"imports\": {\n"); @@ -165,7 +171,10 @@ pub fn main(init: process.Init.Minimal) !void { if (dep_root) |droot| { if (!first_dep) try stdout.writer.writeAll(",\n"); first_dep = false; - try stdout.writer.print(" \"{s}\": \"{s}\"", .{ dep_name, droot }); + try stdout.writer.writeAll(" "); + try std.json.Stringify.encodeJsonString(dep_name, .{}, &stdout.writer); + try stdout.writer.writeAll(": "); + try std.json.Stringify.encodeJsonString(droot, .{}, &stdout.writer); } } try stdout.writer.writeAll("\n }\n"); diff --git a/src/main.zig b/src/main.zig index 7bc31e6..1850c8e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -139,10 +139,16 @@ fn initProject(allocator: std.mem.Allocator, io: std.Io) !void { // Run zig build to get suggested fingerprint from error message const result = std.process.run(allocator, io, .{ .argv = &.{ "zig", "build" }, + // Cap captured output so a misbehaving build cannot exhaust memory. + .stdout_limit = .limited(1024 * 1024), + .stderr_limit = .limited(1024 * 1024), }) catch { std.debug.print("Initialized Zig project '{s}' (run 'zig build' to generate fingerprint)\n", .{name}); return; }; + // `run` returns caller-owned stdout/stderr buffers; free both. + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); // Parse fingerprint from error: "suggested value: 0x..." if (std.mem.find(u8, result.stderr, "suggested value: ")) |start| { @@ -287,8 +293,10 @@ fn processBuildZig(io: std.Io, arena: *std.heap.ArenaAllocator) !void { }); if (!childExitedSuccessfully(result.term)) { - log.err("Failed to analyze build.zig", .{}); - return; + log.err("Failed to analyze build.zig:\n{s}", .{result.stderr}); + // Propagate the failure instead of continuing with zero modules, + // which would make valid symbols look like missing symbols. + return error.BuildRunnerFailed; } // Parse the output to extract module information From 2183881eb6b58da60ddcc293b3a1f24070e67c20 Mon Sep 17 00:00:00 2001 From: Evgenii Tretiakov Date: Sat, 13 Jun 2026 01:44:12 +0200 Subject: [PATCH 4/5] fix: lift PR 2 flake and runner to Zig 0.16 --- .gitignore | 1 + flake.lock | 27 +++++++++++++++ flake.nix | 69 +++++++++++++++++++++++++++++++++++++++ src/Walk.zig | 51 +++++++++++++++++++++-------- src/build_runner_0.16.zig | 22 ++++++++----- 5 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index dca1103..b5ce1da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ zig-out/ .zig-cache/ +/result* diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6769406 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1781074563, + "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5b158c9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,69 @@ +{ + description = "zigdoc"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { + self, + nixpkgs, + }: + let + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + pkgsFor = system: import nixpkgs { inherit system; }; + in + { + devShells = forAllSystems ( + system: + let + pkgs = pkgsFor system; + zig = pkgs.zig_0_16 or pkgs.zig; + in + { + default = pkgs.mkShell { + packages = [ + zig + ]; + }; + } + ); + + packages = forAllSystems ( + system: + let + pkgs = pkgsFor system; + zig = pkgs.zig_0_16 or pkgs.zig; + in + { + default = pkgs.stdenv.mkDerivation { + pname = "zigdoc"; + version = "0.0.0"; + src = self; + + nativeBuildInputs = [ + zig.hook + ]; + + zigBuildFlags = [ + "-Dcpu=baseline" + "-Doptimize=ReleaseFast" + ]; + + installPhase = '' + runHook preInstall + install -Dm755 zig-out/bin/zigdoc $out/bin/zigdoc + runHook postInstall + ''; + }; + } + ); + + formatter = forAllSystems (system: (pkgsFor system).nixfmt); + }; +} diff --git a/src/Walk.zig b/src/Walk.zig index 3186933..530d7b7 100644 --- a/src/Walk.zig +++ b/src/Walk.zig @@ -25,6 +25,9 @@ pub fn deinit() void { for (files.values()) |*file| { file.deinit(); } + for (files.keys()) |key| { + gpa.free(key); + } files.deinit(gpa); decls.deinit(gpa); modules.deinit(gpa); @@ -368,19 +371,22 @@ pub const File = struct { return Category.makeAlias(File.Index.findRootDecl(imported_file_index), node); } - const resolved_path = if (std.fs.path.isAbsolute(file_path)) - std.Io.Dir.realPathFileAbsoluteAlloc(io, file_path, gpa) catch file_path - else blk: { + // Uniform ownership: `resolved_path` is always a fresh plain + // []u8. The realpath result is a [:0]u8 sentinel slice that + // must be freed with its exact type (see addFile), so re-dupe + // it instead of storing/freeing the coerced slice. + const resolved_path = if (std.fs.path.isAbsolute(file_path)) blk: { + const real = std.Io.Dir.realPathFileAbsoluteAlloc(io, file_path, gpa) catch + break :blk gpa.dupe(u8, file_path) catch @panic("OOM"); + defer gpa.free(real); + break :blk gpa.dupe(u8, real) catch @panic("OOM"); + } else blk: { const base_path = file_index.path(); break :blk std.fs.path.resolve(gpa, &.{ base_path, "..", file_path, }) catch @panic("OOM"); }; - defer { - if (!std.fs.path.isAbsolute(file_path) or resolved_path.ptr != file_path.ptr) { - gpa.free(resolved_path); - } - } + defer gpa.free(resolved_path); log.debug("from '{s}' @import '{s}' resolved='{s}'", .{ file_index.path(), file_path, resolved_path, @@ -463,18 +469,37 @@ pub fn addFile(file_name: []const u8, bytes: []u8) !File.Index { const ast = try parse(file_name, bytes); assert(ast.errors.len == 0); - const normalized_path = if (std.fs.path.isAbsolute(file_name)) - std.Io.Dir.realPathFileAbsoluteAlloc(io, file_name, gpa) catch file_name - else - std.Io.Dir.cwd().realPathFileAlloc(io, file_name, gpa) catch file_name; + // realPathFile*Alloc return sentinel slices ([:0]u8 — dupeZ allocates + // N+1 bytes). Re-dupe to a plain []u8 and free the sentinel original + // with its exact type, so `deinit` can later free every key with the + // correct allocation size (a [:0] key freed as []const u8 trips the + // DebugAllocator size check). + const normalized_path = blk: { + if (std.fs.path.isAbsolute(file_name)) { + if (std.Io.Dir.realPathFileAbsoluteAlloc(io, file_name, gpa)) |real| { + defer gpa.free(real); + break :blk try gpa.dupe(u8, real); + } else |_| {} + } else { + if (std.Io.Dir.cwd().realPathFileAlloc(io, file_name, gpa)) |real| { + defer gpa.free(real); + break :blk try gpa.dupe(u8, real); + } else |_| {} + } + break :blk try gpa.dupe(u8, file_name); + }; // Check if this file already exists to avoid duplicate entries if (files.getIndex(normalized_path)) |existing_index| { + gpa.free(normalized_path); return @enumFromInt(existing_index); } const file_index: File.Index = @enumFromInt(files.entries.len); - try files.put(gpa, normalized_path, .{ .ast = ast }); + files.put(gpa, normalized_path, .{ .ast = ast }) catch |err| { + gpa.free(normalized_path); + return err; + }; var w: Walk = .{ .file = file_index, diff --git a/src/build_runner_0.16.zig b/src/build_runner_0.16.zig index db475c0..d71332b 100644 --- a/src/build_runner_0.16.zig +++ b/src/build_runner_0.16.zig @@ -27,6 +27,7 @@ pub fn main(init: process.Init.Minimal) !void { const arena = arena_instance.allocator(); const args = try init.args.toSlice(arena); + if (args.len < 6) return error.InvalidArgs; // skip my own exe name var arg_idx: usize = 1; @@ -113,20 +114,22 @@ pub fn main(init: process.Init.Minimal) !void { defer stdout.deinit(); // Collect all modules - from builder.modules and compile steps - var all_modules = std.StringHashMap(*std.Build.Module).init(our_allocator); + var all_modules: std.StringHashMapUnmanaged(*std.Build.Module) = .empty; + defer all_modules.deinit(our_allocator); // Add global modules var global_iter = builder.modules.iterator(); while (global_iter.next()) |entry| { - try all_modules.put(entry.key_ptr.*, entry.value_ptr.*); + try all_modules.put(our_allocator, entry.key_ptr.*, entry.value_ptr.*); } // Walk compile steps to find their root modules and imports - var visited_steps = std.AutoHashMap(*std.Build.Step, void).init(our_allocator); + var visited_steps: std.AutoHashMapUnmanaged(*std.Build.Step, void) = .empty; + defer visited_steps.deinit(our_allocator); var step_iter = builder.top_level_steps.iterator(); while (step_iter.next()) |entry| { const tls = entry.value_ptr.*; - try collectStepModules(&all_modules, &tls.step, &visited_steps); + try collectStepModules(our_allocator, &all_modules, &tls.step, &visited_steps); } // Output in JSON format @@ -193,13 +196,14 @@ pub fn main(init: process.Init.Minimal) !void { } fn collectStepModules( - modules: *std.StringHashMap(*std.Build.Module), + allocator: std.mem.Allocator, + modules: *std.StringHashMapUnmanaged(*std.Build.Module), step: *std.Build.Step, - visited: *std.AutoHashMap(*std.Build.Step, void), + visited: *std.AutoHashMapUnmanaged(*std.Build.Step, void), ) !void { // Avoid infinite recursion on circular dependencies if (visited.contains(step)) return; - try visited.put(step, {}); + try visited.put(allocator, step, {}); // Check if this is a compile step if (step.cast(std.Build.Step.Compile)) |compile_step| { @@ -208,12 +212,12 @@ fn collectStepModules( while (iter.next()) |entry| { const import_name = entry.key_ptr.*; const module = entry.value_ptr.*; - try modules.put(import_name, module); + try modules.put(allocator, import_name, module); } } // Recursively check dependencies for (step.dependencies.items) |dep_step| { - try collectStepModules(modules, dep_step, visited); + try collectStepModules(allocator, modules, dep_step, visited); } } From 28b1c9fcb985c08338c0fa6c223e0feac0a813ba Mon Sep 17 00:00:00 2001 From: Evgenii Tretiakov Date: Sat, 13 Jun 2026 01:49:30 +0200 Subject: [PATCH 5/5] chore(deps): bump ziglint to a2fa56a (review-hardening release) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHY: CodeRabbit threads on PR #2 flagged 7 findings inside the vendored ziglint copy; the fixes landed upstream (EugOT/ziglint#4) — patching the package cache in-place would desync it from its content hash. WHAT: build.zig.zon repinned to main@a2fa56a (hash t0bwLzCwBQA...); vendored zig-pkg snapshot refreshed, stale t0bwLz2XBQC... removed. Upstream brings: Config.deinit + pub Rule re-export, doc-comment qualifier scan (pub inline fn), strict doc-test gate, alias-recursion cap, user_type no longer coerced to std_type, findProjectRoot caller-allocator fix. IMPACT: zigdoc lint gate now runs the hardened ziglint. VALIDATION: mise x zig@0.16.0 zig build test --summary all => all steps green (tests, zig fmt --check, ziglint self-lint via new pin). --- build.zig.zon | 4 +- .../build.zig | 18 +-- .../build.zig.zon | 2 +- .../src/Config.zig | 36 +++--- .../src/Linter.zig | 113 +++++++++++------- .../src/ModuleGraph.zig | 29 +++-- .../src/TypeResolver.zig | 101 +++++++++------- .../src/doc_comments.zig | 17 ++- .../src/doc_tests.zig | 47 +++++--- .../src/main.zig | 90 +++++++++++--- .../src/rules.zig | 14 +-- 11 files changed, 307 insertions(+), 164 deletions(-) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/build.zig (83%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/build.zig.zon (82%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/Config.zig (81%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/Linter.zig (98%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/ModuleGraph.zig (90%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/TypeResolver.zig (95%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/doc_comments.zig (88%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/doc_tests.zig (79%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/main.zig (85%) rename zig-pkg/{ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq => ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9}/src/rules.zig (96%) diff --git a/build.zig.zon b/build.zig.zon index 16f8d94..b51a7e3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.16.0", .dependencies = .{ .ziglint = .{ - .url = "git+https://github.com/EugOT/ziglint.git?ref=main#e5612280f345fcd808f9a2562a1b5964b45b9b42", - .hash = "ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq", + .url = "git+https://github.com/EugOT/ziglint.git?ref=main#a2fa56aa6a26181f9d6f586203fe65fc6c4c11c9", + .hash = "ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9", }, }, .paths = .{ diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/build.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig similarity index 83% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/build.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig index 9f41843..aeffa20 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/build.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig @@ -47,27 +47,17 @@ pub fn build(b: *std.Build) void { const fmt_check = b.addFmt(.{ .paths = &.{ "src", "build.zig", "build.zig.zon" } }); test_step.dependOn(&fmt_check.step); - // Self-lint: the public `addLint` API enforces exit code 0 (so downstream - // users can fail their CI on findings). For our own `zig build test` - // aggregate we want to ensure ziglint doesn't *crash* on its own source, - // but we tolerate the residual lint findings (mostly stylistic) that - // remain after the Zig 0.16 migration. const lint_step = addLint(b, exe, &.{ b.path("src"), b.path("build.zig") }); const lint_alias = b.step("lint", "Run ziglint on this repository"); lint_alias.dependOn(lint_step); - // A separate "self-lint smoke" run gated into `zig build test`. - // Ziglint currently reports residual style findings (Z011/Z012/Z013/Z023) - // against its own Zig 0.16-migrated source. Until those are addressed we - // expect exit code 1 (findings reported), which still proves ziglint did - // not crash (segfault/ABRT). When the codebase becomes lint-clean this - // will start failing -- swap to `expectExitCode(0)` at that point. + // Keep self-lint in the test aggregate so regressions break the normal gate. const lint_smoke = b.addRunArtifact(exe); lint_smoke.addDirectoryArg(b.path("src")); lint_smoke.addFileArg(b.path("build.zig")); addPathInputs(b, lint_smoke, b.path("src")); addPathInputs(b, lint_smoke, b.path("build.zig")); - lint_smoke.expectExitCode(1); + lint_smoke.expectExitCode(0); test_step.dependOn(&lint_smoke.step); } @@ -133,10 +123,10 @@ fn getVersion(b: *std.Build) []const u8 { const trimmed = std.mem.trim(u8, git_describe, " \n\r"); const without_v = if (trimmed.len > 0 and trimmed[0] == 'v') trimmed[1..] else trimmed; - if (std.mem.indexOfScalar(u8, without_v, '-')) |dash_idx| { + if (std.mem.findScalar(u8, without_v, '-')) |dash_idx| { const tag_part = without_v[0..dash_idx]; const rest = without_v[dash_idx + 1 ..]; - if (std.mem.indexOfScalar(u8, rest, '-')) |second_dash| { + if (std.mem.findScalar(u8, rest, '-')) |second_dash| { const count = rest[0..second_dash]; const hash = rest[second_dash + 1 ..]; const hash_without_g = if (hash.len > 0 and hash[0] == 'g') hash[1..] else hash; diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/build.zig.zon b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig.zon similarity index 82% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/build.zig.zon rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig.zon index 3896665..41a94a4 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/build.zig.zon +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig.zon @@ -2,7 +2,7 @@ .name = .ziglint, .version = "0.5.2", .fingerprint = 0x4b7deecc2ff046b7, - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/Config.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Config.zig similarity index 81% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/Config.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Config.zig index a90edb6..2bd4bb5 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/Config.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Config.zig @@ -3,7 +3,9 @@ const std = @import("std"); -const Rule = @import("rules.zig").Rule; +/// Public re-export: `isRuleEnabled`/`setRuleEnabled` take a `Rule`, so the +/// type must be reachable from this namespace by callers (ziglint Z012). +pub const Rule = @import("rules.zig").Rule; const Config = @This(); @@ -28,6 +30,15 @@ pub fn isRuleEnabled(self: *const Config, rule: Rule) bool { return true; } +/// Frees the `paths` entries allocated by `parseConfigSource` and leaves +/// the config invalidated. Safe to call on a default-initialized `Config` +/// (the default `paths` slice is empty and never freed). +pub fn deinit(self: *Config, allocator: std.mem.Allocator) void { + for (self.paths) |p| allocator.free(p); + if (self.paths.len > 0) allocator.free(self.paths); + self.* = undefined; +} + /// Set whether a rule is enabled. pub fn setRuleEnabled(self: *Config, rule: Rule, enabled: bool) void { inline for (@typeInfo(Rule).@"enum".fields) |field| { @@ -39,20 +50,20 @@ pub fn setRuleEnabled(self: *Config, rule: Rule, enabled: bool) void { } /// Load config from .ziglint.zon file, searching from start_path up to root. -pub fn load(io: std.Io, allocator: std.mem.Allocator, start_path: ?[]const u8) !Config { - const config_path = try findConfigFile(io, allocator, start_path) orelse return .{}; +pub fn load(allocator: std.mem.Allocator, io: std.Io, start_path: ?[]const u8) !Config { + const config_path = try findConfigFile(allocator, io, start_path) orelse return .{}; defer allocator.free(config_path); - return parseConfigFile(io, allocator, config_path) catch |err| { + return parseConfigFile(allocator, io, config_path) catch |err| { std.debug.print("warning: failed to parse {s}: {}\n", .{ config_path, err }); return .{}; }; } /// Find .ziglint.zon by walking up from start_path. -fn findConfigFile(io: std.Io, allocator: std.mem.Allocator, start_path: ?[]const u8) !?[]const u8 { +fn findConfigFile(allocator: std.mem.Allocator, io: std.Io, start_path: ?[]const u8) !?[]const u8 { const path = start_path orelse { - return findConfigInDir(io, allocator, "."); + return findConfigInDir(allocator, io, "."); }; const abs_path_z = std.Io.Dir.cwd().realPathFileAlloc(io, path, allocator) catch null; @@ -61,7 +72,7 @@ fn findConfigFile(io: std.Io, allocator: std.mem.Allocator, start_path: ?[]const var current = abs_path; while (true) { - if (try findConfigInDir(io, allocator, current)) |config_path| { + if (try findConfigInDir(allocator, io, current)) |config_path| { return config_path; } @@ -73,7 +84,7 @@ fn findConfigFile(io: std.Io, allocator: std.mem.Allocator, start_path: ?[]const return null; } -fn findConfigInDir(io: std.Io, allocator: std.mem.Allocator, dir_path: []const u8) !?[]const u8 { +fn findConfigInDir(allocator: std.mem.Allocator, io: std.Io, dir_path: []const u8) !?[]const u8 { const config_path = try std.fs.path.join(allocator, &.{ dir_path, ".ziglint.zon" }); errdefer allocator.free(config_path); @@ -91,7 +102,7 @@ const ZonConfig = struct { rules: ?Rule.Config = null, }; -fn parseConfigFile(io: std.Io, allocator: std.mem.Allocator, path: []const u8) !Config { +fn parseConfigFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8) !Config { const source = try std.Io.Dir.cwd().readFileAllocOptions( io, path, @@ -194,11 +205,8 @@ test "parse paths config" { \\ }, \\} ; - const config = try parseConfigSource(std.testing.allocator, source); - defer { - for (config.paths) |item| std.testing.allocator.free(item); - std.testing.allocator.free(config.paths); - } + var config = try parseConfigSource(std.testing.allocator, source); + defer config.deinit(std.testing.allocator); try std.testing.expectEqual(2, config.paths.len); try std.testing.expectEqualStrings("src", config.paths[0]); try std.testing.expectEqualStrings("lib", config.paths[1]); diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/Linter.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Linter.zig similarity index 98% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/Linter.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Linter.zig index fc72181..d294353 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/Linter.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Linter.zig @@ -28,10 +28,10 @@ const Linter = @This(); /// in Linter is diagnostic-only (perf debug), so we degrade reads to 0 here and /// leave the wider Io plumbing for a follow-up refactor. const Timer = struct { - pub fn start() error{}!Timer { + fn start() error{}!Timer { return .{}; } - pub fn read(_: *Timer) u64 { + fn read(_: *Timer) u64 { return 0; } }; @@ -983,7 +983,7 @@ fn isTypeExpression(self: *Linter, node: Ast.Node.Index) bool { // Builtin type constructors .builtin_call_two, .builtin_call_two_comma, .builtin_call, .builtin_call_comma => blk: { const token = self.tree.tokenSlice(self.tree.nodeMainToken(node)); - break :blk std.mem.eql(u8, token, "@Type"); + break :blk isBuiltinTypeConstructor(token); }, // Identifier referencing another type (type alias) .identifier => blk: { @@ -1081,6 +1081,38 @@ fn isBuiltinType(name: []const u8) bool { return false; } +fn isBuiltinTypeConstructor(name: []const u8) bool { + const builtins = [_][]const u8{ + "@This", + "@import", + "@Type", + "@TypeOf", + "@Int", + "@Float", + "@Pointer", + "@Array", + "@Vector", + "@Optional", + "@ErrorUnion", + "@ErrorSet", + "@Enum", + // @EnumLiteral() type and @Tuple(comptime field_types: []const type) type + // are documented Zig 0.16 type-constructor builtins (langref §@EnumLiteral, + // §@Tuple). They return `type`, so they belong in this list. + "@EnumLiteral", + "@Union", + "@Struct", + "@Opaque", + "@Fn", + "@Frame", + "@Tuple", + }; + for (builtins) |b| { + if (std.mem.eql(u8, name, b)) return true; + } + return false; +} + const ParamKind = enum { type_param, allocator, @@ -2404,7 +2436,7 @@ fn checkRedundantType(self: *Linter, node: Ast.Node.Index, check_field_access: b const full_expr = self.getNodeSource(node); const type_name = self.tree.tokenSlice(type_token); // Find where the type name ends and extract the { ... } part - const brace_start = std.mem.indexOf(u8, full_expr, "{") orelse return; + const brace_start = std.mem.find(u8, full_expr, "{") orelse return; const fields_part = truncateExpr(full_expr[brace_start..]); const full_truncated = truncateExpr(full_expr); const msg = std.fmt.allocPrint(self.allocator, ".{s}\x00{s}", .{ fields_part, full_truncated }) catch return; @@ -2457,7 +2489,7 @@ fn truncateExpr(expr: []const u8) []const u8 { const max_len = 32; if (expr.len <= max_len) return expr; // Find a good break point (after opening brace if present) - if (std.mem.indexOf(u8, expr[0..@min(max_len, expr.len)], "{")) |brace| { + if (std.mem.find(u8, expr[0..@min(max_len, expr.len)], "{")) |brace| { return expr[0 .. brace + 1]; } return expr[0..max_len]; @@ -2691,10 +2723,7 @@ fn isTypeAlias(self: *Linter, var_decl: Ast.full.VarDecl) bool { }, .builtin_call_two, .builtin_call_two_comma, .builtin_call, .builtin_call_comma => blk: { const token = self.tree.tokenSlice(self.tree.nodeMainToken(init_node)); - break :blk std.mem.eql(u8, token, "@This") or - std.mem.eql(u8, token, "@import") or - std.mem.eql(u8, token, "@Type") or - std.mem.eql(u8, token, "@TypeOf"); + break :blk isBuiltinTypeConstructor(token); }, .call_one, .call_one_comma => blk: { // Check if calling a PascalCase function (type constructor) @@ -2819,9 +2848,9 @@ fn isIgnored(self: *Linter, line: usize, rule: rules.Rule) bool { } fn lineHasIgnore(_: *Linter, line_text: []const u8, rule: rules.Rule) bool { - if (std.mem.indexOf(u8, line_text, "// ziglint-ignore:")) |idx| { + if (std.mem.find(u8, line_text, "// ziglint-ignore:")) |idx| { const ignore_part = line_text[idx + 18 ..]; - if (std.mem.indexOf(u8, ignore_part, rule.code()) != null) return true; + if (std.mem.find(u8, ignore_part, rule.code()) != null) return true; } return false; } @@ -3099,8 +3128,8 @@ test "Z006: allow type alias with @import()" { } } -test "Z006: allow type alias with @Type()" { - var linter: Linter = .init(std.testing.allocator, "const MyType = @Type(.{ .int = .{ .signedness = .unsigned, .bits = 8 } });", "test.zig", null); +test "Z006: allow type alias with @Int()" { + var linter: Linter = .init(std.testing.allocator, "const MyType = @Int(.unsigned, 8);", "test.zig", null); defer linter.deinit(); linter.lint(); try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); @@ -3181,12 +3210,10 @@ test "Z006: allow type alias with primitive type" { test "Z006: allow PascalCase labeled block type alias" { var linter: Linter = .init(std.testing.allocator, \\const Config = blk: { - \\ break :blk @Type(.{ .@"struct" = .{ - \\ .layout = .auto, - \\ .fields = &.{}, - \\ .decls = &.{}, - \\ .is_tuple = false, - \\ } }); + \\ const field_names = [_][]const u8{}; + \\ const field_types = [_]type{}; + \\ const field_attrs = [_]std.builtin.Type.StructField.Attributes{}; + \\ break :blk @Struct(.auto, null, &field_names, &field_types, &field_attrs); \\}; , "test.zig", null); defer linter.deinit(); @@ -3526,7 +3553,7 @@ test "Z011: detect deprecated method call" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3570,7 +3597,7 @@ test "Z011: no warning for non-deprecated method" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3624,7 +3651,7 @@ test "Z011: detect deprecated direct function call" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3663,7 +3690,7 @@ test "Z011: detect deprecated function alias" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3703,7 +3730,7 @@ test "Z011: detect deprecated type function" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3744,7 +3771,7 @@ test "Z011: detect deprecated type function alias" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3783,9 +3810,9 @@ test "Z011: detect deprecated stdlib function (ArrayListUnmanaged)" { } const needle = ".lib_dir = \""; - const start_idx = std.mem.indexOf(u8, result.stdout, needle) orelse break :blk null; + const start_idx = std.mem.find(u8, result.stdout, needle) orelse break :blk null; const value_start = start_idx + needle.len; - const end_idx = std.mem.indexOfPos(u8, result.stdout, value_start, "\"") orelse break :blk null; + const end_idx = std.mem.findPos(u8, result.stdout, value_start, "\"") orelse break :blk null; break :blk std.testing.allocator.dupe(u8, result.stdout[value_start..end_idx]) catch null; }; @@ -3807,7 +3834,7 @@ test "Z011: detect deprecated stdlib function (ArrayListUnmanaged)" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, zig_lib_path); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, zig_lib_path); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -3846,9 +3873,9 @@ test "Z011: deprecated stdlib corpus - real Zig 0.15.2 deprecations" { } const needle = ".lib_dir = \""; - const start_idx = std.mem.indexOf(u8, result.stdout, needle) orelse break :blk null; + const start_idx = std.mem.find(u8, result.stdout, needle) orelse break :blk null; const value_start = start_idx + needle.len; - const end_idx = std.mem.indexOfPos(u8, result.stdout, value_start, "\"") orelse break :blk null; + const end_idx = std.mem.findPos(u8, result.stdout, value_start, "\"") orelse break :blk null; break :blk std.testing.allocator.dupe(u8, result.stdout[value_start..end_idx]) catch null; }; @@ -3999,7 +4026,7 @@ test "Z011: deprecated stdlib corpus - real Zig 0.15.2 deprecations" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, zig_lib_path); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, zig_lib_path); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -4683,7 +4710,7 @@ test "Z023: argument order - aliased Allocator" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -4717,7 +4744,7 @@ test "Z023: argument order - aliased Io" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -4785,7 +4812,7 @@ test "Z023: receiver param with struct name is ok (semantic)" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -4817,7 +4844,7 @@ test "Z023: file-as-struct receiver is ok (semantic)" { const path = try tmp_dir.dir.realPathFileAlloc(io, "Terminal.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5188,7 +5215,7 @@ test "Z027: flag instance accessing const" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5229,7 +5256,7 @@ test "Z027: flag instance accessing static fn" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5272,7 +5299,7 @@ test "Z027: allow instance method call" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5309,7 +5336,7 @@ test "Z027: allow field access" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5346,7 +5373,7 @@ test "Z027: allow type-level access" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5406,7 +5433,7 @@ test "Z027: allow method with Self receiver" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5446,7 +5473,7 @@ test "Z027: allow method with named type receiver" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -5538,7 +5565,7 @@ test "Z029: detect redundant @as in method call arg" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/ModuleGraph.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/ModuleGraph.zig similarity index 90% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/ModuleGraph.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/ModuleGraph.zig index f08e2d1..8763bbb 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/ModuleGraph.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/ModuleGraph.zig @@ -17,13 +17,18 @@ root_dir: []const u8, modules: std.StringHashMapUnmanaged(Module), pub const Module = struct { - path: []const u8, + /// Sentinel-terminated to match the buffer returned by `realPathFileAlloc`, + /// which allocates `len + 1` bytes. Storing it as `[:0]const u8` keeps the + /// free path size-accurate (no implicit slice-shortening) and avoids the + /// previous "alloc then dupe" double allocation. The sentinel is a useful + /// extra (paths can be passed to libc-style APIs without re-duping). + path: [:0]const u8, source: [:0]const u8, tree: Ast, zir: ?Zir = null, }; -pub fn init(io: std.Io, allocator: std.mem.Allocator, root_source: []const u8, zig_lib_path: ?[]const u8) !ModuleGraph { +pub fn init(allocator: std.mem.Allocator, io: std.Io, root_source: []const u8, zig_lib_path: ?[]const u8) !ModuleGraph { const root_dir = std.fs.path.dirname(root_source) orelse "."; var graph: ModuleGraph = .{ @@ -56,9 +61,15 @@ pub fn addModulePublic(self: *ModuleGraph, path: []const u8) void { } fn addModule(self: *ModuleGraph, path: []const u8) !void { - const canonical_z = try std.Io.Dir.cwd().realPathFileAlloc(self.io, path, self.allocator); - defer self.allocator.free(canonical_z); - const canonical = try self.allocator.dupe(u8, canonical_z); + // `realPathFileAlloc` returns a sentinel-terminated `[:0]u8` (allocated as + // `len + 1` bytes). We store the canonical path with its sentinel intact so + // the matching `free` later sees the same slice it was allocated as -- this + // is what avoids the "Allocation size N+1 vs free size N" debug-allocator + // panic. The graph owns this buffer for its lifetime; it is freed in + // `deinit`. No extra `dupe` is needed (the previous code did one purely to + // strip the sentinel for typing reasons). + const canonical: [:0]const u8 = try std.Io.Dir.cwd().realPathFileAlloc(self.io, path, self.allocator); + errdefer self.allocator.free(canonical); if (self.modules.contains(canonical)) { self.allocator.free(canonical); @@ -280,7 +291,7 @@ test "parse simple module" { const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); try std.testing.expectEqual(1, graph.moduleCount()); @@ -297,7 +308,7 @@ test "resolve relative import" { const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); try std.testing.expectEqual(2, graph.moduleCount()); @@ -313,7 +324,7 @@ test "handle missing import gracefully" { const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); try std.testing.expectEqual(1, graph.moduleCount()); @@ -334,7 +345,7 @@ test "no duplicate modules" { const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); // main.zig, other.zig, shared.zig - no duplicates diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/TypeResolver.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/TypeResolver.zig similarity index 95% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/TypeResolver.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/TypeResolver.zig index c6345bd..31d5a80 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/TypeResolver.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/TypeResolver.zig @@ -446,6 +446,12 @@ fn findDeclInModule(self: *TypeResolver, tree: *const Ast, name: []const u8, mod if (std.mem.endsWith(u8, import_str, ".zig")) { const module_dir = std.fs.path.dirname(module_path) orelse "."; const resolved = std.fs.path.join(self.allocator, &.{ module_dir, import_str }) catch continue; + // `realPathFileAlloc` returns a sentinel-terminated `[:0]u8` + // (allocated `len + 1`). We dupe into a plain `[]u8` so the + // returned `DeclResult.import_path` type-matches the non-sentinel + // sibling (`current_module_path`) used by callers, which uniformly + // free the slice with the same length they received -- avoiding the + // sentinel/non-sentinel size mismatch in DebugAllocator. const canonical_z = std.Io.Dir.cwd().realPathFileAlloc(self.graph.io, resolved, self.allocator) catch { self.allocator.free(resolved); continue; @@ -485,8 +491,22 @@ fn findMethodInModule(self: *TypeResolver, module_path: []const u8, type_name: [ return self.findMethodInType(tree, type_node, method_name, mod.path); } +/// Alias chains (`const a = b;`) are followed at most this many hops so +/// cyclic aliases (`const a = b; const b = a;`) cannot recurse unbounded. +const max_alias_depth: u32 = 32; + /// For file-as-struct modules (like fs/File.zig), look for methods in root declarations. fn findMethodInFileAsStruct(self: *TypeResolver, module_path: []const u8, method_name: []const u8) ?MethodDef { + return self.findMethodInFileAsStructDepth(module_path, method_name, 0); +} + +fn findMethodInFileAsStructDepth( + self: *TypeResolver, + module_path: []const u8, + method_name: []const u8, + depth: u32, +) ?MethodDef { + if (depth >= max_alias_depth) return null; self.graph.addModulePublic(module_path); const mod = self.graph.getModule(module_path) orelse return null; const tree = &mod.tree; @@ -524,7 +544,7 @@ fn findMethodInFileAsStruct(self: *TypeResolver, module_path: []const u8, method // It's an alias like `const ArrayListUnmanaged = ArrayList;` // Follow the alias to find the actual function const alias_target = tree.tokenSlice(tree.nodeMainToken(init_node)); - return self.findMethodInFileAsStruct(module_path, alias_target); + return self.findMethodInFileAsStructDepth(module_path, alias_target, depth + 1); } else if (init_tag == .fn_decl) { // Inline function definition var buf: [1]Ast.Node.Index = undefined; @@ -816,9 +836,10 @@ fn resolveFieldAccess(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Inde return .{ .std_type = .{ .path = full_path } }; }, .user_type => { - // Accessing a field on a user type - could be a nested type - const full_path = self.buildStdTypePath(tree, node); - return .{ .std_type = .{ .path = full_path } }; + // A field access on a user-defined type is not a stdlib type. + // Coercing it into `.std_type` conflated user namespaces with + // `std` ones and misrouted downstream semantic resolution. + return .unknown; }, .unknown => { const lhs_tag = tree.nodeTag(lhs_node); @@ -1107,9 +1128,9 @@ fn resolveNumberLiteral(_: *TypeResolver, tree: *const Ast, node: Ast.Node.Index const main_token = tree.nodeMainToken(node); const text = tree.tokenSlice(main_token); - if (std.mem.indexOf(u8, text, ".") != null or - std.mem.indexOf(u8, text, "e") != null or - std.mem.indexOf(u8, text, "E") != null) + if (std.mem.find(u8, text, ".") != null or + std.mem.find(u8, text, "e") != null or + std.mem.find(u8, text, "E") != null) { return .{ .primitive = .comptime_float }; } @@ -1208,7 +1229,7 @@ test "resolve primitive types" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1236,7 +1257,7 @@ test "resolve bool literal" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1262,7 +1283,7 @@ test "resolve import std" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1287,7 +1308,7 @@ test "resolve function returns type" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1312,7 +1333,7 @@ test "resolve number literal int" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1338,7 +1359,7 @@ test "resolve number literal float" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1367,7 +1388,7 @@ test "resolve field access on std" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1398,7 +1419,7 @@ test "resolve nested field access std.fs.File" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1429,7 +1450,7 @@ test "resolve field access on aliased std import" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1460,7 +1481,7 @@ test "resolve nested field access on aliased std import" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1488,7 +1509,7 @@ test "resolve string literal" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1513,7 +1534,7 @@ test "resolve function declaration" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1542,7 +1563,7 @@ test "find method in user type" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1570,7 +1591,7 @@ test "find method not found returns null" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1592,7 +1613,7 @@ test "find method in std_type without zig_lib_path returns null" { defer std.testing.allocator.free(path); // No zig_lib_path, so stdlib can't be resolved - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1620,7 +1641,7 @@ test "resolve function call return type" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1648,7 +1669,7 @@ test "resolve function call returning bool" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1680,7 +1701,7 @@ test "resolve method call return type" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1708,7 +1729,7 @@ test "resolve type-returning function call" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1732,7 +1753,7 @@ test "resolve unknown function call" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1754,7 +1775,7 @@ test "isTypeRef: struct declaration" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1778,7 +1799,7 @@ test "isTypeRef: instance with type annotation" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1800,7 +1821,7 @@ test "isTypeRef: primitive typed variable" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1821,7 +1842,7 @@ test "isTypeRef: explicit type annotation" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1842,7 +1863,7 @@ test "isTypeRef: enum declaration" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1868,7 +1889,7 @@ test "isTypeRef: function call returning type" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1894,7 +1915,7 @@ test "isTypeRef: function call returning value" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1918,7 +1939,7 @@ test "isTypeRef: identifier resolving to struct" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1945,7 +1966,7 @@ test "isTypeRef: identifier resolving to instance" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1974,7 +1995,7 @@ test "findFnInCurrentModule: type function" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -1998,7 +2019,7 @@ test "findFnInCurrentModule: direct function" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -2023,7 +2044,7 @@ test "findFnInCurrentModule: const alias to function" { const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); defer std.testing.allocator.free(path); - var graph = try ModuleGraph.init(io, std.testing.allocator, path, null); + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); defer graph.deinit(); var resolver: TypeResolver = .init(std.testing.allocator, &graph); @@ -2037,5 +2058,5 @@ test "findFnInCurrentModule: const alias to function" { const doc = doc_comments.getDocComment(std.testing.allocator, &mod.tree, method_def.?.node); defer if (doc) |d| std.testing.allocator.free(d); try std.testing.expect(doc != null); - try std.testing.expect(std.mem.indexOf(u8, doc.?, "Deprecated") != null); + try std.testing.expect(std.mem.find(u8, doc.?, "Deprecated") != null); } diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/doc_comments.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_comments.zig similarity index 88% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/doc_comments.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_comments.zig index 0f5a687..5f73b76 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/doc_comments.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_comments.zig @@ -23,8 +23,21 @@ pub fn getDocComment(allocator: std.mem.Allocator, tree: *const Ast, node: Ast.N if (tag == .doc_comment) { doc_tokens.append(allocator, token) catch return null; } else { - // Stop at non-doc-comment tokens (skip pub keyword) - if (tag != .keyword_pub) break; + // Stop at non-doc-comment tokens, skipping declaration + // qualifiers (`pub inline fn`, `pub extern "c" fn`, ...) that + // may sit between the doc comment and the declaration itself. + switch (tag) { + .keyword_pub, + .keyword_inline, + .keyword_noinline, + .keyword_extern, + .keyword_export, + .keyword_threadlocal, + .keyword_comptime, + .string_literal, + => {}, + else => break, + } } if (token == 0) break; token -= 1; diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/doc_tests.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_tests.zig similarity index 79% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/doc_tests.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_tests.zig index bfd4467..ff5b5cf 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/doc_tests.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_tests.zig @@ -36,7 +36,7 @@ fn parseMarkdown(allocator: std.mem.Allocator, content: []const u8) !ParsedDoc { // Parse frontmatter for rule identifier if (std.mem.startsWith(u8, content, "---\n")) { - if (std.mem.indexOf(u8, content[4..], "\n---")) |end| { + if (std.mem.find(u8, content[4..], "\n---")) |end| { const frontmatter = content[4..][0..end]; var lines = std.mem.splitScalar(u8, frontmatter, '\n'); while (lines.next()) |line| { @@ -51,7 +51,7 @@ fn parseMarkdown(allocator: std.mem.Allocator, content: []const u8) !ParsedDoc { // Find all zig code blocks var line_num: usize = 1; var pos: usize = 0; - while (std.mem.indexOfPos(u8, content, pos, "```zig\n")) |start| { + while (std.mem.findPos(u8, content, pos, "```zig\n")) |start| { // Count lines up to this point for (content[pos..start]) |c| { if (c == '\n') line_num += 1; @@ -59,14 +59,14 @@ fn parseMarkdown(allocator: std.mem.Allocator, content: []const u8) !ParsedDoc { line_num += 1; // for the ```zig line const code_start = start + 7; - if (std.mem.indexOfPos(u8, content, code_start, "\n```")) |end| { + if (std.mem.findPos(u8, content, code_start, "\n```")) |end| { const code = content[code_start..end]; // Parse expected rules from `// expect: ZXXX` comments var expected: std.ArrayList(rules.Rule) = .empty; var code_lines = std.mem.splitScalar(u8, code, '\n'); while (code_lines.next()) |code_line| { - if (std.mem.indexOf(u8, code_line, "// expect:")) |expect_pos| { + if (std.mem.find(u8, code_line, "// expect:")) |expect_pos| { var expect_str = code_line[expect_pos + 10 ..]; expect_str = std.mem.trim(u8, expect_str, " "); // Handle multiple expectations: `// expect: Z001, Z002` @@ -111,7 +111,7 @@ fn runDocTest(allocator: std.mem.Allocator, doc_path: []const u8, doc_test: DocT defer allocator.free(path); // Try to create ModuleGraph for semantic analysis (may fail for invalid code) - var graph: ?ModuleGraph = ModuleGraph.init(io, allocator, path, null) catch null; + var graph: ?ModuleGraph = ModuleGraph.init(allocator, io, path, null) catch null; defer if (graph) |*g| g.deinit(); var resolver: ?TypeResolver = if (graph) |*g| TypeResolver.init(allocator, g) else null; @@ -138,19 +138,21 @@ fn runDocTest(allocator: std.mem.Allocator, doc_path: []const u8, doc_test: DocT } } - // If no expectations, should have no diagnostics - if (doc_test.expected_rules.len == 0) { - const total = linter.diagnostics.items.len; - if (total > 0) { - std.debug.print("\n{s}:{d}: expected no diagnostics but got {d}\n", .{ + // Reject diagnostics for rules that were not expected. Extra findings + // must not ride along silently just because one expectation matched — + // and with no expectations at all, any diagnostic is unexpected. + for (linter.diagnostics.items) |d| { + const expected = for (doc_test.expected_rules) |expected_rule| { + if (expected_rule == d.rule) break true; + } else false; + if (!expected) { + std.debug.print("\n{s}:{d}: unexpected {s} diagnostic: {s}\n", .{ doc_path, doc_test.line_in_doc, - total, + d.rule.code(), + d.context, }); std.debug.print("Code:\n{s}\n", .{doc_test.code}); - for (linter.diagnostics.items) |d| { - std.debug.print(" - {s}: {s}\n", .{ d.rule.code(), d.context }); - } return error.UnexpectedDiagnostic; } } @@ -175,6 +177,7 @@ pub fn runAllDocTests(allocator: std.mem.Allocator) !void { var file_count: usize = 0; var test_count: usize = 0; + var failure_count: usize = 0; var iter = dir.iterate(); while (try iter.next(io)) |entry| { if (entry.kind != .file) continue; @@ -193,11 +196,25 @@ pub fn runAllDocTests(allocator: std.mem.Allocator) !void { defer allocator.free(full_path); for (doc.tests) |doc_test| { - try runDocTest(allocator, full_path, doc_test, &tmp_dir); + // Run every fixture before failing so one gate run reports all + // offending docs instead of stopping at the first. Only the two + // assertion errors count as fixture failures; infrastructure + // errors (I/O, OOM, ...) propagate immediately. + runDocTest(allocator, full_path, doc_test, &tmp_dir) catch |err| switch (err) { + error.MissingExpectedDiagnostic, + error.UnexpectedDiagnostic, + => failure_count += 1, + else => return err, + }; test_count += 1; } file_count += 1; } + + if (failure_count > 0) { + std.debug.print("\n{d}/{d} doc tests failed\n", .{ failure_count, test_count }); + return error.DocTestsFailed; + } } test "doc tests" { diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/main.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/main.zig similarity index 85% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/main.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/main.zig index ad3662a..82075f7 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/main.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/main.zig @@ -1,7 +1,6 @@ //! ziglint - A linter for Zig source code const std = @import("std"); -const builtin = @import("builtin"); const build_options = @import("build_options"); pub const version = build_options.version; @@ -49,7 +48,7 @@ pub fn main(init: std.process.Init) !u8 { const cfg_alloc = config_arena.allocator(); const stderr_file = std.Io.File.stderr(); - const use_color = detectColorSupport(stderr_file, io, environ_map); + const use_color = detectColorSupport(io, stderr_file, environ_map); var stdout_buf: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(io, &stdout_buf); @@ -67,7 +66,7 @@ pub fn main(init: std.process.Init) !u8 { // Load config file from first CLI path (or current directory) const start_path = if (config.paths.len > 0) config.paths[0] else null; - config.file_config = FileConfig.load(io, cfg_alloc, start_path) catch .{}; + config.file_config = FileConfig.load(cfg_alloc, io, start_path) catch .{}; applyOnlyRules(&config); @@ -89,7 +88,9 @@ pub fn main(init: std.process.Init) !u8 { const abs_path_z = std.Io.Dir.cwd().realPathFileAlloc(io, path, allocator) catch null; defer if (abs_path_z) |p| allocator.free(p); const abs_path: []const u8 = if (abs_path_z) |p| p else path; - const project_root = findProjectRoot(io, abs_path); + // `project_root` is allocated from the process-lifetime arena (`cfg_alloc`) + // so it lives until end-of-main without a manual free. + const project_root = findProjectRoot(cfg_alloc, io, abs_path); total_issues += try lintPath(allocator, io, path, zig_lib_path, &config, use_color, project_root, &stderr.interface); } @@ -104,23 +105,26 @@ pub fn main(init: std.process.Init) !u8 { return if (total_issues > 0) 1 else 0; } -fn detectColorSupport(file: std.Io.File, io: std.Io, environ_map: *const std.process.Environ.Map) bool { +fn detectColorSupport(io: std.Io, file: std.Io.File, environ_map: *const std.process.Environ.Map) bool { // NO_COLOR takes precedence (https://no-color.org/) if (environ_map.get("NO_COLOR")) |_| return false; if (environ_map.get("FORCE_COLOR")) |_| return true; - // Otherwise, use color if stdout is a TTY + // Otherwise, use color if the provided file (caller-selected stream, e.g. stderr) is a TTY return file.isTty(io) catch false; } -fn findProjectRoot(io: std.Io, start_path: []const u8) ?[]const u8 { +/// Walks upwards from `start_path` looking for a directory containing `build.zig`. +/// The returned slice is owned by `allocator` (callers typically pass a process-lifetime +/// arena). Returns `null` if no project root is found or on allocation/IO failure. +fn findProjectRoot(allocator: std.mem.Allocator, io: std.Io, start_path: []const u8) ?[]const u8 { var path = start_path; while (true) { // Check if build.zig exists in this directory - const build_zig = std.fs.path.join(std.heap.page_allocator, &.{ path, "build.zig" }) catch return null; - defer std.heap.page_allocator.free(build_zig); + const build_zig = std.fs.path.join(allocator, &.{ path, "build.zig" }) catch return null; + defer allocator.free(build_zig); if (std.Io.Dir.cwd().access(io, build_zig, .{})) |_| { - return std.heap.page_allocator.dupe(u8, path) catch null; + return allocator.dupe(u8, path) catch null; } else |_| {} // Move up one directory @@ -256,9 +260,9 @@ fn detectZigLibPath(out_alloc: std.mem.Allocator, tmp_alloc: std.mem.Allocator, fn parseLibDirFromZigEnv(allocator: std.mem.Allocator, output: []const u8) ?[]const u8 { const needle = ".lib_dir = \""; - const start_idx = std.mem.indexOf(u8, output, needle) orelse return null; + const start_idx = std.mem.find(u8, output, needle) orelse return null; const value_start = start_idx + needle.len; - const end_idx = std.mem.indexOfPos(u8, output, value_start, "\"") orelse return null; + const end_idx = std.mem.findPos(u8, output, value_start, "\"") orelse return null; return allocator.dupe(u8, output[value_start..end_idx]) catch null; } @@ -285,7 +289,7 @@ fn lintDirectory(allocator: std.mem.Allocator, io: std.Io, path: []const u8, zig }; defer dir.close(io); - const gitignore = loadGitignore(io, allocator, dir); + const gitignore = loadGitignore(allocator, io, dir); defer if (gitignore) |g| allocator.free(g); // Collect all .zig files first @@ -327,7 +331,7 @@ fn lintDirectory(allocator: std.mem.Allocator, io: std.Io, path: []const u8, zig // Build module graph once using first file as root, then add all others const graph_start = if (timer) |*t| t.read(io) else 0; - var graph = ModuleGraph.init(io, allocator, files.items[0], zig_lib_path) catch { + var graph = ModuleGraph.init(allocator, io, files.items[0], zig_lib_path) catch { // Fall back to per-file linting without semantics if (config.verbose) { try writer.print("{s}│ module graph failed, using simple linting{s}\n", .{ dim, reset }); @@ -413,10 +417,10 @@ fn matchesGitignore(path: []const u8, pattern: []const u8) bool { if (std.mem.eql(u8, component, clean_pattern)) return true; } - return std.mem.indexOf(u8, path, clean_pattern) != null; + return std.mem.find(u8, path, clean_pattern) != null; } -fn loadGitignore(io: std.Io, allocator: std.mem.Allocator, dir: std.Io.Dir) ?[]const u8 { +fn loadGitignore(allocator: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ?[]const u8 { return dir.readFileAlloc(io, ".gitignore", allocator, .limited(1024 * 64)) catch null; } @@ -432,7 +436,7 @@ fn lintFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, zig_lib_ } const graph_start = if (timer) |*t| t.read(io) else 0; - var graph = ModuleGraph.init(io, allocator, path, zig_lib_path) catch { + var graph = ModuleGraph.init(allocator, io, path, zig_lib_path) catch { return lintFileSimple(allocator, io, path, config, use_color, project_root, writer); }; defer graph.deinit(); @@ -633,3 +637,55 @@ test "applyOnlyRules keeps ignore precedence" { try std.testing.expect(!config.file_config.isRuleEnabled(.Z002)); try std.testing.expect(!config.file_config.isRuleEnabled(.Z003)); } + +/// Test-only Io provider matching the convention used by ModuleGraph/TypeResolver. +fn testIo() std.Io { + return std.Io.Threaded.global_single_threaded.io(); +} + +test "findProjectRoot returns owned slice from caller allocator (no leak)" { + // Regression: previously `findProjectRoot` allocated via std.heap.page_allocator + // and the result was never freed by the caller (memory leak per Copilot finding). + // The fixed signature takes an explicit allocator. Using std.testing.allocator + // here ensures the caller-side free succeeds and that no other allocations + // escape the function. + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const io = testIo(); + try tmp.dir.writeFile(io, .{ .sub_path = "build.zig", .data = "// minimal build.zig" }); + try tmp.dir.createDirPath(io, "src"); + + const root_abs = try tmp.dir.realPathFileAlloc(io, "build.zig", std.testing.allocator); + defer std.testing.allocator.free(root_abs); + + // Pass the build.zig path's directory as start; findProjectRoot should locate it. + const dir_of_root = std.fs.path.dirname(root_abs) orelse unreachable; + + const found = findProjectRoot(std.testing.allocator, io, dir_of_root); + try std.testing.expect(found != null); + defer std.testing.allocator.free(found.?); + + try std.testing.expectEqualStrings(dir_of_root, found.?); +} + +test "findProjectRoot returns null when no build.zig is found" { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const io = testIo(); + try tmp.dir.createDirPath(io, "isolated/nested"); + + // realPathFileAlloc requires a regular file; resolve via a sentinel file. + try tmp.dir.writeFile(io, .{ .sub_path = "isolated/nested/sentinel", .data = "" }); + const sentinel = try tmp.dir.realPathFileAlloc(io, "isolated/nested/sentinel", std.testing.allocator); + defer std.testing.allocator.free(sentinel); + const start = std.fs.path.dirname(sentinel) orelse unreachable; + + // Walking up will eventually hit filesystem root without finding build.zig + // (the temp dir tree we created has no build.zig anywhere on the path). + // To make this deterministic regardless of host layout, just verify that + // either we get null OR a slice we can free without a leak. + const found = findProjectRoot(std.testing.allocator, io, start); + if (found) |p| std.testing.allocator.free(p); +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/rules.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/rules.zig similarity index 96% rename from zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/rules.zig rename to zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/rules.zig index fa7d80c..bf33ff3 100644 --- a/zig-pkg/ziglint-0.5.2-t0bwLz2XBQCgnA0PTY0KM9YgrmdjLU7wwjXablNijOuq/src/rules.zig +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/rules.zig @@ -102,7 +102,7 @@ pub const Rule = enum(u16) { // syntax highlight: .{...} over Type{...} // context is "preferred\x00original" format .Z010 => { - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const preferred = context[0..sep]; const original = if (sep < context.len) context[sep + 1 ..] else context; try writer.print("prefer {s}`{s}", .{ d, r }); @@ -152,7 +152,7 @@ pub const Rule = enum(u16) { // file-struct @This() alias should match filename // context is "alias\x00expected" format .Z021 => { - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const alias = context[0..sep]; const expected = if (sep < context.len) context[sep + 1 ..] else context; try writer.print("{s}@This(){s} alias {s}'{s}'{s} should match filename {s}'{s}'{s}", .{ b, r, y, alias, r, y, expected, r }); @@ -164,7 +164,7 @@ pub const Rule = enum(u16) { // argument order: type params, Allocator, Io, then other args // context is "current_kind\x00expected_before" format .Z023 => { - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const current = context[0..sep]; const before = if (sep < context.len) context[sep + 1 ..] else ""; try writer.print("{s}'{s}'{s} parameter should come before {s}'{s}'{s}", .{ y, current, r, y, before, r }); @@ -172,7 +172,7 @@ pub const Rule = enum(u16) { // line length exceeds limit // context is "actual_len\x00max_len" format .Z024 => { - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const actual = context[0..sep]; const max = if (sep < context.len) context[sep + 1 ..] else "120"; try writer.print("line exceeds {s}{s}{s} characters ({s}{s}{s} chars)", .{ y, max, r, y, actual, r }); @@ -194,7 +194,7 @@ pub const Rule = enum(u16) { // instance.decl -> Type.decl // context is "field_name\x00type_name" .Z027 => { - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const field = context[0..sep]; const type_name = if (sep < context.len) context[sep + 1 ..] else ""; try writer.print("access {s}'{s}'{s} through type {s}'{s}'{s} instead of instance", .{ @@ -221,7 +221,7 @@ pub const Rule = enum(u16) { }, .Z032 => { // context is "name\x00suggestion" - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const name = context[0..sep]; const suggestion = if (sep < context.len) context[sep + 1 ..] else ""; if (suggestion.len > 0) { @@ -232,7 +232,7 @@ pub const Rule = enum(u16) { }, .Z033 => { // context is "name\x00word" - const sep = std.mem.indexOfScalar(u8, context, 0) orelse context.len; + const sep = std.mem.findScalar(u8, context, 0) orelse context.len; const name = context[0..sep]; const word = if (sep < context.len) context[sep + 1 ..] else ""; try writer.print("identifier {s}'{s}'{s} contains redundant word {s}'{s}'{s}", .{ y, name, r, y, word, r });