Files
pkl/docs/modules/style-guide/pages/index.adoc
2025-10-29 16:00:05 +01:00

792 lines
13 KiB
Plaintext

= Pkl Style Guide
:icons: font
:source-highlighter: highlight.js
:pkl-expr: pkl expression
:pkl: pkl
:sectnums:
This document serves as the Pkl team's recommended coding standard for the Pkl configuration language.
== Files
=== Filename
Use the `.pkl` extension for all files.
Follow these rules for casing the file's name:
[cols="1,3,1"]
|===
| Casing | Description | Example
| PascalCase
| It is designed to be used as a template, or used as a class (i.e. imported and instantiated).
| `K8sResource.pkl`
| camelCase
| It is designed to be used as a value.
| `myDeployment.pkl`
| kebab-case
| It is designed to be used as a CLI tool.
| `do-convert.pkl`
|===
*Exception*: If a file is meant to render into a static configuration file, the filename should match the target file's name without the extension.
For example, `config.pkl` turns into `config.yml`.
*Exception*: The `PklProject` file cannot have any extension.
=== File Encoding
Encode all files using UTF-8.
== Module Structure
=== Header
Separate each section of the module header by one blank line.
A module header consists of the following clauses, each of which is optional:
- Module clause
- `amends` or `extends` clause
- Import clauses
.module.pkl
[source%parsed,{pkl}]
----
module com.example.Foo // <1>
extends "Bar.pkl" // <2>
import "baz.pkl" // <3>
import "Buz.pkl" // <3>
----
<1> Module clause
<2> `extends` clause
<3> Import clause
==== Module name
Match the name of the module with the name of the file.
.MyModule.pkl
[source%tested,{pkl}]
----
module MyModule
----
If a module is meant to be published, add a module clause, `@ModuleInfo` annotation, and doc comments.
Modules that do not get published anywhere may omit a module clause.
.MyModule.pkl
[source%tested,{pkl}]
----
/// Used for some type of purpose. <1>
@ModuleInfo { minPklVersion = "0.24.0" } // <2>
module MyModule // <3>
----
<1> Doc comments
<2> `@ModuleInfo` annotation
<3> Module clause
==== `amends` vs. `extends` clause
A module that doesn't add new properties shouldn't use the `extends` clause.
==== Imports
Sort imports sections using https://en.wikipedia.org/wiki/Natural_sort_order[natural sorting] by their module URI.
Relative path and package imports should be in their own section, separated by a newline.
There should be no unused imports.
[source%parsed,{pkl}]
----
import "modulepath:/foo.pkl"
import "package://example.com/mypackage@1.0.0#/foo.pkl"
import "@mypackage/baz.pkl"
import ".../my/file/bar2.pkl"
import ".../my/file/bar11.pkl"
----
=== Module body
Within a module body, define members in this order:
1. Properties
2. Methods
3. Classes and type aliases
4. The amended xref:language-reference:index.adoc#in-language[output] property.
*Exception*: local members can be close to their usage.
*Exception*: functions meant to be a class constructor can be next to the class declaration.
.constructor.pkl
[source%tested,{pkl}]
----
function MyClass(_name: String): MyClass = new { name = _name }
class MyClass {
name: String
}
----
=== Module URIs
If possible, use xref:language-reference:index.adoc#triple-dot-module-uris[triple-dot Module URIs] to reference ancestor modules
instead of multiple `../`.
.good.pkl
[source%parsed,{pkl}]
----
amends ".../ancestor.pkl"
import ".../ancestor2.pkl"
----
.bad.pkl
[source%parsed,{pkl}]
----
amends "../../../ancestor.pkl"
import "../../../ancestor2.pkl"
----
== Objects
=== Member spacing
Object members (properties, elements, and entries) should be separated by at most one blank line.
.good.pkl
[source%tested,{pkl}]
----
foo = "bar"
baz = "buz"
----
.good.pkl
[source%tested,{pkl}]
----
foo = "bar"
baz = "buz"
----
.bad.pkl
[source%tested,{pkl}]
----
foo = "bar"
baz = "buz"
----
Too many lines separate `foo` and `baz`.
=== Overridden properties
Properties that override an existing property shouldn't have doc comments nor type annotations,
unless the type is intentionally overridden via `extends`.
[source%tested,{pkl}]
----
amends "myOtherModule.pkl"
foo = "bar"
----
=== New property definitions
Each property definition should have a type annotation and <<doc-comment,doc comment>>.
Successive definitions should be separated by a blank line.
.good.pkl
[source%parsed,{pkl}]
----
/// Denotes something.
myFoo: String
/// Something else
myOtherFoo: String
----
.bad.pkl
[source%parsed,{pkl}]
----
/// Denotes something.
myFoo: String
/// Something else
myOtherFoo: String
----
=== Objects with `new`
When initializing a `Typed` object using `new`, omit the type.
For example, use `new {}` instead of `new Foo {}`.
This rule does not apply when initializing a property to a subtype of the property's declared type.
.good.pkl
[source%parsed,{pkl}]
----
myFoo: Foo = new { foo = "bar" }
----
.good.pkl
[source%parsed,{pkl}]
----
open class Foo {}
class Bar extends Foo {}
foo: Foo = new Bar {}
----
This is okay because this is meaning to initialize `Bar` instead of `Foo`.
.bad.pkl
[source%parsed,{pkl}]
----
myFoo1: Foo = new Foo { foo = "bar" } // <1>
myFoo2 = new Foo { foo = "bar" } // <2>
----
<1> Unnecessary `new Foo { ... }`
<2> Unless amending/extending a module where `myFoo2` is already defined, `myFoo2` is effectively the `unknown` type, i.e. `myFoo2: unknown`.
== Comments
Use doc comments to convey information to users of a module.
Use line comments or block comments to convey implementation concerns to authors of a module, or to comment out code.
[[doc-comment]]
=== Doc comments
Doc comments should start with a one sentence summary paragraph, followed by additional paragraphs if necessary.
Start new sentences on their own line.
[source%parsed,{pkl}]
----
/// The time allotted for eating lunch.
///
/// Note:
/// * Hamburgers typically take longer to eat than salad.
/// * Pizza gets prepared per-order.
///
/// Orders must be placed on-prem.
/// See <https://cafeteria.com> for more details.
lunchHours: Duration
----
=== Line comments
If a comment relates to a property definition, place it after the property's doc comments.
Add a single space after `//`.
.good.pkl
[source%parsed,{pkl}]
----
/// Designates whether it is zebra party time.
// TODO: Add constraints here?
partyTime: Boolean
----
A line comment may also be placed at the end of a line, as long as the line doesn't exceed 100 characters.
.good.pkl
[source%tested,{pkl}]
----
/// Designates whether it is zebra party time.
partyTime: Boolean // TODO: Add constraints here?
----
=== Block comments
A single-line block comment should have a single space after `+++/*+++` and before `+++*/+++`.
.good.pkl
[source%tested,{pkl}]
----
/* Let's have a zebra party */
----
.bad.pkl
[source%tested,{pkl}]
----
/*Let's have a zebra party*/
----
== Classes
=== Class names
Name classes in PascalCase.
.good.pkl
[source%tested,{pkl}]
----
class ZebraParty {}
----
.bad.pkl
[source%tested,{pkl}]
----
class zebraParty {}
----
== Strings
=== Custom String Delimiters
Use xref:language-reference:index.adoc#custom-string-delimiters[custom string delimiters] to avoid the need for string escaping.
.good.pkl
[source%tested,{pkl}]
----
myString = #"foo \ bar \ baz"#
----
.bad.pkl
[source%tested,{pkl}]
----
myString = "foo \\ bar \\ baz"
----
NOTE: Sometimes, using custom string delimiters makes source code harder to read. For example, the `+\#+` literal reads better using escapes (`"\\#"`) than using custom string delimiters (`+##"\#"##+`).
=== Interpolation
Prefer interpolation to string concatenation.
.good.pkl
[source%parsed,{pkl}]
----
greeting = "Hello, \(name)"
----
.bad.pkl
[source%parsed,{pkl}]
----
greeting = "Hello, " + name
----
== Formatting
=== Line width
Lines shouldn't exceed 100 characters.
*Exceptions:*
1. String literals
2. Code snippets within doc comments
=== Indentation
Use two spaces per indentation level.
==== Members within braces
Members within braces should be indented one level deeper than their parents.
[source%tested,{pkl}]
----
foo {
bar {
baz = "hi"
}
}
----
==== Assignment operator (`=`)
An assignee that starts after a newline should be indented.
.good.pkl
[source%tested,{pkl}]
----
foo =
"foo"
bar =
new {
baz = "baz"
biz = "biz"
}
----
.bad.pkl
[source%tested,{pkl}]
----
foo =
"foo"
bar =
new {
baz = "baz"
biz = "biz"
}
----
An assignee that starts on the same line should not be indented.
.good.pkl
[source%tested,{pkl}]
----
foo = new {
baz = "baz"
biz = "biz"
}
----
.bad.pkl
[source%tested,{pkl}]
----
foo = new {
baz = "baz"
biz = "biz"
}
----
==== `if` and `let` expressions
`if` and `let` bodies that start on their own line should be indented.
Child bodies may also be inline, and the `else` branch of `if` expressions may be inline of `if`.
.good.pkl
[source%parsed,{pkl-expr}]
----
if (bar)
bar
else
foo
----
.good.pkl
[source%parsed,{pkl-expr}]
----
if (bar) bar else foo
----
.good.pkl
[source%parsed,{pkl-expr}]
----
let (foo = "bar")
foo.toUpperCase()
----
.good.pkl
[source%parsed,{pkl-expr}]
----
let (foo = "bar") foo.toUpperCase()
----
.bad.pkl
[source%parsed,{pkl-expr}]
----
if (bar)
bar
else
foo
----
.bad.pkl
[source%parsed,{pkl-expr}]
----
let (foo = "bar")
foo.toUpperCase()
----
*Exception*: A nested `if` expression within the `else` branch should have the same indentation level as its parent, and start on the same line as the parent `else` keyword.
.good.pkl
[source%parsed,{pkl-expr}]
----
if (bar)
bar
else if (baz)
baz
else
foo
----
.bad.pkl
[source%parsed,{pkl-expr}]
----
if (bar)
bar
else
if (baz)
baz
else
foo
----
==== Multiline chained method calls
Indent successive multiline chained method calls.
[source%parsed,{pkl-expr}]
----
foo()
.bar()
.baz()
.biz()
----
==== Multiline binary operators
Place operators after the newline, and indent successive lines to the same level.
.good.pkl
[source%parsed,{pkl}]
----
foo = bar
|> baz
|> biz
myNum = 1
+ 2
+ 3
+ 4
----
.bad.pkl
[source%parsed,{pkl}]
----
foo = bar |>
baz |>
biz
myNum = 1 +
2 +
3 +
4
----
.bad.pkl
[source%tested,{pkl}]
----
foo = bar
|> baz
|> biz
----
.bad.pkl
[source%tested,{pkl}]
----
foo = bar
|> baz
|> biz
----
*Exception*: the minus operator must come before the newline, because otherwise it is parsed as a unary minus.
.good.pkl
[source%tested,{pkl}]
----
myNum = 1 -
2 -
3 -
4
----
.bad.pkl
[source%tested,{pkl}]
----
myNum = 1
- 2
- 3
- 4
----
=== Spaces
Add a space:
[source%parsed,{pkl}]
----
amends "Foo.pkl" // <1>
res1 { "foo" } // <2>
res2 = 1 + 2 // <3>
res3 = res2 as Number // <3>
res4 = List(1, 2, 3) // <4>
res5 = if (foo) bar else baz // <5>
typealias Foo = "foo" | "bar" | "baz" // <6>
----
<1> After keywords
<2> Before and after braces
<3> Around infix operators
<4> After a comma
<5> Before opening parentheses in control operators (`if`, `for`, `when` are control operators)
<6> Before and after the pipe symbol (`|`)
=== Object bodies
==== Single line
An object body may be a single line if it only consists of primitive elements, or if it contains two or fewer members.
Otherwise, split them into multiple lines.
Separate each member of a single line object with a semicolon and a space.
.good.pkl
[source%tested,{pkl}]
----
res1 = new { bar = "bar"; baz = "baz" }
res2 = new { 1; 2; 3; 4; 5; 6 }
----
.bad.pkl
[source%parsed,{pkl}]
----
res1 = new { bar = "bar"; baz = "baz"; biz = "biz"; } // <1>
res2 = new { 1 2 3 4 5 6 } // <2>
----
<1> Too many members and trailing `;`
<2> No semicolon
==== Multiline
Multiline objects should have their members separated by at least one line break and at most one blank line.
.good.pkl
[source%tested,{pkl}]
----
res {
foo = "foo"
bar = "bar"
}
res2 {
["foo"] = "foo"
["bar"] = "bar"
}
res3 {
"foo"
"bar"
}
----
.good.pkl
[source%tested,{pkl}]
----
res {
foo = "foo"
bar = "bar"
}
res2 {
["foo"] = "foo"
["bar"] = "bar"
}
res3 {
"foo"
"bar"
}
----
.bad.pkl
[source%tested,{pkl}]
----
res {
foo = "foo"
bar = "bar" // <1>
}
res2 {
["foo"] = "foo"
["bar"] = "bar" // <1>
}
res3 {
"foo"
"bar" // <1>
}
res4 {
foo = "foo"; bar = "bar" // <2>
}
----
<1> Too many blank lines between members
<2> No line break separating members
Put the opening brace on the same line.
.good.pkl
[source%tested,{pkl}]
----
res {
foo = "foo"
bar = "bar"
}
----
.bad.pkl
[source%tested,{pkl}]
----
res
{
foo = "foo"
bar = "bar"
}
----
== Programming Practices
=== Prefer `for` generators
When programmatically creating elements and entries, prefer
xref:language-reference:index.adoc#for-generators[for generators] over using the collection API.
Using for generators preserves xref:language-reference:index.adoc#late-binding[late binding].
.good.pkl
[source%tested,{pkl}]
----
numbers {
1
2
3
4
}
squares {
for (num in numbers) {
num ** 2
}
}
----
.bad.pkl
[source%tested,{pkl}]
----
numbers {
1
2
3
4
}
squares = numbers.toList().map((num) -> num ** 2).toListing()
----