mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
252 lines
8.3 KiB
Zig
252 lines
8.3 KiB
Zig
const std = @import("std");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const PathList = std.ArrayList([]u8);
|
|
|
|
const max_file_bytes: usize = 16 * 1024 * 1024;
|
|
|
|
const TermColor = struct {
|
|
pub const red = "\x1b[0;31m";
|
|
pub const green = "\x1b[0;32m";
|
|
pub const reset = "\x1b[0m";
|
|
};
|
|
|
|
pub fn main() !void {
|
|
var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa_impl.deinit();
|
|
const gpa = gpa_impl.allocator();
|
|
|
|
var stdout_buffer: [4096]u8 = undefined;
|
|
var stdout_state = std.fs.File.stdout().writer(&stdout_buffer);
|
|
const stdout = &stdout_state.interface;
|
|
|
|
var found_errors = false;
|
|
|
|
// Lint 1: Check for pub declarations without doc comments
|
|
try stdout.print("Checking for pub declarations without doc comments...\n", .{});
|
|
|
|
var zig_files = PathList{};
|
|
defer freePathList(&zig_files, gpa);
|
|
|
|
try walkTree(gpa, "src", &zig_files);
|
|
|
|
for (zig_files.items) |file_path| {
|
|
const errors = try checkPubDocComments(gpa, file_path);
|
|
defer gpa.free(errors);
|
|
|
|
if (errors.len > 0) {
|
|
try stdout.print("{s}", .{errors});
|
|
found_errors = true;
|
|
}
|
|
}
|
|
|
|
if (found_errors) {
|
|
try stdout.print("\n", .{});
|
|
try stdout.print("Please add doc comments to the spots listed above, they make the code easier to understand for everyone.\n", .{});
|
|
try stdout.print("\n", .{});
|
|
try stdout.flush();
|
|
std.process.exit(1);
|
|
}
|
|
|
|
// Lint 2: Check for top level comments in new Zig files
|
|
try stdout.print("Checking for top level comments in new Zig files...\n", .{});
|
|
|
|
var new_zig_files = try getNewZigFiles(gpa);
|
|
defer {
|
|
for (new_zig_files.items) |path| {
|
|
gpa.free(path);
|
|
}
|
|
new_zig_files.deinit(gpa);
|
|
}
|
|
|
|
if (new_zig_files.items.len == 0) {
|
|
try stdout.print("{s}[OK]{s} All lints passed!\n", .{ TermColor.green, TermColor.reset });
|
|
try stdout.flush();
|
|
return;
|
|
}
|
|
|
|
var failed_files = PathList{};
|
|
defer freePathList(&failed_files, gpa);
|
|
|
|
for (new_zig_files.items) |file_path| {
|
|
if (!try fileHasTopLevelComment(gpa, file_path)) {
|
|
try stdout.print("Error: {s} is missing top level comment (//!)\n", .{file_path});
|
|
try failed_files.append(gpa, try gpa.dupe(u8, file_path));
|
|
}
|
|
}
|
|
|
|
if (failed_files.items.len > 0) {
|
|
try stdout.print("\n", .{});
|
|
try stdout.print("The following files are missing a top level comment:\n", .{});
|
|
for (failed_files.items) |path| {
|
|
try stdout.print(" {s}\n", .{path});
|
|
}
|
|
try stdout.print("\n", .{});
|
|
try stdout.print("Add a //! comment BEFORE any other code that explains the purpose of the file.\n", .{});
|
|
try stdout.flush();
|
|
std.process.exit(1);
|
|
}
|
|
|
|
try stdout.print("{s}[OK]{s} All lints passed!\n", .{ TermColor.green, TermColor.reset });
|
|
try stdout.flush();
|
|
}
|
|
|
|
fn walkTree(allocator: Allocator, dir_path: []const u8, zig_files: *PathList) !void {
|
|
var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
|
|
defer dir.close();
|
|
|
|
var it = dir.iterate();
|
|
while (try it.next()) |entry| {
|
|
if (entry.kind == .sym_link) continue;
|
|
|
|
const next_path = try std.fs.path.join(allocator, &.{ dir_path, entry.name });
|
|
|
|
switch (entry.kind) {
|
|
.directory => {
|
|
// Skip .zig-cache directories
|
|
if (std.mem.eql(u8, entry.name, ".zig-cache")) {
|
|
allocator.free(next_path);
|
|
continue;
|
|
}
|
|
defer allocator.free(next_path);
|
|
try walkTree(allocator, next_path, zig_files);
|
|
},
|
|
.file => {
|
|
if (std.mem.endsWith(u8, entry.name, ".zig")) {
|
|
try zig_files.append(allocator, next_path);
|
|
} else {
|
|
allocator.free(next_path);
|
|
}
|
|
},
|
|
else => allocator.free(next_path),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn checkPubDocComments(allocator: Allocator, file_path: []const u8) ![]u8 {
|
|
const source = readSourceFile(allocator, file_path) catch |err| {
|
|
// Skip files we can't read
|
|
if (err == error.FileNotFound) return try allocator.dupe(u8, "");
|
|
return err;
|
|
};
|
|
defer allocator.free(source);
|
|
|
|
var errors = std.ArrayList(u8){};
|
|
errdefer errors.deinit(allocator);
|
|
|
|
var line_num: usize = 1;
|
|
var prev_line: []const u8 = "";
|
|
var lines = std.mem.splitScalar(u8, source, '\n');
|
|
|
|
while (lines.next()) |line| {
|
|
defer {
|
|
prev_line = line;
|
|
line_num += 1;
|
|
}
|
|
|
|
// Check if line starts with "pub " (no leading whitespace - only top-level declarations)
|
|
if (!std.mem.startsWith(u8, line, "pub ")) continue;
|
|
|
|
// Check if previous line is a doc comment (allow indented doc comments)
|
|
const prev_trimmed = std.mem.trimLeft(u8, prev_line, " \t");
|
|
if (std.mem.startsWith(u8, prev_trimmed, "///")) continue;
|
|
|
|
// Skip exceptions: init, deinit, @import, and pub const re-exports
|
|
// Note: "pub.*fn init\(" in bash matches "init" anywhere in function name
|
|
if (std.mem.indexOf(u8, line, "fn init") != null) continue;
|
|
if (std.mem.indexOf(u8, line, "fn deinit") != null) continue;
|
|
if (std.mem.indexOf(u8, line, "@import") != null) continue;
|
|
|
|
// Check for pub const re-exports (e.g., "pub const Foo = bar.Baz;")
|
|
if (isReExport(line)) continue;
|
|
|
|
try errors.writer(allocator).print("{s}:{d}: pub declaration without doc comment `///`\n", .{ file_path, line_num });
|
|
}
|
|
|
|
return errors.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn isReExport(line: []const u8) bool {
|
|
// Match pattern: pub const X = lowercase.something;
|
|
// This detects re-exports like "pub const Foo = bar.Baz;"
|
|
if (!std.mem.startsWith(u8, line, "pub const ")) return false;
|
|
|
|
// Find the '=' sign
|
|
const eq_pos = std.mem.indexOf(u8, line, "=") orelse return false;
|
|
const after_eq = std.mem.trimLeft(u8, line[eq_pos + 1 ..], " \t");
|
|
|
|
// Check if it starts with a lowercase letter (module reference)
|
|
if (after_eq.len == 0) return false;
|
|
const first_char = after_eq[0];
|
|
if (first_char < 'a' or first_char > 'z') return false;
|
|
|
|
// Check if it contains a dot and ends with semicolon (but not a function call)
|
|
if (std.mem.indexOf(u8, after_eq, ".") == null) return false;
|
|
if (std.mem.indexOf(u8, after_eq, "(") != null) return false;
|
|
if (!std.mem.endsWith(u8, std.mem.trimRight(u8, after_eq, " \t"), ";")) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
fn getNewZigFiles(allocator: Allocator) !PathList {
|
|
var result = PathList{};
|
|
errdefer {
|
|
for (result.items) |path| {
|
|
allocator.free(path);
|
|
}
|
|
result.deinit(allocator);
|
|
}
|
|
|
|
// Run git diff to get new files
|
|
var child = std.process.Child.init(&.{ "git", "diff", "--name-only", "--diff-filter=A", "origin/main", "HEAD", "--", "src/" }, allocator);
|
|
child.stdout_behavior = .Pipe;
|
|
child.stderr_behavior = .Ignore;
|
|
|
|
_ = child.spawn() catch {
|
|
// Git not available or not in a repo - return empty list
|
|
return result;
|
|
};
|
|
|
|
const stdout = child.stdout orelse return result;
|
|
const output = stdout.readToEndAlloc(allocator, max_file_bytes) catch return result;
|
|
defer allocator.free(output);
|
|
|
|
const term = child.wait() catch return result;
|
|
if (term.Exited != 0) return result;
|
|
|
|
// Parse output line by line
|
|
var lines = std.mem.splitScalar(u8, output, '\n');
|
|
while (lines.next()) |line| {
|
|
if (line.len == 0) continue;
|
|
if (!std.mem.endsWith(u8, line, ".zig")) continue;
|
|
|
|
try result.append(allocator, try allocator.dupe(u8, line));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
fn fileHasTopLevelComment(allocator: Allocator, file_path: []const u8) !bool {
|
|
const source = try readSourceFile(allocator, file_path);
|
|
defer allocator.free(source);
|
|
|
|
return std.mem.indexOf(u8, source, "//!") != null;
|
|
}
|
|
|
|
fn readSourceFile(allocator: Allocator, path: []const u8) ![:0]u8 {
|
|
return try std.fs.cwd().readFileAllocOptions(
|
|
allocator,
|
|
path,
|
|
max_file_bytes,
|
|
null,
|
|
std.mem.Alignment.of(u8),
|
|
0,
|
|
);
|
|
}
|
|
|
|
fn freePathList(list: *PathList, allocator: Allocator) void {
|
|
for (list.items) |path| {
|
|
allocator.free(path);
|
|
}
|
|
list.deinit(allocator);
|
|
}
|