Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): add line-length rule #232

Merged
merged 15 commits into from
Feb 20, 2025
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
36 changes: 36 additions & 0 deletions docs/rules/line-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# `line-length`

> Category: style
>
> Enabled by default?: No

## What This Rule Does

Checks if any line goes beyond a given number of columns.

## Examples

Examples of **incorrect** code for this rule (with a threshold of 120 columns):

```zig
const std = @import("std");
const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 };
fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 {
return 123;
}
```

Examples of **correct** code for this rule (with a threshold of 120 columns):

```zig
const std = @import("std");
const longStructInMultipleLines = struct {
max_length: u32 = 120,
a: usize = 123,
b: usize = 12354,
c: usize = 1234352,
};
fn Get123Constant() u32 {
return 123;
}
```
2 changes: 1 addition & 1 deletion src/linter/config/rule_config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn RuleConfig(RuleImpl: type) type {
return Self{ .severity = severity, .rule_impl = rule_impl };
}
pub fn rule(self: *Self) Rule {
const rule_impl: *RuleImpl = @ptrCast(@constCast(self.rule_impl));
const rule_impl: *RuleImpl = @ptrCast(@alignCast(@constCast(self.rule_impl)));
return rule_impl.rule();
}
};
Expand Down
1 change: 1 addition & 0 deletions src/linter/config/rules_config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ pub const RulesConfig = struct {
unsafe_undefined: RuleConfig(rules.UnsafeUndefined) = .{},
unused_decls: RuleConfig(rules.UnusedDecls) = .{},
must_return_ref: RuleConfig(rules.MustReturnRef) = .{},
line_length: RuleConfig(rules.LineLength) = .{},
};
5 changes: 4 additions & 1 deletion src/linter/rule.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const VTable = struct {
/// ## Creating Rules
/// Rules are structs (or anything else that can have methods) that have 1 or
/// more of the following methods:
/// - `runOnce(*self, *ctx)`
/// - `runOnNode(*self, node, *ctx)`
/// - `runOnSymbol(*self, symbol, *ctx)`
/// `Rule` provides a uniform interface to `Linter`. `Rule.init` will look for
Expand All @@ -45,6 +46,7 @@ pub const Rule = struct {
id: Id,
ptr: *anyopaque,
vtable: VTable,
// runOnceFn: RunOnceFn,
// runOnNodeFn: RunOnNodeFn,
// runOnSymbolFn: RunOnSymbolFn,

Expand All @@ -70,6 +72,7 @@ pub const Rule = struct {
suspicious,
restriction,
pedantic,
style,
};

pub const WithSeverity = struct {
Expand Down Expand Up @@ -100,7 +103,7 @@ pub const Rule = struct {
const gen = struct {
pub fn runOnce(pointer: *const anyopaque, ctx: *LinterContext) anyerror!void {
if (@hasDecl(ptr_info.child, "runOnce")) {
const self: T = @ptrCast(@constCast(pointer));
const self: T = @ptrCast(@alignCast(@constCast(pointer)));
return ptr_info.child.runOnce(self, ctx);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/linter/rules.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub const SuppressedErrors = @import("./rules/suppressed_errors.zig");
pub const UnsafeUndefined = @import("./rules/unsafe_undefined.zig");
pub const UnusedDecls = @import("./rules/unused_decls.zig");
pub const MustReturnRef = @import("./rules/must_return_ref.zig");
pub const LineLength = @import("./rules/line_length.zig");
125 changes: 125 additions & 0 deletions src/linter/rules/line_length.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! ## What This Rule Does
//!
//! Checks if any line goes beyond a given number of columns.
//!
//! ## Examples
//!
//! Examples of **incorrect** code for this rule (with a threshold of 120 columns):
//! ```zig
//! const std = @import("std");
//! const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 };
//! fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 {
//! return 123;
//! }
//! ```
//!
//! Examples of **correct** code for this rule (with a threshold of 120 columns):
//! ```zig
//! const std = @import("std");
//! const longStructInMultipleLines = struct {
//! max_length: u32 = 120,
//! a: usize = 123,
//! b: usize = 12354,
//! c: usize = 1234352,
//! };
//! fn Get123Constant() u32 {
//! return 123;
//! }
//! ```

const std = @import("std");
const _rule = @import("../rule.zig");
const span = @import("../../span.zig");

const LinterContext = @import("../lint_context.zig");
const Rule = _rule.Rule;
const Error = @import("../../Error.zig");
const LabeledSpan = span.LabeledSpan;

max_length: u32 = 120,

const LineLength = @This();
pub const meta: Rule.Meta = .{
.name = "line-length",
.category = .style,
.default = .off,
};

pub fn lineLengthDiagnostic(ctx: *LinterContext, line_start: u32, line_length: u32) Error {
return ctx.diagnosticf(
"line length of {} characters is too big.",
.{line_length},
.{LabeledSpan.unlabeled(line_start, line_start + line_length)},
);
}

pub fn getNewlineOffset(line: []const u8) u32 {
if (line.len > 1 and line[line.len - 2] == '\r') {
return 2;

Check warning on line 58 in src/linter/rules/line_length.zig

View check run for this annotation

Codecov / codecov/patch

src/linter/rules/line_length.zig#L58

Added line #L58 was not covered by tests
}
return 1;
}

pub fn runOnce(self: *const LineLength, ctx: *LinterContext) void {
var line_start_idx: u32 = 0;
var lines = std.mem.splitSequence(u8, ctx.source.text(), "\n");
const newline_offset = getNewlineOffset(lines.first());
lines.reset();
while (lines.next()) |line| {
const line_length = @as(u32, @intCast(line.len));
if (line.len > self.max_length) {
ctx.report(lineLengthDiagnostic(ctx, line_start_idx, line_length));
}
line_start_idx += line_length + newline_offset;
}
}

pub fn rule(self: *LineLength) Rule {
return Rule.init(self);
}

const RuleTester = @import("../tester.zig");
test LineLength {
const t = std.testing;

var line_length = LineLength{};
var runner = RuleTester.init(t.allocator, line_length.rule());
defer runner.deinit();

const pass = &[_][:0]const u8{
\\const std = @import("std");
\\fn foo() std.mem.Allocator.Error!void {
\\ _ = try std.heap.page_allocator.alloc(u8, 8);
\\}
,
\\const std = @import("std");
\\const longStructInMultipleLines = struct {
\\ max_length: u32 = 120,
\\ a: usize = 123,
\\ b: usize = 12354,
\\ c: usize = 1234352,
\\};
\\fn Get123Constant() u32 {
\\ return 123;
\\}
};

const fail = &[_][:0]const u8{
\\const std = @import("std");
\\fn foo() std.mem.Allocator.Error!void {
\\ // ok so this is a super unnecessary line that is artificially being made long through this self-referential comment thats keeps on going until hitting a number of columns that violates the rule
\\ _ = try std.heap.page_allocator.alloc(u8, 8);
\\}
,
\\const std = @import("std");
\\const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 };
\\fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 {
\\ return 123;
\\}
};

try runner

Check warning on line 121 in src/linter/rules/line_length.zig

View check run for this annotation

Codecov / codecov/patch

src/linter/rules/line_length.zig#L121

Added line #L121 was not covered by tests
.withPass(pass)
.withFail(fail)
.run();
}
24 changes: 24 additions & 0 deletions src/linter/rules/snapshots/line-length.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
𝙭 line-length: line length of 196 characters is too big.
╭─[line-length.zig:3:1]
2 │ fn foo() std.mem.Allocator.Error!void {
3 │ // ok so this is a super unnecessary line that is artificially being made long through this self-referential comment thats keeps on going until hitting a number of columns that violates the rule
· ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
4 │ _ = try std.heap.page_allocator.alloc(u8, 8);
╰────

𝙭 line-length: line length of 126 characters is too big.
╭─[line-length.zig:2:1]
1 │ const std = @import("std");
2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 };
· ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 {
╰────

𝙭 line-length: line length of 151 characters is too big.
╭─[line-length.zig:3:1]
2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 };
3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 {
· ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
4 │ return 123;
╰────