Skip to content

Commit a1a45ec

Browse files
authored
Add style/method-declaration-format and style/type-parameter-format lint rules (#4892)
Two new pony-lint rules that check formatting of multiline declarations per the style guide: - method-declaration-format: each param on its own line, return type ':' indented one level from the method keyword, '=>' aligned with it. - type-parameter-format: '[' on same line as name, each type param on its own line, 'is' keyword indented one level from entity keyword. Also applies the new rules to pony-lint's own source (glob_match.pony, _ast_test_helper.pony, _test_type_naming.pony).
1 parent b68f28b commit a1a45ec

File tree

9 files changed

+1109
-9
lines changed

9 files changed

+1109
-9
lines changed

tools/pony-lint/glob_match.pony

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ primitive GlobMatch
1818
_matches(pattern, 0, pattern.size(), text, 0, text.size())
1919

2020
fun _matches(
21-
p: String val, ps: USize, pe: USize,
22-
t: String val, ts: USize, te: USize)
21+
p: String val,
22+
ps: USize,
23+
pe: USize,
24+
t: String val,
25+
ts: USize,
26+
te: USize)
2327
: Bool
2428
=>
2529
"""
@@ -100,8 +104,12 @@ primitive GlobMatch
100104
_match_segment(p, ps, pe, t, ts, te)
101105

102106
fun _match_leading_star(
103-
p: String val, ps: USize, pe: USize,
104-
t: String val, ts: USize, te: USize)
107+
p: String val,
108+
ps: USize,
109+
pe: USize,
110+
t: String val,
111+
ts: USize,
112+
te: USize)
105113
: Bool
106114
=>
107115
"""
@@ -119,8 +127,12 @@ primitive GlobMatch
119127
false
120128

