Rule Reference¶
Each rule is listed with its default status, a description of what it checks, and examples of incorrect and correct code.
safety/exhaustive-match¶
Default: on
Flags match expressions where the compiler has determined all cases are covered but the \exhaustive\ annotation is missing. Without \exhaustive\, adding a new variant to a union type compiles silently — the compiler injects else None for the missing case instead of raising an error. Adding the annotation makes the compiler error when a case is missing.
This rule requires type information, so pony-lint compiles the source through PassExpr before checking.
Incorrect:
type Color is (Red | Green | Blue)
primitive Red
primitive Green
primitive Blue
class Foo
fun name(color: Color): String =>
match color
| Red => "red"
| Green => "green"
| Blue => "blue"
end
All cases are handled, but without \exhaustive\, adding a Yellow variant to Color would compile silently with an implicit else None.
Correct:
type Color is (Red | Green | Blue)
primitive Red
primitive Green
primitive Blue
class Foo
fun name(color: Color): String =>
match \exhaustive\ color
| Red => "red"
| Green => "green"
| Blue => "blue"
end
With \exhaustive\, adding a Yellow variant to Color without adding a corresponding case produces a compiler error.
style/acronym-casing¶
Default: on
Known acronyms in type names must be fully uppercased. The recognized acronyms are: API, AST, CSS, DNS, FFI, FIFO, HTML, HTTP, IMAP, JSON, MIME, SMTP, SSH, SSL, TCP, TLS, UDP, URI, URL, UUID, XML.
Incorrect:
class JsonParser
primitive HttpMethod
Correct:
class JSONParser
primitive HTTPMethod
style/array-literal-format¶
Default: on
Checks formatting of multiline array literals. For array literals that span multiple lines:
- The opening
[must be the first non-whitespace on its line (no hanging indent after=or other expression context). - A space is required after
[when there is content on the same line.
Single-line array literals are not checked.
Incorrect:
actor Main
new create(env: Env) =>
let ns = [as USize:
1
2
]
Correct:
actor Main
new create(env: Env) =>
let ns =
[ as USize:
1
2 ]
The closing ] may also be on its own line:
actor Main
new create(env: Env) =>
let ns =
[ as USize:
1
2
]
style/assignment-indent¶
Default: on
When an assignment’s right-hand side spans multiple lines, the RHS must start on the line after the =, not on the same line. Single-line assignments are not checked.
Incorrect:
class Foo
fun apply(x: U32): U32 =>
var y: U32 = if x > 0 then
x
else
U32(0)
end
y
Correct:
class Foo
fun apply(x: U32): U32 =>
var y: U32 =
if x > 0 then
x
else
U32(0)
end
y
style/blank-lines¶
Default: on
Blank line conventions within and between entities.
Within entities:
- No blank line between the entity declaration and the first body content (docstring or first member).
- No blank lines between consecutive fields.
- Exactly one blank line before each method. Exception: when both the preceding method and the current method are one-liners, zero blank lines are also allowed.
Between entities:
- Exactly one blank line between consecutive entities. Exception: when both entities are one-liners, zero blank lines are also allowed.
Incorrect:
class Foo
let x: U32 = 0
let y: U32 = 1
fun apply(): U32 => x + y
Correct:
class Foo
let x: U32 = 0
let y: U32 = 1
fun apply(): U32 => x + y
style/call-argument-format¶
Default: on
Checks formatting of multiline function call arguments. For calls with two or more positional arguments that span multiple lines:
- No arguments start on the
(line — arguments should begin on the next line. - Arguments are either all on one continuation line, or each on its own line.
Single-line calls, single-argument calls, and named-argument-only calls are not checked. The where clause is allowed on its own line and is not subject to layout checks. These rules apply to both regular calls and FFI calls.
Incorrect:
// Arguments start on the ( line
foo.bar(U32(1),
U32(2), U32(3))
// Mixed layout — some args share a line, others don't
foo.bar(
U32(1), U32(2),
U32(3))
Correct:
// Single-line — not checked
foo.bar(U32(1), U32(2), U32(3))
// All on one continuation line
foo.bar(
U32(1), U32(2), U32(3))
// Each on its own line
foo.bar(
U32(1),
U32(2),
U32(3))
// Where clause on its own line
foo.bar(
U32(1),
U32(2)
where y = U32(3))
// FFI call — same rules
@pony_os_errno(
U32(1),
U32(2))
style/comment-spacing¶
Default: on
Line comments (//) must be followed by exactly one space. An empty comment (// with nothing after it) is also acceptable. This rule does not apply inside string literals, including triple-quoted strings.
Incorrect:
//This comment has no space
// This comment has two spaces
Correct:
// This comment has one space
//
// Empty comment above is fine
let url = "http://example.com" // inside strings is ignored
style/control-structure-alignment¶
Default: on
Control structure keywords must be vertically aligned. Each keyword in a control structure must start at the same column as the opening keyword. Single-line structures are exempt. The do keyword is not checked because it always appears on the same line as its opening keyword.
The following keyword groups are checked:
if/elseif/else/endifdef/elseif/else/endwhile/else/endfor/else/endtry/else/then/endrepeat/until/else/endwith/end
Incorrect:
if condition then
do_something()
else
do_other()
end
Correct:
if condition then
do_something()
else
do_other()
end
style/docstring-format¶
Default: on
Docstring """ delimiters must each be on their own line. Single-line docstrings like """text""" are flagged. Types and methods annotated with \nodoc\ are exempt, as are methods inside \nodoc\-annotated entities.
Incorrect:
class Foo
"""Foo docstring."""
fun bar(): None =>
"""Bar docstring."""
None
Correct:
class Foo
"""
Foo docstring.
"""
fun bar(): None =>
"""
Bar docstring.
"""
None
style/dot-spacing¶
Default: on
. must have no spaces around it. .> must be spaced as an infix operator with spaces on both sides. Both operators skip the “before” check on continuation lines where the operator is the first non-whitespace character.
Incorrect:
let x = foo .bar
let y = foo. bar
let z = foo.>bar
Correct:
let x = foo.bar
let y = foo .> bar
// Continuation lines are fine
let z = foo
.bar
.> baz
style/file-naming¶
Default: on
The file name must match the principal type defined in the file, converted to snake_case. The principal type is determined by a heuristic: if the file contains a single entity, that entity is principal. If a trait or interface is present alongside other types, the trait or interface is principal. Files with multiple unrelated types (no clear principal) are not checked. Private types preserve their leading underscore in the filename.
Files named _test.pony that contain a Main actor are exempt (standard test runner convention).
Incorrect:
// File: parser.pony
class FooParser
// Error: expected file name foo_parser.pony
Correct:
// File: foo_parser.pony
class FooParser
// File: _private_helper.pony
class _PrivateHelper
// File: runnable.pony
trait Runnable
class Runner // trait is principal, so runnable.pony is correct
style/hard-tabs¶
Default: on
Tab characters are not allowed anywhere in source files. Pony uses spaces for indentation.
A violation is a literal tab character in the source. Since tabs are invisible in rendered text, a tab-indented line looks like a space-indented line but triggers this rule. Editors should be configured to insert spaces instead of tabs.
Correct:
actor Main
new create(env: Env) =>
env.out.print("spaces, not tabs")
style/indentation-size¶
Default: on
Leading-space indentation must be a multiple of 2. Lines inside triple-quoted strings, blank lines, lines with zero indentation, and tab-indented lines are not checked. Tab indentation is handled by style/hard-tabs.
Incorrect:
class Foo
fun apply(): None =>
let x = 1
let y = 2
None
Correct:
class Foo
fun apply(): None =>
let x = 1
let y = 2
None
style/lambda-spacing¶
Default: on
Checks whitespace inside lambda braces per the style guide. Lambda expressions must have no space after {; a space before } is required only on single-line lambdas. Lambda types must have no space on either side ({ or }). Bare lambdas (@{) follow the same rules.
Incorrect:
// Space after {
{ (a: USize, b: USize): USize => a + b }
// No space before } on single-line
{(a: USize, b: USize): USize => a + b}
// Space before } on multi-line
{(a: USize, b: USize): USize =>
a + b }
// Space inside lambda type braces
type Foo is { (USize, USize): USize } box
Correct:
// Single-line: no space after {, space before }
{(a: USize, b: USize): USize => a + b }
// Multi-line: no space after {, } on its own line
{(a: USize, b: USize): USize =>
a + b
}
// Lambda type: no space on either side
type Foo is {(USize, USize): USize} box
// Bare lambda: same rules
@{(a: USize, b: USize): USize => a + b }
style/line-length¶
Default: on
Lines must not exceed 80 codepoints. Multi-byte UTF-8 characters count as one codepoint each. A line of exactly 80 codepoints is acceptable; 81 or more triggers a violation.
Incorrect:
// The following line is too long
let description = "This string is far too long to fit within the eighty codepoint limit for lines"
Correct:
let description =
"This string has been split" +
" across multiple lines"
style/match-case-indent¶
Default: on
The | introducing each match case must align with the match keyword’s column. This makes the match structure visually clear regardless of nesting depth. Inline continuation pipes (e.g., | "a" | "b") are not checked since only the leading | on each line needs alignment.
Incorrect:
match x
| let s: String => s
| let n: U32 => n.string()
end
Correct:
match x
| let s: String => s
| let n: U32 => n.string()
end
style/match-no-single-line¶
Default: on
Match expressions must span multiple lines. A single-line match is harder to scan and usually indicates the expression would be clearer as an if/else chain.
Incorrect:
let y = match x | let s: String => s else "" end
Correct:
let y =
match x
| let s: String => s
else
""
end
style/member-naming¶
Default: on
Fields, methods, parameters, and local variables must use snake_case. Names may start with a leading underscore for private visibility. The discard pattern _ by itself is not checked. Trailing primes (e.g., value') are permitted.
Incorrect:
class Foo
let myField: U32 = 0
var someValue: String = ""
fun doSomething(): None =>
let myVar: U32 = 1
None
Correct:
class Foo
let my_field: U32 = 0
var _some_value: String = ""
fun do_something(): None =>
let my_var: U32 = 1
None
style/method-declaration-format¶
Default: on
Checks formatting of multiline method declarations. When a method’s parameters span multiple lines, each parameter must be on its own line. When a method declaration spans multiple lines, the return type : must be indented one level (2 spaces) from the method keyword, and the => must align with the method keyword. Single-line declarations are not checked.
Applies to fun, new, and be declarations.
Incorrect:
class Foo
fun find(
value: U32, offset: USize)
: USize
=>
offset
Two parameters share a line.
class Foo
fun find(
value: U32,
offset: USize)
: USize
=>
offset
The : is indented too far — it should be at the method keyword’s column + 2.
class Foo
fun find(
value: U32,
offset: USize)
: USize
=>
offset
The => is indented too far — it should align with fun.
Correct:
class Foo
fun find(
value: U32,
offset: USize)
: USize
=>
offset
// Constructor
class Foo
let _x: U32
let _y: U32
new create(
x: U32,
y: U32)
=>
_x = x
_y = y
// Behavior
actor Foo
be apply(
x: U32,
y: U32)
=>
None
style/operator-spacing¶
Default: on
Checks whitespace around operators per the style guide. Binary operators require a space before and after. The not keyword requires a space after; before not, either a space or a non-alphanumeric character (e.g., () is acceptable. Unary minus must NOT have a space after the -. The “before” check is skipped on continuation lines where the operator is the first non-whitespace character.
Incorrect:
let x = 1+2
let y = a ==b
if not(x) then -a end
let z = - a
Correct:
let x = 1 + 2
let y = a == b
if not x then -a end
// Continuation lines are fine
let z =
+ a
style/package-naming¶
Default: off
The package directory name must be snake_case, containing only lowercase letters, digits, and underscores. This rule is off by default because application packages often use hyphens to match CLI naming conventions.
Incorrect (if enabled):
MyPackage/
main.pony
my-package/
main.pony
Correct:
my_package/
main.pony
style/package-docstring¶
Default: on
Each package should have a file named after the package (e.g., my_package.pony for a package in the my_package/ directory) containing a package-level docstring as the first expression. Directory names with hyphens are normalized to underscores (e.g., pony-lint/ expects pony_lint.pony).
Incorrect:
my_package/
main.pony # no my_package.pony file
// File: my_package/my_package.pony
use "collections" # no docstring before the first use statement
Correct:
// File: my_package/my_package.pony
"""
Provides utilities for working with packages.
"""
use "collections"
style/partial-call-spacing¶
Default: on
The ? at partial call sites must immediately follow ) with no space. This is the opposite convention from method declarations.
Incorrect:
let x = foo() ?
let y = bar(1, 2) ?
Correct:
let x = foo()?
let y = bar(1, 2)?
style/partial-spacing¶
Default: on
The ? in partial method declarations must have surrounding spaces. A space is required before ? and a space or end-of-line after it.
Incorrect:
class Foo
fun bar()? => None
Correct:
class Foo
fun bar() ? => None
style/prefer-chaining¶
Default: on
Flags patterns where a value is bound to a local variable and methods are called on it — when .> chaining would be cleaner. Triggers on both let and var bindings. Fires with 2 or more consecutive dot-calls (whether or not the variable is returned), or with a single call followed by a bare reference to return the variable.
Incorrect — multiple calls with trailing reference:
class Foo
fun make_list(): Array[U32] =>
let r = Array[U32]
r.push(1)
r.push(2)
r.push(3)
r
Correct:
class Foo
fun make_list(): Array[U32] =>
Array[U32]
.> push(1)
.> push(2)
.> push(3)
Incorrect — single call with trailing reference:
class Foo
fun make_set(): Set[String] =>
let s = Set[String]
s.set("foo")
s
Correct:
class Foo
fun make_set(): Set[String] =>
Set[String] .> set("foo")
Also incorrect — multiple calls without trailing reference:
class Foo
fun apply(): None =>
let x = Bar
x.baz(1)
x.qux(2)
Correct:
class Foo
fun apply(): None =>
Bar .> baz(1) .> qux(2)
style/public-docstring¶
Default: on
Public types and public methods must have a docstring. A type or method is public if its name does not start with _.
Several exemptions apply. No docstring is required for:
- Types or methods annotated with
\nodoc\ - Methods inside private types or
\nodoc\-annotated types Mainactors- Well-known method names:
create,string,eq,ne,hash,hash64,compare,lt,le,ge,gt,dispose,has_next,next,values,size - Type aliases (Pony does not support docstrings on type aliases)
- Methods inside anonymous
objectliterals - Concrete methods with 3 or fewer top-level expressions in the body (abstract methods are not exempt)
Incorrect:
class Foo
fun my_method(a: U32, b: U32, c: U32, d: U32): U32 =>
let x = a + b
let y = c + d
let z = x * y
z + 1
Correct:
class Foo
"""A class that does arithmetic."""
fun my_method(a: U32, b: U32, c: U32, d: U32): U32 =>
"""Combine four values through addition and multiplication."""
let x = a + b
let y = c + d
let z = x * y
z + 1
fun apply(): U32 =>
// Exempt: 3 or fewer expressions
let x: U32 = 1
let y: U32 = 2
x + y
style/trailing-whitespace¶
Default: on
Lines must not have trailing spaces or tabs. Completely empty lines (zero length) are not flagged.
Because trailing whitespace is invisible in rendered text, violations are not visible in code examples. A violation is one or more space or tab characters between the last visible character on a line and the line ending.
Correct:
class Foo
fun apply(): None =>
let x = 1
None
style/type-alias-format¶
Default: on
Checks formatting of multiline type alias declarations. For type aliases with a multiline union or intersection type:
- The opening
(must be the first non-whitespace on its line (no hanging indent afteris). - A space is required after
(. - For each
|or&that is the first non-whitespace on its line, a space is required after it. - A space is required before the closing
).
Single-line type aliases and simple aliases (no union/intersection) are not checked. Only |/& at line starts (first non-whitespace) are checked — mid-line separators in nested types are not.
Incorrect:
// Hanging indent — ( must be on its own line
type Signed is (I8
| I16
| I32 )
// Missing space after (
type Signed is
(I8
| I16
| I32 )
// Missing space after |
type Signed is
( I8
|I16
| I32 )
// Missing space before )
type Signed is
( I8
| I16
| I32)
Correct:
// Single-line — not checked
type Signed is (I8 | I16 | I32)
// Multiline with ) inline
type Signed is
( I8
| I16
| I32 )
// Multiline with ) on its own line
type Signed is
( I8
| I16
| I32
)
// Intersection type
type Both is
( Hashable
& Stringable )
style/type-naming¶
Default: on
Type names must use CamelCase. Names may start with a leading underscore for private visibility. After the optional leading underscore, the name must begin with an uppercase letter and contain only letters and digits. Underscores within the name are not allowed. Trailing primes are permitted.
Incorrect:
class foo_parser
primitive my_helper
actor some_actor
Correct:
class FooParser
primitive _MyHelper
actor SomeActor
style/type-parameter-format¶
Default: on
Checks formatting of multiline type parameter lists. The opening [ must always be on the same line as the entity or method name. When type parameters span multiple lines, each type parameter must be on its own line. For entities with a provides clause (is), the is keyword must be indented one level (2 spaces) from the entity keyword when it appears on its own line. Single-line type parameter lists are not checked (except for the [ same-line requirement, which always applies).
Applies to entities (class, actor, primitive, struct, trait, interface, type) and methods (fun, new, be).
Incorrect:
class Foo
[A, B]
The [ is on a different line than the name.
class Foo[
A, B,
C]
Two type parameters share a line.
interface Hashable
class Foo[
A,
B]
is Hashable
The is keyword is indented too far — it should be at the entity keyword’s column + 2.
Correct:
// Single-line — not checked
class Foo[A, B]
// Multiline class
class Foo[
A,
B]
// Entity with provides clause
interface Hashable
trait Foo[
A,
B]
is Hashable
// Method type parameters
class Foo
fun bar[
A,
B](x: A)
=>
None