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/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/build.zig.zon b/build.zig.zon index c5077ed..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=fix/0.16-build-compat#40f26f1283468032ec993c93fca921bd3c106adb", - .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/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/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 96fb0eb..530d7b7 100644 --- a/src/Walk.zig +++ b/src/Walk.zig @@ -6,11 +6,11 @@ const Ast = std.zig.Ast; const assert = std.debug.assert; const log = std.log; var gpa: std.mem.Allocator = undefined; -var io_global: std.Io = undefined; +var io: std.Io = undefined; -pub fn init(allocator: std.mem.Allocator, io: std.Io) void { +pub fn init(allocator: std.mem.Allocator, io_arg: std.Io) void { gpa = allocator; - io_global = io; + io = io_arg; } const Oom = error{OutOfMemory}; @@ -21,6 +21,21 @@ pub var files: std.array_hash_map.String(File) = .empty; pub var decls: std.ArrayList(Decl) = .empty; pub var modules: std.array_hash_map.String(File.Index) = .empty; +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); + files = .empty; + decls = .empty; + modules = .empty; +} + file: File.Index, /// keep in sync with "CAT_" constants in main.js @@ -69,6 +84,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; @@ -339,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.cwd().realPathFileAlloc(io_global, 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, @@ -363,7 +398,7 @@ pub const File = struct { ); } else { const import_content = std.Io.Dir.cwd().readFileAlloc( - io_global, + io, resolved_path, gpa, .limited(10 * 1024 * 1024), @@ -434,21 +469,44 @@ 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.Io.Dir.cwd().realPathFileAlloc(io_global, 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, }; 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()); @@ -518,11 +576,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/build_runner_0.16.zig b/src/build_runner_0.16.zig new file mode 100644 index 0000000..d71332b --- /dev/null +++ b/src/build_runner_0.16.zig @@ -0,0 +1,223 @@ +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); + if (args.len < 6) return error.InvalidArgs; + + // 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.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(our_allocator, entry.key_ptr.*, entry.value_ptr.*); + } + + // Walk compile steps to find their root modules and imports + 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(our_allocator, &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; + + // 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"); + 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.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"); + } 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( + allocator: std.mem.Allocator, + modules: *std.StringHashMapUnmanaged(*std.Build.Module), + step: *std.Build.Step, + visited: *std.AutoHashMapUnmanaged(*std.Build.Step, void), +) !void { + // Avoid infinite recursion on circular dependencies + if (visited.contains(step)) return; + try visited.put(allocator, 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(allocator, import_name, module); + } + } + + // Recursively check dependencies + for (step.dependencies.items) |dep_step| { + try collectStepModules(allocator, modules, dep_step, visited); + } +} diff --git a/src/main.zig b/src/main.zig index 8434773..1850c8e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,7 @@ 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"); @@ -13,16 +14,14 @@ const template_gitignore = @embedFile("templates/.gitignore.template"); 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 init.minimal.args.iterateAllocator(gpa); + defer args.deinit(); + _ = args.skip(); // skip program name - var args_iter = try init.minimal.args.iterateAllocator(arena.allocator()); - defer args_iter.deinit(); - _ = args_iter.skip(); // skip program name - - const symbol = args_iter.next(); + const symbol = args.next(); if (symbol == null) { try printUsage(io); @@ -35,7 +34,7 @@ pub fn main(init: std.process.Init) !void { } if (std.mem.eql(u8, symbol.?, "--dump-imports")) { - try dumpImports(io, &arena); + try dumpImports(io, arena); return; } @@ -47,18 +46,24 @@ 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")) { - try walkStdLib(io, &arena, std_dir_path); + try walkStdLib(io, arena, 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(io, &arena); + try processBuildZig(io, arena); } try printDocs(arena.allocator(), io, symbol.?, std_dir_path); @@ -90,8 +95,15 @@ fn printUsage(io: std.Io) !void { \\ @init Initialize a new Zig project with AGENTS.md \\ ); - try stdout_writer.interface.flush(); - try stdout_writer.end(); + 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 { @@ -104,9 +116,8 @@ fn initProject(allocator: std.mem.Allocator, io: std.Io) !void { } else |_| {} // Get project name from current directory - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const cwd_path_len = try cwd.realPath(io, &path_buf); - const cwd_path = path_buf[0..cwd_path_len]; + 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 @@ -128,11 +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| { @@ -156,15 +172,31 @@ fn initProject(allocator: std.mem.Allocator, io: std.Io) !void { } 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(io: std.Io, arena: *std.heap.ArenaAllocator) !void { - const cwd = std.Io.Dir.cwd(); +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(io: std.Io, arena: *std.heap.ArenaAllocator) !void { // Check if build.zig exists - cwd.access(io, "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; }; @@ -180,41 +212,38 @@ fn dumpImports(io: std.Io, arena: *std.heap.ArenaAllocator) !void { "--build-runner", ".zig-cache/zigdoc_build_runner.zig", }, - .stdout_limit = .limited(64 * 1024 * 1024), }); - switch (result.term) { - .exited => |code| if (code != 0) { - std.debug.print("Error running build runner:\n{s}\n", .{result.stderr}); - return error.BuildRunnerFailed; - }, - else => { - std.debug.print("Build runner terminated abnormally:\n{s}\n", .{result.stderr}); - return error.BuildRunnerFailed; - }, + 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.Io.File.stdout().writer(io, &stdout_buf); - try stdout_writer.interface.writeAll(result.stdout); - try stdout_writer.interface.flush(); - try stdout_writer.end(); + try std.Io.File.stdout().writeStreamingAll(io, result.stdout); } const ZigEnv = struct { std_dir: []const u8, }; +fn childExitedSuccessfully(term: std.process.Child.Term) bool { + return switch (term) { + .exited => |code| code == 0, + else => false, + }; +} + 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" }, - .stdout_limit = .limited(4 * 1024), - }); + }) catch |err| switch (err) { + error.FileNotFound => return error.ZigNotFound, + else => return err, + }; - switch (version_result.term) { - .exited => |code| if (code != 0) return error.ZigVersionFailed, - else => return error.ZigVersionFailed, + if (!childExitedSuccessfully(version_result.term)) { + return error.ZigVersionFailed; } const version_str = std.mem.trim(u8, version_result.stdout, &std.ascii.whitespace); @@ -224,30 +253,28 @@ fn getZigVersion(io: std.Io, arena: *std.heap.ArenaAllocator) !std.SemanticVersi fn setupBuildRunner(io: std.Io, arena: *std.heap.ArenaAllocator) !void { const version = try getZigVersion(io, arena); - const runner_src = switch (version.minor) { + const runner_src = if (version.major == 0) switch (version.minor) { 14 => build_runner_0_14, - 15, 16 => build_runner_0_15, + 15 => build_runner_0_15, + 16 => build_runner_0_16, else => return error.UnsupportedZigVersion, - }; + } else return error.UnsupportedZigVersion; - const cwd = std.Io.Dir.cwd(); - cwd.createDir(io, ".zig-cache", .default_dir) 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 cwd.writeFile(io, .{ + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = runner_path, .data = runner_src, }); } fn processBuildZig(io: std.Io, arena: *std.heap.ArenaAllocator) !void { - const cwd = std.Io.Dir.cwd(); - // Check if build.zig exists - cwd.access(io, "build.zig", .{}) catch { + std.Io.Dir.cwd().access(io, "build.zig", .{}) catch { // No build.zig, nothing to do return; }; @@ -263,18 +290,13 @@ fn processBuildZig(io: std.Io, arena: *std.heap.ArenaAllocator) !void { "--build-runner", ".zig-cache/zigdoc_build_runner.zig", }, - .stdout_limit = .limited(64 * 1024 * 1024), }); - switch (result.term) { - .exited => |code| if (code != 0) { - log.err("Failed to analyze build.zig", .{}); - return; - }, - else => { - log.err("Failed to analyze build.zig", .{}); - return; - }, + if (!childExitedSuccessfully(result.term)) { + 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 @@ -288,8 +310,6 @@ fn parseBuildOutput(allocator: std.mem.Allocator, io: std.Io, output: []const u8 const root_obj = parsed.value.object; const modules_obj = root_obj.get("modules") orelse return; - const cwd = std.Io.Dir.cwd(); - var modules_iter = modules_obj.object.iterator(); while (modules_iter.next()) |entry| { const module_name = entry.key_ptr.*; @@ -301,7 +321,7 @@ fn parseBuildOutput(allocator: std.mem.Allocator, io: std.Io, output: []const u8 if (!std.mem.endsWith(u8, root_path, ".zig")) continue; // Read and add the module file - const file_content = cwd.readFileAlloc( + const file_content = std.Io.Dir.cwd().readFileAlloc( io, root_path, allocator, @@ -325,7 +345,7 @@ fn parseBuildOutput(allocator: std.mem.Allocator, io: std.Io, output: []const u8 if (!std.mem.endsWith(u8, import_path, ".zig")) continue; // Read and add the imported file - const import_content = cwd.readFileAlloc( + const import_content = std.Io.Dir.cwd().readFileAlloc( io, import_path, allocator, @@ -349,12 +369,10 @@ fn getStdDir(io: std.Io, arena: *std.heap.ArenaAllocator) ![]const u8 { const result = try std.process.run(arena.allocator(), io, .{ .argv = &[_][]const u8{ "zig", "env" }, - .stdout_limit = .limited(64 * 1024), }); - switch (result.term) { - .exited => |code| if (code != 0) return error.ZigEnvFailed, - else => return error.ZigEnvFailed, + if (!childExitedSuccessfully(result.term)) { + return error.ZigEnvFailed; } const stdout = try arena.allocator().dupeZ(u8, result.stdout); @@ -392,17 +410,13 @@ fn walkStdLib(io: std.Io, arena: *std.heap.ArenaAllocator, std_dir_path: []const if (!std.mem.endsWith(u8, entry.basename, ".zig")) continue; if (std.mem.endsWith(u8, entry.basename, "test.zig")) continue; - // `Walk.parse` mutates the source buffer in place (writes a 0 sentinel - // over the trailing newline) so the buffer must be a true `[]u8`. - // `readFileAlloc` already returns mutable bytes, matching the calls in - // `walkBuildModules` (search "Walk.addFile" above). No `@constCast` - // is required and using it here would mask a real const-correctness - // violation if the upstream signature changed. See PR #1 review. - const file_content = try entry.dir.readFileAlloc( + const file_content = try entry.dir.readFileAllocOptions( io, entry.basename, allocator, .limited(10 * 1024 * 1024), + .of(u8), + null, ); const file_name = try std.fmt.allocPrint(allocator, "std/{s}", .{entry.path}); @@ -570,7 +584,6 @@ fn printDocs(allocator: std.mem.Allocator, io: std.Io, symbol: []const u8, std_d if (try resolveHierarchical(allocator, symbol)) |decl| { try printDeclInfo(allocator, stdout, decl, symbol, std_dir_path); try stdout.flush(); - try stdout_writer.end(); return; } } @@ -607,7 +620,6 @@ fn printDocs(allocator: std.mem.Allocator, io: std.Io, symbol: []const u8, std_d const first_part = parts.next() orelse { try stdout.writeAll("Tip: Specify a symbol like 'std.ArrayList' or 'moduleName.Symbol'\n"); try stdout.flush(); - try stdout_writer.end(); std.process.exit(1); }; @@ -640,12 +652,10 @@ fn printDocs(allocator: std.mem.Allocator, io: std.Io, symbol: []const u8, std_d } try stdout.flush(); - try stdout_writer.end(); std.process.exit(1); } try stdout.flush(); - try stdout_writer.end(); } fn printMembers(allocator: std.mem.Allocator, writer: anytype, decl: *const Walk.Decl, category: Walk.Category) !bool { @@ -1037,35 +1047,3 @@ fn printSource(writer: anytype, ast: *const std.zig.Ast, node: std.zig.Ast.Node. try writer.print(" {s}\n", .{line}); } } - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -// Pins the const-correctness contract between `Dir.readFileAlloc` and -// `Walk.addFile`: PR #1 review (PRRT_kwDOST2NKs6AThMy) flagged a -// `@constCast(file_content)` call where `Walk.parse` mutates the buffer in -// place (writes a 0 sentinel over the trailing newline). The fix reads the -// file via `readFileAlloc` whose return type is already mutable, so no -// const-cast is required. If a future stdlib change makes `readFileAlloc` -// return `[]const u8`, this test fails to compile and forces the issue -// back into review instead of silently re-introducing the unsafe cast. -test "readFileAlloc return is mutable []u8 (no @constCast needed for Walk.addFile)" { - const t = std.testing; - const Dir = std.Io.Dir; - const ReadAllocReturn = @typeInfo(@typeInfo(@TypeOf(Dir.readFileAlloc)).@"fn".return_type.?).error_union.payload; - const info = @typeInfo(ReadAllocReturn).pointer; - try t.expectEqual(false, info.is_const); - try t.expectEqual(@as(type, u8), info.child); - - // `Walk.addFile`'s second parameter must remain `[]u8` (mutable). - // `Walk.parse` writes a 0 sentinel into the buffer in place; if the - // signature ever changed to `[]const u8`, callers would silently get - // away with `@constCast` again. Pin the contract via type introspection - // instead of compiling a real call (which would require Walk.init). - const fn_info = @typeInfo(@TypeOf(Walk.addFile)).@"fn"; - const second_param = fn_info.params[1].type.?; - const second_info = @typeInfo(second_param).pointer; - try t.expectEqual(false, second_info.is_const); - try t.expectEqual(@as(type, u8), second_info.child); -} 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/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", 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; }; diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig new file mode 100644 index 0000000..aeffa20 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig @@ -0,0 +1,137 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const version = getVersion(b); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const options = b.addOptions(); + options.addOption([]const u8, "version", version); + exe_mod.addOptions("build_options", options); + + const exe = b.addExecutable(.{ + .name = "ziglint", + .root_module = exe_mod, + }); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the application"); + run_step.dependOn(&run_cmd.step); + + const test_step = b.step("test", "Run unit tests"); + const test_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + test_mod.addOptions("build_options", options); + const exe_tests = b.addTest(.{ + .root_module = test_mod, + }); + test_step.dependOn(&b.addRunArtifact(exe_tests).step); + + const fmt_check = b.addFmt(.{ .paths = &.{ "src", "build.zig", "build.zig.zon" } }); + test_step.dependOn(&fmt_check.step); + + 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); + + // 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(0); + test_step.dependOn(&lint_smoke.step); +} + +/// Add a ziglint step to your build. Use as a dependency to run linting. +/// The ziglint executable is always built with ReleaseFast for speed. +/// Example usage in a downstream project: +/// ```zig +/// const ziglint_dep = b.dependency("ziglint", .{ .optimize = .ReleaseFast }); +/// const lint_step = ziglint.addLint(b, ziglint_dep, &.{ b.path("src"), b.path("build.zig") }); +/// b.step("lint", "Run ziglint").dependOn(lint_step); +/// ``` +pub fn addLint( + b: *std.Build, + ziglint_dep: anytype, + paths: []const std.Build.LazyPath, +) *std.Build.Step { + const exe = switch (@TypeOf(ziglint_dep)) { + *std.Build.Step.Compile => ziglint_dep, + *std.Build.Dependency => ziglint_dep.artifact("ziglint"), + *const std.Build.Dependency => ziglint_dep.artifact("ziglint"), + else => @compileError("expected *Compile or *Dependency"), + }; + + const run = b.addRunArtifact(exe); + for (paths) |path| { + run.addDirectoryArg(path); + addPathInputs(b, run, path); + } + run.expectExitCode(0); + return &run.step; +} + +fn addPathInputs(b: *std.Build, run: *std.Build.Step.Run, lazy_path: std.Build.LazyPath) void { + // Only handle src_path (from b.path()) - other variants may not be resolved yet + const src = switch (lazy_path) { + .src_path => |src| src, + else => return, + }; + + const full_path = src.owner.build_root.join(b.allocator, &.{src.sub_path}) catch return; + + const stat = std.Io.Dir.cwd().statFile(b.graph.io, full_path, .{}) catch return; + if (stat.kind == .directory) { + var dir = std.Io.Dir.cwd().openDir(b.graph.io, full_path, .{ .iterate = true }) catch return; + defer dir.close(b.graph.io); + var walker = dir.walk(b.allocator) catch return; + defer walker.deinit(); + while (walker.next(b.graph.io) catch null) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.basename, ".zig")) { + run.addFileInput(lazy_path.path(run.step.owner, entry.path)); + } + } + } else if (stat.kind == .file) { + run.addFileInput(lazy_path); + } +} + +fn getVersion(b: *std.Build) []const u8 { + var code: u8 = undefined; + const git_describe = b.runAllowFail(&.{ "git", "describe", "--match", "v*.*.*", "--tags" }, &code, .ignore) catch { + return "unknown"; + }; + 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.findScalar(u8, without_v, '-')) |dash_idx| { + const tag_part = without_v[0..dash_idx]; + const rest = without_v[dash_idx + 1 ..]; + 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; + return b.fmt("{s}-{s}+{s}", .{ tag_part, count, hash_without_g }); + } + } + return without_v; +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig.zon b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig.zon new file mode 100644 index 0000000..41a94a4 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = .ziglint, + .version = "0.5.2", + .fingerprint = 0x4b7deecc2ff046b7, + .minimum_zig_version = "0.16.0", + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Config.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Config.zig new file mode 100644 index 0000000..2bd4bb5 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Config.zig @@ -0,0 +1,213 @@ +//! Configuration file loading and parsing for ziglint. +//! Supports .ziglint.zon config files with rule-specific settings. + +const std = @import("std"); + +/// 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(); + +/// Global settings +paths: []const []const u8 = &.{}, + +/// Per-rule configuration +rules: Rule.Config = .{}, + +/// Returns the effective line length limit for Z024. +pub fn getLineLength(self: *const Config) u32 { + return self.rules.Z024.max_length; +} + +/// Check if a rule is enabled (considering config). +pub fn isRuleEnabled(self: *const Config, rule: Rule) bool { + inline for (@typeInfo(Rule).@"enum".fields) |field| { + if (field.value == @intFromEnum(rule)) { + return @field(self.rules, field.name).enabled; + } + } + 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| { + if (field.value == @intFromEnum(rule)) { + @field(self.rules, field.name).enabled = enabled; + return; + } + } +} + +/// Load config from .ziglint.zon file, searching from start_path up to root. +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(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(allocator: std.mem.Allocator, io: std.Io, start_path: ?[]const u8) !?[]const u8 { + const path = start_path orelse { + return findConfigInDir(allocator, io, "."); + }; + + 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; + + var current = abs_path; + while (true) { + if (try findConfigInDir(allocator, io, current)) |config_path| { + return config_path; + } + + const parent = std.fs.path.dirname(current) orelse break; + if (std.mem.eql(u8, parent, current)) break; + current = parent; + } + + return null; +} + +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); + + std.Io.Dir.cwd().access(io, config_path, .{}) catch { + allocator.free(config_path); + return null; + }; + + return config_path; +} + +/// ZON schema for the config file +const ZonConfig = struct { + paths: ?[]const []const u8 = null, + rules: ?Rule.Config = null, +}; + +fn parseConfigFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8) !Config { + const source = try std.Io.Dir.cwd().readFileAllocOptions( + io, + path, + allocator, + .limited(1024 * 1024), + .@"1", + 0, + ); + defer allocator.free(source); + + return parseConfigSource(allocator, source); +} + +fn parseConfigSource(allocator: std.mem.Allocator, source: [:0]const u8) !Config { + const zon_config = std.zon.parse.fromSliceAlloc(ZonConfig, allocator, source, null, .{}) catch { + return error.ParseError; + }; + defer std.zon.parse.free(allocator, zon_config); + + var config: Config = .{}; + + if (zon_config.paths) |zon_paths| { + var paths_list: std.ArrayList([]const u8) = .empty; + errdefer { + for (paths_list.items) |item| allocator.free(item); + paths_list.deinit(allocator); + } + for (zon_paths) |p| { + try paths_list.append(allocator, try allocator.dupe(u8, p)); + } + config.paths = try paths_list.toOwnedSlice(allocator); + } + + if (zon_config.rules) |zon_rules| { + config.rules = zon_rules; + } + + return config; +} + +// Tests +test "default config" { + const config: Config = .{}; + try std.testing.expectEqual(120, config.getLineLength()); + try std.testing.expect(config.isRuleEnabled(.Z001)); + try std.testing.expect(config.isRuleEnabled(.Z024)); +} + +test "parse simple config" { + const source = + \\.{ + \\ .rules = .{ + \\ .Z024 = .{ .max_length = 100 }, + \\ }, + \\} + ; + const config = try parseConfigSource(std.testing.allocator, source); + try std.testing.expectEqual(100, config.getLineLength()); +} + +test "parse rules config" { + const source = + \\.{ + \\ .rules = .{ + \\ .Z001 = .{ .enabled = false }, + \\ .Z024 = .{ .max_length = 80 }, + \\ }, + \\} + ; + const config = try parseConfigSource(std.testing.allocator, source); + try std.testing.expect(!config.isRuleEnabled(.Z001)); + try std.testing.expect(config.isRuleEnabled(.Z024)); + try std.testing.expectEqual(80, config.getLineLength()); +} + +test "runtime rule check" { + const config: Config = .{}; + const rule: Rule = .Z001; + try std.testing.expect(config.isRuleEnabled(rule)); +} + +test "set rule enabled" { + var config: Config = .{}; + + try std.testing.expect(!config.isRuleEnabled(.Z033)); + config.setRuleEnabled(.Z033, true); + try std.testing.expect(config.isRuleEnabled(.Z033)); + + try std.testing.expect(config.isRuleEnabled(.Z001)); + config.setRuleEnabled(.Z001, false); + try std.testing.expect(!config.isRuleEnabled(.Z001)); +} + +test "parse paths config" { + const source = + \\.{ + \\ .paths = .{ + \\ "src", + \\ "lib", + \\ }, + \\} + ; + 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-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Linter.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Linter.zig new file mode 100644 index 0000000..d294353 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/Linter.zig @@ -0,0 +1,6094 @@ +//! Core linter that parses Zig source and runs lint rules. + +const std = @import("std"); +const Ast = std.zig.Ast; +const rules = @import("rules.zig"); +const TypeResolver = @import("TypeResolver.zig"); +const doc_comments = @import("doc_comments.zig"); +const Config = @import("Config.zig"); +const ModuleGraph = @import("ModuleGraph.zig"); + +pub const DeprecationKey = struct { + module_path_hash: u64, + node: Ast.Node.Index, + + pub fn init(module_path: []const u8, node: Ast.Node.Index) DeprecationKey { + return .{ + .module_path_hash = std.hash.Wyhash.hash(0, module_path), + .node = node, + }; + } +}; + +const Linter = @This(); + +/// Stub Timer for Zig 0.16 compatibility. +/// Timer was removed in 0.16; the proper replacement requires threading +/// std.Io through Linter, which would touch 200+ test call sites. Verbose timing +/// 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 { + fn start() error{}!Timer { + return .{}; + } + fn read(_: *Timer) u64 { + return 0; + } +}; + +/// Test-only Io provider. Tests are synchronous and don't need cancelation, +/// so we use the global single-threaded Io instance. +fn testIo() std.Io { + return std.Io.Threaded.global_single_threaded.io(); +} + +allocator: std.mem.Allocator, +source: [:0]const u8, +path: []const u8, +tree: Ast, +diagnostics: std.ArrayList(Diagnostic), +seen_imports: std.StringHashMapUnmanaged(Ast.TokenIndex), +type_resolver: ?*TypeResolver = null, +module_path: ?[]const u8 = null, +config: *const Config = &default_config, +allocated_contexts: std.ArrayList([]const u8) = .empty, +public_types: std.StringHashMapUnmanaged(void) = .empty, +imported_types: std.StringHashMapUnmanaged(void) = .empty, +import_bindings: std.StringHashMapUnmanaged(ImportInfo) = .empty, +used_identifiers: std.StringHashMapUnmanaged(void) = .empty, +current_fn_return_type: Ast.Node.OptionalIndex = .none, +parent_map: []Ast.Node.OptionalIndex = &.{}, +/// Cache of (module_path, node) -> is_deprecated to avoid re-parsing doc comments +deprecation_cache: std.AutoHashMapUnmanaged(DeprecationKey, bool) = .empty, +verbose: bool = false, +use_color: bool = false, +/// Track time spent checking each rule type (nanoseconds) +rule_timings: std.AutoHashMapUnmanaged(rules.Rule, u64) = .empty, +rule_timer: ?Timer = null, + +const default_config: Config = .{}; + +const ImportInfo = struct { + name_token: Ast.TokenIndex, + is_pub: bool, + is_discard: bool, +}; + +pub const Diagnostic = struct { + path: []const u8, + line: u32, + column: u32, + rule: rules.Rule, + context: []const u8 = "", + + // ANSI escape codes + const dim = "\x1b[2m"; + const cyan = "\x1b[36m"; + const yellow = "\x1b[33m"; + const reset = "\x1b[0m"; + + pub fn write(self: Diagnostic, writer: *std.Io.Writer, use_color: bool, display_path: []const u8) !void { + if (use_color) { + try writer.print("{s}{s}{s}{s}:{s} {s}{s}{s}:{s}{s}{}{s}{s}:{s} ", .{ + yellow, + self.rule.code(), + reset, + dim, + reset, + dim, + display_path, + reset, + dim, + cyan, + self.line, + reset, + dim, + reset, + }); + } else { + try writer.print("{s}: {s}:{}: ", .{ + self.rule.code(), + display_path, + self.line, + }); + } + try self.rule.writeMessage(writer, self.context, use_color); + try writer.writeByte('\n'); + } +}; + +pub fn init(allocator: std.mem.Allocator, source: [:0]const u8, path: []const u8, config: ?*const Config) Linter { + return .{ + .allocator = allocator, + .source = source, + .path = path, + .tree = Ast.parse(allocator, source, .zig) catch unreachable, + .diagnostics = .empty, + .seen_imports = .empty, + .config = config orelse &default_config, + }; +} + +pub fn initWithSemantics( + allocator: std.mem.Allocator, + source: [:0]const u8, + path: []const u8, + type_resolver: *TypeResolver, + module_path: []const u8, + config: ?*const Config, +) Linter { + return .{ + .allocator = allocator, + .source = source, + .path = path, + .tree = Ast.parse(allocator, source, .zig) catch unreachable, + .diagnostics = .empty, + .seen_imports = .empty, + .type_resolver = type_resolver, + .module_path = module_path, + .config = config orelse &default_config, + }; +} + +pub fn deinit(self: *Linter) void { + for (self.allocated_contexts.items) |ctx| { + self.allocator.free(ctx); + } + self.allocated_contexts.deinit(self.allocator); + self.tree.deinit(self.allocator); + self.diagnostics.deinit(self.allocator); + self.seen_imports.deinit(self.allocator); + self.public_types.deinit(self.allocator); + self.imported_types.deinit(self.allocator); + self.import_bindings.deinit(self.allocator); + self.used_identifiers.deinit(self.allocator); + self.deprecation_cache.deinit(self.allocator); + self.rule_timings.deinit(self.allocator); + if (self.parent_map.len > 0) { + self.allocator.free(self.parent_map); + } + self.* = undefined; +} + +pub fn lint(self: *Linter) void { + var timer = if (self.verbose) Timer.start() catch null else null; + if (self.verbose) { + self.rule_timer = Timer.start() catch null; + } + + const dim = if (self.use_color) "\x1b[2m" else ""; + const cyan = if (self.use_color) "\x1b[36m" else ""; + const yellow = if (self.use_color) "\x1b[33m" else ""; + const reset = if (self.use_color) "\x1b[0m" else ""; + + self.checkParseErrors(); + if (self.tree.errors.len > 0) return; + + self.checkLineLength(); + self.checkFileAsStruct(); + + const setup_start = if (timer) |*t| t.read() else 0; + self.buildPublicTypesMap(); + self.collectAllIdentifiers(); + self.buildParentMap(); + + if (self.verbose and timer != null) { + const elapsed = timer.?.read() - setup_start; + std.debug.print("{s}│ setup: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + const visit_start = if (timer) |*t| t.read() else 0; + for (self.tree.rootDecls()) |node| { + self.visitNode(node); + } + + if (self.verbose and timer != null) { + const elapsed = timer.?.read() - visit_start; + std.debug.print("{s}│ visit: {s}{d:>7.2}ms{s} ({d} nodes)\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset, self.tree.nodes.len }); + } + + const checks_start = if (timer) |*t| t.read() else 0; + self.checkUnusedImports(); + self.checkThisBuiltin(); + self.checkInlineImports(); + self.checkCatchReturnAll(); + self.checkEmptyCatchAll(); + self.checkInstanceDeclAccess(); + + if (self.verbose and timer != null) { + const elapsed = timer.?.read() - checks_start; + std.debug.print("{s}│ checks: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + // Print per-rule timing breakdown if verbose + if (self.verbose and self.rule_timings.count() > 0) { + std.debug.print("{s}│ rules:{s}\n", .{ dim, reset }); + + // Sort rules by time spent (descending) + const Entry = struct { rule: rules.Rule, time_ns: u64 }; + var entries: std.ArrayList(Entry) = .empty; + defer entries.deinit(self.allocator); + + var iter = self.rule_timings.iterator(); + while (iter.next()) |entry| { + entries.append(self.allocator, .{ .rule = entry.key_ptr.*, .time_ns = entry.value_ptr.* }) catch continue; + } + + // Simple bubble sort by time (descending) + for (0..entries.items.len) |i| { + for (i + 1..entries.items.len) |j| { + if (entries.items[j].time_ns > entries.items[i].time_ns) { + const tmp = entries.items[i]; + entries.items[i] = entries.items[j]; + entries.items[j] = tmp; + } + } + } + + for (entries.items) |entry| { + const time_ms = @as(f64, @floatFromInt(entry.time_ns)) / 1_000_000.0; + std.debug.print("{s}│ {s}{s}{s}: {s}{d:>7.2}ms{s}\n", .{ dim, yellow, entry.rule.code(), reset, cyan, time_ms, reset }); + } + } +} + +fn trackRuleTime(self: *Linter, rule: rules.Rule, time_ns: u64) void { + if (!self.verbose) return; + + const entry = self.rule_timings.getOrPut(self.allocator, rule) catch return; + if (!entry.found_existing) { + entry.value_ptr.* = 0; + } + entry.value_ptr.* += time_ns; +} + +fn collectAllIdentifiers(self: *Linter) void { + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + const tag = self.tree.nodeTag(node); + switch (tag) { + .identifier => { + const name = self.tree.tokenSlice(self.tree.nodeMainToken(node)); + // ziglint-ignore: Z026 + self.used_identifiers.put(self.allocator, name, {}) catch {}; + }, + .field_access => { + // Walk field_access chain, tracking all field names and root identifier + var current = node; + while (self.tree.nodeTag(current) == .field_access) { + const data = self.tree.nodeData(current).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + // ziglint-ignore: Z026 + self.used_identifiers.put(self.allocator, field_name, {}) catch {}; + current = data[0]; + } + if (self.tree.nodeTag(current) == .identifier) { + const name = self.tree.tokenSlice(self.tree.nodeMainToken(current)); + // ziglint-ignore: Z026 + self.used_identifiers.put(self.allocator, name, {}) catch {}; + } + }, + else => {}, + } + } +} + +fn checkParseErrors(self: *Linter) void { + for (self.tree.errors) |err| { + const loc = self.tree.tokenLocation(0, err.token); + self.report(loc, .Z003, ""); + } +} + +fn checkUnusedImports(self: *Linter) void { + var it = self.import_bindings.iterator(); + while (it.next()) |entry| { + const name = entry.key_ptr.*; + const info = entry.value_ptr.*; + + // Skip pub re-exports - they're intentionally exposed + if (info.is_pub) continue; + + // Discarded imports `_ = @import(...)` are always unused + if (info.is_discard) { + const loc = self.tree.tokenLocation(0, info.name_token); + self.report(loc, .Z013, name); + continue; + } + + // Check if the bound name is used elsewhere + if (!self.used_identifiers.contains(name)) { + const loc = self.tree.tokenLocation(0, info.name_token); + self.report(loc, .Z013, name); + } + } +} + +fn buildParentMap(self: *Linter) void { + self.parent_map = self.allocator.alloc(Ast.Node.OptionalIndex, self.tree.nodes.len) catch return; + @memset(self.parent_map, .none); + + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + + // Handle nodes with potentially many children directly + const tag = self.tree.nodeTag(node); + switch (tag) { + .block, .block_semicolon => { + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, node) orelse continue; + for (stmts) |stmt| { + self.parent_map[@intFromEnum(stmt)] = node.toOptional(); + } + continue; + }, + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => { + var buf: [2]Ast.Node.Index = undefined; + const container = self.tree.fullContainerDecl(&buf, node) orelse continue; + for (container.ast.members) |member| { + self.parent_map[@intFromEnum(member)] = node.toOptional(); + } + continue; + }, + else => {}, + } + + // Use ChildList for nodes with bounded children + const children = self.getNodeChildren(node); + for (children.slice()) |child| { + self.parent_map[@intFromEnum(child)] = node.toOptional(); + } + } +} + +const ChildList = struct { + items: [8]Ast.Node.Index = undefined, + len: usize = 0, + + fn append(self: *ChildList, item: Ast.Node.Index) void { + if (self.len < 8) { + self.items[self.len] = item; + self.len += 1; + } + } + + fn slice(self: *const ChildList) []const Ast.Node.Index { + return self.items[0..self.len]; + } +}; + +fn getNodeChildren(self: *Linter, node: Ast.Node.Index) ChildList { + var children: ChildList = .{}; + const tag = self.tree.nodeTag(node); + + switch (tag) { + // fn_decl: node_and_node = [fn_proto, body] + .fn_decl => { + const pair = self.tree.nodeData(node).node_and_node; + children.append(pair[0]); + children.append(pair[1]); + }, + + // test_decl: opt_token_and_node = [name_token, block] + .test_decl => { + const data = self.tree.nodeData(node).opt_token_and_node; + children.append(data[1]); + }, + + // Var decl types - use fullVarDecl for safe access + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = self.tree.fullVarDecl(node) orelse return children; + if (var_decl.ast.type_node.unwrap()) |n| children.append(n); + if (var_decl.ast.init_node.unwrap()) |n| children.append(n); + }, + + // Builtin calls - opt_node_and_opt_node + .builtin_call_two, .builtin_call_two_comma => { + const data = self.tree.nodeData(node).opt_node_and_opt_node; + if (data[0].unwrap()) |n| children.append(n); + if (data[1].unwrap()) |n| children.append(n); + }, + + // Container declarations (structs, enums, unions) - use fullContainerDecl for safe access + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => { + var buf: [2]Ast.Node.Index = undefined; + const container = self.tree.fullContainerDecl(&buf, node) orelse return children; + for (container.ast.members) |member| { + children.append(member); + } + }, + + // Block - use blockStatements for safe access + .block, .block_semicolon => { + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, node) orelse return children; + for (stmts) |stmt| children.append(stmt); + }, + + .block_two, .block_two_semicolon => { + const data = self.tree.nodeData(node).opt_node_and_opt_node; + if (data[0].unwrap()) |n| children.append(n); + if (data[1].unwrap()) |n| children.append(n); + }, + + // If expressions + .if_simple, .@"if" => { + const full_if = self.tree.fullIf(node) orelse return children; + children.append(full_if.ast.cond_expr); + children.append(full_if.ast.then_expr); + if (full_if.ast.else_expr.unwrap()) |n| children.append(n); + }, + + // While loops + .while_simple, .while_cont, .@"while" => { + const full_while = self.tree.fullWhile(node) orelse return children; + children.append(full_while.ast.cond_expr); + children.append(full_while.ast.then_expr); + if (full_while.ast.else_expr.unwrap()) |n| children.append(n); + }, + + // For loops + .for_simple, .@"for" => { + const full_for = self.tree.fullFor(node) orelse return children; + children.append(full_for.ast.then_expr); + if (full_for.ast.else_expr.unwrap()) |n| children.append(n); + }, + + .@"defer" => { + children.append(self.tree.nodeData(node).node); + }, + + .@"errdefer" => { + const data = self.tree.nodeData(node).opt_token_and_node; + children.append(data[1]); + }, + + // field_access: node_and_token = [lhs, field_token] + .field_access => { + const data = self.tree.nodeData(node).node_and_token; + children.append(data[0]); + }, + + // assign: node_and_node = [lhs, rhs] + .assign => { + const data = self.tree.nodeData(node).node_and_node; + children.append(data[0]); + children.append(data[1]); + }, + + // call_one: node_and_opt_node = [callee, arg] + .call_one, .call_one_comma => { + const data = self.tree.nodeData(node).node_and_opt_node; + children.append(data[0]); + if (data[1].unwrap()) |arg| children.append(arg); + }, + + // call: sub_range = [callee, args...] + .call, .call_comma => { + var buf: [1]Ast.Node.Index = undefined; + const full_call = self.tree.fullCall(&buf, node) orelse return children; + children.append(full_call.ast.fn_expr); + for (full_call.ast.params) |param| children.append(param); + }, + + // try: node = [inner_expr] + .@"try" => { + children.append(self.tree.nodeData(node).node); + }, + + // catch: node_and_node = [lhs, rhs] + .@"catch" => { + const data = self.tree.nodeData(node).node_and_node; + children.append(data[0]); // The expression being caught + children.append(data[1]); // The catch body + }, + + else => {}, + } + + return children; +} + +fn checkThisBuiltin(self: *Linter) void { + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + const tag = self.tree.nodeTag(node); + + // Look for @This() calls + if (tag != .builtin_call_two and tag != .builtin_call_two_comma) continue; + const main_token = self.tree.nodeMainToken(node); + const builtin_name = self.tree.tokenSlice(main_token); + if (!std.mem.eql(u8, builtin_name, "@This")) continue; + + // Found @This() - check if it's valid + const loc = self.tree.tokenLocation(0, main_token); + + // Check 1: Is it in the form `const X = @This();`? + const parent = self.parent_map[@intFromEnum(node)].unwrap() orelse { + // Z020: inline @This() + self.report(loc, .Z020, ""); + continue; + }; + + const parent_tag = self.tree.nodeTag(parent); + + // Allow @This() as a function argument (e.g., testing.refAllDecls(@This())) + if (parent_tag == .call_one or parent_tag == .call_one_comma or + parent_tag == .call or parent_tag == .call_comma) + { + continue; + } + + const const_decl_info: ?struct { name: []const u8 } = switch (parent_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => blk: { + const var_decl = self.tree.fullVarDecl(parent) orelse break :blk null; + const mut_token = self.tree.tokenSlice(var_decl.ast.mut_token); + const init_node = var_decl.ast.init_node.unwrap() orelse break :blk null; + if (!std.mem.eql(u8, mut_token, "const") or init_node != node) break :blk null; + const name_token = var_decl.ast.mut_token + 1; + break :blk .{ .name = self.tree.tokenSlice(name_token) }; + }, + else => null, + }; + + if (const_decl_info == null) { + // Z020: inline @This() + self.report(loc, .Z020, ""); + continue; + } + + const alias_name = const_decl_info.?.name; + + // Check 2: Is the enclosing struct anonymous? + const struct_name = self.findEnclosingStructName(node); + if (struct_name) |name| { + // Z019: @This() in named struct + self.report(loc, .Z019, name); + continue; + } + + // Check 3: If at file level, alias should match filename or be Self + if (self.isAtFileLevel(node)) { + const basename = std.fs.path.basename(self.path); + const expected = if (std.mem.endsWith(u8, basename, ".zig")) + basename[0 .. basename.len - 4] + else + basename; + + // Allow "Self" or the filename (case-insensitive) + const is_self = std.mem.eql(u8, alias_name, "Self"); + const matches_filename = std.ascii.eqlIgnoreCase(alias_name, expected); + if (!is_self and !matches_filename) { + // Z021: alias doesn't match filename or Self (only for file-as-struct) + if (self.hasTopLevelFields()) { + const context = self.allocator.alloc(u8, alias_name.len + 1 + expected.len) catch continue; + @memcpy(context[0..alias_name.len], alias_name); + context[alias_name.len] = 0; + @memcpy(context[alias_name.len + 1 ..], expected); + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + self.report(loc, .Z021, context); + } + } + } else { + // Check 4: In anonymous/local struct, alias must be "Self" + if (!std.mem.eql(u8, alias_name, "Self")) { + self.report(loc, .Z022, alias_name); + } + } + } +} + +fn checkInlineImports(self: *Linter) void { + if (!self.config.isRuleEnabled(.Z028)) return; + + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + const tag = self.tree.nodeTag(node); + + // Look for @import() calls + if (tag != .builtin_call_two and tag != .builtin_call_two_comma) continue; + const main_token = self.tree.nodeMainToken(node); + const builtin_name = self.tree.tokenSlice(main_token); + if (!std.mem.eql(u8, builtin_name, "@import")) continue; + + // Walk up through field_access chain to find var decl + // e.g., `const Rule = @import("rules.zig").Rule;` + var current = node; + while (true) { + const parent = self.parent_map[@intFromEnum(current)].unwrap() orelse { + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + break; + }; + + const parent_tag = self.tree.nodeTag(parent); + switch (parent_tag) { + .field_access => { + // Continue walking up through field access chain + current = parent; + continue; + }, + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = self.tree.fullVarDecl(parent) orelse { + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + break; + }; + const mut_token = self.tree.tokenSlice(var_decl.ast.mut_token); + const init_node = var_decl.ast.init_node.unwrap() orelse { + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + break; + }; + // Must be `const` and the init must be our current node (or an ancestor) + if (!std.mem.eql(u8, mut_token, "const") or init_node != current) { + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + break; + } + // Check that the var decl is at file level (not inside a function) + // Allow imports in test blocks + if (!self.isAtFileLevel(parent) and !self.isInTestBlock(parent)) { + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + } + break; + }, + .assign => { + // Allow `_ = @import(...)` pattern for pulling in tests + const data = self.tree.nodeData(parent).node_and_node; + const lhs = data[0]; + if (self.tree.nodeTag(lhs) == .identifier) { + const lhs_name = self.tree.tokenSlice(self.tree.nodeMainToken(lhs)); + if (std.mem.eql(u8, lhs_name, "_")) { + // Discarding import is allowed + break; + } + } + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + break; + }, + else => { + const loc = self.tree.tokenLocation(0, main_token); + self.report(loc, .Z028, ""); + break; + }, + } + } + } +} + +fn hasTopLevelFields(self: *Linter) bool { + for (self.tree.rootDecls()) |node| { + const tag = self.tree.nodeTag(node); + if (tag == .container_field_init or tag == .container_field) { + return true; + } + } + return false; +} + +fn isAtFileLevel(self: *Linter, start_node: Ast.Node.Index) bool { + var current = start_node; + + while (true) { + const parent_opt = self.parent_map[@intFromEnum(current)]; + const parent = parent_opt.unwrap() orelse return true; // No parent = root level + const parent_tag = self.tree.nodeTag(parent); + + // If we hit a container, fn, or test, we're not at file level + switch (parent_tag) { + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + .fn_decl, + .test_decl, + => return false, + else => {}, + } + + current = parent; + } +} + +fn isInTestBlock(self: *Linter, start_node: Ast.Node.Index) bool { + var current = start_node; + + while (true) { + const parent_opt = self.parent_map[@intFromEnum(current)]; + const parent = parent_opt.unwrap() orelse return false; + const parent_tag = self.tree.nodeTag(parent); + + if (parent_tag == .test_decl) return true; + if (parent_tag == .fn_decl) return false; // Functions block test scope + + current = parent; + } +} + +fn findEnclosingStructName(self: *Linter, start_node: Ast.Node.Index) ?[]const u8 { + var current = start_node; + var enclosing_container: ?Ast.Node.Index = null; + + // First pass: find the enclosing container + while (true) { + const parent_opt = self.parent_map[@intFromEnum(current)]; + const parent = parent_opt.unwrap() orelse break; + const parent_tag = self.tree.nodeTag(parent); + + const is_container = switch (parent_tag) { + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + => true, + else => false, + }; + + if (is_container) { + enclosing_container = parent; + break; + } + + current = parent; + } + + const container = enclosing_container orelse return null; + + // Second pass: check if container is inside a function/test block + // If so, the struct name isn't accessible from inside, so @This() is valid + current = container; + while (true) { + const parent_opt = self.parent_map[@intFromEnum(current)]; + const parent = parent_opt.unwrap() orelse break; + const parent_tag = self.tree.nodeTag(parent); + + if (parent_tag == .fn_decl or parent_tag == .test_decl) { + return null; // Local struct - name not accessible + } + + current = parent; + } + + // Container is at module level - check if it's named + const container_parent_opt = self.parent_map[@intFromEnum(container)]; + const container_parent = container_parent_opt.unwrap() orelse return null; + const container_parent_tag = self.tree.nodeTag(container_parent); + + return switch (container_parent_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => blk: { + const var_decl = self.tree.fullVarDecl(container_parent) orelse break :blk null; + const name_token = var_decl.ast.mut_token + 1; + break :blk self.tree.tokenSlice(name_token); + }, + else => null, // Anonymous (returned, passed as arg, etc.) + }; +} + +/// Find the enclosing container (struct/union/enum) for a given node +fn findEnclosingContainer(self: *Linter, start_node: Ast.Node.Index) Ast.Node.OptionalIndex { + var current = start_node; + + while (true) { + const parent_opt = self.parent_map[@intFromEnum(current)]; + const parent = parent_opt.unwrap() orelse return .none; + const parent_tag = self.tree.nodeTag(parent); + + const is_container = switch (parent_tag) { + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + => true, + else => false, + }; + + if (is_container) { + return parent.toOptional(); + } + + current = parent; + } +} + +fn checkFileAsStruct(self: *Linter) void { + // Check if file has top-level fields (container fields at root level) + var has_top_level_fields = false; + for (self.tree.rootDecls()) |node| { + const tag = self.tree.nodeTag(node); + if (tag == .container_field_init or tag == .container_field) { + has_top_level_fields = true; + break; + } + } + + if (!has_top_level_fields) return; + + // File has top-level fields, check if filename is PascalCase + const basename = std.fs.path.basename(self.path); + const name = if (std.mem.endsWith(u8, basename, ".zig")) + basename[0 .. basename.len - 4] + else + basename; + + if (!isPascalCase(name)) { + self.report(.{ .line = 0, .column = 0, .line_start = 0, .line_end = 0 }, .Z009, basename); + } +} + +fn buildPublicTypesMap(self: *Linter) void { + for (self.tree.rootDecls()) |node| { + const tag = self.tree.nodeTag(node); + switch (tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = self.tree.fullVarDecl(node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const name = self.tree.tokenSlice(name_token); + + // Track imported types (field access ending in PascalCase, e.g., std.mem.Allocator) + if (self.isImportedType(var_decl)) { + // ziglint-ignore: Z026 + self.imported_types.put(self.allocator, name, {}) catch {}; + continue; + } + + if (!self.isPublicDecl(node)) continue; + if (!self.isTypeDecl(var_decl)) continue; + + // ziglint-ignore: Z026 + self.public_types.put(self.allocator, name, {}) catch {}; + }, + else => {}, + } + } +} + +fn isImportedType(self: *Linter, var_decl: Ast.full.VarDecl) bool { + const init_node = var_decl.ast.init_node.unwrap() orelse return false; + + // Use type resolver if available + if (self.type_resolver) |resolver| { + if (self.module_path) |mod_path| { + const type_info = resolver.typeOf(mod_path, init_node); + return switch (type_info) { + .type_type, .std_type, .user_type => true, + else => false, + }; + } + } + + // Fallback: check if it's a field access ending in PascalCase + const tag = self.tree.nodeTag(init_node); + return switch (tag) { + .field_access => blk: { + const data = self.tree.nodeData(init_node).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + break :blk isPascalCase(field_name); + }, + else => false, + }; +} + +fn isPublicDecl(self: *Linter, node: Ast.Node.Index) bool { + const main_token = self.tree.nodeMainToken(node); + if (main_token == 0) return false; + const prev_token = main_token - 1; + const prev_slice = self.tree.tokenSlice(prev_token); + return std.mem.eql(u8, prev_slice, "pub"); +} + +fn isTypeDecl(self: *Linter, var_decl: Ast.full.VarDecl) bool { + const init_node = var_decl.ast.init_node.unwrap() orelse return false; + return self.isTypeExpression(init_node); +} + +fn isTypeExpression(self: *Linter, node: Ast.Node.Index) bool { + const tag = self.tree.nodeTag(node); + return switch (tag) { + // Container types (struct, enum, union) + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => true, + // Pointer types (e.g., *anyopaque, *T) + .ptr_type_aligned, + .ptr_type_sentinel, + .ptr_type, + .ptr_type_bit_range, + => true, + // Optional types (e.g., ?T) + .optional_type => true, + // Array types (e.g., [N]T) + .array_type, + .array_type_sentinel, + => true, + // Error union types (e.g., E!T) + .error_union => true, + // Error set declarations (e.g., error{A, B}) + .error_set_decl => true, + // Function types + .fn_proto, + .fn_proto_multi, + .fn_proto_one, + .fn_proto_simple, + => true, + // 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 isBuiltinTypeConstructor(token); + }, + // Identifier referencing another type (type alias) + .identifier => blk: { + const name = self.tree.tokenSlice(self.tree.nodeMainToken(node)); + break :blk isPascalCase(name) or isBuiltinType(name); + }, + // Labeled blocks (comptime type construction, e.g., `key: { break :key @Type(...); }`) + .block_two, .block_two_semicolon, .block, .block_semicolon => true, + // Switch expressions (comptime type selection, e.g., `switch (os) { .linux => T1, else => T2 }`) + .@"switch", .switch_comma => true, + // If expressions (comptime type selection) + .@"if", .if_simple => true, + // Generic type instantiation (e.g., ArrayList(T), HashMap(K, V)) + .call_one, .call_one_comma, .call, .call_comma => true, + // Field access (e.g., std.AutoHashMapUnmanaged) + .field_access => blk: { + const data = self.tree.nodeData(node).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + break :blk isPascalCase(field_name); + }, + else => false, + }; +} + +fn isPrivateTypeRef(self: *Linter, name: []const u8, enclosing_container: Ast.Node.OptionalIndex) bool { + if (!isPascalCase(name)) return false; + if (isBuiltinType(name)) return false; + if (self.public_types.contains(name)) return false; + if (self.imported_types.contains(name)) return false; + // Don't flag Self type (type matching filename for file-as-struct pattern) + if (self.isSelfType(name)) return false; + // Check if type is pub within the enclosing container + if (self.isPublicInContainer(name, enclosing_container)) return false; + return true; +} + +/// Check if a type name is declared as `pub const` within the given container +fn isPublicInContainer(self: *Linter, name: []const u8, container_opt: Ast.Node.OptionalIndex) bool { + const container = container_opt.unwrap() orelse return false; + const members = self.getContainerMembers(container) orelse return false; + + for (members) |member| { + const tag = self.tree.nodeTag(member); + switch (tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + // Check if this is a pub declaration + if (!self.isPublicDecl(member)) continue; + + // Check if the name matches + const var_decl = self.tree.fullVarDecl(member) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const decl_name = self.tree.tokenSlice(name_token); + if (std.mem.eql(u8, decl_name, name)) return true; + }, + else => {}, + } + } + return false; +} + +/// Get the member declarations of a container node +fn getContainerMembers(self: *Linter, node: Ast.Node.Index) ?[]const Ast.Node.Index { + const tag = self.tree.nodeTag(node); + return switch (tag) { + .container_decl, .container_decl_trailing => self.tree.containerDecl(node).ast.members, + .container_decl_two, .container_decl_two_trailing => blk: { + var buf: [2]Ast.Node.Index = undefined; + break :blk self.tree.containerDeclTwo(&buf, node).ast.members; + }, + .container_decl_arg, .container_decl_arg_trailing => self.tree.containerDeclArg(node).ast.members, + else => null, + }; +} + +fn isSelfType(self: *Linter, name: []const u8) bool { + const basename = std.fs.path.basename(self.path); + const stem = if (std.mem.endsWith(u8, basename, ".zig")) + basename[0 .. basename.len - 4] + else + basename; + return std.mem.eql(u8, name, stem); +} + +fn isBuiltinType(name: []const u8) bool { + const builtins = [_][]const u8{ + "u8", "u16", "u32", "u64", "u128", "usize", + "i8", "i16", "i32", "i64", "i128", "isize", + "f16", "f32", "f64", "f80", "f128", "bool", + "void", "noreturn", "type", "anyerror", "anytype", "anyframe", + "comptime_int", "comptime_float", + }; + for (builtins) |b| { + if (std.mem.eql(u8, name, b)) return true; + } + 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, + io, + comptime_value, + other, + + fn order(self: ParamKind) u8 { + return switch (self) { + .type_param => 0, + .allocator => 1, + .io => 2, + .comptime_value => 3, + .other => 4, + }; + } + + fn name(self: ParamKind) []const u8 { + return switch (self) { + .type_param => "type", + .allocator => "Allocator", + .io => "Io", + .comptime_value => "comptime", + .other => "other", + }; + } +}; + +fn checkArgumentOrder(self: *Linter, node: Ast.Node.Index) void { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = self.tree.fullFnProto(&buf, node) orelse return; + + var max_order: u8 = 0; + var max_kind: ParamKind = .type_param; + var max_token: Ast.TokenIndex = 0; + var is_first = true; + + var it = fn_proto.iterate(&self.tree); + while (it.next()) |param| { + // Skip first param if it's a receiver (type refers to @This() or container type) + if (is_first) { + is_first = false; + if (self.isReceiverParam(param)) continue; + } + + const kind = self.classifyParam(param); + + const current_order = kind.order(); + + if (current_order < max_order) { + // This parameter is out of order + const token = param.name_token orelse + (if (param.type_expr) |te| self.tree.nodeMainToken(te) else continue); + const loc = self.tree.tokenLocation(0, token); + + const context = std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ + kind.name(), + max_kind.name(), + }) catch continue; + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + self.report(loc, .Z023, context); + } + + if (current_order >= max_order) { + max_order = current_order; + max_kind = kind; + max_token = param.name_token orelse max_token; + } + } +} + +fn isComptimeParam(self: *Linter, param: Ast.full.FnProto.Param) bool { + const comptime_token = param.comptime_noalias orelse return false; + const token_tag = self.tree.tokenTag(comptime_token); + return token_tag == .keyword_comptime; +} + +fn classifyParam(self: *Linter, param: Ast.full.FnProto.Param) ParamKind { + const type_node = param.type_expr orelse return .other; + const base_kind = self.classifyTypeNode(type_node); + + // If it's a comptime param that's not a type param, classify as comptime_value + if (base_kind == .other and self.isComptimeParam(param)) { + return .comptime_value; + } + + return base_kind; +} + +fn isReceiverParam(self: *Linter, param: Ast.full.FnProto.Param) bool { + const type_node = param.type_expr orelse return false; + + // Use TypeResolver if available for accurate type resolution + if (self.type_resolver) |resolver| { + if (self.module_path) |mod_path| { + const inner_node = self.unwrapPointerType(type_node); + const type_info = resolver.typeOf(mod_path, inner_node); + if (type_info == .user_type) { + // Check if the type's module matches current module (file-as-struct or local struct) + if (std.mem.eql(u8, type_info.user_type.module_path, mod_path)) { + return true; + } + } + } + } + + // Fallback: check for @This() or Self + return self.typeRefersToThis(type_node); +} + +fn unwrapPointerType(self: *Linter, node: Ast.Node.Index) Ast.Node.Index { + const tag = self.tree.nodeTag(node); + return switch (tag) { + .ptr_type_aligned, .ptr_type_sentinel, .ptr_type, .ptr_type_bit_range => blk: { + const ptr_type = self.tree.fullPtrType(node) orelse break :blk node; + break :blk self.unwrapPointerType(ptr_type.ast.child_type); + }, + else => node, + }; +} + +fn typeRefersToThis(self: *Linter, node: Ast.Node.Index) bool { + const tag = self.tree.nodeTag(node); + return switch (tag) { + .builtin_call_two => blk: { + const main_token = self.tree.nodeMainToken(node); + const builtin_name = self.tree.tokenSlice(main_token); + break :blk std.mem.eql(u8, builtin_name, "@This"); + }, + .ptr_type_aligned, .ptr_type_sentinel, .ptr_type, .ptr_type_bit_range => blk: { + const ptr_type = self.tree.fullPtrType(node) orelse break :blk false; + break :blk self.typeRefersToThis(ptr_type.ast.child_type); + }, + .identifier => blk: { + const name = self.tree.tokenSlice(self.tree.nodeMainToken(node)); + break :blk std.mem.eql(u8, name, "Self"); + }, + else => false, + }; +} + +fn classifyTypeNode(self: *Linter, type_node: Ast.Node.Index) ParamKind { + // Check for `type` (comptime type parameter) + if (self.tree.nodeTag(type_node) == .identifier) { + const type_name = self.tree.tokenSlice(self.tree.nodeMainToken(type_node)); + if (std.mem.eql(u8, type_name, "type")) return .type_param; + } + + // Use TypeResolver for semantic type resolution (handles aliases) + if (self.type_resolver) |resolver| { + if (self.module_path) |mod_path| { + const type_info = resolver.typeOf(mod_path, type_node); + if (type_info == .std_type) { + return self.classifyPath(type_info.std_type.path); + } + } + } + + // Fallback: check for field access chains like std.mem.Allocator or std.Io + const path = self.getFieldAccessPath(type_node) orelse return .other; + return self.classifyPath(path); +} + +fn classifyPath(self: *Linter, path: []const u8) ParamKind { + _ = self; + if (std.mem.eql(u8, path, "std.mem.Allocator") or + std.mem.eql(u8, path, "mem.Allocator") or + std.mem.eql(u8, path, "Allocator")) + { + return .allocator; + } + + if (std.mem.eql(u8, path, "std.Io") or + std.mem.eql(u8, path, "Io")) + { + return .io; + } + + return .other; +} + +fn getFieldAccessPath(self: *Linter, node: Ast.Node.Index) ?[]const u8 { + var parts: [8][]const u8 = undefined; + var count: usize = 0; + + var current = node; + while (true) { + const tag = self.tree.nodeTag(current); + if (tag == .field_access) { + const data = self.tree.nodeData(current).node_and_token; + if (count < parts.len) { + parts[parts.len - 1 - count] = self.tree.tokenSlice(data[1]); + count += 1; + } + current = data[0]; + } else if (tag == .identifier) { + if (count < parts.len) { + parts[parts.len - 1 - count] = self.tree.tokenSlice(self.tree.nodeMainToken(current)); + count += 1; + } + break; + } else { + return null; + } + } + + if (count == 0) return null; + + // Build the path string + var total_len: usize = 0; + for (parts[parts.len - count ..]) |p| { + total_len += p.len + 1; + } + + const result = self.allocator.alloc(u8, total_len - 1) catch return null; + var pos: usize = 0; + for (parts[parts.len - count ..], 0..) |p, i| { + if (i > 0) { + result[pos] = '.'; + pos += 1; + } + @memcpy(result[pos..][0..p.len], p); + pos += p.len; + } + + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, result) catch {}; + return result; +} + +fn checkExposedPrivateType(self: *Linter, node: Ast.Node.Index) void { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = self.tree.fullFnProto(&buf, node) orelse return; + + if (!self.isPublicDecl(node)) return; + + // Find enclosing container (struct/union/enum) if any + const enclosing_container = self.findEnclosingContainer(node); + + // Collect generic type parameter names + var generic_params: [16][]const u8 = undefined; + var generic_count: usize = 0; + var it = fn_proto.iterate(&self.tree); + while (it.next()) |param| { + const type_node = param.type_expr orelse continue; + if (self.tree.nodeTag(type_node) == .identifier) { + const type_name = self.tree.tokenSlice(self.tree.nodeMainToken(type_node)); + if (std.mem.eql(u8, type_name, "type")) { + if (param.name_token) |name_tok| { + if (generic_count < 16) { + generic_params[generic_count] = self.tree.tokenSlice(name_tok); + generic_count += 1; + } + } + } + } + } + + // Check return type + if (fn_proto.ast.return_type.unwrap()) |ret_node| { + self.checkTypeNodeForPrivateWithGenerics(ret_node, fn_proto, generic_params[0..generic_count], enclosing_container); + } + + // Check parameter types + var it2 = fn_proto.iterate(&self.tree); + while (it2.next()) |param| { + const type_node = param.type_expr orelse continue; + // Skip generic parameters (comptime T: type) + if (self.tree.nodeTag(type_node) == .identifier) { + const type_name = self.tree.tokenSlice(self.tree.nodeMainToken(type_node)); + if (std.mem.eql(u8, type_name, "type")) continue; + } + self.checkTypeNodeForPrivateWithGenerics(type_node, fn_proto, generic_params[0..generic_count], enclosing_container); + } +} + +fn checkTypeNodeForPrivateWithGenerics( + self: *Linter, + type_node: Ast.Node.Index, + fn_proto: Ast.full.FnProto, + generic_params: []const []const u8, + enclosing_container: Ast.Node.OptionalIndex, +) void { + self.checkTypeNodeForPrivateImpl(type_node, fn_proto, generic_params, false, enclosing_container); +} + +fn checkTypeNodeForPrivateImpl( + self: *Linter, + type_node: Ast.Node.Index, + fn_proto: Ast.full.FnProto, + generic_params: []const []const u8, + is_error_position: bool, + enclosing_container: Ast.Node.OptionalIndex, +) void { + const tag = self.tree.nodeTag(type_node); + + switch (tag) { + .identifier => { + const type_name = self.tree.tokenSlice(self.tree.nodeMainToken(type_node)); + // Skip generic type parameters + for (generic_params) |gp| { + if (std.mem.eql(u8, type_name, gp)) return; + } + if (self.isPrivateTypeRef(type_name, enclosing_container)) { + self.reportPrivateType(fn_proto, type_name, is_error_position); + } + }, + .optional_type => { + const child = self.tree.nodeData(type_node).node; + self.checkTypeNodeForPrivateImpl(child, fn_proto, generic_params, is_error_position, enclosing_container); + }, + .error_union => { + const data = self.tree.nodeData(type_node).node_and_node; + self.checkTypeNodeForPrivateImpl(data[0], fn_proto, generic_params, true, enclosing_container); + self.checkTypeNodeForPrivateImpl(data[1], fn_proto, generic_params, false, enclosing_container); + }, + else => {}, + } +} + +fn reportPrivateType(self: *Linter, fn_proto: Ast.full.FnProto, type_name: []const u8, is_error: bool) void { + const name_token = fn_proto.name_token orelse return; + const loc = self.tree.tokenLocation(0, name_token); + const rule: rules.Rule = if (is_error) .Z015 else .Z012; + self.report(loc, rule, type_name); +} + +fn visitNode(self: *Linter, node: Ast.Node.Index) void { + const tag = self.tree.nodeTag(node); + + switch (tag) { + .fn_decl => self.checkFnDecl(node), + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + self.checkVarDecl(node); + }, + .@"return" => self.checkReturn(node), + .call_one, .call_one_comma, .call, .call_comma => { + self.checkCallArgs(node); + self.checkRedundantAsInCallArgs(node); + + if (self.rule_timer != null) { + const deprecated_start = self.rule_timer.?.read(); + self.checkDeprecatedCall(node); + self.trackRuleTime(.Z011, self.rule_timer.?.read() - deprecated_start); + } else { + self.checkDeprecatedCall(node); + } + + if (self.rule_timer != null) { + const assert_start = self.rule_timer.?.read(); + self.checkCompoundAssert(node); + self.trackRuleTime(.Z016, self.rule_timer.?.read() - assert_start); + } else { + self.checkCompoundAssert(node); + } + }, + .array_init_one, + .array_init_one_comma, + .array_init_dot_two, + .array_init_dot_two_comma, + .array_init_dot, + .array_init_dot_comma, + .array_init, + .array_init_comma, + => { + self.checkRedundantAsInArrayInit(node); + }, + .struct_init_one, + .struct_init_one_comma, + .struct_init_dot_two, + .struct_init_dot_two_comma, + .struct_init_dot, + .struct_init_dot_comma, + .struct_init, + .struct_init_comma, + => { + self.checkRedundantAsInStructInit(node); + }, + else => {}, + } + + self.visitChildren(node); +} + +fn visitChildren(self: *Linter, node: Ast.Node.Index) void { + const tag = self.tree.nodeTag(node); + switch (tag) { + .fn_decl => { + const data = self.tree.nodeData(node).node_and_node; + // Track the function's return type for checks inside the body + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = self.tree.fullFnProto(&buf, node); + const prev_return_type = self.current_fn_return_type; + if (fn_proto) |proto| { + self.current_fn_return_type = proto.ast.return_type; + } + self.visitNode(data[0]); + self.visitNode(data[1]); + self.current_fn_return_type = prev_return_type; + }, + .block, .block_semicolon => { + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, node) orelse return; + for (stmts) |stmt| self.visitNode(stmt); + }, + .block_two, .block_two_semicolon => { + const data = self.tree.nodeData(node).opt_node_and_opt_node; + if (data[0].unwrap()) |n| self.visitNode(n); + if (data[1].unwrap()) |n| self.visitNode(n); + }, + // Assignments - visit both sides to catch calls in RHS like `_ = deprecatedFunc()` + .assign => { + const data = self.tree.nodeData(node).node_and_node; + self.visitNode(data[0]); + self.visitNode(data[1]); + }, + // Variable declarations - visit both type annotation and init node + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = self.tree.fullVarDecl(node) orelse return; + // Visit type annotation (e.g., for `var x: std.ArrayListUnmanaged(u8)` to catch deprecated calls) + if (var_decl.ast.type_node.unwrap()) |type_node| self.visitNode(type_node); + if (var_decl.ast.init_node.unwrap()) |init_node| self.visitNode(init_node); + }, + // Container declarations (structs, enums, unions) - visit members + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => { + var buf: [2]Ast.Node.Index = undefined; + const container = self.tree.fullContainerDecl(&buf, node) orelse return; + for (container.ast.members) |member| { + self.visitNode(member); + } + }, + else => { + // For all other node types, use getNodeChildren to visit children + const children = self.getNodeChildren(node); + for (children.slice()) |child| { + self.visitNode(child); + } + }, + } +} + +fn checkFnDecl(self: *Linter, node: Ast.Node.Index) void { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = self.tree.fullFnProto(&buf, node) orelse return; + + const name_token = fn_proto.name_token orelse return; + const name = self.tree.tokenSlice(name_token); + + const returns_type = if (fn_proto.ast.return_type.unwrap()) |ret| blk: { + break :blk self.tree.nodeTag(ret) == .identifier and + std.mem.eql(u8, self.tree.tokenSlice(self.tree.nodeMainToken(ret)), "type"); + } else false; + + if (returns_type) { + if (!isPascalCase(name)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z005, name); + } + } else { + if (!isValidFunctionName(name)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z001, name); + } + } + + // Check for underscore prefix + if (self.config.isRuleEnabled(.Z031) and hasUnderscorePrefix(name)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z031, name); + } + + // Check for acronym casing + // Check for acronym casing + if (self.config.isRuleEnabled(.Z032)) { + if (checkAcronymCasing(self.allocator, name)) |suggestion| { + const context = self.allocator.alloc(u8, name.len + 1 + suggestion.len) catch { + self.allocator.free(suggestion); + return; + }; + @memcpy(context[0..name.len], name); + context[name.len] = 0; + @memcpy(context[name.len + 1 ..], suggestion); + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, suggestion) catch {}; + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z032, context); + } + } + + // Check for redundant words (disabled by default) + if (self.config.isRuleEnabled(.Z033)) { + if (findRedundantWord(name)) |word| { + const context = self.allocator.alloc(u8, name.len + 1 + word.len) catch return; + @memcpy(context[0..name.len], name); + context[name.len] = 0; + @memcpy(context[name.len + 1 ..], word); + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z033, context); + } + } + + self.checkExposedPrivateType(node); + self.checkArgumentOrder(node); + self.checkDeinitUndefined(node, fn_proto); +} + +fn checkDeinitUndefined(self: *Linter, node: Ast.Node.Index, fn_proto: Ast.full.FnProto) void { + if (!self.config.isRuleEnabled(.Z030)) return; + + // Only check functions named "deinit" + const name_token = fn_proto.name_token orelse return; + const name = self.tree.tokenSlice(name_token); + if (!std.mem.eql(u8, name, "deinit")) return; + + // Check if first parameter is a pointer type + var param_it = fn_proto.iterate(&self.tree); + const first_param = param_it.next() orelse return; + const param_type = first_param.type_expr orelse return; + if (!self.isPointerType(param_type)) return; + + // Get the parameter name (usually "self") + const param_name = if (first_param.name_token) |t| self.tree.tokenSlice(t) else return; + + // Get function body + const body_node = self.tree.nodeData(node).node_and_node[1]; + + // Check for `defer self.* = undefined;` anywhere in body - this handles all paths + if (self.hasDeferSelfUndefined(body_node, param_name)) return; + + // Check for early returns (which would skip final self.* = undefined) + if (self.hasEarlyReturn(body_node)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z030, "has early return without defer"); + return; + } + + // Check if last statement is `self.* = undefined;` + if (self.lastStatementIsSelfUndefined(body_node, param_name)) return; + + // None of the valid patterns found + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z030, ""); +} + +fn isPointerType(self: *Linter, node: Ast.Node.Index) bool { + const tag = self.tree.nodeTag(node); + return switch (tag) { + .ptr_type_aligned, .ptr_type_sentinel, .ptr_type, .ptr_type_bit_range => true, + else => false, + }; +} + +fn hasDeferSelfUndefined(self: *Linter, body_node: Ast.Node.Index, param_name: []const u8) bool { + // Iterate through all nodes looking for defer + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, body_node) orelse return false; + + for (stmts) |stmt| { + if (self.tree.nodeTag(stmt) == .@"defer") { + const defer_expr = self.tree.nodeData(stmt).node; + if (self.isSelfUndefinedAssign(defer_expr, param_name)) return true; + } + } + return false; +} + +fn hasEarlyReturn(self: *Linter, body_node: Ast.Node.Index) bool { + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, body_node) orelse return false; + + // Check all statements except the last one for returns + if (stmts.len <= 1) return false; + + for (stmts[0 .. stmts.len - 1]) |stmt| { + if (self.containsReturn(stmt)) return true; + } + return false; +} + +fn containsReturn(self: *Linter, node: Ast.Node.Index) bool { + const tag = self.tree.nodeTag(node); + if (tag == .@"return") return true; + + // Recursively check children for returns (e.g., in if blocks) + switch (tag) { + .@"if", .if_simple => { + const full_if = self.tree.fullIf(node) orelse return false; + if (self.containsReturn(full_if.ast.then_expr)) return true; + if (full_if.ast.else_expr.unwrap()) |else_node| { + if (self.containsReturn(else_node)) return true; + } + }, + .block, .block_semicolon, .block_two, .block_two_semicolon => { + var block_buf: [2]Ast.Node.Index = undefined; + const block_stmts = self.tree.blockStatements(&block_buf, node) orelse return false; + for (block_stmts) |stmt| { + if (self.containsReturn(stmt)) return true; + } + }, + else => {}, + } + return false; +} + +fn lastStatementIsSelfUndefined(self: *Linter, body_node: Ast.Node.Index, param_name: []const u8) bool { + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, body_node) orelse return false; + if (stmts.len == 0) return false; + + const last_stmt = stmts[stmts.len - 1]; + return self.isSelfUndefinedAssign(last_stmt, param_name); +} + +fn isSelfUndefinedAssign(self: *Linter, node: Ast.Node.Index, param_name: []const u8) bool { + if (self.tree.nodeTag(node) != .assign) return false; + + const assign_data = self.tree.nodeData(node).node_and_node; + const lhs = assign_data[0]; + const rhs = assign_data[1]; + + // LHS must be deref (self.*) + if (self.tree.nodeTag(lhs) != .deref) return false; + + // The dereferenced expression must be the parameter name + const deref_inner = self.tree.nodeData(lhs).node; + if (self.tree.nodeTag(deref_inner) != .identifier) return false; + const deref_name = self.tree.tokenSlice(self.tree.nodeMainToken(deref_inner)); + if (!std.mem.eql(u8, deref_name, param_name)) return false; + + // RHS must be undefined + if (self.tree.nodeTag(rhs) != .identifier) return false; + const rhs_name = self.tree.tokenSlice(self.tree.nodeMainToken(rhs)); + return std.mem.eql(u8, rhs_name, "undefined"); +} + +fn checkVarDecl(self: *Linter, node: Ast.Node.Index) void { + const var_decl = self.tree.fullVarDecl(node) orelse return; + + const name_token = var_decl.ast.mut_token + 1; + const name = self.tree.tokenSlice(name_token); + + if (name.len > 0 and name[0] == '_' and name.len > 1 and name[1] != '_') { + if (var_decl.ast.init_node != .none) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z002, name); + } + } + + if (!isSnakeCase(name) and !isTypeAlias(self, var_decl) and !isFunctionAlias(self, var_decl)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z006, name); + } + + // Check for underscore prefix + if (self.config.isRuleEnabled(.Z031) and hasUnderscorePrefix(name)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z031, name); + } + + // Check that error sets are PascalCase + if (var_decl.ast.init_node.unwrap()) |init_node| { + if (self.tree.nodeTag(init_node) == .error_set_decl and !isPascalCase(name)) { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z014, name); + } + } + + if (var_decl.ast.type_node == .none) { + if (var_decl.ast.init_node.unwrap()) |init_node| { + if (isExplicitStructInit(self.tree.nodeTag(init_node))) { + const loc = self.tree.tokenLocation(0, var_decl.ast.mut_token); + self.report(loc, .Z004, name); + } + } + } + + // Check for acronym casing (for type aliases) + if (self.config.isRuleEnabled(.Z032) and isTypeAlias(self, var_decl)) { + if (checkAcronymCasing(self.allocator, name)) |suggestion| { + const context = self.allocator.alloc(u8, name.len + 1 + suggestion.len) catch { + self.allocator.free(suggestion); + return; + }; + @memcpy(context[0..name.len], name); + context[name.len] = 0; + @memcpy(context[name.len + 1 ..], suggestion); + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, suggestion) catch {}; + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z032, context); + } + } + + // Check for redundant words (disabled by default) + if (self.config.isRuleEnabled(.Z033) and isTypeAlias(self, var_decl)) { + if (findRedundantWord(name)) |word| { + const context = self.allocator.alloc(u8, name.len + 1 + word.len) catch return; + @memcpy(context[0..name.len], name); + context[name.len] = 0; + @memcpy(context[name.len + 1 ..], word); + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z033, context); + } + } + + self.checkDupeImport(var_decl, name_token); + self.trackImportBinding(node, var_decl, name_token); + self.checkRedundantAsInVarDecl(var_decl); +} + +fn checkRedundantAsInVarDecl(self: *Linter, var_decl: Ast.full.VarDecl) void { + // Need both a type annotation and an init expression + const type_node = var_decl.ast.type_node.unwrap() orelse return; + const init_node = var_decl.ast.init_node.unwrap() orelse return; + + // Get the declared type name + const decl_type_name = self.getTypeNodeName(type_node) orelse return; + + // Get the @as type from init expression + const as_type_name = self.getAsTypeName(init_node) orelse return; + + // Compare types + if (std.mem.eql(u8, decl_type_name, as_type_name)) { + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(init_node)); + self.report(loc, .Z018, as_type_name); + } +} + +fn trackImportBinding(self: *Linter, node: Ast.Node.Index, var_decl: Ast.full.VarDecl, name_token: Ast.TokenIndex) void { + const init_node = var_decl.ast.init_node.unwrap() orelse return; + + // Check if this is a @import call + const main_token = self.tree.nodeMainToken(init_node); + const builtin_name = self.tree.tokenSlice(main_token); + if (!std.mem.eql(u8, builtin_name, "@import")) return; + + const name = self.tree.tokenSlice(name_token); + const is_discard = std.mem.eql(u8, name, "_"); + const is_pub = self.isPublicDecl(node); + + self.import_bindings.put(self.allocator, name, .{ + .name_token = name_token, + .is_pub = is_pub, + .is_discard = is_discard, + // ziglint-ignore: Z026 + }) catch {}; +} + +fn checkDupeImport(self: *Linter, var_decl: Ast.full.VarDecl, name_token: Ast.TokenIndex) void { + const init_node = var_decl.ast.init_node.unwrap() orelse return; + + // Check if this is a @import call + const main_token = self.tree.nodeMainToken(init_node); + const builtin_name = self.tree.tokenSlice(main_token); + if (!std.mem.eql(u8, builtin_name, "@import")) return; + + // Get the import argument + var buf: [2]Ast.Node.Index = undefined; + const params = self.tree.builtinCallParams(&buf, init_node) orelse return; + if (params.len == 0) return; + + const arg_token = self.tree.nodeMainToken(params[0]); + const import_path = self.tree.tokenSlice(arg_token); + + // Check for duplicate + if (self.seen_imports.get(import_path)) |_| { + const loc = self.tree.tokenLocation(0, name_token); + self.report(loc, .Z007, import_path); + } else { + // ziglint-ignore: Z026 + self.seen_imports.put(self.allocator, import_path, name_token) catch {}; + } +} + +fn checkReturn(self: *Linter, node: Ast.Node.Index) void { + const return_expr = self.tree.nodeData(node).opt_node.unwrap() orelse return; + self.checkRedundantType(return_expr, true); + + if (self.rule_timer) |*t| { + const start = t.read(); + self.checkReturnTry(node, return_expr); + self.trackRuleTime(.Z017, t.read() - start); + } else { + self.checkReturnTry(node, return_expr); + } + + self.checkRedundantAsInReturn(node, return_expr); +} + +fn checkReturnTry(self: *Linter, return_node: Ast.Node.Index, return_expr: Ast.Node.Index) void { + // Check if the return expression is a try + if (self.tree.nodeTag(return_expr) != .@"try") return; + + // Get the inner expression being tried + const try_expr = self.tree.nodeData(return_expr).node; + const expr_source = self.getNodeSource(try_expr); + const truncated = truncateExpr(expr_source); + + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(return_node)); + self.report(loc, .Z017, truncated); +} + +fn checkCatchReturnAll(self: *Linter) void { + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + if (self.tree.nodeTag(node) != .@"catch") continue; + + const data = self.tree.nodeData(node).node_and_node; + const rhs = data[1]; + + if (self.tree.nodeTag(rhs) != .@"return") continue; + + // Must have a capture payload: `catch |err|` + const catch_token = self.tree.nodeMainToken(node); + const pipe_token = catch_token + 1; + if (self.tree.tokenTag(pipe_token) != .pipe) continue; + + const payload_token = pipe_token + 1; + const payload_name = self.tree.tokenSlice(payload_token); + + // The return expression must be an identifier matching the payload + const return_expr = self.tree.nodeData(rhs).opt_node.unwrap() orelse continue; + if (self.tree.nodeTag(return_expr) != .identifier) continue; + const return_name = self.tree.tokenSlice(self.tree.nodeMainToken(return_expr)); + + if (!std.mem.eql(u8, payload_name, return_name)) continue; + + const loc = self.tree.tokenLocation(0, catch_token); + self.report(loc, .Z025, payload_name); + } +} + +fn checkEmptyCatchAll(self: *Linter) void { + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + if (self.tree.nodeTag(node) != .@"catch") continue; + + const data = self.tree.nodeData(node).node_and_node; + const rhs = data[1]; + + if (!self.isEmptyBlock(rhs)) continue; + + // Skip if under a defer node + if (self.isUnderDefer(node)) continue; + + const catch_token = self.tree.nodeMainToken(node); + const loc = self.tree.tokenLocation(0, catch_token); + self.report(loc, .Z026, ""); + } +} + +fn checkInstanceDeclAccess(self: *Linter) void { + const resolver = self.type_resolver orelse return; + const mod_path = self.module_path orelse return; + + for (0..self.tree.nodes.len) |i| { + const node: Ast.Node.Index = @enumFromInt(i); + if (self.tree.nodeTag(node) != .field_access) continue; + + const data = self.tree.nodeData(node).node_and_token; + const lhs_node = data[0]; + const field_token = data[1]; + const field_name = self.tree.tokenSlice(field_token); + + if (resolver.isTypeRef(mod_path, lhs_node)) continue; + + const lhs_type = resolver.typeOf(mod_path, lhs_node); + if (!resolver.isContainerLevelDecl(lhs_type, field_name)) continue; + + const type_name = switch (lhs_type) { + .user_type => |u| u.name, + else => continue, + }; + + const loc = self.tree.tokenLocation(0, field_token); + const context = std.fmt.allocPrint(self.allocator, "{s}\x00{s}", .{ + field_name, type_name, + }) catch continue; + // ziglint-ignore: Z026 + self.allocated_contexts.append(self.allocator, context) catch {}; + self.report(loc, .Z027, context); + } +} + +fn isEmptyBlock(self: *Linter, node: Ast.Node.Index) bool { + const tag = self.tree.nodeTag(node); + return switch (tag) { + .block_two, .block_two_semicolon => { + const block_data = self.tree.nodeData(node).opt_node_and_opt_node; + return block_data[0] == .none and block_data[1] == .none; + }, + .block, .block_semicolon => { + var buf: [2]Ast.Node.Index = undefined; + const stmts = self.tree.blockStatements(&buf, node) orelse return true; + return stmts.len == 0; + }, + else => false, + }; +} + +fn isUnderDefer(self: *Linter, node: Ast.Node.Index) bool { + if (self.parent_map.len == 0) return false; + var current = node; + while (true) { + const parent_opt = self.parent_map[@intFromEnum(current)]; + const parent = parent_opt.unwrap() orelse return false; + const tag = self.tree.nodeTag(parent); + if (tag == .@"defer" or tag == .@"errdefer") return true; + current = parent; + } +} + +fn checkRedundantAsInReturn(self: *Linter, return_node: Ast.Node.Index, return_expr: Ast.Node.Index) void { + // Get the @as type from return expression + const as_type_name = self.getAsTypeName(return_expr) orelse return; + + // Get function's return type + const fn_return_type = self.current_fn_return_type.unwrap() orelse return; + const fn_return_name = self.getTypeNodeName(fn_return_type) orelse return; + + // Compare types + if (std.mem.eql(u8, as_type_name, fn_return_name)) { + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(return_node)); + self.report(loc, .Z018, as_type_name); + } +} + +fn getAsTypeName(self: *Linter, node: Ast.Node.Index) ?[]const u8 { + const tag = self.tree.nodeTag(node); + if (tag != .builtin_call_two and tag != .builtin_call_two_comma and + tag != .builtin_call and tag != .builtin_call_comma) return null; + + const main_token = self.tree.nodeMainToken(node); + const builtin_name = self.tree.tokenSlice(main_token); + if (!std.mem.eql(u8, builtin_name, "@as")) return null; + + var buf: [2]Ast.Node.Index = undefined; + const params = self.tree.builtinCallParams(&buf, node) orelse return null; + if (params.len < 1) return null; + + return self.getTypeNodeName(params[0]); +} + +fn getTypeNodeName(self: *Linter, type_node: Ast.Node.Index) ?[]const u8 { + const tag = self.tree.nodeTag(type_node); + return switch (tag) { + .identifier => self.tree.tokenSlice(self.tree.nodeMainToken(type_node)), + else => null, + }; +} + +fn checkRedundantAsInCallArgs(self: *Linter, node: Ast.Node.Index) void { + var call_buf: [1]Ast.Node.Index = undefined; + const call = self.tree.fullCall(&call_buf, node) orelse return; + const params = call.ast.params; + if (params.len == 0) return; + + // Resolve the called function's prototype and determine param offset + const resolved = self.resolveCalledFnProto(call.ast.fn_expr) orelse return; + const fn_tree = resolved.tree; + var fn_buf: [1]Ast.Node.Index = undefined; + const fn_proto = fn_tree.fullFnProto(&fn_buf, resolved.fn_node) orelse return; + + // For method calls (field_access), skip the receiver param + const skip_receiver = self.tree.nodeTag(call.ast.fn_expr) == .field_access; + + var it = fn_proto.iterate(fn_tree); + var param_idx: usize = 0; + + if (skip_receiver) { + _ = it.next(); + } + + while (it.next()) |param| : (param_idx += 1) { + if (param_idx >= params.len) break; + const arg = params[param_idx]; + + const as_type_name = self.getAsTypeName(arg) orelse continue; + const type_node = param.type_expr orelse continue; + const param_type_name = self.getTypeNodeNameFromTree(fn_tree, type_node) orelse continue; + + if (std.mem.eql(u8, as_type_name, param_type_name)) { + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(arg)); + self.report(loc, .Z029, as_type_name); + } + } +} + +const ResolvedFnProto = struct { + tree: *const Ast, + fn_node: Ast.Node.Index, +}; + +fn resolveCalledFnProto(self: *Linter, callee_expr: Ast.Node.Index) ?ResolvedFnProto { + const tag = self.tree.nodeTag(callee_expr); + switch (tag) { + .identifier => { + const fn_name = self.tree.tokenSlice(self.tree.nodeMainToken(callee_expr)); + for (self.tree.rootDecls()) |decl_node| { + if (self.tree.nodeTag(decl_node) != .fn_decl) continue; + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = self.tree.fullFnProto(&buf, decl_node) orelse continue; + const name_token = fn_proto.name_token orelse continue; + if (std.mem.eql(u8, self.tree.tokenSlice(name_token), fn_name)) { + return .{ .tree = &self.tree, .fn_node = decl_node }; + } + } + }, + .field_access => { + const resolver = self.type_resolver orelse return null; + const mod_path = self.module_path orelse return null; + const data = self.tree.nodeData(callee_expr).node_and_token; + const receiver_node = data[0]; + const method_name = self.tree.tokenSlice(data[1]); + const receiver_type = resolver.typeOf(mod_path, receiver_node); + const method_def = resolver.findMethodDef(receiver_type, method_name) orelse return null; + const mod = resolver.graph.getModule(method_def.module_path) orelse return null; + return .{ .tree = &mod.tree, .fn_node = method_def.node }; + }, + else => {}, + } + return null; +} + +fn checkRedundantAsInArrayInit(self: *Linter, node: Ast.Node.Index) void { + var buf: [2]Ast.Node.Index = undefined; + const array_init = self.tree.fullArrayInit(&buf, node) orelse return; + const type_expr = array_init.ast.type_expr.unwrap() orelse return; + + // Get the element type from the array type expression + const elem_type_name = self.getArrayElemTypeName(type_expr) orelse return; + + for (array_init.ast.elements) |elem| { + const as_type_name = self.getAsTypeName(elem) orelse continue; + if (std.mem.eql(u8, as_type_name, elem_type_name)) { + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(elem)); + self.report(loc, .Z029, as_type_name); + } + } +} + +fn getArrayElemTypeName(self: *Linter, type_expr: Ast.Node.Index) ?[]const u8 { + const type_tag = self.tree.nodeTag(type_expr); + return switch (type_tag) { + .array_type, .array_type_sentinel => { + const array_type = self.tree.fullArrayType(type_expr) orelse return null; + return self.getTypeNodeName(array_type.ast.elem_type); + }, + else => null, + }; +} + +fn getTypeNodeNameFromTree(_: *Linter, tree: *const Ast, type_node: Ast.Node.Index) ?[]const u8 { + const tag = tree.nodeTag(type_node); + return switch (tag) { + .identifier => tree.tokenSlice(tree.nodeMainToken(type_node)), + else => null, + }; +} + +fn checkRedundantAsInStructInit(self: *Linter, node: Ast.Node.Index) void { + var buf: [2]Ast.Node.Index = undefined; + const struct_init = self.tree.fullStructInit(&buf, node) orelse return; + if (struct_init.ast.fields.len == 0) return; + + // Determine the struct type name + const struct_type_name = self.resolveStructInitTypeName(node, struct_init) orelse return; + + for (struct_init.ast.fields) |field_value| { + const as_type_name = self.getAsTypeName(field_value) orelse continue; + + // Get field name: token before '=' before the value expression + const value_main_token = self.tree.nodeMainToken(field_value); + if (value_main_token < 2) continue; + const field_name_token = value_main_token - 2; + if (self.tree.tokenTag(field_name_token) != .identifier) continue; + const field_name = self.tree.tokenSlice(field_name_token); + + const field_type_name = self.findContainerFieldType(struct_type_name, field_name) orelse continue; + if (std.mem.eql(u8, as_type_name, field_type_name)) { + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(field_value)); + self.report(loc, .Z029, as_type_name); + } + } +} + +fn resolveStructInitTypeName(self: *Linter, node: Ast.Node.Index, struct_init: Ast.full.StructInit) ?[]const u8 { + // Case 1: explicit type expression (e.g., Foo{ .x = 1 }) + if (struct_init.ast.type_expr.unwrap()) |type_expr| { + return self.getTypeNodeName(type_expr); + } + + // Case 2: anonymous init — walk parent_map to find type context + if (self.parent_map.len == 0) return null; + const parent = self.parent_map[@intFromEnum(node)].unwrap() orelse return null; + const parent_tag = self.tree.nodeTag(parent); + switch (parent_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = self.tree.fullVarDecl(parent) orelse return null; + const type_node = var_decl.ast.type_node.unwrap() orelse return null; + return self.getTypeNodeName(type_node); + }, + else => return null, + } +} + +fn findContainerFieldType(self: *Linter, struct_type_name: []const u8, field_name: []const u8) ?[]const u8 { + return self.findContainerFieldTypeInTree(&self.tree, struct_type_name, field_name); +} + +fn findContainerFieldTypeInTree(self: *Linter, tree: *const Ast, struct_type_name: []const u8, field_name: []const u8) ?[]const u8 { + _ = self; + for (tree.rootDecls()) |decl_node| { + const tag = tree.nodeTag(decl_node); + switch (tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + if (!std.mem.eql(u8, tree.tokenSlice(name_token), struct_type_name)) continue; + const init_node = var_decl.ast.init_node.unwrap() orelse continue; + + var container_buf: [2]Ast.Node.Index = undefined; + const container = tree.fullContainerDecl(&container_buf, init_node) orelse continue; + for (container.ast.members) |member| { + const field = tree.fullContainerField(member) orelse continue; + if (!std.mem.eql(u8, tree.tokenSlice(field.ast.main_token), field_name)) continue; + const type_node = field.ast.type_expr.unwrap() orelse return null; + const field_type_tag = tree.nodeTag(type_node); + return switch (field_type_tag) { + .identifier => tree.tokenSlice(tree.nodeMainToken(type_node)), + else => null, + }; + } + }, + else => {}, + } + } + return null; +} + +fn checkCallArgs(self: *Linter, node: Ast.Node.Index) void { + var buf: [1]Ast.Node.Index = undefined; + const call = self.tree.fullCall(&buf, node) orelse return; + for (call.ast.params) |arg| { + // Don't check field_access in call args - can't distinguish type params from enum values + self.checkRedundantType(arg, false); + } +} + +fn checkDeprecatedCall(self: *Linter, node: Ast.Node.Index) void { + const resolver = self.type_resolver orelse return; + const mod_path = self.module_path orelse return; + + var buf: [1]Ast.Node.Index = undefined; + const call = self.tree.fullCall(&buf, node) orelse return; + + const fn_expr = call.ast.fn_expr; + const fn_expr_tag = self.tree.nodeTag(fn_expr); + + var method_def: ?TypeResolver.MethodDef = null; + var stdlib_decl: ?TypeResolver.MethodDef = null; + var name_token: Ast.TokenIndex = undefined; + var fn_name: []const u8 = undefined; + + if (fn_expr_tag == .field_access) { + const data = self.tree.nodeData(fn_expr).node_and_token; + const receiver_node = data[0]; + name_token = data[1]; + fn_name = self.tree.tokenSlice(name_token); + + const receiver_type = resolver.typeOf(mod_path, receiver_node); + method_def = resolver.findMethodDef(receiver_type, fn_name); + + // For stdlib types, also get the declaration without following aliases + // (the alias itself may be deprecated, like std.ArrayListUnmanaged) + if (receiver_type == .std_type) { + stdlib_decl = resolver.findStdlibDecl(receiver_type.std_type.path, fn_name); + } + } else if (fn_expr_tag == .identifier) { + // Direct function call in the same module (e.g., `DeprecatedFunc()`) + name_token = self.tree.nodeMainToken(fn_expr); + fn_name = self.tree.tokenSlice(name_token); + + // Look up the function in the current module + method_def = resolver.findFnInCurrentModule(mod_path, fn_name); + } else { + return; + } + + // Check the target definition for deprecation + if (method_def) |def| { + if (self.checkDefDeprecation(def, name_token, fn_name)) return; + } + + // Also check the stdlib declaration itself (for deprecated aliases like std.ArrayListUnmanaged) + if (stdlib_decl) |decl| { + _ = self.checkDefDeprecation(decl, name_token, fn_name); + } +} + +fn checkDefDeprecation(self: *Linter, def: TypeResolver.MethodDef, name_token: Ast.TokenIndex, fn_name: []const u8) bool { + const resolver = self.type_resolver orelse return false; + const mod = resolver.graph.getModule(def.module_path) orelse return false; + + // Check cache first + const cache_key = DeprecationKey.init(def.module_path, def.node); + if (self.deprecation_cache.get(cache_key)) |is_deprecated| { + if (!is_deprecated) return false; + + // Still deprecated, extract doc comment for the error message + const doc = doc_comments.getDocComment(self.allocator, &mod.tree, def.node) orelse return false; + defer self.allocator.free(doc); + + const loc = self.tree.tokenLocation(0, name_token); + const msg = std.fmt.allocPrint(self.allocator, "'{s}' is deprecated: {s}", .{ fn_name, doc }) catch return false; + self.allocated_contexts.append(self.allocator, msg) catch { + self.allocator.free(msg); + return false; + }; + self.report(loc, .Z011, msg); + return true; + } + + // Not in cache, extract doc comment and cache the result + const doc = doc_comments.getDocComment(self.allocator, &mod.tree, def.node) orelse { + // No doc comment, cache as not deprecated (cache failure is non-critical) + self.deprecation_cache.put(self.allocator, cache_key, false) catch return false; + return false; + }; + defer self.allocator.free(doc); + + const is_deprecated = containsDeprecated(doc); + + if (is_deprecated) { + // Cache before reporting (cache failure is non-critical) + self.deprecation_cache.put(self.allocator, cache_key, true) catch return true; + + const loc = self.tree.tokenLocation(0, name_token); + const msg = std.fmt.allocPrint(self.allocator, "'{s}' is deprecated: {s}", .{ fn_name, doc }) catch return false; + self.allocated_contexts.append(self.allocator, msg) catch { + self.allocator.free(msg); + return false; + }; + self.report(loc, .Z011, msg); + return true; + } + + // Not deprecated, cache the result (cache failure is non-critical) + self.deprecation_cache.put(self.allocator, cache_key, false) catch return false; + return false; +} + +fn containsDeprecated(text: []const u8) bool { + var i: usize = 0; + while (i + 10 <= text.len) : (i += 1) { + const slice = text[i .. i + 10]; + if (std.ascii.eqlIgnoreCase(slice, "deprecated")) return true; + } + return false; +} + +fn checkCompoundAssert(self: *Linter, node: Ast.Node.Index) void { + var buf: [1]Ast.Node.Index = undefined; + const call = self.tree.fullCall(&buf, node) orelse return; + + // Check if this is a call to "assert" + const fn_expr = call.ast.fn_expr; + const is_assert = switch (self.tree.nodeTag(fn_expr)) { + .identifier => std.mem.eql(u8, self.tree.tokenSlice(self.tree.nodeMainToken(fn_expr)), "assert"), + .field_access => blk: { + const data = self.tree.nodeData(fn_expr).node_and_token; + break :blk std.mem.eql(u8, self.tree.tokenSlice(data[1]), "assert"); + }, + else => false, + }; + if (!is_assert) return; + + // Check if argument is a compound bool_and or bool_or + if (call.ast.params.len == 0) return; + const arg = call.ast.params[0]; + const arg_tag = self.tree.nodeTag(arg); + + // Only flag `and` - `assert(a or b)` is not equivalent to `assert(a); assert(b);` + if (arg_tag != .bool_and) return; + + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(node)); + self.report(loc, .Z016, "and"); +} + +fn checkRedundantType(self: *Linter, node: Ast.Node.Index, check_field_access: bool) void { + const tag = self.tree.nodeTag(node); + + if (isExplicitStructInit(tag)) { + var buf: [2]Ast.Node.Index = undefined; + const struct_init = self.tree.fullStructInit(&buf, node) orelse return; + const type_node = struct_init.ast.type_expr.unwrap() orelse return; + const type_token = self.tree.nodeMainToken(type_node); + const loc = self.tree.tokenLocation(0, type_token); + // Get the fields part (everything after the type name) + 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.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; + self.allocated_contexts.append(self.allocator, msg) catch { + self.allocator.free(msg); + return; + }; + _ = type_name; + self.report(loc, .Z010, msg); + } else if (check_field_access and tag == .field_access) { + // Only flag if the LHS is a PascalCase identifier (likely a type/enum) + const data = self.tree.nodeData(node).node_and_token; + const lhs = data[0]; + if (self.tree.nodeTag(lhs) != .identifier) return; + const lhs_name = self.tree.tokenSlice(self.tree.nodeMainToken(lhs)); + if (!isPascalCase(lhs_name)) return; + + // Skip error sets - explicit Error.X is often preferred for clarity + if (self.type_resolver) |resolver| { + if (self.module_path) |mod_path| { + const lhs_type = resolver.typeOf(mod_path, lhs); + if (lhs_type == .error_set) return; + } + } + + const field_token = data[1]; + const field_name = self.tree.tokenSlice(field_token); + const loc = self.tree.tokenLocation(0, self.tree.nodeMainToken(lhs)); + // Full expression is "Type.field" + const full_expr = truncateExpr(self.getNodeSource(node)); + const msg = std.fmt.allocPrint(self.allocator, ".{s}\x00{s}", .{ field_name, full_expr }) catch return; + self.allocated_contexts.append(self.allocator, msg) catch { + self.allocator.free(msg); + return; + }; + self.report(loc, .Z010, msg); + } +} + +fn getNodeSource(self: *Linter, node: Ast.Node.Index) []const u8 { + const token_starts = self.tree.tokens.items(.start); + const first_token = self.tree.firstToken(node); + const last_token = self.tree.lastToken(node); + const start = token_starts[first_token]; + const end = token_starts[last_token] + self.tree.tokenSlice(last_token).len; + return self.source[start..end]; +} + +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.find(u8, expr[0..@min(max_len, expr.len)], "{")) |brace| { + return expr[0 .. brace + 1]; + } + return expr[0..max_len]; +} + +fn isExplicitStructInit(tag: Ast.Node.Tag) bool { + return switch (tag) { + .struct_init, + .struct_init_comma, + .struct_init_one, + .struct_init_one_comma, + => true, + else => false, + }; +} + +fn isValidFunctionName(name: []const u8) bool { + if (name.len == 0) return false; + // Must start with lowercase letter + if (name[0] >= 'A' and name[0] <= 'Z') return false; + // Leading underscore is allowed (private/internal convention) + if (name[0] == '_') return true; + + // No underscores allowed in camelCase (except leading) + for (name) |c| { + if (c == '_') return false; + } + + return true; +} + +fn isPascalCase(name: []const u8) bool { + if (name.len == 0) return false; + if (name[0] < 'A' or name[0] > 'Z') return false; + for (name) |c| { + if (c == '_') return false; + } + return true; +} + +fn isSnakeCase(name: []const u8) bool { + if (name.len == 0) return false; + if (name[0] == '_') return true; + for (name) |c| { + if (c >= 'A' and c <= 'Z') return false; + } + return true; +} + +/// Returns true if the identifier has an underscore prefix (but not just `_` or `__`). +fn hasUnderscorePrefix(name: []const u8) bool { + if (name.len < 2) return false; + if (name[0] != '_') return false; + // Allow `__` double-underscore prefix (e.g., `__builtin`) + if (name[1] == '_') return false; + return true; +} + +/// Checks if a name has incorrect acronym casing (e.g., XMLParser instead of XmlParser). +/// Returns the suggested fix if there's an issue, null otherwise. +fn checkAcronymCasing(allocator: std.mem.Allocator, name: []const u8) ?[]const u8 { + if (name.len < 2) return null; + + // Find sequences of 2+ uppercase letters that should be title-cased + // XMLParser -> XmlParser (XML at start) + // readXML -> readXml (XML at end) + // HTTPSConnection -> HttpsConnection (HTTPS at start) + // getHTTPSConnection -> getHttpsConnection (HTTPS in middle) + + var result = allocator.alloc(u8, name.len) catch return null; + @memcpy(result, name); + var has_issue = false; + + var i: usize = 0; + while (i < name.len) { + if (isUppercase(name[i])) { + // Found start of potential acronym + const start = i; + var end = i + 1; + while (end < name.len and isUppercase(name[end])) { + end += 1; + } + + const acronym_len = end - start; + if (acronym_len >= 2) { + // We have 2+ consecutive uppercase letters + // Check if this is at the end or followed by lowercase + if (end < name.len and isLowercase(name[end])) { + // e.g., "XMLParser" - the last letter of "XML" is part of "Parser" + // Convert all but last uppercase to lowercase: "XmlParser" + for (start + 1..end - 1) |j| { + result[j] = toLowercase(name[j]); + } + if (acronym_len > 2) has_issue = true; + } else { + // At end or followed by non-alpha (e.g., "readXML" or "XML123") + // Convert all but first to lowercase: "readXml" or "Xml123" + for (start + 1..end) |j| { + result[j] = toLowercase(name[j]); + } + if (acronym_len >= 2) has_issue = true; + } + } + i = end; + } else { + i += 1; + } + } + + if (has_issue) { + return result; + } else { + allocator.free(result); + return null; + } +} + +fn isUppercase(c: u8) bool { + return c >= 'A' and c <= 'Z'; +} + +fn isLowercase(c: u8) bool { + return c >= 'a' and c <= 'z'; +} + +fn toLowercase(c: u8) u8 { + if (c >= 'A' and c <= 'Z') return c + 32; + return c; +} + +/// Words that are considered redundant in identifier names per the Zig style guide. +const redundant_words = [_][]const u8{ + "Value", + "Data", + "Context", + "Manager", + "State", + "utils", + "misc", + "Util", + "Utils", + "Misc", +}; + +/// Checks if a name contains a redundant word and returns it if found. +fn findRedundantWord(name: []const u8) ?[]const u8 { + // Check if the name contains any redundant word as a complete word boundary + for (redundant_words) |word| { + if (containsWordBoundary(name, word)) { + return word; + } + } + return null; +} + +/// Returns true if name contains word at a word boundary (start, end, or camelCase boundary). +fn containsWordBoundary(name: []const u8, word: []const u8) bool { + if (word.len > name.len) return false; + + // Check if name equals word exactly + if (std.mem.eql(u8, name, word)) return true; + + // Check if name starts with word followed by uppercase or end + if (std.mem.startsWith(u8, name, word)) { + if (word.len == name.len) return true; + const next = name[word.len]; + // Word boundary: followed by uppercase (camelCase) or non-alpha + if (isUppercase(next) or (!isLowercase(next) and !isUppercase(next))) return true; + } + + // Check if name ends with word preceded by lowercase + if (std.mem.endsWith(u8, name, word) and name.len > word.len) { + const prev = name[name.len - word.len - 1]; + if (isLowercase(prev)) return true; + } + + // Check for word in middle with camelCase boundaries + var i: usize = 1; + while (i + word.len <= name.len) { + if (std.mem.startsWith(u8, name[i..], word)) { + const prev = name[i - 1]; + // Must be preceded by lowercase (camelCase boundary) + if (isLowercase(prev)) { + if (i + word.len == name.len) return true; + const next = name[i + word.len]; + // Must be followed by uppercase or non-alpha + if (isUppercase(next) or (!isLowercase(next) and !isUppercase(next))) return true; + } + } + i += 1; + } + + return false; +} + +/// Returns true if the identifier is a primitive type (e.g., i32, u8, f64, bool, etc.) +fn isPrimitiveType(name: []const u8) bool { + const primitives = [_][]const u8{ + "bool", "true", "false", "null", "undefined", + "noreturn", "void", "anyopaque", "anyerror", "anytype", + "anyframe", "comptime_int", "comptime_float", "isize", "usize", + "c_char", "c_short", "c_ushort", "c_int", "c_uint", + "c_long", "c_ulong", "c_longlong", "c_ulonglong", "c_longdouble", + "f16", "f32", "f64", "f80", "f128", + }; + for (primitives) |p| { + if (std.mem.eql(u8, name, p)) return true; + } + // Check for integer types: i{N}, u{N} where N is digits + if (name.len >= 2 and (name[0] == 'i' or name[0] == 'u')) { + for (name[1..]) |c| { + if (c < '0' or c > '9') return false; + } + return true; + } + return false; +} + +fn isTypeAlias(self: *Linter, var_decl: Ast.full.VarDecl) bool { + const init_node = var_decl.ast.init_node.unwrap() orelse return false; + const tag = self.tree.nodeTag(init_node); + return switch (tag) { + .identifier => blk: { + const token = self.tree.tokenSlice(self.tree.nodeMainToken(init_node)); + break :blk std.mem.eql(u8, token, "type") or isPascalCase(token) or isPrimitiveType(token); + }, + .field_access => blk: { + const data = self.tree.nodeData(init_node).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + break :blk isPascalCase(field_name); + }, + .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 isBuiltinTypeConstructor(token); + }, + .call_one, .call_one_comma => blk: { + // Check if calling a PascalCase function (type constructor) + const callee = self.tree.nodeData(init_node).node_and_opt_node[0]; + const callee_tag = self.tree.nodeTag(callee); + if (callee_tag == .identifier) { + const fn_name = self.tree.tokenSlice(self.tree.nodeMainToken(callee)); + break :blk isPascalCase(fn_name); + } else if (callee_tag == .field_access) { + const data = self.tree.nodeData(callee).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + break :blk isPascalCase(field_name); + } + break :blk false; + }, + .call, .call_comma => blk: { + // Check if calling a PascalCase function (type constructor) + const callee = self.tree.nodeData(init_node).node_and_extra[0]; + const callee_tag = self.tree.nodeTag(callee); + if (callee_tag == .identifier) { + const fn_name = self.tree.tokenSlice(self.tree.nodeMainToken(callee)); + break :blk isPascalCase(fn_name); + } else if (callee_tag == .field_access) { + const data = self.tree.nodeData(callee).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + break :blk isPascalCase(field_name); + } + break :blk false; + }, + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + .error_set_decl, + .merge_error_sets, + // Type expressions + .array_type, + .array_type_sentinel, + .ptr_type_aligned, + .ptr_type_sentinel, + .ptr_type, + .ptr_type_bit_range, + .optional_type, + .error_union, + .fn_proto, + .fn_proto_multi, + .fn_proto_one, + .fn_proto_simple, + => true, + .block_two, + .block_two_semicolon, + .block, + .block_semicolon, + .@"if", + .if_simple, + .@"switch", + .switch_comma, + => blk: { + // These expressions can return either types or values. + // Use PascalCase name as heuristic for type alias. + const name_token = var_decl.ast.mut_token + 1; + const name = self.tree.tokenSlice(name_token); + break :blk isPascalCase(name); + }, + else => false, + }; +} + +/// Checks if the variable declaration is a function alias (assigning a camelCase identifier). +/// Function aliases are allowed to use camelCase names. +fn isFunctionAlias(self: *Linter, var_decl: Ast.full.VarDecl) bool { + const init_node = var_decl.ast.init_node.unwrap() orelse return false; + const tag = self.tree.nodeTag(init_node); + return switch (tag) { + .identifier => blk: { + const token = self.tree.tokenSlice(self.tree.nodeMainToken(init_node)); + break :blk isCamelCase(token); + }, + .field_access => blk: { + const data = self.tree.nodeData(init_node).node_and_token; + const field_name = self.tree.tokenSlice(data[1]); + break :blk isCamelCase(field_name); + }, + else => false, + }; +} + +fn isCamelCase(name: []const u8) bool { + if (name.len == 0) return false; + // Must start with lowercase letter + if (name[0] < 'a' or name[0] > 'z') return false; + // No underscores allowed + for (name) |c| { + if (c == '_') return false; + } + return true; +} + +fn isIgnored(self: *Linter, line: usize, rule: rules.Rule) bool { + // Check inline comment on current line + if (self.lineHasIgnore(self.getLineText(line), rule)) return true; + + // Check preceding comment-only lines (walk back through consecutive comments) + var check_line = line; + while (check_line > 0) { + check_line -= 1; + const prev_line = self.getLineText(check_line); + const trimmed = std.mem.trimStart(u8, prev_line, " \t"); + if (!std.mem.startsWith(u8, trimmed, "//")) break; + if (self.lineHasIgnore(prev_line, rule)) return true; + } + + return false; +} + +fn lineHasIgnore(_: *Linter, line_text: []const u8, rule: rules.Rule) bool { + if (std.mem.find(u8, line_text, "// ziglint-ignore:")) |idx| { + const ignore_part = line_text[idx + 18 ..]; + if (std.mem.find(u8, ignore_part, rule.code()) != null) return true; + } + return false; +} + +fn getLineText(self: *Linter, line: usize) []const u8 { + const line_start = if (line == 0) 0 else blk: { + var newlines: usize = 0; + for (self.source, 0..) |c, i| { + if (c == '\n') { + newlines += 1; + if (newlines == line) break :blk i + 1; + } + } + break :blk self.source.len; + }; + + const line_end = for (self.source[line_start..], line_start..) |c, i| { + if (c == '\n') break i; + } else self.source.len; + + return self.source[line_start..line_end]; +} + +fn checkLineLength(self: *Linter) void { + const max_len = self.config.getLineLength(); + var line_num: usize = 0; + var line_start: usize = 0; + + for (self.source, 0..) |c, i| { + if (c == '\n') { + const line_len = i - line_start; + if (line_len > max_len) { + // Format: "actual_len\x00max_len" for the error message + const context = std.fmt.allocPrint(self.allocator, "{}\x00{}", .{ line_len, max_len }) catch continue; + self.allocated_contexts.append(self.allocator, context) catch { + self.allocator.free(context); + continue; + }; + self.reportLineLength(line_num, context, max_len); + } + line_num += 1; + line_start = i + 1; + } + } + + // Check last line if not terminated with newline + if (line_start < self.source.len) { + const line_len = self.source.len - line_start; + if (line_len > max_len) { + const context = std.fmt.allocPrint(self.allocator, "{}\x00{}", .{ line_len, max_len }) catch return; + self.allocated_contexts.append(self.allocator, context) catch { + self.allocator.free(context); + return; + }; + self.reportLineLength(line_num, context, max_len); + } + } +} + +fn reportLineLength(self: *Linter, line: usize, context: []const u8, max_len: u32) void { + if (self.isIgnored(line, .Z024)) return; + + self.diagnostics.append(self.allocator, .{ + .path = self.path, + .line = @intCast(line + 1), + .column = @intCast(max_len + 1), + .rule = .Z024, + .context = context, + // ziglint-ignore: Z026 + }) catch {}; +} + +pub fn diagnosticCount(self: *const Linter, rule: rules.Rule) usize { + var count: usize = 0; + for (self.diagnostics.items) |d| { + if (d.rule == rule) count += 1; + } + return count; +} + +fn report(self: *Linter, loc: Ast.Location, rule: rules.Rule, context: []const u8) void { + if (self.isIgnored(loc.line, rule)) return; + + self.diagnostics.append(self.allocator, .{ + .path = self.path, + .line = @intCast(loc.line + 1), + .column = @intCast(loc.column + 1), + .rule = rule, + .context = context, + // ziglint-ignore: Z026 + }) catch {}; +} + +test "Z001: detect PascalCase function" { + var linter: Linter = .init(std.testing.allocator, "fn MyFunc() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z001)); +} + +test "Z001: allow camelCase function" { + var linter: Linter = .init(std.testing.allocator, "fn myFunc() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); +} + +test "Z001: detect snake_case function" { + var linter: Linter = .init(std.testing.allocator, "fn my_func() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z001)); +} + +test "Z001: allow underscore prefix (private)" { + var linter: Linter = .init(std.testing.allocator, "fn _privateFunc() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); +} + +test "Z001: allow single lowercase letter" { + var linter: Linter = .init(std.testing.allocator, "fn f() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); +} + +test "Z002: detect unused variable with value" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const _x = 1; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z002)); +} + +test "Z002: allow plain discard _" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const _ = bar(); }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z002); + } +} + +test "Z002: allow double underscore __" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const __x = 1; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z002); + } +} + +test "Z003: detect parse error" { + var linter: Linter = .init(std.testing.allocator, "const x = ", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expect(linter.diagnostics.items.len > 0); + try std.testing.expectEqual(rules.Rule.Z003, linter.diagnostics.items[0].rule); +} + +test "Z003: valid code no parse error" { + var linter: Linter = .init(std.testing.allocator, "const x: u32 = 42;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z003)); +} + +test "Z004: detect explicit struct init" { + var linter: Linter = .init(std.testing.allocator, "const Foo = struct {}; fn bar() void { const x = Foo{}; _ = x; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z004)); +} + +test "Z004: detect explicit struct init with fields" { + var linter: Linter = .init(std.testing.allocator, "const Foo = struct { x: u32 }; fn bar() void { const f = Foo{ .x = 1 }; _ = f; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z004)); +} + +test "Z004: allow anonymous struct init with type annotation" { + var linter: Linter = .init(std.testing.allocator, "const Foo = struct {}; fn bar() void { const x: Foo = .{}; _ = x; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z004)); +} + +test "Z004: allow anonymous struct init with fields" { + var linter: Linter = .init(std.testing.allocator, "const Foo = struct { x: u32 }; fn bar() void { const f: Foo = .{ .x = 1 }; _ = f; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z004)); +} + +test "Z005: detect lowercase type function" { + var linter: Linter = .init(std.testing.allocator, "fn myType() type { return struct {}; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z005)); +} + +test "Z005: detect snake_case type function" { + var linter: Linter = .init(std.testing.allocator, "fn my_type() type { return struct {}; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z005)); +} + +test "Z005: allow PascalCase type function" { + var linter: Linter = .init(std.testing.allocator, "fn MyType() type { return struct {}; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z005)); +} + +test "Z005: allow PascalCase generic type function" { + var linter: Linter = .init(std.testing.allocator, "fn ArrayList(comptime T: type) type { return struct {}; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z005)); +} + +test "Z006: detect camelCase variable" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const myVar = 1; _ = myVar; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: detect PascalCase variable (not type)" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const MyVar = 1; _ = MyVar; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow snake_case variable" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const my_var = 1; _ = my_var; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow single lowercase letter" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const x = 1; _ = x; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow underscore prefix" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const _unused: u32 = undefined; _ = _unused; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z006); + } +} + +test "Z006: allow type alias with @This()" { + var linter: Linter = .init(std.testing.allocator, "const MyType = @This();", "MyType.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with @import()" { + var linter: Linter = .init(std.testing.allocator, "const Foo = @import(\"foo.zig\");", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z006); + } +} + +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)); +} + +test "Z006: allow type alias with @TypeOf()" { + var linter: Linter = .init(std.testing.allocator, "fn foo(value: anytype) void { const T = @TypeOf(value); _ = T; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with struct" { + var linter: Linter = .init(std.testing.allocator, "const MyStruct = struct { x: u32 };", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with enum" { + var linter: Linter = .init(std.testing.allocator, "const MyEnum = enum { a, b, c };", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with union" { + var linter: Linter = .init(std.testing.allocator, "const MyUnion = union { x: u32, y: f32 };", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow error set type" { + var linter: Linter = .init(std.testing.allocator, "const Oom = error{OutOfMemory};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type function call" { + var linter: Linter = .init(std.testing.allocator, "fn GenericType(comptime T: type) type { return struct {}; } const MyType = GenericType(u32);", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z006); + } +} + +test "Z006: allow type alias with field access ending in PascalCase" { + var linter: Linter = .init(std.testing.allocator, "const std = @import(\"std\"); const Ast = std.zig.Ast;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: detect field access ending in snake_case" { + var linter: Linter = .init(std.testing.allocator, "const std = @import(\"std\"); const Thing = std.some_value;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow PascalCase identifier assignment" { + var linter: Linter = .init(std.testing.allocator, "const MyType = SomeType;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with primitive type" { + var linter: Linter = .init(std.testing.allocator, "const Days = i32; const Nanoseconds = i128; const Float = f64;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow PascalCase labeled block type alias" { + var linter: Linter = .init(std.testing.allocator, + \\const Config = blk: { + \\ 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(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: detect camelCase labeled block (not type)" { + var linter: Linter = .init(std.testing.allocator, + \\const myValue = blk: { + \\ break :blk 42; + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: detect snake_case identifier assignment" { + var linter: Linter = .init(std.testing.allocator, "const MyThing = some_value;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with array type" { + var linter: Linter = .init(std.testing.allocator, "const Buffer = [8192]u8;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with sentinel array type" { + var linter: Linter = .init(std.testing.allocator, "const CString = [*:0]const u8;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with pointer type" { + var linter: Linter = .init(std.testing.allocator, "const BytePtr = *const u8;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with optional type" { + var linter: Linter = .init(std.testing.allocator, "const MaybeInt = ?i32;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with error union type" { + var linter: Linter = .init(std.testing.allocator, "const Result = anyerror!i32;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with function pointer type" { + var linter: Linter = .init(std.testing.allocator, "const Handler = *const fn (*u8) anyerror!void;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow type alias with bare function type" { + var linter: Linter = .init(std.testing.allocator, "const Callback = fn (i32) void;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow PascalCase comptime if type alias" { + var linter: Linter = .init(std.testing.allocator, "const ThreadPool = if (true) u32 else void;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: detect camelCase comptime if (not type)" { + var linter: Linter = .init(std.testing.allocator, "const threadPool = if (true) u32 else void;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow PascalCase switch type alias" { + var linter: Linter = .init(std.testing.allocator, "const MyType = switch (x) { .a => u32, .b => i32 };", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: detect camelCase switch (not type)" { + var linter: Linter = .init(std.testing.allocator, "const myValue = switch (x) { .a => 1, .b => 2 };", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow camelCase function alias" { + var linter: Linter = .init(std.testing.allocator, "fn fooBar() void {} const myAlias = fooBar;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z006: allow camelCase function alias with field access" { + var linter: Linter = .init(std.testing.allocator, "const std = @import(\"std\"); const myAlias = std.someFunc;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "inline ignore: single rule" { + var linter: Linter = .init(std.testing.allocator, "fn foo() void { const myVar = 1; _ = myVar; } // ziglint-ignore: Z006", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "inline ignore: multiple rules" { + var linter: Linter = .init(std.testing.allocator, "fn MyFunc() void { const myVar = 1; _ = myVar; } // ziglint-ignore: Z001 Z006", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "inline ignore: only ignores specified rule" { + var linter: Linter = .init(std.testing.allocator, "fn MyFunc() void {} // ziglint-ignore: Z006", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z001)); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "inline ignore: multiline - only affects that line" { + var linter: Linter = .init(std.testing.allocator, + \\fn MyFunc() void {} // ziglint-ignore: Z001 + \\fn AnotherBad() void {} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z001)); +} + +test "inline ignore: preceding line comment" { + var linter: Linter = .init(std.testing.allocator, + \\// ziglint-ignore: Z001 + \\fn MyFunc() void {} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); +} + +test "inline ignore: preceding line only affects next line" { + var linter: Linter = .init(std.testing.allocator, + \\// ziglint-ignore: Z001 + \\fn MyFunc() void {} + \\fn AnotherBad() void {} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z001)); +} + +test "inline ignore: multiple preceding comment lines" { + var linter: Linter = .init(std.testing.allocator, + \\// ziglint-ignore: Z001 + \\// ziglint-ignore: Z006 + \\fn MyFunc() void { const myVar = 1; _ = myVar; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "inline ignore: multiple preceding with other comments" { + var linter: Linter = .init(std.testing.allocator, + \\// ziglint-ignore: Z001 + \\// This function does something important + \\// ziglint-ignore: Z006 + \\fn MyFunc() void { const myVar = 1; _ = myVar; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z001)); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z006)); +} + +test "Z007: duplicate import" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const std2 = @import("std"); + \\const x = std; + \\const y = std2; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var z007_count: usize = 0; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z007) z007_count += 1; + } + try std.testing.expectEqual(1, z007_count); +} + +test "Z007: different imports allowed" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const foo = @import("foo.zig"); + \\const x = std; + \\const y = foo; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z007); + } +} + +test "Z007: multiple duplicates" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const std2 = @import("std"); + \\const std3 = @import("std"); + \\const x = std; + \\const y = std2; + \\const z = std3; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var z007_count: usize = 0; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z007) z007_count += 1; + } + try std.testing.expectEqual(2, z007_count); +} + +test "Z009: file with top-level fields needs PascalCase name" { + var linter: Linter = .init(std.testing.allocator, "foo: u32 = 0,", "my_module.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z009, linter.diagnostics.items[0].rule); +} + +test "Z009: file with top-level fields and PascalCase name is ok" { + var linter: Linter = .init(std.testing.allocator, "foo: u32 = 0,", "MyModule.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnostics.items.len); +} + +test "Z009: file without top-level fields can be lowercase" { + var linter: Linter = .init(std.testing.allocator, "const x: u32 = 0;", "main.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z009)); +} + +test "Z010: detect explicit struct in return" { + var linter: Linter = .init(std.testing.allocator, "fn foo() Foo { return Foo{}; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z010)); +} + +test "Z010: allow anonymous struct in return" { + var linter: Linter = .init(std.testing.allocator, "fn foo() Foo { return .{}; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z010)); +} + +test "Z010: detect explicit struct in function arg" { + var linter: Linter = .init(std.testing.allocator, "fn bar(x: Foo) void {} fn foo() void { bar(Foo{}); }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z010)); +} + +test "Z010: allow anonymous struct in function arg" { + var linter: Linter = .init(std.testing.allocator, "fn bar(x: Foo) void {} fn foo() void { bar(.{}); }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z010)); +} + +test "Z010: detect explicit enum in return" { + var linter: Linter = .init(std.testing.allocator, "fn foo() Mode { return Mode.fast; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z010)); +} + +test "Z010: allow anonymous enum in return" { + var linter: Linter = .init(std.testing.allocator, "fn foo() Mode { return .fast; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z010)); +} + +test "Z010: allow field access on non-type (self.field)" { + var linter: Linter = .init(std.testing.allocator, "fn foo(self: *Self) u32 { return self.value; }", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z010); + } +} + +test "Z011: detect deprecated method call" { + const source = + \\const MyType = struct { + \\ value: u32 = 0, + \\ /// Deprecated: use newMethod instead + \\ pub fn oldMethod(self: *@This()) void { + \\ _ = self; + \\ } + \\}; + \\const instance: MyType = .{}; + \\pub fn main() void { + \\ instance.oldMethod(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found_z011 = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + found_z011 = true; + break; + } + } + try std.testing.expect(found_z011); +} + +test "Z011: no warning for non-deprecated method" { + const source = + \\const MyType = struct { + \\ value: u32, + \\ /// Does something useful + \\ pub fn goodMethod(self: *@This()) void { + \\ _ = self; + \\ } + \\}; + \\pub fn main() void { + \\ var x: MyType = .{ .value = 0 }; + \\ x.goodMethod(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z011); + } +} + +test "Z011: without semantic context, no Z011 warnings" { + const source = + \\const MyType = struct { + \\ /// Deprecated + \\ pub fn oldMethod(self: *@This()) void { _ = self; } + \\}; + \\pub fn main() void { + \\ var x: MyType = .{}; + \\ x.oldMethod(); + \\} + ; + + var linter: Linter = .init(std.testing.allocator, source, "test.zig", null); + defer linter.deinit(); + linter.lint(); + + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z011); + } +} + +test "Z011: detect deprecated direct function call" { + const source = + \\/// Deprecated: use newFunc instead + \\pub fn oldFunc() void {} + \\pub fn main() void { + \\ oldFunc(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found_z011 = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + found_z011 = true; + break; + } + } + try std.testing.expect(found_z011); +} + +test "Z011: detect deprecated function alias" { + const source = + \\/// Deprecated: use newFunc instead + \\pub fn oldFunc() void {} + \\pub const aliasFunc = oldFunc; + \\pub fn main() void { + \\ aliasFunc(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found_z011 = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + found_z011 = true; + break; + } + } + try std.testing.expect(found_z011); +} + +test "Z011: detect deprecated type function" { + const source = + \\/// Deprecated: use NewList instead + \\pub fn OldList(comptime T: type) type { + \\ return struct { items: []T }; + \\} + \\pub fn main() void { + \\ _ = OldList(u8); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found_z011 = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + found_z011 = true; + break; + } + } + try std.testing.expect(found_z011); +} + +test "Z011: detect deprecated type function alias" { + const source = + \\/// Deprecated: use NewList instead + \\pub fn OldList(comptime T: type) type { + \\ return struct { items: []T }; + \\} + \\pub const DeprecatedAlias = OldList; + \\pub fn main() void { + \\ _ = DeprecatedAlias(u8); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found_z011 = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + found_z011 = true; + break; + } + } + try std.testing.expect(found_z011); +} + +test "Z011: detect deprecated stdlib function (ArrayListUnmanaged)" { + + // Detect zig lib path by running zig env + const io = testIo(); + const zig_lib_path = blk: { + const result = std.process.run(std.testing.allocator, io, .{ + .argv = &.{ "zig", "env" }, + .stderr_limit = .nothing, + }) catch break :blk null; + defer std.testing.allocator.free(result.stdout); + defer std.testing.allocator.free(result.stderr); + + switch (result.term) { + .exited => |code| if (code != 0) break :blk null, + else => break :blk null, + } + + const needle = ".lib_dir = \""; + 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.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; + }; + + // Skip test if zig isn't available + if (zig_lib_path == null) return; + defer std.testing.allocator.free(zig_lib_path.?); + + const source = + \\const std = @import("std"); + \\pub fn main() void { + \\ _ = std.ArrayListUnmanaged(u8); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(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); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found_z011 = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + found_z011 = true; + break; + } + } + try std.testing.expect(found_z011); +} + +test "Z011: deprecated stdlib corpus - real Zig 0.15.2 deprecations" { + + // Detect zig lib path by running zig env + const io = testIo(); + const zig_lib_path = blk: { + const result = std.process.run(std.testing.allocator, io, .{ + .argv = &.{ "zig", "env" }, + .stderr_limit = .nothing, + }) catch break :blk null; + defer std.testing.allocator.free(result.stdout); + defer std.testing.allocator.free(result.stderr); + + switch (result.term) { + .exited => |code| if (code != 0) break :blk null, + else => break :blk null, + } + + const needle = ".lib_dir = \""; + 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.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; + }; + + // Skip test if zig isn't available + if (zig_lib_path == null) return; + defer std.testing.allocator.free(zig_lib_path.?); + + // Test cases: each uses a real deprecated function from Zig 0.15.2 stdlib + const test_cases = [_]struct { + name: []const u8, + source: [:0]const u8, + expected_count: usize, // Minimum number of Z011 warnings expected + skip: bool = false, // Skip test if true (for known issues) + skip_reason: []const u8 = "", + }{ + .{ + .name = "std.mem.copyBackwards", + .source = + \\const std = @import("std"); + \\pub fn main() void { + \\ var dest: [5]u8 = undefined; + \\ const src = [_]u8{ 1, 2, 3, 4, 5 }; + \\ std.mem.copyBackwards(u8, &dest, &src); + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.meta.intToEnum", + .source = + \\const std = @import("std"); + \\const MyEnum = enum { a, b, c }; + \\pub fn main() !void { + \\ _ = try std.meta.intToEnum(MyEnum, 1); + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.meta.TagPayload", + .source = + \\const std = @import("std"); + \\const U = union(enum) { a: u32, b: []const u8 }; + \\pub fn main() void { + \\ const T = std.meta.TagPayload(U, U.a); + \\ _ = T; + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.meta.TagPayloadByName", + .source = + \\const std = @import("std"); + \\const U = union(enum) { a: u32, b: []const u8 }; + \\pub fn main() void { + \\ const T = std.meta.TagPayloadByName(U, "a"); + \\ _ = T; + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.enums.nameCast", + .source = + \\const std = @import("std"); + \\const E = enum { foo, bar }; + \\pub fn main() void { + \\ const e = std.enums.nameCast(E, .foo); + \\ _ = e; + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.unicode.utf8Decode", + .source = + \\const std = @import("std"); + \\pub fn main() !void { + \\ const bytes = "x"; + \\ _ = try std.unicode.utf8Decode(bytes); + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.Io.null_writer", + .source = + \\const std = @import("std"); + \\pub fn main() void { + \\ _ = std.Io.null_writer; + \\} + , + .expected_count = 1, + .skip = true, + .skip_reason = "const value, not a function call - requires different detection", + }, + .{ + .name = "std.ArrayListAligned", + .source = + \\const std = @import("std"); + \\pub fn main() void { + \\ _ = std.ArrayListAligned(u8, 8); + \\} + , + .expected_count = 1, + }, + .{ + .name = "std.fs.File.readToEndAlloc", + .source = + \\const std = @import("std"); + \\pub fn main() !void { + \\ const file = try std.fs.cwd().openFile("test.txt", .{}); + \\ defer file.close(); + \\ _ = try file.readToEndAlloc(std.heap.page_allocator, 1024); + \\} + , + .expected_count = 1, + .skip = true, + .skip_reason = "TypeResolver cannot resolve types of local variables (requires flow analysis)", + }, + .{ + .name = "std.fs.File.deprecatedReader", + .source = + \\const std = @import("std"); + \\pub fn main() !void { + \\ const file = try std.fs.cwd().openFile("test.txt", .{}); + \\ defer file.close(); + \\ _ = file.deprecatedReader(); + \\} + , + .expected_count = 1, + .skip = true, + .skip_reason = "TypeResolver cannot resolve types of local variables (requires flow analysis)", + }, + }; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + for (test_cases) |tc| { + if (tc.skip) { + std.debug.print("Test case '{s}': SKIP ({s})\n", .{ tc.name, tc.skip_reason }); + continue; + } + + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = tc.source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(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); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, tc.source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var z011_count: usize = 0; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z011) { + z011_count += 1; + } + } + + std.debug.print("Test case '{s}': expected >={d}, got {d} ", .{ tc.name, tc.expected_count, z011_count }); + if (z011_count < tc.expected_count) { + std.debug.print("FAIL\n", .{}); + std.debug.print(" All diagnostics:\n", .{}); + for (linter.diagnostics.items) |d| { + std.debug.print(" {s}: line {d}\n", .{ d.rule.code(), d.line }); + } + } else { + std.debug.print("PASS\n", .{}); + } + try std.testing.expect(z011_count >= tc.expected_count); + } +} + +test "containsDeprecated" { + try std.testing.expect(containsDeprecated("Deprecated: use X instead")); + try std.testing.expect(containsDeprecated("deprecated function")); + try std.testing.expect(containsDeprecated("This is DEPRECATED")); + try std.testing.expect(!containsDeprecated("This function is useful")); + try std.testing.expect(!containsDeprecated("deprecat")); // too short +} + +test "Z012: pub fn returning private type" { + var linter: Linter = .init(std.testing.allocator, + \\const Private = struct {}; + \\pub fn getPrivate() Private { return .{}; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z012, linter.diagnostics.items[0].rule); +} + +test "Z012: pub fn accepting private type parameter" { + var linter: Linter = .init(std.testing.allocator, + \\const Private = struct {}; + \\pub fn usePrivate(p: Private) void { _ = p; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z012, linter.diagnostics.items[0].rule); +} + +test "Z012: pub fn returning optional private type" { + var linter: Linter = .init(std.testing.allocator, + \\const Private = struct {}; + \\pub fn maybePrivate() ?Private { return null; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z012, linter.diagnostics.items[0].rule); +} + +test "Z012: pub fn returning error union with private type" { + var linter: Linter = .init(std.testing.allocator, + \\const Private = struct {}; + \\pub fn getPrivateOrError() !Private { + \\ if (false) return error.Fail; + \\ return .{}; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z012, linter.diagnostics.items[0].rule); +} + +test "Z015: pub fn returning private error set" { + var linter: Linter = .init(std.testing.allocator, + \\const Oom = error{OutOfMemory}; + \\pub fn doThing() Oom!void { + \\ return error.OutOfMemory; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z015, linter.diagnostics.items[0].rule); +} + +test "Z012: pub fn returning public type is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub const Public = struct {}; + \\pub fn getPublic() Public { return .{}; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn using pub type from enclosing struct is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ pub const Inner = struct {}; + \\ pub fn a(i: Inner) void { _ = i; } + \\ pub fn getInner() Inner { return .{}; } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn using non-pub type from enclosing struct is error" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ const Inner = struct {}; + \\ pub fn a(i: Inner) void { _ = i; } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z012, linter.diagnostics.items[0].rule); +} + +test "Z012: pub fn returning builtin type is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn getValue() u32 { return 42; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: non-pub fn returning private type is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const Private = struct {}; + \\fn getPrivate() Private { return .{}; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: generic parameter with comptime T: type is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn genericFn(comptime T: type) T { return undefined; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public pointer type alias is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub const Queue = *anyopaque; + \\pub fn getMain() Queue { return undefined; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public comptime block type is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub const Key = key: { break :key u32; }; + \\pub fn getKey() Key { return 0; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public switch type alias is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub const MyType = switch (true) { true => u32, false => i32 }; + \\pub fn getValue() MyType { return 0; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public generic type instantiation is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\pub const MyList = std.ArrayList(u32); + \\pub fn getList() MyList { return undefined; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public field access type alias is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\pub const Allocator = std.mem.Allocator; + \\pub fn getAllocator() Allocator { return undefined; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public error set is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub const MyError = error{ OutOfMemory, InvalidInput }; + \\pub fn toInt(err: MyError) u32 { _ = err; return 0; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z012: pub fn returning public if expression type is ok" { + var linter: Linter = .init(std.testing.allocator, + \\pub const MyType = if (true) u32 else i32; + \\pub fn getValue() MyType { return 0; } + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z012); + } +} + +test "Z013: detect unused import" { + var linter: Linter = .init(std.testing.allocator, + \\const foo = @import("foo.zig"); + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z013, linter.diagnostics.items[0].rule); +} + +test "Z013: import used via field access is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const mem = std.mem; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z013); + } +} + +test "Z013: discarded import at root should warn" { + var linter: Linter = .init(std.testing.allocator, + \\const _ = @import("foo.zig"); + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnostics.items.len); + try std.testing.expectEqual(rules.Rule.Z013, linter.diagnostics.items[0].rule); +} + +test "Z013: discarded import in test block is ok" { + var linter: Linter = .init(std.testing.allocator, + \\test { + \\ _ = @import("foo.zig"); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z013); + } +} + +test "Z013: pub re-export is not unused" { + var linter: Linter = .init(std.testing.allocator, + \\pub const foo = @import("foo.zig"); + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z013); + } +} + +test "Z013: import used as identifier is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const foo = @import("foo.zig"); + \\const bar = foo; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z013); + } +} + +test "Z014: detect snake_case error set" { + var linter: Linter = .init(std.testing.allocator, "const my_error = error{OutOfMemory};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z014)); +} + +test "Z014: allow PascalCase error set" { + var linter: Linter = .init(std.testing.allocator, "const Oom = error{OutOfMemory};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z014); + } +} + +test "Z016: detect compound assert with and" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn foo() void { + \\ std.debug.assert(a and b); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z016) { + found = true; + try std.testing.expectEqualStrings("and", d.context); + } + } + try std.testing.expect(found); +} + +test "Z016: assert with or is ok (different semantics)" { + var linter: Linter = .init(std.testing.allocator, + \\const assert = @import("std").debug.assert; + \\fn foo() void { + \\ assert(x or y); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z016); + } +} + +test "Z016: simple assert is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn foo() void { + \\ std.debug.assert(a); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z016); + } +} + +test "Z019: @This() in named struct should warn" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ const Self = @This(); + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z019) { + found = true; + try std.testing.expectEqualStrings("Foo", d.context); + } + } + try std.testing.expect(found); +} + +test "Z019: @This() in anonymous struct is ok" { + var linter: Linter = .init(std.testing.allocator, + \\fn Generic(comptime T: type) type { + \\ _ = T; + \\ return struct { + \\ const Self = @This(); + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z019); + } +} + +test "Z019: nested named struct should warn" { + var linter: Linter = .init(std.testing.allocator, + \\const Outer = struct { + \\ const Inner = struct { + \\ const Self = @This(); + \\ }; + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z019) { + found = true; + try std.testing.expectEqualStrings("Inner", d.context); + } + } + try std.testing.expect(found); +} + +test "Z019: @This() in local struct (inside fn) is ok" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ const Local = struct { + \\ const Self = @This(); + \\ }; + \\ _ = Local; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z019); + } +} + +test "Z019: @This() in local struct (inside test) is ok" { + var linter: Linter = .init(std.testing.allocator, + \\test { + \\ const Local = struct { + \\ const Self = @This(); + \\ }; + \\ _ = Local; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z019); + } +} + +test "Z019: @This() in local struct inside nested if blocks is ok" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ if (true) { + \\ if (true) { + \\ if (true) { + \\ const Local = struct { + \\ const Self = @This(); + \\ }; + \\ _ = Local; + \\ } + \\ } + \\ } + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z019); + } +} + +test "Z020: inline @This() should warn" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ fn method() @This() { return undefined; } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z020) found = true; + } + try std.testing.expect(found); +} + +test "Z020: @This() as function argument is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\test { + \\ std.testing.refAllDecls(@This()); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z020); + } +} + +test "Z021: file-struct @This() alias should match filename or Self" { + var linter: Linter = .init(std.testing.allocator, + \\const SelfType = @This(); + \\value: u32, + , "Writer.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z021) found = true; + } + try std.testing.expect(found); +} + +test "Z021: file-struct @This() alias matching filename is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const Writer = @This(); + \\value: u32, + , "Writer.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z021); + } +} + +test "Z021: file-struct @This() alias Self is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const Self = @This(); + \\value: u32, + , "Writer.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z021); + } +} + +test "Z022: anonymous struct @This() alias should be Self" { + var linter: Linter = .init(std.testing.allocator, + \\fn Generic(comptime T: type) type { + \\ _ = T; + \\ return struct { + \\ const This = @This(); + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z022) { + found = true; + try std.testing.expectEqualStrings("This", d.context); + } + } + try std.testing.expect(found); +} + +test "Z022: anonymous struct @This() alias Self is ok" { + var linter: Linter = .init(std.testing.allocator, + \\fn Generic(comptime T: type) type { + \\ _ = T; + \\ return struct { + \\ const Self = @This(); + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z022); + } +} + +test "Z022: local struct @This() alias should be Self" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ const Local = struct { + \\ const This = @This(); + \\ }; + \\ _ = Local; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z022) found = true; + } + try std.testing.expect(found); +} + +test "Z023: argument order - allocator before type param" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn bad(allocator: std.mem.Allocator, comptime T: type) void { + \\ _ = .{ allocator, T }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z023: argument order - io before allocator" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn bad(io: std.Io, allocator: std.mem.Allocator) void { + \\ _ = .{ io, allocator }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z023: argument order - correct order is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn good(comptime T: type, allocator: std.mem.Allocator, io: std.Io, value: u32) void { + \\ _ = .{ T, allocator, io, value }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z023); + } +} + +test "Z023: argument order - aliased Allocator" { + const source = + \\const std = @import("std"); + \\const Alloc = std.mem.Allocator; + \\fn bad(value: u32, alloc: Alloc) void { + \\ _ = .{ value, alloc }; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + linter.lint(); + + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z023: argument order - aliased Io" { + const source = + \\const std = @import("std"); + \\const MyIo = std.Io; + \\fn bad(value: u32, io: MyIo) void { + \\ _ = .{ value, io }; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + linter.lint(); + + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z023: receiver param with @This() is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const Foo = struct { + \\ pub fn bar(self: *@This(), alloc: std.mem.Allocator) void { + \\ _ = .{ self, alloc }; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z023); + } +} + +test "Z023: receiver param with Self is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const Foo = struct { + \\ const Self = @This(); + \\ pub fn bar(self: *Self, alloc: std.mem.Allocator) void { + \\ _ = .{ self, alloc }; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z023); + } +} + +test "Z023: receiver param with struct name is ok (semantic)" { + const source = + \\const std = @import("std"); + \\const Foo = struct { + \\ pub fn bar(self: *Foo, alloc: std.mem.Allocator) void { + \\ _ = .{ self, alloc }; + \\ } + \\}; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + linter.lint(); + + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z023); + } +} + +test "Z023: file-as-struct receiver is ok (semantic)" { + const source = + \\const std = @import("std"); + \\const Terminal = @This(); + \\pub fn deinit(self: *Terminal, alloc: std.mem.Allocator) void { + \\ _ = .{ self, alloc }; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "Terminal.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "Terminal.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + linter.lint(); + + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z023); + } +} + +test "Z023: non-receiver first param still checked" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn bad(value: u32, alloc: std.mem.Allocator) void { + \\ _ = .{ value, alloc }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z023: multiple violations reported" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn bad(io: std.Io, comptime T: type, alloc: std.mem.Allocator) void { + \\ _ = .{ io, T, alloc }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var count: usize = 0; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) count += 1; + } + try std.testing.expect(count >= 2); +} + +test "Z023: comptime value after allocator is ok" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn good(alloc: std.mem.Allocator, comptime size: usize) void { + \\ _ = .{ alloc, size }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + try std.testing.expect(d.rule != rules.Rule.Z023); + } +} + +test "Z023: comptime value before allocator is bad" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\fn bad(comptime size: usize, alloc: std.mem.Allocator) void { + \\ _ = .{ size, alloc }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z023: comptime value before other is bad" { + var linter: Linter = .init(std.testing.allocator, + \\fn bad(value: u32, comptime size: usize) void { + \\ _ = .{ value, size }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z023) found = true; + } + try std.testing.expect(found); +} + +test "Z024: detect line exceeding 120 characters" { + // Line with 121 characters (11 + 108 + 2) + var linter: Linter = .init(std.testing.allocator, "const x = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\";\n", "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z024) found = true; + } + try std.testing.expect(found); +} + +test "Z024: allow line with exactly 120 characters" { + // Line with exactly 120 characters (11 + 107 + 2) + var linter: Linter = .init(std.testing.allocator, "const x = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\";\n", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z024) { + // Should not find Z024 for exactly 120 characters + try std.testing.expect(false); + } + } +} + +test "Z024: allow short line" { + var linter: Linter = .init(std.testing.allocator, "const x = 1;\n", "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z024) { + try std.testing.expect(false); + } + } +} + +test "Z025: detect catch return" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ return bar() catch |err| return err; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z025) found = true; + } + try std.testing.expect(found); +} + +test "Z025: allow catch with different body" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ return bar() catch |err| { + \\ log.err("{}", .{err}); + \\ return err; + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z025) { + try std.testing.expect(false); + } + } +} + +test "Z025: allow catch without payload" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ return bar() catch return; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z025) { + try std.testing.expect(false); + } + } +} + +test "Z025: allow catch returning different value" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ return bar() catch |err| return error.Other; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z025) { + try std.testing.expect(false); + } + } +} + +test "Z025: detect catch return in assignment context" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ const x = bar() catch |err| return err; + \\ return x; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z025)); +} + +test "Z025: allow catch with discard payload" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ return bar() catch |_| return error.Other; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z025)); +} + +test "Z025: no false positive on clean code" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() !u32 { + \\ return try bar(); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z025)); +} + +test "Z026: detect empty catch block" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ bar() catch {}; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z026) found = true; + } + try std.testing.expect(found); +} + +test "Z026: detect empty catch with payload" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ bar() catch |_| {}; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z026) found = true; + } + try std.testing.expect(found); +} + +test "Z026: allow catch in defer" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ defer writer.flush() catch {}; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z026) { + try std.testing.expect(false); + } + } +} + +test "Z026: allow catch in errdefer" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ errdefer writer.flush() catch {}; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z026) { + try std.testing.expect(false); + } + } +} + +test "Z026: allow non-empty catch" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ bar() catch |err| { + \\ log.err("{}", .{err}); + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z026) { + try std.testing.expect(false); + } + } +} + +test "Z026: allow catch with @panic" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ bar() catch |err| { + \\ @panic("unexpected"); + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z026)); +} + +test "Z026: allow catch with unreachable" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ bar() catch |err| { + \\ unreachable; + \\ }; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z026)); +} + +test "Z026: no false positive on clean code" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ try bar(); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z026)); +} + +test "Z026: detect multiple empty catches" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo() void { + \\ bar() catch {}; + \\ baz() catch {}; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(2, linter.diagnosticCount(.Z026)); +} + +test "Z027: flag instance accessing const" { + const source = + \\const Foo = struct { + \\ field: u32, + \\ const bar = 42; + \\}; + \\const instance: Foo = .{ .field = 0 }; + \\pub fn main() void { + \\ _ = instance.bar; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + found = true; + break; + } + } + try std.testing.expect(found); +} + +test "Z027: flag instance accessing static fn" { + const source = + \\const Foo = struct { + \\ field: u32, + \\ fn staticFn() void {} + \\}; + \\const instance: Foo = .{ .field = 0 }; + \\pub fn main() void { + \\ instance.staticFn(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + var found = false; + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + found = true; + break; + } + } + try std.testing.expect(found); +} + +test "Z027: allow instance method call" { + const source = + \\const Foo = struct { + \\ field: u32, + \\ fn getField(self: *@This()) u32 { + \\ return self.field; + \\ } + \\}; + \\const instance: Foo = .{ .field = 0 }; + \\pub fn main() void { + \\ _ = instance.getField(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + try std.testing.expect(false); + } + } +} + +test "Z027: allow field access" { + const source = + \\const Foo = struct { + \\ field: u32, + \\}; + \\const instance: Foo = .{ .field = 0 }; + \\pub fn main() void { + \\ _ = instance.field; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + try std.testing.expect(false); + } + } +} + +test "Z027: allow type-level access" { + const source = + \\const Foo = struct { + \\ field: u32, + \\ const bar = 42; + \\}; + \\pub fn main() void { + \\ _ = Foo.bar; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + try std.testing.expect(false); + } + } +} + +test "Z027: no warning without semantic context" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ const bar = 42; + \\}; + \\const instance: Foo = .{}; + \\pub fn main() void { + \\ _ = instance.bar; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + try std.testing.expect(false); + } + } +} + +test "Z027: allow method with Self receiver" { + const source = + \\const Foo = struct { + \\ const Self = @This(); + \\ field: u32, + \\ fn getField(self: *Self) u32 { + \\ return self.field; + \\ } + \\}; + \\const instance: Foo = .{ .field = 0 }; + \\pub fn main() void { + \\ _ = instance.getField(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + try std.testing.expect(false); + } + } +} + +test "Z027: allow method with named type receiver" { + const source = + \\const Foo = struct { + \\ field: u32, + \\ fn getField(self: *Foo) u32 { + \\ return self.field; + \\ } + \\}; + \\const instance: Foo = .{ .field = 0 }; + \\pub fn main() void { + \\ _ = instance.getField(); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + for (linter.diagnostics.items) |d| { + if (d.rule == rules.Rule.Z027) { + try std.testing.expect(false); + } + } +} + +test "Z029: detect redundant @as in call arg" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo(x: u32) void { + \\ _ = x; + \\} + \\pub fn main() void { + \\ foo(@as(u32, 1)); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z029)); +} + +test "Z029: allow @as with different type in call arg" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo(x: u32) void { + \\ _ = x; + \\} + \\pub fn main() void { + \\ foo(@as(u16, 1)); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect redundant @as in array init" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const xs = [_]u32{@as(u32, 1)}; + \\ _ = xs; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z029)); +} + +test "Z029: allow @as with different type in array init" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const xs = [_]u32{@as(u16, 1)}; + \\ _ = xs; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect redundant @as in method call arg" { + const source = + \\const MyType = struct { + \\ value: u32, + \\ pub fn setValue(self: *@This(), v: u32) void { + \\ self.value = v; + \\ } + \\}; + \\const instance: MyType = .{ .value = 0 }; + \\pub fn main() void { + \\ instance.setValue(@as(u32, 42)); + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + var linter: Linter = .initWithSemantics(std.testing.allocator, source, path, &resolver, path, null); + defer linter.deinit(); + + linter.lint(); + + try std.testing.expectEqual(1, linter.diagnosticCount(.Z029)); +} + +test "Z029: multiple args with redundant @as" { + var linter: Linter = .init(std.testing.allocator, + \\fn bar(a: u32, b: u32) void { + \\ _ = a; + \\ _ = b; + \\} + \\pub fn main() void { + \\ bar(@as(u32, 1), @as(u32, 2)); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(2, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect redundant @as in struct field init" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ x: u32, + \\ y: u32, + \\}; + \\pub fn main() void { + \\ const f: Foo = .{ .x = @as(u32, 1), .y = 2 }; + \\ _ = f; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z029)); +} + +test "Z029: allow @as with different type in struct field init" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ x: u32, + \\}; + \\pub fn main() void { + \\ const f: Foo = .{ .x = @as(u16, 1) }; + \\ _ = f; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect redundant @as in explicit struct init" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ x: u32, + \\}; + \\pub fn main() void { + \\ const f = Foo{ .x = @as(u32, 1) }; + \\ _ = f; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z029)); +} + +test "Z029: no false positive on clean call args" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo(x: u32) void { + \\ _ = x; + \\} + \\pub fn main() void { + \\ foo(42); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: skip unresolvable function call" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ unknown_fn(@as(u32, 1)); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: skip call with anytype param" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo(x: anytype) void { + \\ _ = x; + \\} + \\pub fn main() void { + \\ foo(@as(u32, 1)); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect multiple redundant @as in array init" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const xs = [_]u32{ @as(u32, 1), @as(u32, 2), 3 }; + \\ _ = xs; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(2, linter.diagnosticCount(.Z029)); +} + +test "Z029: skip array init without type annotation" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const xs = .{ @as(u32, 1), @as(u32, 2) }; + \\ _ = xs; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect in sized array init" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const xs = [2]u32{ @as(u32, 1), @as(u32, 2) }; + \\ _ = xs; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(2, linter.diagnosticCount(.Z029)); +} + +test "Z029: skip struct init without known type" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const f = .{ .x = @as(u32, 1) }; + \\ _ = f; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: detect multiple redundant @as in struct fields" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ x: u32, + \\ y: u32, + \\}; + \\pub fn main() void { + \\ const f: Foo = .{ .x = @as(u32, 1), .y = @as(u32, 2) }; + \\ _ = f; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(2, linter.diagnosticCount(.Z029)); +} + +test "Z029: skip struct field with unknown type in container" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const f: UnknownType = .{ .x = @as(u32, 1) }; + \\ _ = f; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z029)); +} + +test "Z029: mixed match and mismatch in call args" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo(a: u32, b: u16) void { + \\ _ = a; + \\ _ = b; + \\} + \\pub fn main() void { + \\ foo(@as(u32, 1), @as(u32, 2)); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z029)); +} + +test "Z028: inline import in switch" { + var linter: Linter = .init(std.testing.allocator, + \\const builtin = @import("builtin"); + \\const Backend = switch (builtin.os.tag) { + \\ .linux => @import("linux.zig"), + \\ .macos => @import("macos.zig"), + \\ else => @compileError("unsupported"), + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(2, linter.diagnosticCount(.Z028)); +} + +test "Z028: inline import in function call" { + var linter: Linter = .init(std.testing.allocator, + \\fn foo(x: anytype) void { + \\ _ = x; + \\} + \\pub fn main() void { + \\ foo(@import("bar.zig").baz); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z028)); +} + +test "Z028: allow top-level const import" { + var linter: Linter = .init(std.testing.allocator, + \\const std = @import("std"); + \\const other = @import("other.zig"); + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z028)); +} + +test "Z028: disallow import inside function" { + var linter: Linter = .init(std.testing.allocator, + \\pub fn main() void { + \\ const std = @import("std"); + \\ _ = std; + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z028)); +} + +test "Z028: allow discard import for pulling in tests" { + var linter: Linter = .init(std.testing.allocator, + \\test { + \\ _ = @import("other.zig"); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z028)); +} + +test "Z028: allow const import in test block" { + var linter: Linter = .init(std.testing.allocator, + \\test "example" { + \\ const testing = @import("testing.zig"); + \\ testing.check(); + \\} + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z028)); +} + +test "Z028: allow field access on import" { + var linter: Linter = .init(std.testing.allocator, + \\const Rule = @import("rules.zig").Rule; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z028)); +} + +test "Z030: detect missing self.* = undefined in deinit" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(self: *Foo) void { + \\ _ = self; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z030)); +} + +test "Z030: allow deinit with self.* = undefined at end" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(self: *Foo) void { + \\ self.a = 0; + \\ self.* = undefined; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z030)); +} + +test "Z030: allow deinit with defer self.* = undefined" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(self: *Foo) void { + \\ defer self.* = undefined; + \\ cleanup(); + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z030)); +} + +test "Z030: detect early return without defer" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(self: *Foo) void { + \\ if (self.a == 0) return; + \\ self.* = undefined; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z030)); +} + +test "Z030: allow early return with defer" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(self: *Foo) void { + \\ defer self.* = undefined; + \\ if (self.a == 0) return; + \\ cleanup(); + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z030)); +} + +test "Z030: skip non-pointer receiver deinit" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(self: Foo) void { + \\ _ = self; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z030)); +} + +test "Z030: skip non-deinit functions" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn close(self: *Foo) void { + \\ _ = self; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z030)); +} + +test "Z030: handle different self parameter names" { + var linter: Linter = .init(std.testing.allocator, + \\const Foo = struct { + \\ a: u32, + \\ fn deinit(s: *Foo) void { + \\ s.* = undefined; + \\ } + \\}; + , "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z030)); +} + +test "Z031: detect underscore prefix in function" { + var linter: Linter = .init(std.testing.allocator, "fn _privateFunc() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z031)); +} + +test "Z031: detect underscore prefix in variable" { + var linter: Linter = .init(std.testing.allocator, "const _privateVar: u32 = undefined;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z031)); +} + +test "Z031: allow single underscore discard" { + var linter: Linter = .init(std.testing.allocator, "const _ = 42;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z031)); +} + +test "Z031: allow double underscore prefix" { + var linter: Linter = .init(std.testing.allocator, "const __builtin = 42;", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z031)); +} + +test "Z031: allow normal names" { + var linter: Linter = .init(std.testing.allocator, "fn myFunc() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z031)); +} + +test "Z032: detect XMLParser" { + var linter: Linter = .init(std.testing.allocator, "const XMLParser = struct {};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z032)); +} + +test "Z032: detect HTTPSConnection" { + var linter: Linter = .init(std.testing.allocator, "const HTTPSConnection = struct {};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z032)); +} + +test "Z032: detect function with acronym" { + var linter: Linter = .init(std.testing.allocator, "fn parseXML() void {}", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z032)); +} + +test "Z032: allow XmlParser" { + var linter: Linter = .init(std.testing.allocator, "const XmlParser = struct {};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z032)); +} + +test "Z032: allow two-letter at start" { + var linter: Linter = .init(std.testing.allocator, "const IoError = struct {};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z032)); +} + +test "Z032: detect IOError" { + var linter: Linter = .init(std.testing.allocator, "const IOError = struct {};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z032)); +} + +test "Z033: disabled by default" { + var linter: Linter = .init(std.testing.allocator, "const DataManager = struct {};", "test.zig", null); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z033)); +} + +test "Z033: detect Data when enabled" { + const config: Config = .{ .rules = .{ .Z033 = .{ .enabled = true } } }; + var linter: Linter = .init(std.testing.allocator, "const UserData = struct {};", "test.zig", &config); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z033)); +} + +test "Z033: detect Manager when enabled" { + const config: Config = .{ .rules = .{ .Z033 = .{ .enabled = true } } }; + var linter: Linter = .init(std.testing.allocator, "const StateManager = struct {};", "test.zig", &config); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z033)); +} + +test "Z033: detect Context when enabled" { + const config: Config = .{ .rules = .{ .Z033 = .{ .enabled = true } } }; + var linter: Linter = .init(std.testing.allocator, "const AppContext = struct {};", "test.zig", &config); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z033)); +} + +test "Z033: detect utils when enabled" { + const config: Config = .{ .rules = .{ .Z033 = .{ .enabled = true } } }; + var linter: Linter = .init(std.testing.allocator, "fn stringUtils() void {}", "test.zig", &config); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(1, linter.diagnosticCount(.Z033)); +} + +test "Z033: allow normal names when enabled" { + const config: Config = .{ .rules = .{ .Z033 = .{ .enabled = true } } }; + var linter: Linter = .init(std.testing.allocator, "const Parser = struct {};", "test.zig", &config); + defer linter.deinit(); + linter.lint(); + try std.testing.expectEqual(0, linter.diagnosticCount(.Z033)); +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/ModuleGraph.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/ModuleGraph.zig new file mode 100644 index 0000000..8763bbb --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/ModuleGraph.zig @@ -0,0 +1,391 @@ +//! Module graph for semantic analysis. +//! +//! Parses @import calls from AST and recursively resolves all reachable modules, +//! building a graph that maps import paths to parsed ASTs and ZIR. + +const std = @import("std"); +const Ast = std.zig.Ast; +const Zir = std.zig.Zir; +const AstGen = std.zig.AstGen; + +const ModuleGraph = @This(); + +allocator: std.mem.Allocator, +io: std.Io, +zig_lib_path: ?[]const u8, +root_dir: []const u8, +modules: std.StringHashMapUnmanaged(Module), + +pub const Module = struct { + /// 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(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 = .{ + .allocator = allocator, + .io = io, + .zig_lib_path = zig_lib_path, + .root_dir = root_dir, + .modules = .empty, + }; + + try graph.addModule(root_source); + return graph; +} + +pub fn deinit(self: *ModuleGraph) void { + var iter = self.modules.valueIterator(); + while (iter.next()) |mod| { + if (mod.zir) |*zir| zir.deinit(self.allocator); + mod.tree.deinit(self.allocator); + self.allocator.free(mod.source); + self.allocator.free(mod.path); + } + self.modules.deinit(self.allocator); + self.* = undefined; +} + +pub fn addModulePublic(self: *ModuleGraph, path: []const u8) void { + // ziglint-ignore: Z026 + self.addModule(path) catch {}; +} + +fn addModule(self: *ModuleGraph, path: []const u8) !void { + // `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); + return; + } + + const source = std.Io.Dir.cwd().readFileAllocOptions( + self.io, + canonical, + self.allocator, + .limited(1024 * 1024 * 16), + .@"1", + 0, + ) catch |err| { + std.log.warn("cannot read '{s}': {}", .{ canonical, err }); + self.allocator.free(canonical); + return; + }; + + const tree = Ast.parse(self.allocator, source, .zig) catch { + self.allocator.free(source); + self.allocator.free(canonical); + return; + }; + + // ZIR generation is disabled for performance. It was added in commit 14bbe87 (Jan 2026) + // to support future control flow analysis (see issue #18), but no rules currently use it. + // Disabling it provides a ~5x speedup in module graph construction: + // - With ZIR: ~1945ms for build.zig (308 modules) + // - Without ZIR: ~377ms for build.zig (308 modules) + // If control flow analysis is implemented, ZIR can be generated lazily on-demand or + // re-enabled here by uncommenting the code below: + // + // const zir: ?Zir = if (tree.errors.len == 0) + // AstGen.generate(self.allocator, tree) catch |err| blk: { + // std.log.warn("ZIR generation failed for '{s}': {}", .{ canonical, err }); + // break :blk null; + // } + // else + // null; + const zir: ?Zir = null; + + try self.modules.put(self.allocator, canonical, .{ + .path = canonical, + .source = source, + .tree = tree, + .zir = zir, + }); + + // Recursively add imported modules + const imports = try self.extractImports(&tree, canonical); + defer self.allocator.free(imports); + + for (imports) |import_path| { + defer self.allocator.free(import_path); + self.addModule(import_path) catch continue; + } +} + +fn extractImports(self: *ModuleGraph, tree: *const Ast, module_path: []const u8) ![][]const u8 { + var imports: std.ArrayList([]const u8) = .empty; + errdefer { + for (imports.items) |p| self.allocator.free(p); + imports.deinit(self.allocator); + } + + for (tree.rootDecls()) |node| { + try self.collectImportsFromNode(tree, node, module_path, &imports); + } + + return imports.toOwnedSlice(self.allocator); +} + +fn collectImportsFromNode( + self: *ModuleGraph, + tree: *const Ast, + node: Ast.Node.Index, + module_path: []const u8, + imports: *std.ArrayList([]const u8), +) !void { + const tag = tree.nodeTag(node); + + switch (tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(node) orelse return; + const init_node = var_decl.ast.init_node.unwrap() orelse return; + try self.checkForImport(tree, init_node, module_path, imports); + }, + .fn_decl => { + const data = tree.nodeData(node).node_and_node; + try self.collectImportsFromNode(tree, data[0], module_path, imports); + try self.collectImportsFromNode(tree, data[1], module_path, imports); + }, + .block, .block_semicolon => { + var buf: [2]Ast.Node.Index = undefined; + const stmts = tree.blockStatements(&buf, node) orelse return; + for (stmts) |stmt| try self.collectImportsFromNode(tree, stmt, module_path, imports); + }, + .block_two, .block_two_semicolon => { + const data = tree.nodeData(node).opt_node_and_opt_node; + if (data[0].unwrap()) |n| try self.collectImportsFromNode(tree, n, module_path, imports); + if (data[1].unwrap()) |n| try self.collectImportsFromNode(tree, n, module_path, imports); + }, + else => {}, + } +} + +fn checkForImport( + self: *ModuleGraph, + tree: *const Ast, + node: Ast.Node.Index, + module_path: []const u8, + imports: *std.ArrayList([]const u8), +) !void { + const tag = tree.nodeTag(node); + if (tag != .builtin_call_two and tag != .builtin_call_two_comma and + tag != .builtin_call and tag != .builtin_call_comma) + { + return; + } + + const main_token = tree.nodeMainToken(node); + const builtin_name = tree.tokenSlice(main_token); + if (!std.mem.eql(u8, builtin_name, "@import")) return; + + var buf: [2]Ast.Node.Index = undefined; + const params = tree.builtinCallParams(&buf, node) orelse return; + if (params.len == 0) return; + + const arg_token = tree.nodeMainToken(params[0]); + const raw_path = tree.tokenSlice(arg_token); + + // Strip quotes from string literal + if (raw_path.len < 2 or raw_path[0] != '"') return; + const import_str = raw_path[1 .. raw_path.len - 1]; + + const resolved = try self.resolveImportPath(import_str, module_path); + if (resolved) |path| { + try imports.append(self.allocator, path); + } +} + +fn resolveImportPath(self: *ModuleGraph, import_str: []const u8, module_path: []const u8) !?[]const u8 { + // Handle "std" import - add it to the graph but don't recurse into its imports + if (std.mem.eql(u8, import_str, "std")) { + const lib_path = self.zig_lib_path orelse return null; + // ziglint-ignore: Z017 (false positive: join returns ![]u8 but function returns !?[]const u8) + return try std.fs.path.join(self.allocator, &.{ lib_path, "std", "std.zig" }); + } + + // Handle builtin (skip it) + if (std.mem.eql(u8, import_str, "builtin")) { + return null; + } + + // Skip imports from within the Zig standard library to avoid recursively loading 300+ modules. + // When linting user code, we typically don't need to parse all of std (e.g., build.zig + // would trigger loading 308 modules, taking ~900ms for AST parsing alone). + // This optimization loads only the root std.zig (2 modules total instead of 308). + const lib_path = self.zig_lib_path orelse ""; + if (lib_path.len > 0 and std.mem.startsWith(u8, module_path, lib_path)) { + return null; + } + + // Handle relative .zig imports + if (std.mem.endsWith(u8, import_str, ".zig")) { + const module_dir = std.fs.path.dirname(module_path) orelse "."; + // ziglint-ignore: Z017 (false positive: join returns ![]u8 but function returns !?[]const u8) + return try std.fs.path.join(self.allocator, &.{ module_dir, import_str }); + } + + return null; +} + +pub fn moduleCount(self: *const ModuleGraph) usize { + return self.modules.count(); +} + +pub fn getModule(self: *const ModuleGraph, path: []const u8) ?*const Module { + // Fast path: most callers pass paths that came from this graph (already canonical). + // This also avoids a Zig 0.16 stdlib SIMD bug in containsAtLeastScalar2 that can + // segfault on certain short paths inside realPathFileAlloc. + if (self.modules.getPtr(path)) |mod| return mod; + + // Fallback: canonicalize via realpath for uncanonical inputs. + const canonical = std.Io.Dir.cwd().realPathFileAlloc(self.io, path, self.allocator) catch return null; + defer self.allocator.free(canonical); + return self.modules.getPtr(canonical); +} + +pub fn zirCount(self: *const ModuleGraph) usize { + var count: usize = 0; + var iter = self.modules.valueIterator(); + while (iter.next()) |mod| { + if (mod.zir != null) count += 1; + } + return count; +} + +/// Test-only Io provider. Tests are synchronous and don't need cancelation, +/// so we use the global single-threaded Io instance. +fn testIo() std.Io { + return std.Io.Threaded.global_single_threaded.io(); +} + +test "parse simple module" { + const source = + \\const std = @import("std"); + \\pub fn main() void {} + ; + + // Write temp file + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "main.zig", .data = source }); + + const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + try std.testing.expectEqual(1, graph.moduleCount()); +} + +test "resolve relative import" { + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "main.zig", .data = "const utils = @import(\"utils.zig\");" }); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "utils.zig", .data = "pub fn helper() void {}" }); + + const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + try std.testing.expectEqual(2, graph.moduleCount()); +} + +test "handle missing import gracefully" { + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "main.zig", .data = "const missing = @import(\"nonexistent.zig\");" }); + + const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + try std.testing.expectEqual(1, graph.moduleCount()); +} + +test "no duplicate modules" { + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "main.zig", .data = + \\const a = @import("shared.zig"); + \\const b = @import("other.zig"); + }); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "other.zig", .data = "const shared = @import(\"shared.zig\");" }); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "shared.zig", .data = "pub const x = 1;" }); + + const path = try tmp_dir.dir.realPathFileAlloc(io, "main.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + // main.zig, other.zig, shared.zig - no duplicates + try std.testing.expectEqual(3, graph.moduleCount()); +} + +// NOTE: These tests are disabled because ZIR generation is disabled for performance. +// See issue #18 and commit 14bbe87. Re-enable these tests if ZIR is needed in the future. +// test "generate ZIR for valid modules" { +// var tmp_dir = std.testing.tmpDir(.{}); +// defer tmp_dir.cleanup(); + +// try tmp_dir.dir.writeFile(.{ .sub_path = "main.zig", .data = "pub fn main() void {}" }); + +// const path = try tmp_dir.dir.realpathAlloc(std.testing.allocator, "main.zig"); +// defer std.testing.allocator.free(path); + +// var graph = try ModuleGraph.init(std.testing.allocator, path, null); +// defer graph.deinit(); + +// try std.testing.expectEqual(1, graph.moduleCount()); +// try std.testing.expectEqual(1, graph.zirCount()); + +// const mod = graph.getModule(path); +// try std.testing.expect(mod != null); +// try std.testing.expect(mod.?.zir != null); +// } + +// test "skip ZIR for modules with parse errors" { +// var tmp_dir = std.testing.tmpDir(.{}); +// defer tmp_dir.cleanup(); + +// try tmp_dir.dir.writeFile(.{ .sub_path = "main.zig", .data = "fn broken( {}" }); + +// const path = try tmp_dir.dir.realpathAlloc(std.testing.allocator, "main.zig"); +// defer std.testing.allocator.free(path); + +// var graph = try ModuleGraph.init(std.testing.allocator, path, null); +// defer graph.deinit(); + +// try std.testing.expectEqual(1, graph.moduleCount()); +// try std.testing.expectEqual(0, graph.zirCount()); +// } diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/TypeResolver.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/TypeResolver.zig new file mode 100644 index 0000000..31d5a80 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/TypeResolver.zig @@ -0,0 +1,2062 @@ +//! Type resolver for semantic analysis. +//! +//! Resolves expression types using ZIR data combined with AST analysis. +//! Tracks variable declarations, follows field access chains, and resolves +//! function return types. + +const std = @import("std"); +const Ast = std.zig.Ast; +const Zir = std.zig.Zir; +const ModuleGraph = @import("ModuleGraph.zig"); + +const TypeResolver = @This(); + +/// Test-only Io provider. Tests are synchronous and don't need cancelation, +/// so we use the global single-threaded Io instance. +fn testIo() std.Io { + return std.Io.Threaded.global_single_threaded.io(); +} + +allocator: std.mem.Allocator, +graph: *ModuleGraph, + +pub const TypeInfo = union(enum) { + /// A primitive type like u32, bool, void + primitive: Primitive, + /// A type from the standard library (std.fs.File, etc.) + std_type: StdType, + /// A user-defined type from the current module or imports + user_type: UserType, + /// The special 'type' type (for type aliases and comptime type values) + type_type, + /// A function type + function: FunctionType, + /// A pointer to another type + pointer: PointerType, + /// An optional type + optional: OptionalType, + /// An error union type + error_union: ErrorUnionType, + /// An error set type (error{A, B, C}) + error_set, + /// A slice type + slice: SliceType, + /// An array type + array: ArrayType, + /// Type could not be resolved + unknown, + + pub const Primitive = enum { + void, + bool, + u8, + u16, + u32, + u64, + u128, + usize, + i8, + i16, + i32, + i64, + i128, + isize, + f16, + f32, + f64, + f128, + comptime_int, + comptime_float, + noreturn, + anyopaque, + }; + + pub const StdType = struct { + path: []const u8, + }; + + pub const UserType = struct { + module_path: []const u8, + name: []const u8, + }; + + pub const FunctionType = struct { + return_type: ?*const TypeInfo, + }; + + pub const PointerType = struct { + child: ?*const TypeInfo, + is_const: bool, + }; + + pub const OptionalType = struct { + child: ?*const TypeInfo, + }; + + pub const ErrorUnionType = struct { + payload: ?*const TypeInfo, + }; + + pub const SliceType = struct { + child: ?*const TypeInfo, + }; + + pub const ArrayType = struct { + child: ?*const TypeInfo, + len: ?usize, + }; + + pub fn format(self: TypeInfo, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self) { + .primitive => |p| try writer.print("{s}", .{@tagName(p)}), + .std_type => |s| try writer.print("std.{s}", .{s.path}), + .user_type => |u| try writer.print("{s}", .{u.name}), + .type_type => try writer.writeAll("type"), + .function => try writer.writeAll("fn"), + .pointer => |p| { + if (p.is_const) try writer.writeAll("*const ") else try writer.writeAll("*"); + if (p.child) |c| try c.format("", .{}, writer) else try writer.writeAll("?"); + }, + .optional => |o| { + try writer.writeAll("?"); + if (o.child) |c| try c.format("", .{}, writer) else try writer.writeAll("?"); + }, + .error_union => |e| { + try writer.writeAll("!"); + if (e.payload) |p| try p.format("", .{}, writer) else try writer.writeAll("?"); + }, + .slice => |s| { + try writer.writeAll("[]"); + if (s.child) |c| try c.format("", .{}, writer) else try writer.writeAll("?"); + }, + .array => |a| { + if (a.len) |l| try writer.print("[{}]", .{l}) else try writer.writeAll("[?]"); + if (a.child) |c| try c.format("", .{}, writer) else try writer.writeAll("?"); + }, + .unknown => try writer.writeAll("unknown"), + } + } + + pub fn eql(self: TypeInfo, other: TypeInfo) bool { + const self_tag = std.meta.activeTag(self); + const other_tag = std.meta.activeTag(other); + if (self_tag != other_tag) return false; + + return switch (self) { + .primitive => |p| p == other.primitive, + .std_type => |s| std.mem.eql(u8, s.path, other.std_type.path), + .user_type => |u| std.mem.eql(u8, u.name, other.user_type.name) and + std.mem.eql(u8, u.module_path, other.user_type.module_path), + .type_type => true, + .function => true, + .pointer, .optional, .error_union, .slice, .array => true, + .unknown => true, + }; + } +}; + +pub fn init(allocator: std.mem.Allocator, graph: *ModuleGraph) TypeResolver { + return .{ + .allocator = allocator, + .graph = graph, + }; +} + +pub const MethodDef = struct { + module_path: []const u8, + node: Ast.Node.Index, + name_token: Ast.TokenIndex, + line: u32, + column: u32, +}; + +pub fn deinit(self: *TypeResolver) void { + self.* = undefined; +} + +/// Resolves the type of an AST node within a module. +pub fn typeOf(self: *TypeResolver, module_path: []const u8, node: Ast.Node.Index) TypeInfo { + const mod = self.graph.getModule(module_path) orelse return .unknown; + return self.resolveNodeType(&mod.tree, node, module_path); +} + +/// Finds a method definition given a receiver type and method name. +/// For user_type, looks up the method in the type's module. +/// For std_type, resolves the path through stdlib imports to find the type. +pub fn findMethodDef(self: *TypeResolver, receiver_type: TypeInfo, method_name: []const u8) ?MethodDef { + switch (receiver_type) { + .user_type => |u| { + return self.findMethodInModule(u.module_path, u.name, method_name); + }, + .std_type => |s| { + return self.findMethodInStdlib(s.path, method_name); + }, + else => return null, + } +} + +/// Finds a function definition by name in the given module. +/// Handles both direct function declarations and const aliases to functions. +pub fn findFnInCurrentModule(self: *TypeResolver, module_path: []const u8, fn_name: []const u8) ?MethodDef { + return self.findMethodInFileAsStruct(module_path, fn_name); +} + +/// Finds the declaration node for a name in stdlib without following aliases. +/// Used for deprecation checking where the alias itself may be deprecated. +pub fn findStdlibDecl(self: *TypeResolver, path: []const u8, name: []const u8) ?MethodDef { + const lib_path = self.graph.zig_lib_path orelse return null; + + const std_path = std.fs.path.join(self.allocator, &.{ lib_path, "std", "std.zig" }) catch return null; + defer self.allocator.free(std_path); + + // For empty path, look directly in std.zig + if (path.len == 0) { + self.graph.addModulePublic(std_path); + const mod = self.graph.getModule(std_path) orelse return null; + return self.findDeclNodeByName(&mod.tree, name, mod.path); + } + + // For non-empty paths, follow the path but don't follow the final alias + var components = std.mem.splitScalar(u8, path, '.'); + var current_module_path: []const u8 = std_path; + var owns_path = false; + defer if (owns_path) self.allocator.free(current_module_path); + + while (components.next()) |component| { + self.graph.addModulePublic(current_module_path); + const mod = self.graph.getModule(current_module_path) orelse return null; + const tree = &mod.tree; + + if (self.findDeclInModule(tree, component, current_module_path)) |result| { + switch (result) { + .import_path => |imported| { + if (owns_path) self.allocator.free(current_module_path); + current_module_path = imported; + owns_path = true; + }, + .type_node => break, + } + } else { + return null; + } + } + + self.graph.addModulePublic(current_module_path); + const mod = self.graph.getModule(current_module_path) orelse return null; + return self.findDeclNodeByName(&mod.tree, name, mod.path); +} + +/// Finds a declaration node by name without following aliases. +fn findDeclNodeByName(self: *TypeResolver, tree: *const Ast, name: []const u8, module_path: []const u8) ?MethodDef { + _ = self; + for (tree.rootDecls()) |decl| { + const tag = tree.nodeTag(decl); + if (tag == .fn_decl) { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, decl) orelse continue; + const fn_name_token = fn_proto.name_token orelse continue; + if (std.mem.eql(u8, tree.tokenSlice(fn_name_token), name)) { + const loc = tree.tokenLocation(0, fn_name_token); + return .{ + .module_path = module_path, + .node = decl, + .name_token = fn_name_token, + .line = @intCast(loc.line), + .column = @intCast(loc.column), + }; + } + } else if (tag == .simple_var_decl or tag == .aligned_var_decl or + tag == .local_var_decl or tag == .global_var_decl) + { + const var_decl = tree.fullVarDecl(decl) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + if (std.mem.eql(u8, tree.tokenSlice(name_token), name)) { + const loc = tree.tokenLocation(0, name_token); + return .{ + .module_path = module_path, + .node = decl, + .name_token = name_token, + .line = @intCast(loc.line), + .column = @intCast(loc.column), + }; + } + } + } + return null; +} + +/// Returns true if the given node evaluates to a type rather than an instance. +/// Distinguishes `const Foo = struct{...}` (type) from `const obj: Foo = .{}` (instance). +pub fn isTypeRef(self: *TypeResolver, module_path: []const u8, node: Ast.Node.Index) bool { + const mod = self.graph.getModule(module_path) orelse return false; + return self.nodeIsTypeRef(&mod.tree, node, module_path); +} + +/// Returns true if field_name is a container-level declaration (const, non-method fn) +/// in the given type, rather than a struct field. +pub fn isContainerLevelDecl(self: *TypeResolver, receiver_type: TypeInfo, field_name: []const u8) bool { + switch (receiver_type) { + .user_type => |u| { + const mod = self.graph.getModule(u.module_path) orelse return false; + return self.isDeclInContainer(&mod.tree, u.name, field_name); + }, + else => return false, + } +} + +fn isDeclInContainer(self: *TypeResolver, tree: *const Ast, type_name: []const u8, decl_name: []const u8) bool { + const type_node = self.findTypeDecl(tree, type_name) orelse return false; + if (!isContainerDecl(tree.nodeTag(type_node))) return false; + + var buf: [2]Ast.Node.Index = undefined; + const container = tree.fullContainerDecl(&buf, type_node) orelse return false; + + for (container.ast.members) |member| { + switch (tree.nodeTag(member)) { + .fn_decl => { + var fn_buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&fn_buf, member) orelse continue; + const fn_name_token = fn_proto.name_token orelse continue; + if (!std.mem.eql(u8, tree.tokenSlice(fn_name_token), decl_name)) continue; + return !fnHasReceiver(tree, fn_proto, type_name); + }, + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(member) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + if (std.mem.eql(u8, tree.tokenSlice(name_token), decl_name)) return true; + }, + else => {}, + } + } + return false; +} + +fn fnHasReceiver(tree: *const Ast, fn_proto: Ast.full.FnProto, container_name: []const u8) bool { + var it = fn_proto.iterate(tree); + const first_param = it.next() orelse return false; + const type_node = first_param.type_expr orelse return false; + return isReceiverType(tree, type_node, container_name); +} + +fn isReceiverType(tree: *const Ast, node: Ast.Node.Index, container_name: []const u8) bool { + return switch (tree.nodeTag(node)) { + .builtin_call_two, .builtin_call_two_comma => std.mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(node)), "@This"), + .ptr_type_aligned, .ptr_type_sentinel, .ptr_type, .ptr_type_bit_range => blk: { + const ptr_type = tree.fullPtrType(node) orelse break :blk false; + break :blk isReceiverType(tree, ptr_type.ast.child_type, container_name); + }, + .identifier => blk: { + const name = tree.tokenSlice(tree.nodeMainToken(node)); + break :blk std.mem.eql(u8, name, "Self") or std.mem.eql(u8, name, container_name); + }, + else => false, + }; +} + +/// Resolves a stdlib path like "fs.File" to find the method. +/// Follows the import chain: std.zig -> fs.zig -> File type -> method +fn findMethodInStdlib(self: *TypeResolver, path: []const u8, method_name: []const u8) ?MethodDef { + const lib_path = self.graph.zig_lib_path orelse return null; + + // Start at std.zig (lib_dir/std/std.zig) + const std_path = std.fs.path.join(self.allocator, &.{ lib_path, "std", "std.zig" }) catch return null; + defer self.allocator.free(std_path); + + // For empty path (direct std.X access), look in std.zig directly + if (path.len == 0) { + return self.findMethodInFileAsStruct(std_path, method_name); + } + + // Split path into components (e.g., "fs.File" -> ["fs", "File"]) + var components = std.mem.splitScalar(u8, path, '.'); + var current_module_path: []const u8 = std_path; + var owns_path = false; + defer if (owns_path) self.allocator.free(current_module_path); + + var type_name: ?[]const u8 = null; + + while (components.next()) |component| { + // Lazy-load stdlib modules on demand + self.graph.addModulePublic(current_module_path); + const mod = self.graph.getModule(current_module_path) orelse return null; + const tree = &mod.tree; + + // Look for this component as a declaration + if (self.findDeclInModule(tree, component, current_module_path)) |result| { + switch (result) { + .import_path => |imported| { + // It's an import, follow it + if (owns_path) self.allocator.free(current_module_path); + current_module_path = imported; + owns_path = true; + }, + .type_node => { + // It's a type declaration, this should be the last component + type_name = component; + break; + }, + } + } else { + return null; + } + } + + // If we have a type name, look for the method in that type + if (type_name) |tn| { + return self.findMethodInModule(current_module_path, tn, method_name); + } + + // No explicit type - the module itself might be a file-as-struct (like fs/File.zig) + return self.findMethodInFileAsStruct(current_module_path, method_name); +} + +const DeclResult = union(enum) { + import_path: []const u8, + type_node: Ast.Node.Index, +}; + +/// Finds a declaration in a module and returns either an import path or type node. +fn findDeclInModule(self: *TypeResolver, tree: *const Ast, name: []const u8, module_path: []const u8) ?DeclResult { + for (tree.rootDecls()) |decl_node| { + const decl_tag = tree.nodeTag(decl_node); + switch (decl_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const decl_name = tree.tokenSlice(name_token); + if (!std.mem.eql(u8, decl_name, name)) continue; + + const init_node = var_decl.ast.init_node.unwrap() orelse continue; + const init_tag = tree.nodeTag(init_node); + + // Check if it's an @import + if (init_tag == .builtin_call_two or init_tag == .builtin_call_two_comma) { + const main_token = tree.nodeMainToken(init_node); + const builtin_name = tree.tokenSlice(main_token); + if (std.mem.eql(u8, builtin_name, "@import")) { + var buf: [2]Ast.Node.Index = undefined; + const params = tree.builtinCallParams(&buf, init_node) orelse continue; + if (params.len == 0) continue; + + const arg_token = tree.nodeMainToken(params[0]); + const raw_path = tree.tokenSlice(arg_token); + if (raw_path.len < 2 or raw_path[0] != '"') continue; + + const import_str = raw_path[1 .. raw_path.len - 1]; + 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; + }; + self.allocator.free(resolved); + const canonical = self.allocator.dupe(u8, canonical_z) catch { + self.allocator.free(canonical_z); + continue; + }; + self.allocator.free(canonical_z); + return .{ .import_path = canonical }; + } + } + } + + // It's a type declaration + if (isContainerDecl(init_tag)) { + return .{ .type_node = init_node }; + } + }, + else => {}, + } + } + return null; +} + +fn findMethodInModule(self: *TypeResolver, module_path: []const u8, type_name: []const u8, method_name: []const u8) ?MethodDef { + self.graph.addModulePublic(module_path); + const mod = self.graph.getModule(module_path) orelse return null; + const tree = &mod.tree; + + const type_node = self.findTypeDecl(tree, type_name) orelse return null; + + // Use `mod.path` (owned by the graph for the lifetime of the resolver) + // rather than the input `module_path`, which may be a temporary slice + // freed by callers like `findMethodInStdlib` after they return. + 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; + + for (tree.rootDecls()) |decl| { + const tag = tree.nodeTag(decl); + if (tag == .fn_decl) { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, decl) orelse continue; + const fn_name_token = fn_proto.name_token orelse continue; + const fn_name = tree.tokenSlice(fn_name_token); + if (std.mem.eql(u8, fn_name, method_name)) { + const loc = tree.tokenLocation(0, fn_name_token); + return .{ + .module_path = mod.path, + .node = decl, + .name_token = fn_name_token, + .line = @intCast(loc.line), + .column = @intCast(loc.column), + }; + } + } else if (tag == .simple_var_decl or tag == .aligned_var_decl or + tag == .local_var_decl or tag == .global_var_decl) + { + // Handle const declarations (may be aliases to functions) + const var_decl = tree.fullVarDecl(decl) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const decl_name = tree.tokenSlice(name_token); + if (!std.mem.eql(u8, decl_name, method_name)) continue; + + const init_node = var_decl.ast.init_node.unwrap() orelse continue; + const init_tag = tree.nodeTag(init_node); + + if (init_tag == .identifier) { + // 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.findMethodInFileAsStructDepth(module_path, alias_target, depth + 1); + } else if (init_tag == .fn_decl) { + // Inline function definition + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, init_node) orelse continue; + const fn_name_token = fn_proto.name_token orelse name_token; + const loc = tree.tokenLocation(0, fn_name_token); + return .{ + .module_path = mod.path, + .node = init_node, + .name_token = fn_name_token, + .line = @intCast(loc.line), + .column = @intCast(loc.column), + }; + } + } + } + return null; +} + +fn findTypeDecl(self: *TypeResolver, tree: *const Ast, type_name: []const u8) ?Ast.Node.Index { + _ = self; + + for (tree.rootDecls()) |decl_node| { + const decl_tag = tree.nodeTag(decl_node); + switch (decl_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const decl_name = tree.tokenSlice(name_token); + if (std.mem.eql(u8, decl_name, type_name)) { + const init_node = var_decl.ast.init_node.unwrap() orelse continue; + return init_node; + } + }, + else => {}, + } + } + return null; +} + +fn findMethodInType(self: *TypeResolver, tree: *const Ast, type_node: Ast.Node.Index, method_name: []const u8, module_path: []const u8) ?MethodDef { + _ = self; + + const tag = tree.nodeTag(type_node); + + var members_buf: [2]Ast.Node.Index = undefined; + const members: []const Ast.Node.Index = switch (tag) { + .container_decl, .container_decl_trailing => blk: { + const data = tree.nodeData(type_node).extra_range; + const start: usize = @intFromEnum(data.start); + const end: usize = @intFromEnum(data.end); + break :blk @ptrCast(tree.extra_data[start..end]); + }, + .container_decl_two, .container_decl_two_trailing => blk: { + const data = tree.nodeData(type_node).opt_node_and_opt_node; + var len: usize = 0; + if (data[0].unwrap()) |n| { + members_buf[len] = n; + len += 1; + } + if (data[1].unwrap()) |n| { + members_buf[len] = n; + len += 1; + } + break :blk members_buf[0..len]; + }, + else => return null, + }; + + for (members) |member| { + const member_tag = tree.nodeTag(member); + if (member_tag == .fn_decl) { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, member) orelse continue; + const fn_name_token = fn_proto.name_token orelse continue; + const fn_name = tree.tokenSlice(fn_name_token); + if (std.mem.eql(u8, fn_name, method_name)) { + const loc = tree.tokenLocation(0, fn_name_token); + return .{ + .module_path = module_path, + .node = member, + .name_token = fn_name_token, + .line = @intCast(loc.line), + .column = @intCast(loc.column), + }; + } + } + } + + return null; +} + +fn resolveNodeType(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) TypeInfo { + const tag = tree.nodeTag(node); + + return switch (tag) { + .identifier => self.resolveIdentifier(tree, node, module_path), + .field_access => self.resolveFieldAccess(tree, node, module_path), + .builtin_call_two, .builtin_call_two_comma => self.resolveBuiltinCall(tree, node, module_path), + .call_one, .call_one_comma, .call, .call_comma => self.resolveFunctionCall(tree, node, module_path), + .number_literal => self.resolveNumberLiteral(tree, node), + .string_literal, .multiline_string_literal => .{ .slice = .{ .child = &.{ .primitive = .u8 } } }, + .char_literal => .{ .primitive = .u8 }, + .unreachable_literal => .{ .primitive = .noreturn }, + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => self.resolveVarDecl(tree, node, module_path), + .fn_decl => self.resolveFnDecl(tree, node, module_path), + .optional_type => .{ .optional = .{ .child = null } }, + .ptr_type_aligned, .ptr_type_sentinel, .ptr_type, .ptr_type_bit_range => self.resolvePtrType(tree, node), + .error_union => .{ .error_union = .{ .payload = null } }, + .error_set_decl => .error_set, + .array_type, .array_type_sentinel => .{ .array = .{ .child = null, .len = null } }, + else => .unknown, + }; +} + +fn resolveIdentifier(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) TypeInfo { + const main_token = tree.nodeMainToken(node); + const name = tree.tokenSlice(main_token); + + if (resolvePrimitiveType(name)) |prim| { + return .{ .primitive = prim }; + } + + if (std.mem.eql(u8, name, "type")) { + return .type_type; + } + + if (std.mem.eql(u8, name, "true") or std.mem.eql(u8, name, "false")) { + return .{ .primitive = .bool }; + } + + if (std.mem.eql(u8, name, "null")) { + return .unknown; + } + + if (std.mem.eql(u8, name, "undefined")) { + return .unknown; + } + + if (self.findDeclarationInModule(tree, name, module_path)) |decl_type| { + return decl_type; + } + + return .unknown; +} + +fn resolvePrimitiveType(name: []const u8) ?TypeInfo.Primitive { + const primitives = std.StaticStringMap(TypeInfo.Primitive).initComptime(.{ + .{ "void", .void }, + .{ "bool", .bool }, + .{ "u8", .u8 }, + .{ "u16", .u16 }, + .{ "u32", .u32 }, + .{ "u64", .u64 }, + .{ "u128", .u128 }, + .{ "usize", .usize }, + .{ "i8", .i8 }, + .{ "i16", .i16 }, + .{ "i32", .i32 }, + .{ "i64", .i64 }, + .{ "i128", .i128 }, + .{ "isize", .isize }, + .{ "f16", .f16 }, + .{ "f32", .f32 }, + .{ "f64", .f64 }, + .{ "f128", .f128 }, + .{ "comptime_int", .comptime_int }, + .{ "comptime_float", .comptime_float }, + .{ "noreturn", .noreturn }, + .{ "anyopaque", .anyopaque }, + }); + return primitives.get(name); +} + +fn findDeclarationInModule(self: *TypeResolver, tree: *const Ast, name: []const u8, module_path: []const u8) ?TypeInfo { + for (tree.rootDecls()) |decl_node| { + const decl_tag = tree.nodeTag(decl_node); + switch (decl_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const decl_name = tree.tokenSlice(name_token); + if (std.mem.eql(u8, decl_name, name)) { + return self.resolveVarDeclWithName(tree, decl_node, module_path, decl_name); + } + }, + .fn_decl => { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, decl_node) orelse continue; + const fn_name_token = fn_proto.name_token orelse continue; + const fn_name = tree.tokenSlice(fn_name_token); + if (std.mem.eql(u8, fn_name, name)) { + return self.resolveFnDecl(tree, decl_node, module_path); + } + }, + else => {}, + } + } + return null; +} + +fn resolveVarDecl(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) TypeInfo { + const var_decl = tree.fullVarDecl(node) orelse return .unknown; + const name_token = var_decl.ast.mut_token + 1; + const name = tree.tokenSlice(name_token); + return self.resolveVarDeclWithName(tree, node, module_path, name); +} + +fn resolveVarDeclWithName(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8, decl_name: []const u8) TypeInfo { + const var_decl = tree.fullVarDecl(node) orelse return .unknown; + + if (var_decl.ast.type_node.unwrap()) |type_node| { + return self.resolveNodeType(tree, type_node, module_path); + } + + if (var_decl.ast.init_node.unwrap()) |init_node| { + const init_tag = tree.nodeTag(init_node); + if (isContainerDecl(init_tag)) { + return .{ .user_type = .{ .module_path = module_path, .name = decl_name } }; + } + // Check for @This() - treat as a user type alias for the current module + if (init_tag == .builtin_call_two or init_tag == .builtin_call_two_comma) { + const main_token = tree.nodeMainToken(init_node); + const builtin_name = tree.tokenSlice(main_token); + if (std.mem.eql(u8, builtin_name, "@This")) { + return .{ .user_type = .{ .module_path = module_path, .name = decl_name } }; + } + } + return self.resolveNodeType(tree, init_node, module_path); + } + + return .unknown; +} + +fn isContainerDecl(tag: Ast.Node.Tag) bool { + return switch (tag) { + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => true, + else => false, + }; +} + +fn resolveFnDecl(_: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, _: []const u8) TypeInfo { + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, node) orelse return .unknown; + + if (fn_proto.ast.return_type.unwrap()) |ret_type| { + const ret_tag = tree.nodeTag(ret_type); + if (ret_tag == .identifier) { + const ret_name = tree.tokenSlice(tree.nodeMainToken(ret_type)); + if (std.mem.eql(u8, ret_name, "type")) { + return .type_type; + } + } + } + + return .{ .function = .{ .return_type = null } }; +} + +fn resolveFieldAccess(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) TypeInfo { + const data = tree.nodeData(node).node_and_token; + const lhs_node = data[0]; + const field_token = data[1]; + const field_name = tree.tokenSlice(field_token); + + const lhs_type = self.resolveNodeType(tree, lhs_node, module_path); + + switch (lhs_type) { + .std_type => |s| { + // Build the full path: "fs" + "." + "File" = "fs.File" + if (s.path.len == 0) { + return .{ .std_type = .{ .path = field_name } }; + } + // Concatenate paths - we store these as slices into source, so we + // need to build a path string. For now, return the accumulated path + // by looking at the full field access chain. + const full_path = self.buildStdTypePath(tree, node); + return .{ .std_type = .{ .path = full_path } }; + }, + .user_type => { + // 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); + if (lhs_tag == .identifier) { + const lhs_name = tree.tokenSlice(tree.nodeMainToken(lhs_node)); + if (std.mem.eql(u8, lhs_name, "std") or self.isStdImportAlias(tree, lhs_name)) { + return .{ .std_type = .{ .path = field_name } }; + } + } + }, + else => {}, + } + + return .unknown; +} + +/// Builds the full path for a std type by walking up the field access chain. +/// For `std.fs.File`, returns "fs.File". +fn buildStdTypePath(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index) []const u8 { + var parts: [16][]const u8 = undefined; + var count: usize = 0; + + var current = node; + while (count < 16) { + const tag = tree.nodeTag(current); + if (tag != .field_access) break; + + const data = tree.nodeData(current).node_and_token; + const field_token = data[1]; + parts[count] = tree.tokenSlice(field_token); + count += 1; + current = data[0]; + } + + if (count == 0) return ""; + + // Check if root resolves to std (either named "std" or aliased from @import("std")) + const root_tag = tree.nodeTag(current); + if (root_tag == .identifier) { + const root_name = tree.tokenSlice(tree.nodeMainToken(current)); + if (!std.mem.eql(u8, root_name, "std") and !self.isStdImportAlias(tree, root_name)) { + return ""; + } + } + + // Reverse and join - parts are in reverse order + // For simplicity, just return the last part which is the type name + // The full path is used for resolution + if (count >= 1) { + // Build path from parts in reverse order, skipping the type name at index 0 + // e.g., for std.fs.File: parts = ["File", "fs"], we want "fs.File" + // For now, return the joined path from the source directly + // Since we can't easily allocate, we'll use a different approach: + // Return the full path by computing byte range in source + const first_token = tree.nodeMainToken(node); + const last_data = tree.nodeData(node).node_and_token; + _ = last_data; + + // Find the start of the path after the std alias + var start_node = node; + while (tree.nodeTag(start_node) == .field_access) { + const d = tree.nodeData(start_node).node_and_token; + const lhs = d[0]; + if (tree.nodeTag(lhs) == .identifier) { + const lhs_name = tree.tokenSlice(tree.nodeMainToken(lhs)); + if (std.mem.eql(u8, lhs_name, "std") or self.isStdImportAlias(tree, lhs_name)) { + break; + } + } + start_node = lhs; + } + + // Get the token after the std alias + if (tree.nodeTag(start_node) == .field_access) { + const start_data = tree.nodeData(start_node).node_and_token; + const start_field_token = start_data[1]; + const end_field_token = first_token; + _ = end_field_token; + + // Get byte positions + const start_loc = tree.tokenLocation(0, start_field_token); + const end_token_data = tree.nodeData(node).node_and_token; + const end_loc = tree.tokenLocation(0, end_token_data[1]); + const end_slice = tree.tokenSlice(end_token_data[1]); + + const start_byte = start_loc.line_start + start_loc.column; + const end_byte = end_loc.line_start + end_loc.column + end_slice.len; + + if (end_byte > start_byte and end_byte <= tree.source.len) { + return tree.source[start_byte..end_byte]; + } + } + } + + return parts[0]; +} + +/// Checks if an identifier is bound to @import("std"). +fn isStdImportAlias(self: *TypeResolver, tree: *const Ast, name: []const u8) bool { + _ = self; + for (tree.rootDecls()) |decl_node| { + const decl_tag = tree.nodeTag(decl_node); + switch (decl_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + const decl_name = tree.tokenSlice(name_token); + if (!std.mem.eql(u8, decl_name, name)) continue; + + const init_node = var_decl.ast.init_node.unwrap() orelse continue; + const init_tag = tree.nodeTag(init_node); + + if (init_tag == .builtin_call_two or init_tag == .builtin_call_two_comma) { + const main_token = tree.nodeMainToken(init_node); + const builtin_name = tree.tokenSlice(main_token); + if (std.mem.eql(u8, builtin_name, "@import")) { + var buf: [2]Ast.Node.Index = undefined; + const params = tree.builtinCallParams(&buf, init_node) orelse continue; + if (params.len == 0) continue; + + const arg_token = tree.nodeMainToken(params[0]); + const raw_path = tree.tokenSlice(arg_token); + if (raw_path.len >= 5 and std.mem.eql(u8, raw_path, "\"std\"")) { + return true; + } + } + } + }, + else => {}, + } + } + return false; +} + +fn resolveBuiltinCall(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) TypeInfo { + const main_token = tree.nodeMainToken(node); + const builtin_name = tree.tokenSlice(main_token); + + if (std.mem.eql(u8, builtin_name, "@import")) { + var buf: [2]Ast.Node.Index = undefined; + const params = tree.builtinCallParams(&buf, node) orelse return .unknown; + if (params.len == 0) return .unknown; + + const arg_token = tree.nodeMainToken(params[0]); + const raw_path = tree.tokenSlice(arg_token); + if (raw_path.len < 2 or raw_path[0] != '"') return .unknown; + + const import_str = raw_path[1 .. raw_path.len - 1]; + + if (std.mem.eql(u8, import_str, "std")) { + return .{ .std_type = .{ .path = "" } }; + } + + if (std.mem.eql(u8, import_str, "builtin")) { + return .{ .std_type = .{ .path = "builtin" } }; + } + + if (std.mem.endsWith(u8, import_str, ".zig")) { + return .{ .user_type = .{ + .module_path = module_path, + .name = import_str, + } }; + } + } + + if (std.mem.eql(u8, builtin_name, "@This")) { + return .type_type; + } + + if (std.mem.eql(u8, builtin_name, "@TypeOf") or std.mem.eql(u8, builtin_name, "@typeInfo")) { + return .type_type; + } + + if (std.mem.eql(u8, builtin_name, "@as")) { + var buf: [2]Ast.Node.Index = undefined; + const params = tree.builtinCallParams(&buf, node) orelse return .unknown; + if (params.len > 0) { + return self.resolveNodeType(tree, params[0], module_path); + } + } + + return .unknown; +} + +fn resolveFunctionCall(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) TypeInfo { + var buf: [1]Ast.Node.Index = undefined; + const call = tree.fullCall(&buf, node) orelse return .unknown; + const fn_expr = call.ast.fn_expr; + + const fn_expr_tag = tree.nodeTag(fn_expr); + switch (fn_expr_tag) { + .identifier => { + const fn_name = tree.tokenSlice(tree.nodeMainToken(fn_expr)); + return self.resolveCallByName(tree, fn_name, module_path); + }, + .field_access => { + return self.resolveMethodCall(tree, fn_expr, module_path); + }, + else => {}, + } + + return .unknown; +} + +fn resolveCallByName(self: *TypeResolver, tree: *const Ast, fn_name: []const u8, module_path: []const u8) TypeInfo { + for (tree.rootDecls()) |decl_node| { + if (tree.nodeTag(decl_node) != .fn_decl) continue; + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&buf, decl_node) orelse continue; + const name_token = fn_proto.name_token orelse continue; + if (!std.mem.eql(u8, tree.tokenSlice(name_token), fn_name)) continue; + return self.resolveReturnType(tree, fn_proto, module_path); + } + return .unknown; +} + +fn resolveMethodCall(self: *TypeResolver, tree: *const Ast, fn_expr: Ast.Node.Index, module_path: []const u8) TypeInfo { + const data = tree.nodeData(fn_expr).node_and_token; + const lhs_node = data[0]; + const method_name = tree.tokenSlice(data[1]); + const lhs_type = self.resolveNodeType(tree, lhs_node, module_path); + + switch (lhs_type) { + .user_type => |u| { + const mod = self.graph.getModule(u.module_path) orelse return .unknown; + const mod_tree = &mod.tree; + const fn_node = self.findFnInType(mod_tree, u.name, method_name) orelse + return .unknown; + var buf: [1]Ast.Node.Index = undefined; + const fn_proto = mod_tree.fullFnProto(&buf, fn_node) orelse return .unknown; + return self.resolveReturnType(mod_tree, fn_proto, u.module_path); + }, + else => {}, + } + + return .unknown; +} + +fn findFnInType(self: *TypeResolver, tree: *const Ast, type_name: []const u8, fn_name: []const u8) ?Ast.Node.Index { + _ = self; + for (tree.rootDecls()) |decl_node| { + const decl_tag = tree.nodeTag(decl_node); + switch (decl_tag) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + if (!std.mem.eql(u8, tree.tokenSlice(name_token), type_name)) continue; + const init_node = var_decl.ast.init_node.unwrap() orelse continue; + if (!isContainerDecl(tree.nodeTag(init_node))) continue; + + var members_buf: [2]Ast.Node.Index = undefined; + const container = tree.fullContainerDecl(&members_buf, init_node) orelse continue; + for (container.ast.members) |member| { + if (tree.nodeTag(member) != .fn_decl) continue; + var fn_buf: [1]Ast.Node.Index = undefined; + const fn_proto = tree.fullFnProto(&fn_buf, member) orelse continue; + const fn_name_token = fn_proto.name_token orelse continue; + if (std.mem.eql(u8, tree.tokenSlice(fn_name_token), fn_name)) { + return member; + } + } + }, + else => {}, + } + } + return null; +} + +fn resolveReturnType(self: *TypeResolver, tree: *const Ast, fn_proto: std.zig.Ast.full.FnProto, module_path: []const u8) TypeInfo { + const ret_node = fn_proto.ast.return_type.unwrap() orelse return .unknown; + return self.resolveNodeType(tree, ret_node, module_path); +} + +fn resolvePtrType(_: *TypeResolver, tree: *const Ast, node: Ast.Node.Index) TypeInfo { + const main_token = tree.nodeMainToken(node); + const token_tag = tree.tokenTag(main_token); + + if (token_tag == .l_bracket) { + return .{ .slice = .{ .child = null } }; + } + + return .{ .pointer = .{ .child = null, .is_const = false } }; +} + +fn resolveNumberLiteral(_: *TypeResolver, tree: *const Ast, node: Ast.Node.Index) TypeInfo { + const main_token = tree.nodeMainToken(node); + const text = tree.tokenSlice(main_token); + + 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 }; + } + + return .{ .primitive = .comptime_int }; +} + +fn nodeIsTypeRef(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) bool { + return switch (tree.nodeTag(node)) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => self.varDeclIsTypeRef(tree, node, module_path), + .identifier => self.identifierIsTypeRef(tree, node, module_path), + .field_access => self.fieldAccessIsTypeRef(tree, node, module_path), + .builtin_call_two, .builtin_call_two_comma => self.builtinCallIsTypeRef(tree, node), + .container_decl, + .container_decl_trailing, + .container_decl_two, + .container_decl_two_trailing, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union, + .tagged_union_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + => true, + .error_set_decl => true, + .fn_proto, .fn_proto_multi, .fn_proto_one, .fn_proto_simple => true, + .call_one, .call_one_comma, .call, .call_comma => blk: { + const type_info = self.resolveFunctionCall(tree, node, module_path); + break :blk type_info == .type_type; + }, + else => false, + }; +} + +fn varDeclIsTypeRef(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) bool { + const var_decl = tree.fullVarDecl(node) orelse return false; + if (var_decl.ast.type_node.unwrap()) |type_node| { + if (tree.nodeTag(type_node) == .identifier) { + return std.mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(type_node)), "type"); + } + return false; + } + const init_node = var_decl.ast.init_node.unwrap() orelse return false; + return self.nodeIsTypeRef(tree, init_node, module_path); +} + +fn identifierIsTypeRef(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) bool { + const name = tree.tokenSlice(tree.nodeMainToken(node)); + if (resolvePrimitiveType(name) != null) return true; + if (std.mem.eql(u8, name, "type")) return true; + for (tree.rootDecls()) |decl_node| { + switch (tree.nodeTag(decl_node)) { + .simple_var_decl, .aligned_var_decl, .local_var_decl, .global_var_decl => { + const var_decl = tree.fullVarDecl(decl_node) orelse continue; + const name_token = var_decl.ast.mut_token + 1; + if (!std.mem.eql(u8, tree.tokenSlice(name_token), name)) continue; + return self.varDeclIsTypeRef(tree, decl_node, module_path); + }, + else => {}, + } + } + return false; +} + +fn fieldAccessIsTypeRef(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Index, module_path: []const u8) bool { + const data = tree.nodeData(node).node_and_token; + if (!self.nodeIsTypeRef(tree, data[0], module_path)) return false; + const type_info = self.resolveFieldAccess(tree, node, module_path); + return switch (type_info) { + .std_type, .user_type, .type_type => true, + else => false, + }; +} + +fn builtinCallIsTypeRef(_: *TypeResolver, tree: *const Ast, node: Ast.Node.Index) bool { + const builtin_name = tree.tokenSlice(tree.nodeMainToken(node)); + return std.mem.eql(u8, builtin_name, "@import") or + std.mem.eql(u8, builtin_name, "@This") or + std.mem.eql(u8, builtin_name, "@TypeOf") or + std.mem.eql(u8, builtin_name, "@Type"); +} + +test "resolve primitive types" { + const source = "const x: u32 = 0;"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + try std.testing.expect(root_decls.len > 0); + + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.u32, type_info.primitive); +} + +test "resolve bool literal" { + const source = "const x = true;"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.bool, type_info.primitive); +} + +test "resolve import std" { + const source = "const std = @import(\"std\");"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .std_type); +} + +test "resolve function returns type" { + const source = "fn MyType() type { return struct {}; }"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .type_type); +} + +test "resolve number literal int" { + const source = "const x = 42;"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.comptime_int, type_info.primitive); +} + +test "resolve number literal float" { + const source = "const x = 3.14;"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.comptime_float, type_info.primitive); +} + +test "resolve field access on std" { + const source = + \\const std = @import("std"); + \\const fs = std.fs; + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + try std.testing.expect(root_decls.len >= 2); + + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .std_type); + try std.testing.expectEqualStrings("fs", type_info.std_type.path); +} + +test "resolve nested field access std.fs.File" { + const source = + \\const std = @import("std"); + \\const File = std.fs.File; + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + try std.testing.expect(root_decls.len >= 2); + + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .std_type); + try std.testing.expectEqualStrings("fs.File", type_info.std_type.path); +} + +test "resolve field access on aliased std import" { + const source = + \\const stdlib = @import("std"); + \\const fs = stdlib.fs; + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + try std.testing.expect(root_decls.len >= 2); + + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .std_type); + try std.testing.expectEqualStrings("fs", type_info.std_type.path); +} + +test "resolve nested field access on aliased std import" { + const source = + \\const stdlib = @import("std"); + \\const File = stdlib.fs.File; + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + try std.testing.expect(root_decls.len >= 2); + + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .std_type); + try std.testing.expectEqualStrings("fs.File", type_info.std_type.path); +} + +test "resolve string literal" { + const source = "const s = \"hello\";"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .slice); +} + +test "resolve function declaration" { + const source = "fn foo() void {}"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .function); +} + +test "find method in user type" { + const source = + \\const MyType = struct { + \\ value: u32, + \\ pub fn getValue(self: *@This()) u32 { + \\ return self.value; + \\ } + \\}; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const user_type: TypeInfo = .{ .user_type = .{ .module_path = path, .name = "MyType" } }; + const method_def = resolver.findMethodDef(user_type, "getValue"); + + try std.testing.expect(method_def != null); + try std.testing.expectEqual(@as(u32, 2), method_def.?.line); +} + +test "find method not found returns null" { + const source = + \\const MyType = struct { + \\ value: u32, + \\}; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const user_type: TypeInfo = .{ .user_type = .{ .module_path = path, .name = "MyType" } }; + const method_def = resolver.findMethodDef(user_type, "nonexistent"); + + try std.testing.expect(method_def == null); +} + +test "find method in std_type without zig_lib_path returns null" { + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = "const x = 1;" }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + // No zig_lib_path, so stdlib can't be resolved + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const std_type: TypeInfo = .{ .std_type = .{ .path = "fs.File" } }; + const method_def = resolver.findMethodDef(std_type, "read"); + + try std.testing.expect(method_def == null); +} + +test "resolve function call return type" { + const source = + \\fn getCount() u32 { + \\ return 42; + \\} + \\const x = getCount(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.u32, type_info.primitive); +} + +test "resolve function call returning bool" { + const source = + \\fn isValid() bool { + \\ return true; + \\} + \\const x = isValid(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.bool, type_info.primitive); +} + +test "resolve method call return type" { + const source = + \\const MyType = struct { + \\ value: u32, + \\ pub fn getValue(self: *@This()) u32 { + \\ return self.value; + \\ } + \\}; + \\const obj: MyType = undefined; + \\const x = obj.getValue(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[2]); + try std.testing.expect(type_info == .primitive); + try std.testing.expectEqual(TypeInfo.Primitive.u32, type_info.primitive); +} + +test "resolve type-returning function call" { + const source = + \\fn MyType() type { + \\ return struct {}; + \\} + \\const T = MyType(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[1]); + try std.testing.expect(type_info == .type_type); +} + +test "resolve unknown function call" { + const source = + \\const x = unknownFn(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + const type_info = resolver.typeOf(path, root_decls[0]); + try std.testing.expect(type_info == .unknown); +} + +test "isTypeRef: struct declaration" { + const source = "const Foo = struct { value: u32 };"; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(resolver.isTypeRef(path, root_decls[0])); +} + +test "isTypeRef: instance with type annotation" { + const source = + \\const Foo = struct {}; + \\const obj: Foo = .{}; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(resolver.isTypeRef(path, root_decls[0])); + try std.testing.expect(!resolver.isTypeRef(path, root_decls[1])); +} + +test "isTypeRef: primitive typed variable" { + const source = "const x: u32 = 5;"; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(!resolver.isTypeRef(path, root_decls[0])); +} + +test "isTypeRef: explicit type annotation" { + const source = "const T: type = u32;"; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(resolver.isTypeRef(path, root_decls[0])); +} + +test "isTypeRef: enum declaration" { + const source = "const E = enum { a, b, c };"; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(resolver.isTypeRef(path, root_decls[0])); +} + +test "isTypeRef: function call returning type" { + const source = + \\fn MyType() type { + \\ return struct {}; + \\} + \\const T = MyType(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(resolver.isTypeRef(path, root_decls[1])); +} + +test "isTypeRef: function call returning value" { + const source = + \\fn getValue() u32 { + \\ return 5; + \\} + \\const x = getValue(); + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const root_decls = graph.getModule(path).?.tree.rootDecls(); + try std.testing.expect(!resolver.isTypeRef(path, root_decls[1])); +} + +test "isTypeRef: identifier resolving to struct" { + const source = + \\const Foo = struct {}; + \\const Bar = Foo; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const mod = graph.getModule(path).?; + const root_decls = mod.tree.rootDecls(); + const var_decl = mod.tree.fullVarDecl(root_decls[1]).?; + const init_node = var_decl.ast.init_node.unwrap().?; + try std.testing.expect(resolver.isTypeRef(path, init_node)); +} + +test "isTypeRef: identifier resolving to instance" { + const source = + \\const x: u32 = 5; + \\const y = x; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const mod = graph.getModule(path).?; + const root_decls = mod.tree.rootDecls(); + const var_decl = mod.tree.fullVarDecl(root_decls[1]).?; + const init_node = var_decl.ast.init_node.unwrap().?; + try std.testing.expect(!resolver.isTypeRef(path, init_node)); +} + +test "findFnInCurrentModule: type function" { + const source = + \\/// Deprecated: use NewList + \\pub fn OldList(comptime T: type) type { + \\ return struct { items: []T }; + \\} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const method_def = resolver.findFnInCurrentModule(path, "OldList"); + try std.testing.expect(method_def != null); +} + +test "findFnInCurrentModule: direct function" { + const source = + \\/// Deprecated: use newFunc + \\pub fn oldFunc() void {} + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const method_def = resolver.findFnInCurrentModule(path, "oldFunc"); + try std.testing.expect(method_def != null); +} + +test "findFnInCurrentModule: const alias to function" { + const source = + \\/// Deprecated: use newFunc + \\pub fn oldFunc() void {} + \\pub const aliasFunc = oldFunc; + ; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const io = testIo(); + try tmp_dir.dir.writeFile(io, .{ .sub_path = "test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "test.zig", std.testing.allocator); + defer std.testing.allocator.free(path); + + var graph = try ModuleGraph.init(std.testing.allocator, io, path, null); + defer graph.deinit(); + + var resolver: TypeResolver = .init(std.testing.allocator, &graph); + defer resolver.deinit(); + + const method_def = resolver.findFnInCurrentModule(path, "aliasFunc"); + try std.testing.expect(method_def != null); + // Should resolve to oldFunc's node, not the alias + const mod = graph.getModule(path).?; + const doc_comments = @import("doc_comments.zig"); + 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.find(u8, doc.?, "Deprecated") != null); +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_comments.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_comments.zig new file mode 100644 index 0000000..5f73b76 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_comments.zig @@ -0,0 +1,155 @@ +//! Doc comment extraction for semantic analysis. +//! +//! Extracts doc comments (///) from above function and method definitions. + +const std = @import("std"); +const Ast = std.zig.Ast; +const Token = std.zig.Token; + +/// Extracts the doc comment text for a given AST node. +/// Returns null if no doc comment is present. +/// The returned slice references memory owned by the allocator. +pub fn getDocComment(allocator: std.mem.Allocator, tree: *const Ast, node: Ast.Node.Index) ?[]const u8 { + const first_token = tree.firstToken(node); + if (first_token == 0) return null; + + // Walk backwards from the first token to find doc comments + var doc_tokens: std.ArrayList(Ast.TokenIndex) = .empty; + defer doc_tokens.deinit(allocator); + + var token = first_token - 1; + while (true) { + const tag = tree.tokenTag(token); + if (tag == .doc_comment) { + doc_tokens.append(allocator, token) catch return null; + } else { + // 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; + } + + if (doc_tokens.items.len == 0) return null; + + // Reverse to get chronological order + std.mem.reverse(Ast.TokenIndex, doc_tokens.items); + + // Build the combined doc comment text + var result: std.ArrayList(u8) = .empty; + errdefer result.deinit(allocator); + + for (doc_tokens.items, 0..) |doc_token, i| { + if (i > 0) result.append(allocator, '\n') catch return null; + + const slice = tree.tokenSlice(doc_token); + // Strip "/// " or "///" prefix + const content = if (std.mem.startsWith(u8, slice, "/// ")) + slice[4..] + else if (std.mem.startsWith(u8, slice, "///")) + slice[3..] + else + slice; + + result.appendSlice(allocator, content) catch return null; + } + + return result.toOwnedSlice(allocator) catch null; +} + +test "extract single line doc comment" { + const source = + \\/// This is a doc comment + \\fn foo() void {} + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + const root_decls = tree.rootDecls(); + try std.testing.expect(root_decls.len > 0); + + const doc = getDocComment(std.testing.allocator, &tree, root_decls[0]); + defer if (doc) |d| std.testing.allocator.free(d); + + try std.testing.expect(doc != null); + try std.testing.expectEqualStrings("This is a doc comment", doc.?); +} + +test "extract multi-line doc comment" { + const source = + \\/// First line + \\/// Second line + \\/// Third line + \\fn foo() void {} + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + const root_decls = tree.rootDecls(); + const doc = getDocComment(std.testing.allocator, &tree, root_decls[0]); + defer if (doc) |d| std.testing.allocator.free(d); + + try std.testing.expect(doc != null); + try std.testing.expectEqualStrings("First line\nSecond line\nThird line", doc.?); +} + +test "no doc comment returns null" { + const source = "fn foo() void {}"; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + const root_decls = tree.rootDecls(); + const doc = getDocComment(std.testing.allocator, &tree, root_decls[0]); + + try std.testing.expect(doc == null); +} + +test "doc comment with pub function" { + const source = + \\/// Public function doc + \\pub fn foo() void {} + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + const root_decls = tree.rootDecls(); + const doc = getDocComment(std.testing.allocator, &tree, root_decls[0]); + defer if (doc) |d| std.testing.allocator.free(d); + + try std.testing.expect(doc != null); + try std.testing.expectEqualStrings("Public function doc", doc.?); +} + +test "doc comment without space after ///" { + const source = + \\///No space after slashes + \\fn foo() void {} + ; + + var tree = try Ast.parse(std.testing.allocator, source, .zig); + defer tree.deinit(std.testing.allocator); + + const root_decls = tree.rootDecls(); + const doc = getDocComment(std.testing.allocator, &tree, root_decls[0]); + defer if (doc) |d| std.testing.allocator.free(d); + + try std.testing.expect(doc != null); + try std.testing.expectEqualStrings("No space after slashes", doc.?); +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_tests.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_tests.zig new file mode 100644 index 0000000..ff5b5cf --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/doc_tests.zig @@ -0,0 +1,222 @@ +//! Executable documentation tests. +//! +//! Parses markdown files in docs/rules/ and validates that code examples +//! produce the expected linter diagnostics. Similar to Go's example tests. +//! +//! Code blocks with `// expect: Z001` comments should trigger that rule. +//! Code blocks without expect comments should produce no diagnostics. + +const std = @import("std"); +const Linter = @import("Linter.zig"); +const ModuleGraph = @import("ModuleGraph.zig"); +const TypeResolver = @import("TypeResolver.zig"); +const rules = @import("rules.zig"); + +/// Test-only Io provider. Tests are synchronous and don't need cancelation, +/// so we use the global single-threaded Io instance. +fn testIo() std.Io { + return std.Io.Threaded.global_single_threaded.io(); +} + +const DocTest = struct { + code: []const u8, + expected_rules: []const rules.Rule, + line_in_doc: usize, +}; + +const ParsedDoc = struct { + rule: ?rules.Rule, + tests: []const DocTest, +}; + +/// Parses a markdown file and extracts code blocks with their expectations. +fn parseMarkdown(allocator: std.mem.Allocator, content: []const u8) !ParsedDoc { + var tests: std.ArrayList(DocTest) = .empty; + var rule: ?rules.Rule = null; + + // Parse frontmatter for rule identifier + if (std.mem.startsWith(u8, content, "---\n")) { + 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| { + if (std.mem.startsWith(u8, line, "rule: ")) { + const rule_code = line[6..]; + rule = std.meta.stringToEnum(rules.Rule, rule_code); + } + } + } + } + + // Find all zig code blocks + var line_num: usize = 1; + var pos: usize = 0; + 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; + } + line_num += 1; // for the ```zig line + + const code_start = start + 7; + 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.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` + var expects = std.mem.splitSequence(u8, expect_str, ","); + while (expects.next()) |e| { + const trimmed = std.mem.trim(u8, e, " "); + if (std.meta.stringToEnum(rules.Rule, trimmed)) |r| { + try expected.append(allocator, r); + } + } + } + } + + try tests.append(allocator, .{ + .code = code, + .expected_rules = try expected.toOwnedSlice(allocator), + .line_in_doc = line_num, + }); + + pos = end + 4; + } else { + break; + } + } + + return .{ + .rule = rule, + .tests = try tests.toOwnedSlice(allocator), + }; +} + +fn runDocTest(allocator: std.mem.Allocator, doc_path: []const u8, doc_test: DocTest, tmp_dir: *std.testing.TmpDir) !void { + // Linter expects sentinel-terminated source + const source = try allocator.allocSentinel(u8, doc_test.code.len, 0); + defer allocator.free(source); + @memcpy(source, doc_test.code); + + const io = testIo(); + // Write to temp file for semantic analysis + try tmp_dir.dir.writeFile(io, .{ .sub_path = "doc_test.zig", .data = source }); + const path = try tmp_dir.dir.realPathFileAlloc(io, "doc_test.zig", allocator); + defer allocator.free(path); + + // Try to create ModuleGraph for semantic analysis (may fail for invalid code) + 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; + defer if (resolver) |*r| r.deinit(); + + var linter: Linter = if (resolver) |*r| + .initWithSemantics(allocator, source, path, r, path, null) + else + .init(allocator, source, path, null); + defer linter.deinit(); + linter.lint(); + + // Check expected rules + for (doc_test.expected_rules) |expected_rule| { + const count = linter.diagnosticCount(expected_rule); + if (count == 0) { + std.debug.print("\n{s}:{d}: expected {s} but got no diagnostic\n", .{ + doc_path, + doc_test.line_in_doc, + expected_rule.code(), + }); + std.debug.print("Code:\n{s}\n", .{doc_test.code}); + return error.MissingExpectedDiagnostic; + } + } + + // 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, + d.rule.code(), + d.context, + }); + std.debug.print("Code:\n{s}\n", .{doc_test.code}); + return error.UnexpectedDiagnostic; + } + } +} + +pub fn runAllDocTests(allocator: std.mem.Allocator) !void { + const io = testIo(); + // Open docs/rules directory + const docs_path = "docs/rules"; + var dir = std.Io.Dir.cwd().openDir(io, docs_path, .{ .iterate = true }) catch |err| { + if (err == error.FileNotFound) { + std.debug.print("No docs/rules directory found, skipping doc tests\n", .{}); + return; + } + return err; + }; + defer dir.close(io); + + // Create temp directory for semantic analysis + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + 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; + if (!std.mem.endsWith(u8, entry.name, ".md")) continue; + + const content = try dir.readFileAlloc(io, entry.name, allocator, .limited(1024 * 1024)); + defer allocator.free(content); + + const doc = try parseMarkdown(allocator, content); + defer { + for (doc.tests) |t| allocator.free(t.expected_rules); + allocator.free(doc.tests); + } + + const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ docs_path, entry.name }); + defer allocator.free(full_path); + + for (doc.tests) |doc_test| { + // 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" { + try runAllDocTests(std.testing.allocator); +} diff --git a/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/main.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/main.zig new file mode 100644 index 0000000..82075f7 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/main.zig @@ -0,0 +1,691 @@ +//! ziglint - A linter for Zig source code + +const std = @import("std"); +const build_options = @import("build_options"); +pub const version = build_options.version; + +const FileConfig = @import("Config.zig"); +const Linter = @import("Linter.zig"); +const ModuleGraph = @import("ModuleGraph.zig"); +const rules = @import("rules.zig"); +const TypeResolver = @import("TypeResolver.zig"); + +pub const Config = struct { + zig_lib_path: ?[]const u8 = null, + paths: []const []const u8 = &.{}, + only_rules: []const rules.Rule = &.{}, + ignored_rules: []const rules.Rule = &.{}, + file_config: FileConfig = .{}, + verbose: bool = false, +}; + +/// Compatibility shim for the removed std.time.Timer in Zig 0.16. +/// Wraps std.Io.Timestamp + monotonic clock with a Timer-like read() interface. +const VerboseTimer = struct { + start: std.Io.Timestamp, + + fn init(io: std.Io) VerboseTimer { + return .{ .start = std.Io.Timestamp.now(io, .awake) }; + } + + fn read(self: *VerboseTimer, io: std.Io) u64 { + const elapsed = self.start.durationTo(std.Io.Timestamp.now(io, .awake)); + return @intCast(@max(elapsed.nanoseconds, 0)); + } +}; + +pub fn main(init: std.process.Init) !u8 { + const allocator = init.gpa; + const io = init.io; + const environ_map = init.environ_map; + + // Process-lifetime arena for CLI paths, detected `zig env` lib dir, and + // FileConfig dupes. These all live until end-of-main, so an arena is the + // simplest leak-free pattern (avoids hand-tracking individual frees in + // the catch/return paths). + var config_arena: std.heap.ArenaAllocator = .init(allocator); + defer config_arena.deinit(); + const cfg_alloc = config_arena.allocator(); + + const stderr_file = std.Io.File.stderr(); + 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); + defer stdout.end() catch {}; + + var stderr_buf: [4096]u8 = undefined; + var stderr = stderr_file.writer(io, &stderr_buf); + defer stderr.end() catch {}; + + var config = parseArgs(cfg_alloc, init.minimal.args, &stderr.interface) catch |err| switch (err) { + error.HelpOrVersion => return 0, + error.InvalidArgs => return 1, + else => return err, + }; + + // 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(cfg_alloc, io, start_path) catch .{}; + + applyOnlyRules(&config); + + // Use CLI paths, then config file paths, then default to "." + if (config.paths.len == 0) { + if (config.file_config.paths.len > 0) { + config.paths = config.file_config.paths; + } else { + config.paths = &.{"."}; + } + } + + const zig_lib_path = config.zig_lib_path orelse detectZigLibPath(cfg_alloc, allocator, io, &stderr.interface) catch null; + + var total_timer = if (config.verbose) VerboseTimer.init(io) else null; + + var total_issues: usize = 0; + for (config.paths) |path| { + 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; + // `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); + } + + if (config.verbose and total_timer != null) { + const total_ms = @as(f64, @floatFromInt(total_timer.?.read(io))) / 1_000_000.0; + const dim = if (use_color) "\x1b[2m" else ""; + const cyan = if (use_color) "\x1b[36m" else ""; + const reset = if (use_color) "\x1b[0m" else ""; + try stderr.interface.print("\n{s}{s}Total time:{s} {s}{d:.2}ms{s}\n", .{ dim, cyan, reset, cyan, total_ms, reset }); + } + + return if (total_issues > 0) 1 else 0; +} + +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 the provided file (caller-selected stream, e.g. stderr) is a TTY + return file.isTty(io) catch false; +} + +/// 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(allocator, &.{ path, "build.zig" }) catch return null; + defer allocator.free(build_zig); + + if (std.Io.Dir.cwd().access(io, build_zig, .{})) |_| { + return allocator.dupe(u8, path) catch null; + } else |_| {} + + // Move up one directory + const parent = std.fs.path.dirname(path) orelse return null; + if (std.mem.eql(u8, parent, path)) return null; // at root + path = parent; + } +} + +fn makeRelativePath(path: []const u8, project_root: ?[]const u8) []const u8 { + const root = project_root orelse return path; + if (std.mem.startsWith(u8, path, root)) { + var rel = path[root.len..]; + // Skip leading path separator + if (rel.len > 0 and rel[0] == std.fs.path.sep) { + rel = rel[1..]; + } + if (rel.len > 0) return rel; + } + return path; +} + +fn parseArgs(allocator: std.mem.Allocator, args: std.process.Args, writer: *std.Io.Writer) !Config { + var iter = try args.iterateAllocator(allocator); + defer iter.deinit(); + + var config: Config = .{}; + var paths: std.ArrayList([]const u8) = .empty; + var only_rules: std.ArrayList(rules.Rule) = .empty; + var ignored_rules: std.ArrayList(rules.Rule) = .empty; + + // Skip program name (argv[0]) + _ = iter.skip(); + + while (iter.next()) |arg| { + if (std.mem.eql(u8, arg, "--zig-lib-path")) { + const next = iter.next() orelse { + try writer.writeAll("error: --zig-lib-path requires a path argument\n"); + return error.InvalidArgs; + }; + config.zig_lib_path = try allocator.dupe(u8, next); + } else if (std.mem.eql(u8, arg, "--only")) { + const next = iter.next() orelse { + try writer.writeAll("error: --only requires a rule code (e.g., Z001)\n"); + return error.InvalidArgs; + }; + if (parseRuleCode(next)) |rule| { + try only_rules.append(allocator, rule); + } else { + try writer.print("error: unknown rule code '{s}'\n", .{next}); + return error.InvalidArgs; + } + } else if (std.mem.eql(u8, arg, "--ignore")) { + const next = iter.next() orelse { + try writer.writeAll("error: --ignore requires a rule code (e.g., Z001)\n"); + return error.InvalidArgs; + }; + if (parseRuleCode(next)) |rule| { + try ignored_rules.append(allocator, rule); + } else { + try writer.print("error: unknown rule code '{s}'\n", .{next}); + return error.InvalidArgs; + } + } else if (std.mem.eql(u8, arg, "--verbose")) { + config.verbose = true; + } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + try printUsage(writer); + return error.HelpOrVersion; + } else if (std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v")) { + try printVersion(writer); + return error.HelpOrVersion; + } else if (std.mem.startsWith(u8, arg, "-")) { + try writer.print("error: unknown option '{s}'\n", .{arg}); + return error.InvalidArgs; + } else { + try paths.append(allocator, try allocator.dupe(u8, arg)); + } + } + + config.paths = paths.items; + config.only_rules = only_rules.items; + config.ignored_rules = ignored_rules.items; + return config; +} + +fn parseRuleCode(code: []const u8) ?rules.Rule { + inline for (std.meta.fields(rules.Rule)) |field| { + if (std.mem.eql(u8, code, field.name)) { + return @enumFromInt(field.value); + } + } + return null; +} + +fn applyOnlyRules(config: *Config) void { + if (config.only_rules.len == 0) return; + + inline for (std.meta.fields(rules.Rule)) |field| { + const rule: rules.Rule = @enumFromInt(field.value); + config.file_config.setRuleEnabled(rule, false); + } + + for (config.only_rules) |rule| { + config.file_config.setRuleEnabled(rule, true); + } + + // Keep --ignore behavior consistent when used with --only. + for (config.ignored_rules) |rule| { + config.file_config.setRuleEnabled(rule, false); + } +} + +/// `out_alloc` owns the returned slice (process-lifetime arena); +/// `tmp_alloc` owns the temporary `zig env` stdout/stderr buffers. +fn detectZigLibPath(out_alloc: std.mem.Allocator, tmp_alloc: std.mem.Allocator, io: std.Io, writer: *std.Io.Writer) !?[]const u8 { + const result = std.process.run(tmp_alloc, io, .{ + .argv = &.{ "zig", "env" }, + .stdout_limit = .limited(64 * 1024), + }) catch |err| { + try writer.print("warning: could not run 'zig env': {}\n", .{err}); + return null; + }; + defer tmp_alloc.free(result.stdout); + defer tmp_alloc.free(result.stderr); + + switch (result.term) { + .exited => |code| if (code != 0) return null, + else => return null, + } + + return parseLibDirFromZigEnv(out_alloc, result.stdout); +} + +fn parseLibDirFromZigEnv(allocator: std.mem.Allocator, output: []const u8) ?[]const u8 { + const needle = ".lib_dir = \""; + const start_idx = std.mem.find(u8, output, needle) orelse return null; + const value_start = start_idx + needle.len; + const end_idx = std.mem.findPos(u8, output, value_start, "\"") orelse return null; + return allocator.dupe(u8, output[value_start..end_idx]) catch null; +} + +fn lintPath(allocator: std.mem.Allocator, io: std.Io, path: []const u8, zig_lib_path: ?[]const u8, config: *const Config, use_color: bool, project_root: ?[]const u8, writer: *std.Io.Writer) !usize { + const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| { + if (err == error.IsDir) { + return lintDirectory(allocator, io, path, zig_lib_path, config, use_color, project_root, writer); + } + try writer.print("error: cannot access '{s}': {}\n", .{ path, err }); + return 0; + }; + + if (stat.kind == .directory) { + return lintDirectory(allocator, io, path, zig_lib_path, config, use_color, project_root, writer); + } + + return lintFile(allocator, io, path, zig_lib_path, config, use_color, project_root, writer); +} + +fn lintDirectory(allocator: std.mem.Allocator, io: std.Io, path: []const u8, zig_lib_path: ?[]const u8, config: *const Config, use_color: bool, project_root: ?[]const u8, writer: *std.Io.Writer) !usize { + var dir = std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }) catch |err| { + try writer.print("error: cannot open directory '{s}': {}\n", .{ path, err }); + return 0; + }; + defer dir.close(io); + + const gitignore = loadGitignore(allocator, io, dir); + defer if (gitignore) |g| allocator.free(g); + + // Collect all .zig files first + var files: std.ArrayList([]const u8) = .empty; + defer { + for (files.items) |f| allocator.free(f); + files.deinit(allocator); + } + + var walker = dir.walk(allocator) catch |err| { + try writer.print("error: cannot walk directory '{s}': {}\n", .{ path, err }); + return 0; + }; + defer walker.deinit(); + + while (walker.next(io) catch null) |entry| { + if (shouldSkip(entry.path, gitignore)) continue; + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.basename, ".zig")) continue; + + const full_path = std.fs.path.join(allocator, &.{ path, entry.path }) catch continue; + files.append(allocator, full_path) catch { + allocator.free(full_path); + continue; + }; + } + + if (files.items.len == 0) return 0; + + const dim = if (use_color) "\x1b[2m" else ""; + const cyan = if (use_color) "\x1b[36m" else ""; + const reset = if (use_color) "\x1b[0m" else ""; + + if (config.verbose) { + try writer.print("{s}┌─ {s}{s}{s} ({d} files)\n", .{ dim, reset, path, reset, files.items.len }); + } + + var timer = if (config.verbose) VerboseTimer.init(io) else null; + + // 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(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 }); + } + var total: usize = 0; + for (files.items) |file_path| { + total += try lintFileSimple(allocator, io, file_path, config, use_color, project_root, writer); + } + return total; + }; + defer graph.deinit(); + + if (config.verbose and timer != null) { + const elapsed = timer.?.read(io) - graph_start; + try writer.print("{s}│ module graph: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + // Add remaining files to the graph + const add_start = if (timer) |*t| t.read(io) else 0; + for (files.items[1..]) |file_path| { + graph.addModulePublic(file_path); + } + + if (config.verbose and timer != null and files.items.len > 1) { + const elapsed = timer.?.read(io) - add_start; + try writer.print("{s}│ add files: {s}{d:>7.2}ms{s} ({d} files)\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset, files.items.len - 1 }); + } + + const resolver_start = if (timer) |*t| t.read(io) else 0; + var resolver: TypeResolver = .init(allocator, &graph); + defer resolver.deinit(); + + if (config.verbose and timer != null) { + const elapsed = timer.?.read(io) - resolver_start; + try writer.print("{s}│ type resolver: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + var total: usize = 0; + for (files.items) |file_path| { + total += try lintFileWithGraph(allocator, io, file_path, &graph, &resolver, config, use_color, project_root, writer); + } + + if (config.verbose and timer != null) { + const total_time = timer.?.read(io); + try writer.print("{s}└─ total: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(total_time)) / 1_000_000.0, reset }); + } + + return total; +} + +fn shouldSkip(path: []const u8, gitignore: ?[]const u8) bool { + var iter = std.mem.splitScalar(u8, path, std.fs.path.sep); + while (iter.next()) |component| { + if (std.mem.startsWith(u8, component, ".")) return true; + if (std.mem.eql(u8, component, "zig-cache")) return true; + if (std.mem.eql(u8, component, "zig-out")) return true; + } + + if (gitignore) |patterns| { + var lines = std.mem.splitScalar(u8, patterns, '\n'); + while (lines.next()) |line| { + const pattern = std.mem.trim(u8, line, &std.ascii.whitespace); + if (pattern.len == 0 or pattern[0] == '#') continue; + if (matchesGitignore(path, pattern)) return true; + } + } + + return false; +} + +fn matchesGitignore(path: []const u8, pattern: []const u8) bool { + const clean_pattern = if (std.mem.endsWith(u8, pattern, "/")) + pattern[0 .. pattern.len - 1] + else + pattern; + + if (std.mem.startsWith(u8, clean_pattern, "/")) { + return std.mem.startsWith(u8, path, clean_pattern[1..]); + } + + var iter = std.mem.splitScalar(u8, path, std.fs.path.sep); + while (iter.next()) |component| { + if (std.mem.eql(u8, component, clean_pattern)) return true; + } + + return std.mem.find(u8, path, clean_pattern) != null; +} + +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; +} + +fn lintFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, zig_lib_path: ?[]const u8, config: *const Config, use_color: bool, project_root: ?[]const u8, writer: *std.Io.Writer) !usize { + var timer = if (config.verbose) VerboseTimer.init(io) else null; + + const dim = if (use_color) "\x1b[2m" else ""; + const cyan = if (use_color) "\x1b[36m" else ""; + const reset = if (use_color) "\x1b[0m" else ""; + + if (config.verbose) { + try writer.print("{s}┌─ {s}{s}{s}\n", .{ dim, reset, path, reset }); + } + + const graph_start = if (timer) |*t| t.read(io) else 0; + 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(); + + if (config.verbose and timer != null) { + const elapsed = timer.?.read(io) - graph_start; + try writer.print("{s}│ module graph: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + const resolver_start = if (timer) |*t| t.read(io) else 0; + var resolver: TypeResolver = .init(allocator, &graph); + defer resolver.deinit(); + + if (config.verbose and timer != null) { + const elapsed = timer.?.read(io) - resolver_start; + try writer.print("{s}│ type resolver: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + const result = try lintFileWithGraph(allocator, io, path, &graph, &resolver, config, use_color, project_root, writer); + + if (config.verbose and timer != null) { + const total = timer.?.read(io); + try writer.print("{s}└─ total: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(total)) / 1_000_000.0, reset }); + } + + return result; +} + +fn lintFileSimple(allocator: std.mem.Allocator, io: std.Io, path: []const u8, config: *const Config, use_color: bool, project_root: ?[]const u8, writer: *std.Io.Writer) !usize { + const source = std.Io.Dir.cwd().readFileAllocOptions( + io, + path, + allocator, + .limited(1024 * 1024 * 16), + .@"1", + 0, + ) catch |err| { + try writer.print("error: cannot read '{s}': {}\n", .{ path, err }); + return 0; + }; + defer allocator.free(source); + + var linter: Linter = .init(allocator, source, path, &config.file_config); + defer linter.deinit(); + linter.lint(); + return writeDiagnostics(allocator, linter.diagnostics.items, config, use_color, project_root, writer); +} + +fn lintFileWithGraph(allocator: std.mem.Allocator, io: std.Io, path: []const u8, graph: *ModuleGraph, resolver: *TypeResolver, config: *const Config, use_color: bool, project_root: ?[]const u8, writer: *std.Io.Writer) !usize { + const mod = graph.getModule(path) orelse { + return lintFileSimple(allocator, io, path, config, use_color, project_root, writer); + }; + + const dim = if (use_color) "\x1b[2m" else ""; + const cyan = if (use_color) "\x1b[36m" else ""; + const reset = if (use_color) "\x1b[0m" else ""; + + var linter: Linter = .initWithSemantics(allocator, mod.source, mod.path, resolver, mod.path, &config.file_config); + defer linter.deinit(); + + linter.verbose = config.verbose; + linter.use_color = use_color; + + var timer = if (config.verbose) VerboseTimer.init(io) else null; + linter.lint(); + + if (config.verbose and timer != null) { + const elapsed = timer.?.read(io); + try writer.print("{s}│ linting: {s}{d:>7.2}ms{s}\n", .{ dim, cyan, @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, reset }); + } + + return writeDiagnostics(allocator, linter.diagnostics.items, config, use_color, project_root, writer); +} + +fn writeDiagnostics(allocator: std.mem.Allocator, diagnostics: []const Linter.Diagnostic, config: *const Config, use_color: bool, project_root: ?[]const u8, writer: *std.Io.Writer) !usize { + var count: usize = 0; + var rule_counts: std.AutoHashMapUnmanaged(rules.Rule, usize) = .empty; + defer rule_counts.deinit(allocator); + + for (diagnostics) |diag| { + // Track rule counts for verbose output + if (config.verbose) { + const entry = rule_counts.getOrPut(allocator, diag.rule) catch continue; + if (!entry.found_existing) { + entry.value_ptr.* = 0; + } + entry.value_ptr.* += 1; + } + + // Check CLI ignore list + var ignored = false; + for (config.ignored_rules) |ignored_rule| { + if (diag.rule == ignored_rule) { + ignored = true; + break; + } + } + // Check config file rule enabled state + if (!ignored and !config.file_config.isRuleEnabled(diag.rule)) { + ignored = true; + } + if (!ignored) { + const display_path = makeRelativePath(diag.path, project_root); + try diag.write(writer, use_color, display_path); + count += 1; + } + } + + // Print rule summary if verbose + if (config.verbose and rule_counts.count() > 0) { + const dim = if (use_color) "\x1b[2m" else ""; + const yellow = if (use_color) "\x1b[33m" else ""; + const reset = if (use_color) "\x1b[0m" else ""; + + try writer.print("{s}│ rules:{s}\n", .{ dim, reset }); + var iter = rule_counts.iterator(); + while (iter.next()) |entry| { + try writer.print("{s}│ {s}{s}{s}: {d}{s}\n", .{ dim, yellow, entry.key_ptr.code(), reset, entry.value_ptr.*, reset }); + } + } + + return count; +} + +fn printUsage(writer: *std.Io.Writer) !void { + try writer.writeAll( + \\Usage: ziglint [options] + \\ + \\Lint Zig source files for style and correctness issues. + \\ + \\Options: + \\ --only Lint only this rule (e.g., Z001). Can be repeated. + \\ --ignore Ignore a rule (e.g., Z001). Can be repeated. + \\ --zig-lib-path Override the path to the Zig standard library. + \\ Auto-detected from 'zig env' if not specified. + \\ --verbose Show detailed timing and progress information. + \\ -h, --help Show this help message. + \\ -v, --version Show version. + \\ + \\Directories are scanned recursively for .zig files. + \\ + ); +} + +fn printVersion(writer: *std.Io.Writer) !void { + try writer.writeAll("ziglint " ++ version ++ "\n"); +} + +test { + _ = Linter; + _ = ModuleGraph; + _ = FileConfig; + _ = @import("rules.zig"); + _ = @import("doc_comments.zig"); + _ = @import("TypeResolver.zig"); + _ = @import("doc_tests.zig"); +} + +test "parseLibDirFromZigEnv" { + const output = + \\.{ + \\ .zig_exe = "/usr/bin/zig", + \\ .lib_dir = "/usr/lib/zig", + \\ .std_dir = "/usr/lib/zig/std", + \\} + ; + const result = parseLibDirFromZigEnv(std.testing.allocator, output); + defer if (result) |r| std.testing.allocator.free(r); + try std.testing.expectEqualStrings("/usr/lib/zig", result.?); +} + +test "parseLibDirFromZigEnv: missing field" { + const output = ".{ .zig_exe = \"/usr/bin/zig\" }"; + try std.testing.expectEqual(null, parseLibDirFromZigEnv(std.testing.allocator, output)); +} + +test "applyOnlyRules enables selected rules" { + var config: Config = .{ + .only_rules = &.{ .Z001, .Z033 }, + }; + + applyOnlyRules(&config); + + try std.testing.expect(config.file_config.isRuleEnabled(.Z001)); + try std.testing.expect(config.file_config.isRuleEnabled(.Z033)); + try std.testing.expect(!config.file_config.isRuleEnabled(.Z002)); +} + +test "applyOnlyRules keeps ignore precedence" { + var config: Config = .{ + .only_rules = &.{ .Z001, .Z002 }, + .ignored_rules = &.{.Z002}, + }; + + applyOnlyRules(&config); + + try std.testing.expect(config.file_config.isRuleEnabled(.Z001)); + 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-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/rules.zig b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/rules.zig new file mode 100644 index 0000000..bf33ff3 --- /dev/null +++ b/zig-pkg/ziglint-0.5.2-t0bwLzCwBQA69qkf9xumLCiu4N7jqxuzjBy8Abpga7v9/src/rules.zig @@ -0,0 +1,338 @@ +//! Lint rule definitions with unique identifiers. + +const std = @import("std"); + +pub const Rule = enum(u16) { + Z001 = 1, + Z002 = 2, + Z003 = 3, + Z004 = 4, + Z005 = 5, + Z006 = 6, + Z007 = 7, + Z009 = 9, + Z010 = 10, + Z011 = 11, + Z012 = 12, + Z013 = 13, + Z014 = 14, + Z015 = 15, + Z016 = 16, + Z017 = 17, + Z018 = 18, + Z019 = 19, + Z020 = 20, + Z021 = 21, + Z022 = 22, + Z023 = 23, + Z024 = 24, + Z025 = 25, + Z026 = 26, + Z027 = 27, + Z028 = 28, + Z029 = 29, + Z030 = 30, + Z031 = 31, + Z032 = 32, + Z033 = 33, + + /// Returns the config struct type for this rule. + /// All config types have `enabled: bool` (default varies per rule). + /// Some rules have additional fields (e.g., Z024 has max_length). + fn ConfigType(comptime self: Rule) type { + const DefaultConfig = RuleConfig(true, struct {}); + const DisabledConfig = RuleConfig(false, struct {}); + return switch (self) { + .Z024 => RuleConfig(true, struct { max_length: u32 = 120 }), + .Z033 => DisabledConfig, // Redundant name words - disabled by default + else => DefaultConfig, + }; + } + + pub const Config = blk: { + const enum_fields = @typeInfo(Rule).@"enum".fields; + var field_names: [enum_fields.len][]const u8 = undefined; + var field_types: [enum_fields.len]type = undefined; + var field_attrs: [enum_fields.len]std.builtin.Type.StructField.Attributes = undefined; + for (enum_fields, 0..) |field, i| { + const rule: Rule = @enumFromInt(field.value); + const ConfigT = rule.ConfigType(); + const default_value: ConfigT = .{}; + field_names[i] = field.name; + field_types[i] = ConfigT; + field_attrs[i] = .{ + .@"align" = @alignOf(ConfigT), + .default_value_ptr = @ptrCast(&default_value), + }; + } + break :blk @Struct(.auto, null, &field_names, &field_types, &field_attrs); + }; + + pub fn code(self: Rule) []const u8 { + return @tagName(self); + } + + // ANSI escape codes + const blue = "\x1b[34m"; + const yellow = "\x1b[33m"; + const magenta = "\x1b[35m"; + const purple = "\x1b[35m"; + const dim = "\x1b[2m"; + const reset = "\x1b[0m"; + + pub fn writeMessage(self: Rule, writer: *std.Io.Writer, context: []const u8, use_color: bool) !void { + const b = if (use_color) blue else ""; + const y = if (use_color) yellow else ""; + const m = if (use_color) magenta else ""; + const p = if (use_color) purple else ""; + const d = if (use_color) dim else ""; + const r = if (use_color) reset else ""; + + switch (self) { + .Z001 => try writer.print("function {s}'{s}'{s} should be camelCase", .{ y, context, r }), + .Z002 => try writer.print("variable {s}'{s}'{s} is unused but has a value", .{ y, context, r }), + .Z003 => try writer.writeAll("parse error"), + // syntax highlight: `const name: T = .{};` vs `const name = T{};` + // const=purple (keyword), name=yellow (identifier), T=magenta (type) + .Z004 => try writer.print("prefer {s}`{s}{s}const{s} {s}{s}:{s} {s}T{s} = .{{}}{s};{s}{s}`{s} over {s}`{s}{s}const{s} {s} = {s}T{s}{{}}{s};{s}{s}`{s}", .{ d, r, p, r, context, d, r, m, r, d, r, d, r, d, r, p, r, context, m, r, d, r, d, r }), + .Z005 => try writer.print("type function {s}'{s}'{s} should be PascalCase", .{ y, context, r }), + .Z006 => try writer.print("variable {s}'{s}'{s} should be snake_case", .{ y, context, r }), + .Z007 => try writer.print("duplicate import {s}'{s}'{s}", .{ y, context, r }), + .Z009 => try writer.print("file {s}'{s}'{s} has top-level fields and should be PascalCase", .{ y, context, r }), + // syntax highlight: .{...} over Type{...} + // context is "preferred\x00original" format + .Z010 => { + 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 }); + try writeHighlightedStructInit(writer, preferred, m, d, r); + try writer.print("{s}`{s} over {s}`{s}", .{ d, r, d, r }); + try writeHighlightedStructInit(writer, original, m, d, r); + try writer.print("{s}`{s}", .{ d, r }); + }, + .Z011 => try writer.print("{s}", .{context}), + .Z012 => try writer.print("public function exposes private type {s}'{s}'{s}", .{ y, context, r }), + .Z013 => try writer.print("unused import {s}'{s}'{s}", .{ y, context, r }), + .Z014 => try writer.print("error set {s}'{s}'{s} should be PascalCase", .{ y, context, r }), + .Z015 => try writer.print("public function exposes private error set {s}'{s}'{s}", .{ y, context, r }), + .Z016 => { + // assert=blue, and/or=purple, a/b=yellow, punctuation=dim + // `assert(a and b)` -> `assert(a); assert(b);` + try writer.print("split compound assert: {s}`{s}{s}assert{s}{s}({s}{s}a{s} {s}{s}{s} {s}b{s}{s})`{s}", .{ + d, r, b, r, d, r, y, r, p, context, r, y, r, d, r, + }); + try writer.print(" -> {s}`{s}{s}assert{s}{s}({s}{s}a{s}{s}); {s}{s}assert{s}{s}({s}{s}b{s}{s});`{s}", .{ + d, r, b, r, d, r, y, r, d, r, b, r, d, r, y, r, d, r, + }); + }, + // return=purple, try=purple, expr=yellow, punctuation=dim + // `return try expr` -> `return expr` + .Z017 => { + // redundant `try` in `return`: `return try expr` -> `return expr` + try writer.print("redundant {s}try{s} in {s}return{s}: {s}`{s}return try {s}{s}{s}`{s} -> {s}`{s}return {s}{s}{s}`{s}", .{ + p, r, p, r, d, r, y, context, d, r, d, r, y, context, d, r, + }); + }, + // redundant @as when type is already known from context + // context is type name + .Z018 => { + try writer.print("redundant {s}@as{s}{s}({s}{s}{s}{s}, ...){s}: type {s}{s}{s} already known from context", .{ + b, r, d, r, m, context, d, r, m, context, r, + }); + }, + // @This() in named struct - use the type name + .Z019 => { + try writer.print("{s}@This(){s} used in named struct; use {s}'{s}'{s} instead", .{ b, r, y, context, r }); + }, + // inline @This() - assign to a constant + .Z020 => { + try writer.print("{s}@This(){s} should be assigned to a constant", .{ b, r }); + }, + // file-struct @This() alias should match filename + // context is "alias\x00expected" format + .Z021 => { + 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 }); + }, + // @This() alias in anonymous/local struct should be Self + .Z022 => { + try writer.print("{s}@This(){s} alias {s}'{s}'{s} should be {s}'Self'{s}", .{ b, r, y, context, r, y, r }); + }, + // argument order: type params, Allocator, Io, then other args + // context is "current_kind\x00expected_before" format + .Z023 => { + 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 }); + }, + // line length exceeds limit + // context is "actual_len\x00max_len" format + .Z024 => { + 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 }); + }, + // redundant catch: `catch |err| return err` -> use `try` + // catch=purple, try=purple, expr=yellow, punctuation=dim + .Z025 => { + try writer.print("redundant {s}catch{s}: {s}`{s}{s}catch{s} {s}|{s}{s}{s}{s}| {s}{s}return{s} {s}{s}{s}`{s} -> use {s}try{s}", .{ + p, r, d, r, p, r, d, r, y, context, d, r, p, r, y, context, d, r, p, r, + }); + }, + // suppressed error: empty catch block swallows errors + // catch=purple, punctuation=dim + .Z026 => { + try writer.print("empty {s}catch{s} block suppresses errors: {s}`{s}{s}catch{s} {s}{{}}{s}{s}`{s}", .{ + p, r, d, r, p, r, d, r, d, r, + }); + }, + // instance.decl -> Type.decl + // context is "field_name\x00type_name" + .Z027 => { + 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", .{ + y, field, r, m, type_name, r, + }); + }, + .Z028 => { + try writer.print("inline {s}@import{s}; assign to a top-level {s}const{s}", .{ b, r, p, r }); + }, + .Z029 => { + try writer.print("redundant {s}@as{s}{s}({s}{s}{s}{s}, ...){s}: type {s}{s}{s} already known from context", .{ + b, r, d, r, m, context, d, r, m, context, r, + }); + }, + .Z030 => { + // context is "reason" - either "has early return" or "missing assignment" + try writer.print("{s}deinit{s} should set {s}self.* = undefined{s}", .{ y, r, b, r }); + if (context.len > 0) { + try writer.print(" ({s})", .{context}); + } + }, + .Z031 => { + try writer.print("identifier {s}'{s}'{s} has underscore prefix; avoid {s}_like_this{s} naming", .{ y, context, r, y, r }); + }, + .Z032 => { + // context is "name\x00suggestion" + 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) { + try writer.print("acronym in {s}'{s}'{s} should use standard casing: {s}'{s}'{s}", .{ y, name, r, y, suggestion, r }); + } else { + try writer.print("acronym in {s}'{s}'{s} should use standard casing", .{ y, name, r }); + } + }, + .Z033 => { + // context is "name\x00word" + 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 }); + }, + } + } +}; + +fn writeHighlightedStructInit(writer: *std.Io.Writer, code: []const u8, type_color: []const u8, dim: []const u8, reset: []const u8) !void { + const yellow = "\x1b[33m"; + // Handle truncated case: "Type{" -> "Type{...}" + const is_truncated = std.mem.endsWith(u8, code, "{"); + + var i: usize = 0; + var after_dot = false; + var after_eq = false; + var in_braces = false; + + while (i < code.len) { + const c = code[i]; + if (c == '{') { + try writer.print("{s}{c}{s}", .{ dim, c, reset }); + in_braces = true; + i += 1; + } else if (c == '}') { + try writer.print("{s}{c}{s}", .{ dim, c, reset }); + i += 1; + } else if (c == '.') { + try writer.print("{s}{c}{s}", .{ dim, c, reset }); + after_dot = true; + after_eq = false; + i += 1; + } else if (c == '=') { + try writer.print("{s}{c}{s}", .{ dim, c, reset }); + after_eq = true; + after_dot = false; + i += 1; + } else if (c == ',') { + try writer.print("{s}{c}{s}", .{ dim, c, reset }); + after_eq = false; + after_dot = false; + i += 1; + } else if (c == ' ') { + try writer.writeByte(' '); + i += 1; + } else { + // Find end of identifier/value + const start = i; + while (i < code.len and code[i] != '{' and code[i] != '}' and code[i] != '.' and code[i] != ',' and code[i] != '=' and code[i] != ' ') : (i += 1) {} + const token = code[start..i]; + if (!in_braces) { + // Type name before { - magenta + try writer.print("{s}{s}{s}", .{ type_color, token, reset }); + } else if (after_dot) { + // Field name after . - yellow + try writer.print("{s}{s}{s}", .{ yellow, token, reset }); + } else { + // Value after = - no color + try writer.writeAll(token); + } + after_dot = false; + } + } + + if (is_truncated) { + try writer.print("{s}...}}{s}", .{ dim, reset }); + } +} + +/// Generates a config struct with `enabled: bool` plus any extra fields. +// ziglint-ignore: Z023 +fn RuleConfig(comptime enabled_by_default: bool, comptime Extra: type) type { + const extra_fields = @typeInfo(Extra).@"struct".fields; + const total = 1 + extra_fields.len; + + var field_names: [total][]const u8 = undefined; + var field_types: [total]type = undefined; + var field_attrs: [total]std.builtin.Type.StructField.Attributes = undefined; + + const default_enabled: bool = enabled_by_default; + field_names[0] = "enabled"; + field_types[0] = bool; + field_attrs[0] = .{ + .@"align" = @alignOf(bool), + .default_value_ptr = @ptrCast(&default_enabled), + }; + + for (extra_fields, 0..) |f, i| { + field_names[1 + i] = f.name; + field_types[1 + i] = f.type; + field_attrs[1 + i] = .{ + .@"comptime" = f.is_comptime, + .@"align" = f.alignment, + .default_value_ptr = f.default_value_ptr, + }; + } + + return @Struct(.auto, null, &field_names, &field_types, &field_attrs); +} + +test "rule codes" { + try std.testing.expectEqualStrings("Z001", Rule.Z001.code()); +}