Skip to content

Commit

Permalink
Add file_as_struct rule (#12)
Browse files Browse the repository at this point in the history
Check for file name capitalization in the presence of top level
fields.
  • Loading branch information
scheibo authored Jul 28, 2023
1 parent 7539f46 commit c376552
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 3 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,20 @@ This rule checks for cases where `@import` is called multiple times with the sam
### Command line
```bash
ziglint --dupe-import
```

## `file_as_struct`
This rule checks for file name capitalization in the presence of top level fields. Files with top
level fields can be treated as structs and per Zig [naming
conventions](https://ziglang.org/documentation/master/#Names) for types should be capitalized,
otherwise file names should not be capitalized.
### `ziglint.json`
```json
{
"file_as_struct": true
}
```
### Command line
```bash
ziglint --file-as-struct
```
17 changes: 15 additions & 2 deletions src/analysis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ pub const SourceCodeFaultType = union(enum) {
ASTError,
// The source code is not formatted according to Zig standards.
ImproperlyFormatted,
// File is incorrectly capitalized. Value is true if the file should be capitalized.
FileAsStruct: bool,
};

pub const ASTAnalyzer = struct {
// 0 for no checking
max_line_length: u32 = 100,
check_format: bool = true,
dupe_import: bool = false,
file_as_struct: bool = false,

pub fn set_max_line_length(self: *ASTAnalyzer, max_line_length: u32) void {
self.max_line_length = max_line_length;
Expand All @@ -82,7 +85,12 @@ pub const ASTAnalyzer = struct {
//
// Caller must deinit the array.
// TODO: can just return a slice?
pub fn analyze(self: *const ASTAnalyzer, alloc: std.mem.Allocator, tree: std.zig.Ast) !SourceCodeFaultTracker {
pub fn analyze(
self: *const ASTAnalyzer,
alloc: std.mem.Allocator,
file_name: []const u8,
tree: std.zig.Ast,
) !SourceCodeFaultTracker {
var faults = SourceCodeFaultTracker.new(alloc);

// Enforce line length as needed
Expand Down Expand Up @@ -132,6 +140,11 @@ pub const ASTAnalyzer = struct {
}
}

var file_as_struct = @import("rules/file_as_struct.zig").FileAsStruct{};

// per-tree rules
if (self.file_as_struct) try file_as_struct.check_tree(alloc, &faults, file_name, tree);

// TODO: look through AST nodes for other rule enforcements
var check_format = @import("rules/check_format.zig").CheckFormat{};
var dupe_import = @import("rules/dupe_import.zig").DupeImport.init(alloc);
Expand Down Expand Up @@ -171,7 +184,7 @@ const Tests = struct {
var tree = try std.zig.Ast.parse(std.testing.allocator, case.source, .zig);
defer tree.deinit(std.testing.allocator);

var faults = try analyzer.analyze(std.testing.allocator, tree);
var faults = try analyzer.analyze(std.testing.allocator, "name", tree);
defer faults.deinit();

try std.testing.expectEqual(case.expected_faults.len, faults.faults.items.len);
Expand Down
21 changes: 20 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const Configuration = struct {
max_line_length: ?u32 = null,
check_format: ?bool = null,
dupe_import: ?bool = null,
file_as_struct: ?bool = null,
include_gitignored: ?bool = null,
verbose: ?bool = null,
exclude: ?[][]const u8 = null,
Expand Down Expand Up @@ -103,6 +104,9 @@ fn show_help() !void {
\\ --dupe-import
\\ check for cases where @import is called multiple times with the same value within a file
\\
\\ --file-as-struct
\\ check for file name capitalization in the presence of top level fields
\\
\\ --include-gitignored
\\ lint files excluded by .gitignore directives
\\
Expand Down Expand Up @@ -195,6 +199,8 @@ pub fn main() anyerror!void {
switches.check_format = true;
} else if (std.mem.eql(u8, switch_name, "dupe-import")) {
switches.dupe_import = true;
} else if (std.mem.eql(u8, switch_name, "file-as-struct")) {
switches.file_as_struct = true;
} else if (std.mem.eql(u8, switch_name, "include-gitignored")) {
switches.include_gitignored = true;
} else if (std.mem.eql(u8, switch_name, "verbose")) {
Expand Down Expand Up @@ -441,7 +447,7 @@ fn lint(

var ast = try std.zig.Ast.parse(alloc, contents, .zig);
defer ast.deinit(alloc);
var faults = try analyzer.analyze(alloc, ast);
var faults = try analyzer.analyze(alloc, file_name, ast);
defer faults.deinit();
fault_count += faults.faults.items.len;

Expand Down Expand Up @@ -475,6 +481,19 @@ fn lint(
"found {s}duplicate import{s} of {s}",
.{ red_text, end_text_fmt, name },
),
.FileAsStruct => |capitalize| {
if (capitalize) {
try stdout_writer.print(
"found top level fields, file name should be {s}capitalized{s}",
.{ red_text, end_text_fmt },
);
} else {
try stdout_writer.print(
"found no top level fields, file name should be {s}lowercase{s}",
.{ red_text, end_text_fmt },
);
}
},
.ImproperlyFormatted => try stdout_writer.print(
"the file is {s}improperly formatted{s}; try using `zig fmt` to fix it",
.{ red_text, end_text_fmt },
Expand Down
33 changes: 33 additions & 0 deletions src/rules/file_as_struct.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Check for file name capitalization in the presence of top level fields.

const std = @import("std");
const analysis = @import("../analysis.zig");

pub const FileAsStruct = struct {
pub fn check_tree(
self: *FileAsStruct,
allocator: std.mem.Allocator,
fault_tracker: *analysis.SourceCodeFaultTracker,
file_name: []const u8,
tree: std.zig.Ast,
) !void {
_ = self;
_ = allocator;

const tags = tree.nodes.items(.tag);
const rootDecls = tree.rootDecls();

const has_top_level_fields = for (rootDecls) |item| {
if (tags[item].isContainerField()) break true;
} else false;

const capitalized = std.ascii.isUpper(std.fs.path.basename(file_name)[0]);
if (has_top_level_fields != capitalized) {
try fault_tracker.add(analysis.SourceCodeFault{
.line_number = 1,
.column_number = 1,
.fault_type = .{ .FileAsStruct = !capitalized },
});
}
}
};
3 changes: 3 additions & 0 deletions testcases/file_as_struct/NotStruct.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export fn foo(bar: u32) u32 {
return bar + 64;
}
2 changes: 2 additions & 0 deletions testcases/file_as_struct/Structure.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo: u32,
bar: f64,
1 change: 1 addition & 0 deletions testcases/file_as_struct/exitcode.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
3 changes: 3 additions & 0 deletions testcases/file_as_struct/notstructure.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export fn foo(bar: u32) u32 {
return bar + 64;
}
2 changes: 2 additions & 0 deletions testcases/file_as_struct/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
./struct.zig:1:1: found top level fields, file name should be capitalized
./NotStruct.zig:1:1: found no top level fields, file name should be lowercase
2 changes: 2 additions & 0 deletions testcases/file_as_struct/struct.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo: u32,
bar: f64,
3 changes: 3 additions & 0 deletions testcases/file_as_struct/ziglint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"file_as_struct": true
}

0 comments on commit c376552

Please sign in to comment.