zig fmt: fix overindent tracking in sub-renders

This problem also affected determining if an expression became multiline
as that depends on if the line is overindented. As such,
`becomesMultilineExpr` has been replaced by `rendersMultiline` which
constructs a temporary writer which returns `error.WriteFailed` when
newlines are written. This new approach also has the advantage of being
more maintainable.
This commit is contained in:
Kendall Condon
2026-03-22 17:42:57 -04:00
parent 92915a42b5
commit 09e523bd51
2 changed files with 93 additions and 375 deletions
+64 -375
View File
@@ -652,10 +652,12 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
const lhs, const rhs = tree.nodeData(node).node_and_node;
const lbracket = tree.firstToken(rhs) - 1;
const rbracket = tree.lastToken(rhs) + 1;
const one_line = tree.tokensOnSameLine(lbracket, rbracket) and
!becomesMultilineExpr(tree, rhs);
const inner_space = if (one_line) Space.none else Space.newline;
try renderExpression(r, lhs, .none);
// One lien check must come after rendering lhs since it can influence
// isLineOverIndented
const one_line = tree.tokensOnSameLine(lbracket, rbracket) and
!try rendersMultiline(r, rhs);
const inner_space = if (one_line) Space.none else Space.newline;
try ais.pushIndent(.normal);
try renderToken(r, lbracket, inner_space); // [
try renderExpression(r, rhs, inner_space);
@@ -951,380 +953,61 @@ fn renderExpressionFixup(r: *Render, node: Ast.Node.Index, space: Space) Error!v
}
}
/// Same as becomesMultilineExpr, but returns false when `node == .none`
fn optBecomesMultilineExpr(tree: Ast, node: Ast.Node.OptionalIndex) bool {
return if (node.unwrap()) |payload| becomesMultilineExpr(tree, payload) else false;
}
/// May return false if `node` is already multiline
fn becomesMultilineExpr(tree: Ast, node: Ast.Node.Index) bool {
// Conditions related to comments, doc comments, and multiline string literals are ignored
// since they always go to the end of the line, which already make them a multi-line
// expression (since they contain a newline).
switch (tree.nodeTag(node)) {
.identifier,
.number_literal,
.char_literal,
.unreachable_literal,
.anyframe_literal,
.string_literal,
.multiline_string_literal,
.error_value,
.enum_literal,
=> return false,
.container_decl_trailing,
.container_decl_arg_trailing,
.container_decl_two_trailing,
.tagged_union_trailing,
.tagged_union_enum_tag_trailing,
.tagged_union_two_trailing,
.switch_comma,
.builtin_call_two_comma,
.builtin_call_comma,
.call_one_comma,
.call_comma,
.struct_init_one_comma,
.struct_init_dot_two_comma,
.struct_init_dot_comma,
.struct_init_comma,
.array_init_one_comma,
.array_init_dot_two_comma,
.array_init_dot_comma,
.array_init_comma,
// The following always have a non-zero amount of members
// which is also the condition for them to be multi-line.
.block,
.block_semicolon,
=> return true,
.block_two,
.block_two_semicolon,
=> return tree.nodeData(node).opt_node_and_opt_node[0] != .none,
.container_decl,
.container_decl_arg,
.container_decl_two,
.tagged_union,
.tagged_union_enum_tag,
.tagged_union_two,
=> {
var buf: [2]Ast.Node.Index = undefined;
const full = tree.fullContainerDecl(&buf, node).?;
if (full.ast.arg.unwrap()) |arg| {
if (becomesMultilineExpr(tree, arg))
return true;
}
// This does the same checks as `isOneLineContainerDecl`, however it avoids unnecessary
// checks related to comments and multiline strings, which would mean the container is
// already multiple lines.
for (full.ast.members) |member| {
if (tree.fullContainerField(member)) |field_full| {
for ([_]Ast.Node.OptionalIndex{
field_full.ast.type_expr,
field_full.ast.align_expr,
field_full.ast.value_expr,
}) |opt_expr| {
if (opt_expr.unwrap()) |expr| {
if (becomesMultilineExpr(tree, expr))
return true;
}
}
} else return true;
}
return false;
},
.error_set_decl => {
const lbrace, const rbrace = tree.nodeData(node).token_and_token;
return !isOneLineErrorSetDecl(tree, lbrace, rbrace);
},
.@"switch" => {
const op, const extra_index = tree.nodeData(node).node_and_extra;
const case_range = tree.extraData(extra_index, Ast.Node.SubRange);
return @intFromEnum(case_range.end) - @intFromEnum(case_range.start) != 0 or
becomesMultilineExpr(tree, op);
},
.for_simple, .@"for" => {
const full = tree.fullFor(node).?;
if (becomesMultilineExpr(tree, full.ast.then_expr) or
optBecomesMultilineExpr(tree, full.ast.else_expr))
return true;
for (full.ast.inputs) |expr| {
if (if (tree.nodeTag(expr) == .for_range) blk: {
const lhs, const rhs = tree.nodeData(expr).node_and_opt_node;
break :blk becomesMultilineExpr(tree, lhs) or optBecomesMultilineExpr(tree, rhs);
} else becomesMultilineExpr(tree, expr))
return true;
}
const final_input_expr = full.ast.inputs[full.ast.inputs.len - 1];
if (tree.tokenTag(tree.lastToken(final_input_expr) + 1) == .comma)
return true;
const token_tags = tree.tokens.items(.tag);
const payload = full.payload_token;
const pipe = std.mem.indexOfScalarPos(Token.Tag, token_tags, payload, .pipe).?;
return token_tags[@intCast(pipe - 1)] == .comma;
},
.while_simple,
.while_cont,
.@"while",
=> {
const full = tree.fullWhile(node).?;
return becomesMultilineExpr(tree, full.ast.cond_expr) or
becomesMultilineExpr(tree, full.ast.then_expr) or
optBecomesMultilineExpr(tree, full.ast.cont_expr) or
optBecomesMultilineExpr(tree, full.ast.else_expr);
},
.if_simple,
.@"if",
=> {
const full = tree.fullIf(node).?;
return becomesMultilineExpr(tree, full.ast.cond_expr) or
becomesMultilineExpr(tree, full.ast.then_expr) or
optBecomesMultilineExpr(tree, full.ast.else_expr);
},
.fn_proto_simple,
.fn_proto_multi,
.fn_proto_one,
.fn_proto,
=> {
var buf: [1]Ast.Node.Index = undefined;
const fn_proto = tree.fullFnProto(&buf, node).?;
for ([_]Ast.Node.OptionalIndex{
fn_proto.ast.return_type,
fn_proto.ast.align_expr,
fn_proto.ast.addrspace_expr,
fn_proto.ast.section_expr,
fn_proto.ast.callconv_expr,
}) |opt_expr| {
if (opt_expr.unwrap()) |expr| {
if (becomesMultilineExpr(tree, expr))
return true;
}
}
for (fn_proto.ast.params) |expr| {
if (becomesMultilineExpr(tree, expr))
return true;
}
const lparen = fn_proto.ast.fn_token + 1;
const return_type = fn_proto.ast.return_type.unwrap().?;
const maybe_bang = tree.firstToken(return_type) - 1;
const rparen = fnProtoRparen(tree, fn_proto, maybe_bang);
return !isOneLineFnProto(tree, fn_proto, lparen, rparen);
},
.asm_simple,
=> {
const lhs = tree.nodeData(node).node_and_token[0];
return becomesMultilineExpr(tree, lhs);
},
.@"asm",
=> {
const lhs, const extra_index = tree.nodeData(node).node_and_extra;
const asm_extra = tree.extraData(extra_index, Ast.Node.Asm);
return @intFromEnum(asm_extra.items_end) - @intFromEnum(asm_extra.items_start) != 0 or
becomesMultilineExpr(tree, lhs) or optBecomesMultilineExpr(tree, asm_extra.clobbers);
},
.array_type, .array_type_sentinel => {
const array_type = tree.fullArrayType(node).?;
const rbracket = tree.firstToken(array_type.ast.elem_type) - 1;
return !isOneLineArrayType(tree, array_type, rbracket) or
becomesMultilineExpr(tree, array_type.ast.elem_type);
},
.array_access => {
const lhs, const rhs = tree.nodeData(node).node_and_node;
const lbracket = tree.firstToken(rhs) - 1;
const rbracket = tree.lastToken(rhs) + 1;
return !tree.tokensOnSameLine(lbracket, rbracket) or
becomesMultilineExpr(tree, lhs) or
becomesMultilineExpr(tree, rhs);
},
.call_one,
.call,
.builtin_call_two,
.builtin_call,
.array_init_one,
.array_init_dot_two,
.array_init_dot,
.array_init,
.struct_init_one,
.struct_init_dot_two,
.struct_init_dot,
.struct_init,
=> |tag| {
var buf: [2]Ast.Node.Index = undefined;
const opt_lhs: Ast.Node.OptionalIndex, const items = switch (tag) {
.call_one, .call => blk: {
const full = tree.fullCall(buf[0..1], node).?;
break :blk .{ full.ast.fn_expr.toOptional(), full.ast.params };
},
.builtin_call_two, .builtin_call => .{ .none, tree.builtinCallParams(&buf, node).? },
.array_init_one,
.array_init_dot_two,
.array_init_dot,
.array_init,
=> blk: {
const full = tree.fullArrayInit(&buf, node).?;
break :blk .{ full.ast.type_expr, full.ast.elements };
},
.struct_init_one,
.struct_init_dot_two,
.struct_init_dot,
.struct_init,
=> blk: {
const full = tree.fullStructInit(&buf, node).?;
break :blk .{ full.ast.type_expr, full.ast.fields };
},
else => unreachable,
};
if (opt_lhs.unwrap()) |lhs| {
if (becomesMultilineExpr(tree, lhs))
return true;
}
for (items) |expr| {
if (becomesMultilineExpr(tree, expr))
return true;
}
return false;
},
.assign_destructure => {
const full = tree.assignDestructure(node);
for (full.ast.variables) |expr| {
if (becomesMultilineExpr(tree, expr))
return true;
}
return becomesMultilineExpr(tree, full.ast.value_expr);
},
.ptr_type_aligned,
.ptr_type_sentinel,
.ptr_type,
.ptr_type_bit_range,
=> {
const full = tree.fullPtrType(node).?;
return becomesMultilineExpr(tree, full.ast.child_type) or
optBecomesMultilineExpr(tree, full.ast.sentinel) or
optBecomesMultilineExpr(tree, full.ast.align_node) or
optBecomesMultilineExpr(tree, full.ast.addrspace_node) or
optBecomesMultilineExpr(tree, full.ast.bit_range_start) or
optBecomesMultilineExpr(tree, full.ast.bit_range_end);
},
.slice_open,
.slice,
.slice_sentinel,
=> {
const full = tree.fullSlice(node).?;
return becomesMultilineExpr(tree, full.ast.sliced) or
becomesMultilineExpr(tree, full.ast.start) or
optBecomesMultilineExpr(tree, full.ast.end) or
optBecomesMultilineExpr(tree, full.ast.sentinel);
},
.@"comptime",
.@"nosuspend",
.@"suspend",
.@"resume",
.bit_not,
.bool_not,
.negation,
.negation_wrap,
.optional_type,
.address_of,
.deref,
.@"try",
=> return becomesMultilineExpr(tree, tree.nodeData(node).node),
.@"return" => return optBecomesMultilineExpr(tree, tree.nodeData(node).opt_node),
.field_access,
.unwrap_optional,
.grouped_expression,
=> return becomesMultilineExpr(tree, tree.nodeData(node).node_and_token[0]),
.add,
.add_wrap,
.add_sat,
.array_cat,
.array_mult,
.bang_equal,
.bit_and,
.bit_or,
.shl,
.shl_sat,
.shr,
.bit_xor,
.bool_and,
.bool_or,
.div,
.equal_equal,
.greater_or_equal,
.greater_than,
.less_or_equal,
.less_than,
.merge_error_sets,
.mod,
.mul,
.mul_wrap,
.mul_sat,
.sub,
.sub_wrap,
.sub_sat,
.@"orelse",
.@"catch",
.error_union,
.assign,
.assign_bit_and,
.assign_bit_or,
.assign_shl,
.assign_shl_sat,
.assign_shr,
.assign_bit_xor,
.assign_div,
.assign_sub,
.assign_sub_wrap,
.assign_sub_sat,
.assign_mod,
.assign_add,
.assign_add_wrap,
.assign_add_sat,
.assign_mul,
.assign_mul_wrap,
.assign_mul_sat,
=> {
const lhs, const rhs = tree.nodeData(node).node_and_node;
return becomesMultilineExpr(tree, lhs) or becomesMultilineExpr(tree, rhs);
},
.@"break", .@"continue" => {
const opt_expr = tree.nodeData(node).opt_token_and_opt_node[1];
return optBecomesMultilineExpr(tree, opt_expr);
},
.anyframe_type => return becomesMultilineExpr(tree, tree.nodeData(node).token_and_node[1]),
.@"errdefer",
.@"defer",
.for_range,
.switch_range,
.switch_case_one,
.switch_case_inline_one,
.switch_case,
.switch_case_inline,
.asm_output,
.asm_input,
.fn_decl,
.container_field,
.container_field_init,
.container_field_align,
.root,
.global_var_decl,
.local_var_decl,
.simple_var_decl,
.aligned_var_decl,
.test_decl,
=> unreachable,
fn drainNoNewline(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
if (std.mem.indexOfScalar(u8, w.buffered(), '\n') != null) {
return error.WriteFailed;
}
var n: usize = 0;
for (data[0 .. data.len - 1]) |v| {
if (std.mem.indexOfScalar(u8, v, '\n') != null) {
return error.WriteFailed;
}
n += v.len;
}
const pattern = data[data.len - 1];
if (splat != 0 and std.mem.indexOfScalar(u8, pattern, '\n') != null) {
return error.WriteFailed;
}
n += pattern.len * splat;
w.end = 0;
return n;
}
fn isOneLineArrayType(
tree: Ast,
array_type: Ast.full.ArrayType,
rbracket: Ast.TokenIndex,
) bool {
return tree.tokensOnSameLine(array_type.ast.lbracket, rbracket) and
!becomesMultilineExpr(tree, array_type.ast.elem_count) and
!optBecomesMultilineExpr(tree, array_type.ast.sentinel);
fn rendersMultiline(r: *const Render, node: Ast.Node.Index) error{OutOfMemory}!bool {
var no_nl_buf: [64]u8 = undefined;
var no_nl_w: Writer = .{
.vtable = &.{ .drain = drainNoNewline },
.buffer = &no_nl_buf,
};
if (r.ais.disabled_offset != null) return true;
var sub_ais: AutoIndentingStream = .init(r.gpa, &no_nl_w, r.ais.indent_delta);
defer sub_ais.deinit();
// The following are needed to make sure isLineOverIndented is correct
sub_ais.indent_count = r.ais.indent_count;
sub_ais.applied_indent = r.ais.applied_indent;
sub_ais.current_line_empty = r.ais.current_line_empty;
var sub_r: Render = .{
.gpa = r.gpa,
.ais = &sub_ais,
.tree = r.tree,
.fixups = r.fixups,
};
renderExpression(&sub_r, node, .none) catch |e| return switch (e) {
error.OutOfMemory => return error.OutOfMemory,
error.WriteFailed => return true,
};
if (sub_ais.disabled_offset != null) return true;
if (std.mem.indexOfScalar(u8, no_nl_w.buffered(), '\n') != null) {
return true;
}
return false;
}
fn renderArrayType(
@@ -1335,7 +1018,9 @@ fn renderArrayType(
const tree = r.tree;
const ais = r.ais;
const rbracket = tree.firstToken(array_type.ast.elem_type) - 1;
const one_line = isOneLineArrayType(tree, array_type, rbracket);
const one_line = tree.tokensOnSameLine(array_type.ast.lbracket, rbracket) and
!try rendersMultiline(r, array_type.ast.elem_count) and
(if (array_type.ast.sentinel.unwrap()) |s| !try rendersMultiline(r, s) else true);
const inner_space = if (one_line) Space.none else Space.newline;
try ais.pushIndent(.normal);
try renderToken(r, array_type.ast.lbracket, inner_space); // lbracket
@@ -2524,6 +2209,10 @@ fn renderArrayInit(
try renderSpace(&sub_r, after_expr, tokenSliceForRender(tree, after_expr).len, .none);
buf.clearRetainingCapacity();
// The following are needed to make sure isLineOverIndented is not influenced by
// the previous element.
sub_ais.indent_count = 0;
sub_ais.applied_indent = 0;
}
}
+29
View File
@@ -6930,6 +6930,35 @@ test "zig fmt: cast builtins are not reordered with comments" {
);
}
test "zig fmt: inner over-indented if expressions becoming multiline" {
try testTransform(
\\const a = (b or
\\c) and [if (d) {}]T; // If the if-statement is kept on the same line it becomes multiline
\\const a = (b or
\\c)[if (d) {}]; // If the if-statement is kept on the same line it becomes multiline
\\const a = .{a, b, (c or
\\d), if (d) {}, e, f, g,};
\\
,
\\const a = (b or
\\ c) and [
\\ if (d) {}
\\]T; // If the if-statement is kept on the same line it becomes multiline
\\const a = (b or
\\ c)[
\\ if (d) {}
\\]; // If the if-statement is kept on the same line it becomes multiline
\\const a = .{
\\ a, b,
\\ (c or
\\ d),
\\ if (d) {}, e,
\\ f, g,
\\};
\\
);
}
test "recovery: top level" {
try testError(
\\test "" {inline}