apple/pkl · error · GenericParserError
stringIndentationMustMatchLastLine
stringIndentationMustMatchLastLine
Error message
Line must match or exceed indentation of the String's last line.
What it means
In a multi-line string, every line after the first must be indented at least as much as the closing delimiter's line, so Pkl knows how much common indentation to strip. validateStringIndentation computes the closer's indent and throws stringIndentationMustMatchLastLine for any line that starts with less indentation.
Source
Thrown at pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java:1064
if (beforeLast.type == NodeType.STRING_NEWLINE) return;
var text = beforeLast.text(lexer.getSource());
if (!text.isBlank()) {
throw parserError(
ErrorMessages.create("closingStringDelimiterMustBeginOnNewLine"), beforeLast.span);
}
}
private void validateStringIndentation(List<Node> nodes) {
var indentNode = nodes.get(nodes.size() - 2);
if (indentNode.type == NodeType.STRING_NEWLINE) return;
var indent = indentNode.text(lexer.getSource());
var previousNewline = false;
for (var i = 1; i < nodes.size() - 2; i++) {
var child = nodes.get(i);
if (child.type != NodeType.STRING_NEWLINE && previousNewline) {
var text = child.text(lexer.getSource());
if (!text.startsWith(indent)) {
throw parserError(ErrorMessages.create("stringIndentationMustMatchLastLine"), child.span);
}
}
previousNewline =
child.type == NodeType.STRING_NEWLINE || child.type == NodeType.STRING_CONTINUATION;
}
}
private Node parseParenthesizedExpr() {
var children = new ArrayList<Node>();
expect(Token.LPAREN, children, "unexpectedToken", "(");
if (lookahead() == Token.RPAREN) {
ff(children);
children.add(makeTerminal(next()));
return new Node(NodeType.PARENTHESIZED_EXPR, children);
}
var elements = new ArrayList<Node>();
ff(elements);
elements.add(parseExpr(")"));View on GitHub (pinned to f3efcbfc9b)
Solutions
- Indent every content line of the string to at least the closing `"""`'s indentation.
- Use consistent indentation (spaces, not mixed tabs) throughout the block.
- Dedent pasted text uniformly first, then re-indent the whole block together.
- Keep the closing `"""` at the lowest indentation of the block and indent content lines beyond it.
Example fix
// before
sql = """
SELECT 1
FROM t
"""
// after
sql = """
SELECT 1
FROM t
""" Defensive patterns
Strategy: validation
Validate before calling
// Every content line must be indented >= the closing """ line's indent
function stringIndentationOk(block) {
const lines = block.split('\n');
const closerIndent = lines[lines.length - 1].match(/^\s*/)[0].length;
return lines.slice(1, -1).every(l => l.trim() === '' || l.match(/^\s*/)[0].length >= closerIndent);
} Prevention
- Use spaces only — never mix tabs and spaces inside multi-line strings.
- Paste text, then select the whole block and re-indent uniformly in one edit.
- Place the closing `"""` at the least-indented column of the block and indent content lines more.
When it happens
Trigger: A line inside a `"""..."""` block is dedented relative to the closing delimiter's indentation — e.g. the closer is indented two spaces but one content line starts at column 0.
Common situations: Copy-pasting text with varying indentation into a multi-line string; editors auto-dedenting blank-ish lines; mixed tabs/spaces reducing effective indent; generated files with inconsistent indentation.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- missingDelimiter
- unexpectedEndOfFile
- stringContentMustBeginOnNewLine
- stringIndentationMustMatchLastLine
- notAUnion
AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08).
Data as JSON: /api/errors/8414c1e816b4e840.
Report an issue: GitHub.