Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ b.addExecutable(.{
## Zig Code Style

**Naming:**
- `camelCase` for functions and methods
- `snake_case` for variables and parameters
- `camelCase` for functions and methods (`PascalCase` when returning a type)
- `snake_case` for variables, parameters, and value constants — Zig stdlib
convention (e.g. `std.fs.max_path_bytes`), enforced by Z006
- `PascalCase` for types, structs, and enums
- `SCREAMING_SNAKE_CASE` for constants

**Struct initialization:** Prefer explicit type annotation with anonymous literals:
```zig
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/Z002.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Detects variables that are assigned a value but never used. If you don't need th
### Bad

```zig
// expect: Z002
// expect: Z002, Z031
const _unused = getValue();
```

Expand Down
2 changes: 1 addition & 1 deletion docs/rules/Z007.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Detects when the same module is imported multiple times in the same file.
### Bad

```zig
// expect: Z007
// expect: Z007, Z013
const std = @import("std");
const std2 = @import("std");
```
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/Z011.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const MyType = struct {
}
};
const instance: MyType = .{};
// expect: Z011
// expect: Z011, Z020
pub fn main() void {
instance.oldMethod();
}
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/Z021.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ In file-as-struct patterns (files with top-level fields), the `@This()` alias sh
In a file named `Config.zig`:

```zig
// expect: Z021
// expect: Z021, Z009
const Wrong = @This();

value: u32,
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/Z031.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn _privateHelper() void {}
```

```zig
// expect: Z031
// expect: Z031, Z002
const _internalValue: u32 = undefined;
```

Expand Down
20 changes: 14 additions & 6 deletions src/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

const std = @import("std");

const Rule = @import("rules.zig").Rule;
/// Public re-export: `isRuleEnabled`/`setRuleEnabled` take a `Rule`, so the
/// type must be reachable from this namespace by callers (ziglint Z012).
pub const Rule = @import("rules.zig").Rule;

const Config = @This();

Expand All @@ -28,6 +30,15 @@ pub fn isRuleEnabled(self: *const Config, rule: Rule) bool {
return true;
}

/// Frees the `paths` entries allocated by `parseConfigSource` and leaves
/// the config invalidated. Safe to call on a default-initialized `Config`
/// (the default `paths` slice is empty and never freed).
pub fn deinit(self: *Config, allocator: std.mem.Allocator) void {
for (self.paths) |p| allocator.free(p);
if (self.paths.len > 0) allocator.free(self.paths);
self.* = undefined;
}

/// Set whether a rule is enabled.
pub fn setRuleEnabled(self: *Config, rule: Rule, enabled: bool) void {
inline for (@typeInfo(Rule).@"enum".fields) |field| {
Expand Down Expand Up @@ -194,11 +205,8 @@ test "parse paths config" {
\\ },
\\}
;
const config = try parseConfigSource(std.testing.allocator, source);
defer {
for (config.paths) |item| std.testing.allocator.free(item);
std.testing.allocator.free(config.paths);
}
var config = try parseConfigSource(std.testing.allocator, source);
defer config.deinit(std.testing.allocator);
try std.testing.expectEqual(2, config.paths.len);
try std.testing.expectEqualStrings("src", config.paths[0]);
try std.testing.expectEqualStrings("lib", config.paths[1]);
Expand Down
23 changes: 19 additions & 4 deletions src/TypeResolver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,22 @@ fn findMethodInModule(self: *TypeResolver, module_path: []const u8, type_name: [
return self.findMethodInType(tree, type_node, method_name, mod.path);
}

/// Alias chains (`const a = b;`) are followed at most this many hops so
/// cyclic aliases (`const a = b; const b = a;`) cannot recurse unbounded.
const max_alias_depth: u32 = 32;

Comment thread
EugOT marked this conversation as resolved.
/// 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;
Expand Down Expand Up @@ -530,7 +544,7 @@ fn findMethodInFileAsStruct(self: *TypeResolver, module_path: []const u8, method
// It's an alias like `const ArrayListUnmanaged = ArrayList;`
// Follow the alias to find the actual function
const alias_target = tree.tokenSlice(tree.nodeMainToken(init_node));
return self.findMethodInFileAsStruct(module_path, alias_target);
return self.findMethodInFileAsStructDepth(module_path, alias_target, depth + 1);
} else if (init_tag == .fn_decl) {
// Inline function definition
var buf: [1]Ast.Node.Index = undefined;
Expand Down Expand Up @@ -822,9 +836,10 @@ fn resolveFieldAccess(self: *TypeResolver, tree: *const Ast, node: Ast.Node.Inde
return .{ .std_type = .{ .path = full_path } };
},
.user_type => {
// Accessing a field on a user type - could be a nested type
const full_path = self.buildStdTypePath(tree, node);
return .{ .std_type = .{ .path = full_path } };
// A field access on a user-defined type is not a stdlib type.
// Coercing it into `.std_type` conflated user namespaces with
// `std` ones and misrouted downstream semantic resolution.
return .unknown;
},
.unknown => {
const lhs_tag = tree.nodeTag(lhs_node);
Expand Down
17 changes: 15 additions & 2 deletions src/doc_comments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,21 @@ pub fn getDocComment(allocator: std.mem.Allocator, tree: *const Ast, node: Ast.N
if (tag == .doc_comment) {
doc_tokens.append(allocator, token) catch return null;
} else {
// Stop at non-doc-comment tokens (skip pub keyword)
if (tag != .keyword_pub) break;
// Stop at non-doc-comment tokens, skipping declaration
// qualifiers (`pub inline fn`, `pub extern "c" fn`, ...) that
// may sit between the doc comment and the declaration itself.
switch (tag) {
.keyword_pub,
.keyword_inline,
.keyword_noinline,
.keyword_extern,
.keyword_export,
.keyword_threadlocal,
.keyword_comptime,
.string_literal,
=> {},
else => break,
}
}
if (token == 0) break;
token -= 1;
Expand Down
37 changes: 27 additions & 10 deletions src/doc_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,21 @@ fn runDocTest(allocator: std.mem.Allocator, doc_path: []const u8, doc_test: DocT
}
}

// If no expectations, should have no diagnostics
if (doc_test.expected_rules.len == 0) {
const total = linter.diagnostics.items.len;
if (total > 0) {
std.debug.print("\n{s}:{d}: expected no diagnostics but got {d}\n", .{
// Reject diagnostics for rules that were not expected. Extra findings
// must not ride along silently just because one expectation matched —
// and with no expectations at all, any diagnostic is unexpected.
for (linter.diagnostics.items) |d| {
const expected = for (doc_test.expected_rules) |expected_rule| {
if (expected_rule == d.rule) break true;
} else false;
if (!expected) {
std.debug.print("\n{s}:{d}: unexpected {s} diagnostic: {s}\n", .{
doc_path,
doc_test.line_in_doc,
total,
d.rule.code(),
d.context,
});
std.debug.print("Code:\n{s}\n", .{doc_test.code});
for (linter.diagnostics.items) |d| {
std.debug.print(" - {s}: {s}\n", .{ d.rule.code(), d.context });
}
return error.UnexpectedDiagnostic;
}
}
Expand All @@ -175,6 +177,7 @@ pub fn runAllDocTests(allocator: std.mem.Allocator) !void {

var file_count: usize = 0;
var test_count: usize = 0;
var failure_count: usize = 0;
var iter = dir.iterate();
while (try iter.next(io)) |entry| {
if (entry.kind != .file) continue;
Expand All @@ -193,11 +196,25 @@ pub fn runAllDocTests(allocator: std.mem.Allocator) !void {
defer allocator.free(full_path);

for (doc.tests) |doc_test| {
try runDocTest(allocator, full_path, doc_test, &tmp_dir);
// Run every fixture before failing so one gate run reports all
// offending docs instead of stopping at the first. Only the two
// assertion errors count as fixture failures; infrastructure
// errors (I/O, OOM, ...) propagate immediately.
runDocTest(allocator, full_path, doc_test, &tmp_dir) catch |err| switch (err) {
error.MissingExpectedDiagnostic,
error.UnexpectedDiagnostic,
=> failure_count += 1,
else => return err,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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" {
Expand Down
Loading