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 11 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) = .{},
};
2 changes: 2 additions & 0 deletions src/linter/lint_context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
const mem = std.mem;
const util = @import("util");
const _rule = @import("rule.zig");
const _span = @import("../span.zig");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove dead code (zlint --fix-dangerously)

const _source = @import("../source.zig");
const _semantic = @import("../semantic.zig");

Expand All @@ -264,6 +265,7 @@
const Severity = Error.Severity;
const LabeledSpan = @import("../span.zig").LabeledSpan;
const Rule = _rule.Rule;
const Line = _span.Line;

Check warning on line 268 in src/linter/lint_context.zig

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'Line' is declared but never used.
const Semantic = _semantic.Semantic;
const Source = _source.Source;
const string = util.string;
Expand Down
1 change: 1 addition & 0 deletions src/linter/linter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
const Severity = Error.Severity;
const Source = _source.Source;
const Semantic = _semantic.Semantic;
const SemanticBuilder = _semantic.SemanticBuilder;

Check warning on line 15 in src/linter/linter.zig

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'SemanticBuilder' is declared but never used.

const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const assert = std.debug.assert;
const fs = std.fs;

Check warning on line 20 in src/linter/linter.zig

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'fs' is declared but never used.

const Rule = _rule.Rule;
const RuleSet = @import("RuleSet.zig");
const NodeWrapper = _rule.NodeWrapper;
const Line = _rule.Line;

Check warning on line 25 in src/linter/linter.zig

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'Line' is declared but never used.

pub const Config = @import("Config.zig");

Expand Down
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 builtin = @import("builtin");

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

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'builtin' is declared but never used.
const std = @import("std");
const util = @import("util");
const semantic = @import("../../semantic.zig");
const _rule = @import("../rule.zig");
const span = @import("../../span.zig");

const Ast = std.zig.Ast;
const Node = Ast.Node;

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

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'Node' is declared but never used.
const Scope = semantic.Scope;

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

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'Scope' is declared but never used.
const LinterContext = @import("../lint_context.zig");
const Rule = _rule.Rule;
const NodeWrapper = _rule.NodeWrapper;

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

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'NodeWrapper' is declared but never used.
const Symbol = semantic.Symbol;

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

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'Symbol' is declared but never used.
const Error = @import("../../Error.zig");
const Cow = util.Cow(false);

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

View workflow job for this annotation

GitHub Actions / Lint Changed

unused-decls

variable 'Cow' is declared but never used.
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, threshold: u32, line_length: u32) Error {
return ctx.diagnosticf(
"line length of {} characters is too big.",
.{line_length},
.{LabeledSpan.unlabeled(line_start + threshold, line_start + line_length)},
);
}

pub fn runOnce(self: *const LineLength, ctx: *LinterContext) void {
var line_start_idx: u32 = 0;
var lines = std.mem.splitSequence(u8, ctx.source.text(), util.NEWLINE);
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, self.max_length, line_length));
}
line_start_idx += line_length + @as(u32, @intCast(util.NEWLINE.len));
}
}

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:121]
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:121]
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:121]
2 │ const longStructDeclarationInOneLine = struct { max_length: u32 = 120, a: usize = 123, b: usize = 12354, c: usize = 1234352 };
3 │ fn reallyExtraVerboseFunctionNameToThePointOfBeingACodeSmellAndProbablyAHintThatYouCanGetAwayWithAnotherNameOrSplittingThisIntoSeveralFunctions() u32 {
· ───────────────────────────────
4 │ return 123;
╰────

16 changes: 1 addition & 15 deletions src/reporter/formatters/GraphicalFormatter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -406,21 +406,6 @@ fn eatNewlineAfter(src: []const u8, i: *u32) void {
}
}

const Line = struct {
/// 1-indexed line number. 0 used for omitted/null lines.
num: u32,
/// byte offset of the start of the line
offset: u32,
/// String contents of the line. Can be used to get the line's length.
contents: []const u8,

pub const EMPTY = Line{ .num = 0, .offset = 0, .contents = "" };

pub inline fn len(self: Line) u32 {
return @intCast(self.contents.len);
}
};

const ContextInfo = struct {
span: LabeledSpan,
location: Location,
Expand Down Expand Up @@ -484,6 +469,7 @@ const _span = @import("../../span.zig");
const Span = _span.Span;
const LabeledSpan = _span.LabeledSpan;
const Location = _span.Location;
const Line = _span.Line;

const Error = @import("../../Error.zig");

Expand Down
15 changes: 15 additions & 0 deletions src/span.zig
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@ test "Span.shiftLeft" {
try t.expectEqual(Span.new(5, 7), span); // original span is not mutated
}

pub const Line = struct {
/// 1-indexed line number. 0 used for omitted/null lines.
num: u32,
/// byte offset of the start of the line
offset: u32,
/// String contents of the line. Can be used to get the line's length.
contents: []const u8,

pub const EMPTY = Line{ .num = 0, .offset = 0, .contents = "" };

pub inline fn len(self: Line) u32 {
return @intCast(self.contents.len);
}
};

pub const LabeledSpan = struct {
span: Span,
label: ?util.Cow(false) = null,
Expand Down
Loading