Skip to content

Commit 7f63e17

Browse files
Add Lint/VoidOutsideLib
1 parent 26db705 commit 7f63e17

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
require "../../../spec_helper"
2+
3+
module Ameba::Rule::Lint
4+
describe VoidOutsideLib do
5+
subject = VoidOutsideLib.new
6+
7+
it "passes if Void is used in a fun def" do
8+
expect_no_issues subject, <<-CRYSTAL
9+
lib LibFoo
10+
fun bar(baz : Void) : Void
11+
end
12+
CRYSTAL
13+
end
14+
15+
it "passes if `Pointer(Void)` is used as a parameter type restriction" do
16+
expect_no_issues subject, <<-CRYSTAL
17+
def foo(bar : Pointer(Void))
18+
end
19+
CRYSTAL
20+
end
21+
22+
it "fails if `Void` is used as a parameter type restriction" do
23+
expect_issue subject, <<-CRYSTAL
24+
def foo(bar : Void)
25+
# ^^^^ error: `Void` is not allowed in this context
26+
end
27+
CRYSTAL
28+
end
29+
30+
it "passes if `Pointer(Void)` is used as return type restriction" do
31+
expect_no_issues subject, <<-CRYSTAL
32+
def foo(bar) : Pointer(Void)
33+
end
34+
CRYSTAL
35+
end
36+
37+
it "passes if `Pointer(Void) | Nil` is used as return type restriction" do
38+
expect_no_issues subject, <<-CRYSTAL
39+
def foo(bar) : Pointer(Void) | Nil
40+
end
41+
CRYSTAL
42+
end
43+
44+
it "fails if `Pointer(Void | Int32)` is used as return type restriction" do
45+
expect_issue subject, <<-CRYSTAL
46+
def foo(bar) : Pointer(Void | Int32)
47+
# ^^^^ error: `Void` is not allowed in this context
48+
end
49+
CRYSTAL
50+
end
51+
52+
it "fails if `Array(Void)` is used as return type restriction" do
53+
expect_issue subject, <<-CRYSTAL
54+
def foo(bar) : Array(Void)
55+
# ^^^^ error: `Void` is not allowed in this context
56+
end
57+
CRYSTAL
58+
end
59+
60+
it "passes if Void is used as name of a class" do
61+
expect_no_issues subject, <<-CRYSTAL
62+
class Foo
63+
class Void
64+
end
65+
end
66+
CRYSTAL
67+
end
68+
69+
it "fails if Void is inherited from" do
70+
expect_issue subject, <<-CRYSTAL
71+
struct Foo < Void
72+
# ^^^^ error: `Void` is not allowed in this context
73+
end
74+
CRYSTAL
75+
end
76+
77+
it "passes if Void is name of alias" do
78+
expect_no_issues subject, <<-CRYSTAL
79+
alias Void = Foo
80+
CRYSTAL
81+
end
82+
83+
it "fails if Void is value of alias" do
84+
expect_issue subject, <<-CRYSTAL
85+
alias Foo = Void
86+
# ^^^^ error: `Void` is not allowed in this context
87+
CRYSTAL
88+
end
89+
90+
it "fails if `Void` is used for an uninitialized var" do
91+
expect_issue subject, <<-CRYSTAL
92+
var = uninitialized Void
93+
# ^^^^ error: `Void` is not allowed in this context
94+
CRYSTAL
95+
end
96+
end
97+
end
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module Ameba::Rule::Lint
2+
# A rule that disallows uses of `Void` outside C lib bindings.
3+
# Usages of these outside of C lib bindings don't make sense,
4+
# and can sometimes break the compiler. `Nil` should be used instead in these cases.
5+
# `Pointer(Void)` is the only case that's allowed per this rule.
6+
#
7+
# These are considered invalid:
8+
#
9+
# ```
10+
# def foo(bar : Void) : Slice(Void)?
11+
# end
12+
#
13+
# alias Baz = Void
14+
#
15+
# struct Qux < Void
16+
# end
17+
# ```
18+
#
19+
# YAML configuration example:
20+
#
21+
# ```
22+
# Lint/VoidOutsideLib:
23+
# Enabled: true
24+
# ```
25+
class VoidOutsideLib < Base
26+
properties do
27+
since_version "1.7.0"
28+
description "Disallows use of `Void` outside C lib bindings and `Pointer(Void)`"
29+
end
30+
31+
MSG = "`Void` is not allowed in this context"
32+
33+
def test(source)
34+
PathGenericUnionVisitor.new self, source, skip: [Crystal::LibDef]
35+
end
36+
37+
def test(source, node : Crystal::Path)
38+
return unless node.names.join("::") == "Void"
39+
40+
issue_for node, MSG
41+
end
42+
43+
def test(source, node : Crystal::Generic)
44+
# Specifically only allow `Pointer(Void)`
45+
return if (name = node.name).is_a?(Crystal::Path) &&
46+
name.names.join("::") == "Pointer" &&
47+
node.type_vars.size == 1 &&
48+
(var = node.type_vars.first).is_a?(Crystal::Path) &&
49+
var.names.join("::") == "Void"
50+
51+
if (name = node.name).is_a?(Crystal::Path) &&
52+
name.names.join("::") == "Void"
53+
issue_for node, MSG, prefer_name_location: true
54+
end
55+
56+
node.type_vars.each do |type_var|
57+
test(source, type_var)
58+
end
59+
end
60+
61+
def test(source, node : Crystal::Union)
62+
node.types.each do |type|
63+
test(source, type)
64+
end
65+
end
66+
67+
private class PathGenericUnionVisitor < AST::NodeVisitor
68+
def visit(node : Crystal::Generic | Crystal::Path | Crystal::Union)
69+
return false if skip?(node)
70+
71+
@rule.test @source, node
72+
false
73+
end
74+
end
75+
end
76+
end

0 commit comments

Comments
 (0)