From c3765528d0340a2d6f6c2277aee468e5fc72194e Mon Sep 17 00:00:00 2001 From: Kirk Scheibelhut Date: Fri, 28 Jul 2023 00:35:55 -0700 Subject: [PATCH] Add file_as_struct rule (#12) Check for file name capitalization in the presence of top level fields. --- README.md | 16 +++++++++++ src/analysis.zig | 17 ++++++++++-- src/main.zig | 21 ++++++++++++++- src/rules/file_as_struct.zig | 33 +++++++++++++++++++++++ testcases/file_as_struct/NotStruct.zig | 3 +++ testcases/file_as_struct/Structure.zig | 2 ++ testcases/file_as_struct/exitcode.txt | 1 + testcases/file_as_struct/notstructure.zig | 3 +++ testcases/file_as_struct/output.txt | 2 ++ testcases/file_as_struct/struct.zig | 2 ++ testcases/file_as_struct/ziglint.json | 3 +++ 11 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 src/rules/file_as_struct.zig create mode 100644 testcases/file_as_struct/NotStruct.zig create mode 100644 testcases/file_as_struct/Structure.zig create mode 100644 testcases/file_as_struct/exitcode.txt create mode 100644 testcases/file_as_struct/notstructure.zig create mode 100644 testcases/file_as_struct/output.txt create mode 100644 testcases/file_as_struct/struct.zig create mode 100644 testcases/file_as_struct/ziglint.json diff --git a/README.md b/README.md index 26796e8..58c99ea 100644 --- a/README.md +++ b/README.md @@ -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 ``` \ No newline at end of file diff --git a/src/analysis.zig b/src/analysis.zig index 8f5e914..25b2c3b 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -62,6 +62,8 @@ 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 { @@ -69,6 +71,7 @@ pub const ASTAnalyzer = struct { 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; @@ -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 @@ -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); @@ -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); diff --git a/src/main.zig b/src/main.zig index 29db115..98f9e98 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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, @@ -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 \\ @@ -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")) { @@ -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; @@ -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 }, diff --git a/src/rules/file_as_struct.zig b/src/rules/file_as_struct.zig new file mode 100644 index 0000000..aa8878a --- /dev/null +++ b/src/rules/file_as_struct.zig @@ -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 }, + }); + } + } +}; diff --git a/testcases/file_as_struct/NotStruct.zig b/testcases/file_as_struct/NotStruct.zig new file mode 100644 index 0000000..2f83b58 --- /dev/null +++ b/testcases/file_as_struct/NotStruct.zig @@ -0,0 +1,3 @@ +export fn foo(bar: u32) u32 { + return bar + 64; +} diff --git a/testcases/file_as_struct/Structure.zig b/testcases/file_as_struct/Structure.zig new file mode 100644 index 0000000..9dc72b3 --- /dev/null +++ b/testcases/file_as_struct/Structure.zig @@ -0,0 +1,2 @@ +foo: u32, +bar: f64, diff --git a/testcases/file_as_struct/exitcode.txt b/testcases/file_as_struct/exitcode.txt new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/testcases/file_as_struct/exitcode.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/testcases/file_as_struct/notstructure.zig b/testcases/file_as_struct/notstructure.zig new file mode 100644 index 0000000..2f83b58 --- /dev/null +++ b/testcases/file_as_struct/notstructure.zig @@ -0,0 +1,3 @@ +export fn foo(bar: u32) u32 { + return bar + 64; +} diff --git a/testcases/file_as_struct/output.txt b/testcases/file_as_struct/output.txt new file mode 100644 index 0000000..aa2798a --- /dev/null +++ b/testcases/file_as_struct/output.txt @@ -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 diff --git a/testcases/file_as_struct/struct.zig b/testcases/file_as_struct/struct.zig new file mode 100644 index 0000000..9dc72b3 --- /dev/null +++ b/testcases/file_as_struct/struct.zig @@ -0,0 +1,2 @@ +foo: u32, +bar: f64, diff --git a/testcases/file_as_struct/ziglint.json b/testcases/file_as_struct/ziglint.json new file mode 100644 index 0000000..8cc22c2 --- /dev/null +++ b/testcases/file_as_struct/ziglint.json @@ -0,0 +1,3 @@ +{ + "file_as_struct": true +} \ No newline at end of file