121129
fun _match_segment(
122-
p: String val, ps: USize, pe: USize,
123-
t: String val, ts: USize, te: USize)
130+
p: String val,
131+
ps: USize,
132+
pe: USize,
133+
t: String val,
134+
ts: USize,
135+
te: USize)
124136
: Bool
125137
=>
126138
"""

tools/pony-lint/main.pony

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ actor Main
9393
.> push(ControlStructureAlignment)
9494
.> push(TypeAliasFormat)
9595
.> push(ArrayLiteralFormat)
96+
.> push(MethodDeclarationFormat)
97+
.> push(TypeParameterFormat)
9698
end
9799

98100
// Handle --explain
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use ast = "pony_compiler"
2+
3+
primitive MethodDeclarationFormat is ASTRule
4+
"""
5+
Checks formatting of multiline method declarations per the style guide.
6+
7+
When a method's parameters span multiple lines, checks:
8+
- Each parameter is on its own line (no two parameters sharing a line).
9+
10+
When a method declaration spans multiple lines, checks:
11+
- The return type ':' is at the method keyword's column + 2 (one indent
12+
level deeper) when it appears as the first non-whitespace on its line.
13+
- The '=>' is at the method keyword's column when it appears as the first
14+
non-whitespace on its line.
15+
16+
Single-line method declarations are not checked.
17+
"""
18+
fun id(): String val => "style/method-declaration-format"
19+
fun category(): String val => "style"
20+
21+
fun description(): String val =>
22+
"multiline method declaration formatting"
23+
+ " (parameter layout, return type and '=>' alignment)"
24+
25+
fun default_status(): RuleStatus => RuleOn
26+
27+
fun node_filter(): Array[ast.TokenId] val =>
28+
[ ast.TokenIds.tk_fun()
29+
ast.TokenIds.tk_new()
30+
ast.TokenIds.tk_be() ]
31+
32+
fun check(node: ast.AST box, source: SourceFile val)
33+
: Array[Diagnostic val] val
34+
=>
35+
"""
36+
Check formatting of a multiline method declaration.
37+
"""
38+
let keyword_line = node.line()
39+
let keyword_col = node.pos()
40+
41+
// Determine the keyword name for diagnostic messages
42+
let keyword_name = _keyword_name(node.id())
43+
44+
let diags = recover iso Array[Diagnostic val] end
45+
46+
// Check params (child 3)
47+
try
48+
let params = node(3)?
49+
if params.id() != ast.TokenIds.tk_none() then
50+
let num_params = params.num_children()
51+
if num_params > 1 then
52+
// Find line of first and last param to determine if multiline
53+
try
54+
let first_param = params(0)?
55+
let first_line = _param_line(first_param)
56+
let last_line = _last_param_line(params)
57+
if first_line != last_line then
58+
// Multiline params: check each is on its own line
59+
var prev_line = first_line
60+
var i: USize = 1
61+
while i < num_params do
62+
let param_node = params(i)?
63+
let param_l = _param_line(param_node)
64+
if param_l == prev_line then
65+
diags.push(Diagnostic(id(),
66+
"each parameter should be on its own line"
67+
+ " in a multiline declaration",
68+
source.rel_path, param_l, param_node.pos()))
69+
end
70+
prev_line = param_l
71+
i = i + 1
72+
end
73+
end
74+
end
75+
end
76+
end
77+
end
78+
79+
// Check return type ':' alignment (child 4)
80+
try
81+
let ret_type = node(4)?
82+
if ret_type.id() != ast.TokenIds.tk_none() then
83+
let ret_line = ret_type.line()
84+
if ret_line > keyword_line then
85+
try
86+
let line_text = source.lines(ret_line - 1)?
87+
(let first_ch, let first_col) = _first_nonws_char(line_text)
88+
if first_ch == ':' then
89+
let expected_col = keyword_col + 2
90+
let actual_col = first_col + 1 // convert 0-based to 1-based
91+
if actual_col != expected_col then
92+
diags.push(Diagnostic(id(),
93+
"':' should align at column "
94+
+ expected_col.string()
95+
+ " (indented from '" + keyword_name + "')",
96+
source.rel_path, ret_line, actual_col))
97+
end
98+
end
99+
end
100+
end
101+
end
102+
end
103+
104+
// Check '=>' alignment (child 6 = body)
105+
try
106+
let body = node(6)?
107+
if body.id() != ast.TokenIds.tk_none() then
108+
let body_line = body.line()
109+
if body_line > keyword_line then
110+
// Scan backward from the line before the body to find '=>'
111+
var scan_line = body_line - 1
112+
while scan_line > keyword_line do
113+
try
114+
let line_text = source.lines(scan_line - 1)?
115+
(let word, let word_col) =
116+
_first_nonws_word(line_text)
117+
if word == "=>" then
118+
let actual_col = word_col
119+
if actual_col != keyword_col then
120+
diags.push(Diagnostic(id(),
121+
"'=>' should align with '" + keyword_name
122+
+ "' keyword (column "
123+
+ keyword_col.string() + ")",
124+
source.rel_path, scan_line, actual_col))
125+
end
126+
break
127+
end
128+
// Stop scanning if we hit a non-blank line that isn't '=>'
129+
if word.size() > 0 then break end
130+
end
131+
scan_line = scan_line - 1
132+
end
133+
end
134+
end
135+
end
136+
137+
consume diags
138+
139+
fun _keyword_name(token_id: ast.TokenId): String val =>
140+
"""
141+
Return the source keyword for the given method token type.
142+
"""
143+
if token_id == ast.TokenIds.tk_fun() then "fun"
144+
elseif token_id == ast.TokenIds.tk_new() then "new"
145+
elseif token_id == ast.TokenIds.tk_be() then "be"
146+
else "unknown"
147+
end
148+
149+
fun _param_line(param: ast.AST box): USize =>
150+
"""
151+
Get the line number of a parameter node. Since TK_PARAM is abstract
152+
(no source position), use the first child's line (the parameter name
153+
or dontcare).
154+
"""
155+
try param(0)?.line()
156+
else param.line()
157+
end
158+
159+
fun _last_param_line(params: ast.AST box): USize =>
160+
"""
161+
Find the maximum line number across all parameter children.
162+
"""
163+
var max_line: USize = 0
164+
var i: USize = 0
165+
let count = params.num_children()
166+
while i < count do
167+
try
168+
let p = params(i)?
169+
let l = _param_line(p)
170+
if l > max_line then max_line = l end
171+
end
172+
i = i + 1
173+
end
174+
max_line
175+
176+
fun _first_nonws_char(line: String val): (U8, USize) =>
177+
"""
178+
Find the first non-whitespace character on a line.
179+
Returns `(char, 0-based-index)` or `(0, 0)` if blank.
180+
"""
181+
var i: USize = 0
182+
while i < line.size() do
183+
try
184+
let ch = line(i)?
185+
if (ch != ' ') and (ch != '\t') then
186+
return (ch, i)
187+
end
188+
end
189+
i = i + 1
190+
end
191+
(0, 0)
192+
193+
fun _first_nonws_word(line: String val): (String val, USize) =>
194+
"""
195+
Extract the first non-whitespace word from a line, returning the word
196+
and its 1-based column position. Returns `("", 0)` for blank lines.
197+
"""
198+
var i: USize = 0
199+
while i < line.size() do
200+
try
201+
let ch = line(i)?
202+
if (ch != ' ') and (ch != '\t') then break end
203+
end
204+
i = i + 1
205+
end
206+
207+
if i >= line.size() then
208+
return ("", 0)
209+
end
210+
211+
let col = i + 1 // 1-based
212+
let start = i
213+
214+
while i < line.size() do
215+
try
216+
let ch = line(i)?
217+
if (ch == ' ') or (ch == '\t') then break end
218+
end
219+
i = i + 1
220+
end
221+
222+
(recover val line.substring(ISize.from[USize](start),
223+
ISize.from[USize](i)) end, col)

tools/pony-lint/test/_ast_test_helper.pony

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ primitive \nodoc\ _ASTTestHelper
1616
directory, not an installed location, so executable-relative discovery
1717
is not used here.
1818
"""
19-
fun compile(h: TestHelper, source: String val,
19+
fun compile(
20+
h: TestHelper,
21+
source: String val,
2022
filename: String val = "test.pony")
2123
: (ast.Program val, lint.SourceFile val) ?
2224
=>

0 commit comments

Comments
 (0)