authorgravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2026-03-22 17:42:57-04:00
committergravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2026-03-25 17:30:51-04:00
log09e523bd51185ae8d71c347ce606e3b74dea1d5b
treed0ced379f42a3d87ed2caba17b09915314dff6a3
parent92915a42b55f2916f36ed6b1a2d51363331f47f8

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.

2 files changed, 90 insertions(+), 372 deletions(-)

lib/std/zig/Ast/Render.zig+61-372
...@@ -652,10 +652,12 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {...@@ -652,10 +652,12 @@ fn renderExpression(r: *Render, node: Ast.Node.Index, space: Space) Error!void {
652 const lhs, const rhs = tree.nodeData(node).node_and_node;652 const lhs, const rhs = tree.nodeData(node).node_and_node;
653 const lbracket = tree.firstToken(rhs) - 1;653 const lbracket = tree.firstToken(rhs) - 1;
654 const rbracket = tree.lastToken(rhs) + 1;654 const rbracket = tree.lastToken(rhs) + 1;
655 try renderExpression(r, lhs, .none);
656 // One lien check must come after rendering lhs since it can influence
657 // isLineOverIndented
655 const one_line = tree.tokensOnSameLine(lbracket, rbracket) and658 const one_line = tree.tokensOnSameLine(lbracket, rbracket) and
656 !becomesMultilineExpr(tree, rhs);659 !try rendersMultiline(r, rhs);
657 const inner_space = if (one_line) Space.none else Space.newline;660 const inner_space = if (one_line) Space.none else Space.newline;
658 try renderExpression(r, lhs, .none);
659 try ais.pushIndent(.normal);661 try ais.pushIndent(.normal);
660 try renderToken(r, lbracket, inner_space); // [662 try renderToken(r, lbracket, inner_space); // [
661 try renderExpression(r, rhs, inner_space);663 try renderExpression(r, rhs, inner_space);
...@@ -951,380 +953,61 @@ fn renderExpressionFixup(r: *Render, node: Ast.Node.Index, space: Space) Error!v...@@ -951,380 +953,61 @@ fn renderExpressionFixup(r: *Render, node: Ast.Node.Index, space: Space) Error!v
951 }953 }
952}954}
953955
954/// Same as becomesMultilineExpr, but returns false when `node == .none`956fn drainNoNewline(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
955fn optBecomesMultilineExpr(tree: Ast, node: Ast.Node.OptionalIndex) bool {957 if (std.mem.indexOfScalar(u8, w.buffered(), '\n') != null) {
956 return if (node.unwrap()) |payload| becomesMultilineExpr(tree, payload) else false;958 return error.WriteFailed;
957}959 }
958960
959/// May return false if `node` is already multiline961 var n: usize = 0;
960fn becomesMultilineExpr(tree: Ast, node: Ast.Node.Index) bool {962 for (data[0 .. data.len - 1]) |v| {
961 // Conditions related to comments, doc comments, and multiline string literals are ignored963 if (std.mem.indexOfScalar(u8, v, '\n') != null) {
962 // since they always go to the end of the line, which already make them a multi-line964 return error.WriteFailed;
963 // expression (since they contain a newline).965 }
964 switch (tree.nodeTag(node)) {966 n += v.len;
965 .identifier,967 }
966 .number_literal,
967 .char_literal,
968 .unreachable_literal,
969 .anyframe_literal,
970 .string_literal,
971 .multiline_string_literal,
972 .error_value,
973 .enum_literal,
974 => return false,
975 .container_decl_trailing,
976 .container_decl_arg_trailing,
977 .container_decl_two_trailing,
978 .tagged_union_trailing,
979 .tagged_union_enum_tag_trailing,
980 .tagged_union_two_trailing,
981 .switch_comma,
982 .builtin_call_two_comma,
983 .builtin_call_comma,
984 .call_one_comma,
985 .call_comma,
986 .struct_init_one_comma,
987 .struct_init_dot_two_comma,
988 .struct_init_dot_comma,
989 .struct_init_comma,
990 .array_init_one_comma,
991 .array_init_dot_two_comma,
992 .array_init_dot_comma,
993 .array_init_comma,
994 // The following always have a non-zero amount of members
995 // which is also the condition for them to be multi-line.
996 .block,
997 .block_semicolon,
998 => return true,
999 .block_two,
1000 .block_two_semicolon,
1001 => return tree.nodeData(node).opt_node_and_opt_node[0] != .none,
1002 .container_decl,
1003 .container_decl_arg,
1004 .container_decl_two,
1005 .tagged_union,
1006 .tagged_union_enum_tag,
1007 .tagged_union_two,
1008 => {
1009 var buf: [2]Ast.Node.Index = undefined;
1010 const full = tree.fullContainerDecl(&buf, node).?;
1011 if (full.ast.arg.unwrap()) |arg| {
1012 if (becomesMultilineExpr(tree, arg))
1013 return true;
1014 }
1015 // This does the same checks as `isOneLineContainerDecl`, however it avoids unnecessary
1016 // checks related to comments and multiline strings, which would mean the container is
1017 // already multiple lines.
1018 for (full.ast.members) |member| {
1019 if (tree.fullContainerField(member)) |field_full| {
1020 for ([_]Ast.Node.OptionalIndex{
1021 field_full.ast.type_expr,
1022 field_full.ast.align_expr,
1023 field_full.ast.value_expr,
1024 }) |opt_expr| {
1025 if (opt_expr.unwrap()) |expr| {
1026 if (becomesMultilineExpr(tree, expr))
1027 return true;
1028 }
1029 }
1030 } else return true;
1031 }
1032 return false;
1033 },
1034 .error_set_decl => {
1035 const lbrace, const rbrace = tree.nodeData(node).token_and_token;
1036 return !isOneLineErrorSetDecl(tree, lbrace, rbrace);
1037 },
1038 .@"switch" => {
1039 const op, const extra_index = tree.nodeData(node).node_and_extra;
1040 const case_range = tree.extraData(extra_index, Ast.Node.SubRange);
1041 return @intFromEnum(case_range.end) - @intFromEnum(case_range.start) != 0 or
1042 becomesMultilineExpr(tree, op);
1043 },
1044 .for_simple, .@"for" => {
1045 const full = tree.fullFor(node).?;
1046 if (becomesMultilineExpr(tree, full.ast.then_expr) or
1047 optBecomesMultilineExpr(tree, full.ast.else_expr))
1048 return true;
1049
1050 for (full.ast.inputs) |expr| {
1051 if (if (tree.nodeTag(expr) == .for_range) blk: {
1052 const lhs, const rhs = tree.nodeData(expr).node_and_opt_node;
1053 break :blk becomesMultilineExpr(tree, lhs) or optBecomesMultilineExpr(tree, rhs);
1054 } else becomesMultilineExpr(tree, expr))
1055 return true;
1056 }
1057 const final_input_expr = full.ast.inputs[full.ast.inputs.len - 1];
1058 if (tree.tokenTag(tree.lastToken(final_input_expr) + 1) == .comma)
1059 return true;
1060
1061 const token_tags = tree.tokens.items(.tag);
1062 const payload = full.payload_token;
1063 const pipe = std.mem.indexOfScalarPos(Token.Tag, token_tags, payload, .pipe).?;
1064 return token_tags[@intCast(pipe - 1)] == .comma;
1065 },
1066 .while_simple,
1067 .while_cont,
1068 .@"while",
1069 => {
1070 const full = tree.fullWhile(node).?;
1071 return becomesMultilineExpr(tree, full.ast.cond_expr) or
1072 becomesMultilineExpr(tree, full.ast.then_expr) or
1073 optBecomesMultilineExpr(tree, full.ast.cont_expr) or
1074 optBecomesMultilineExpr(tree, full.ast.else_expr);
1075 },
1076 .if_simple,
1077 .@"if",
1078 => {
1079 const full = tree.fullIf(node).?;
1080 return becomesMultilineExpr(tree, full.ast.cond_expr) or
1081 becomesMultilineExpr(tree, full.ast.then_expr) or
1082 optBecomesMultilineExpr(tree, full.ast.else_expr);
1083 },
1084 .fn_proto_simple,
1085 .fn_proto_multi,
1086 .fn_proto_one,
1087 .fn_proto,
1088 => {
1089 var buf: [1]Ast.Node.Index = undefined;
1090 const fn_proto = tree.fullFnProto(&buf, node).?;
1091
1092 for ([_]Ast.Node.OptionalIndex{
1093 fn_proto.ast.return_type,
1094 fn_proto.ast.align_expr,
1095 fn_proto.ast.addrspace_expr,
1096 fn_proto.ast.section_expr,
1097 fn_proto.ast.callconv_expr,
1098 }) |opt_expr| {
1099 if (opt_expr.unwrap()) |expr| {
1100 if (becomesMultilineExpr(tree, expr))
1101 return true;
1102 }
1103 }
1104 for (fn_proto.ast.params) |expr| {
1105 if (becomesMultilineExpr(tree, expr))
1106 return true;
1107 }
1108968
1109 const lparen = fn_proto.ast.fn_token + 1;969 const pattern = data[data.len - 1];
1110 const return_type = fn_proto.ast.return_type.unwrap().?;970 if (splat != 0 and std.mem.indexOfScalar(u8, pattern, '\n') != null) {
1111 const maybe_bang = tree.firstToken(return_type) - 1;971 return error.WriteFailed;
1112 const rparen = fnProtoRparen(tree, fn_proto, maybe_bang);
1113 return !isOneLineFnProto(tree, fn_proto, lparen, rparen);
1114 },
1115 .asm_simple,
1116 => {
1117 const lhs = tree.nodeData(node).node_and_token[0];
1118 return becomesMultilineExpr(tree, lhs);
1119 },
1120 .@"asm",
1121 => {
1122 const lhs, const extra_index = tree.nodeData(node).node_and_extra;
1123 const asm_extra = tree.extraData(extra_index, Ast.Node.Asm);
1124 return @intFromEnum(asm_extra.items_end) - @intFromEnum(asm_extra.items_start) != 0 or
1125 becomesMultilineExpr(tree, lhs) or optBecomesMultilineExpr(tree, asm_extra.clobbers);
1126 },
1127 .array_type, .array_type_sentinel => {
1128 const array_type = tree.fullArrayType(node).?;
1129 const rbracket = tree.firstToken(array_type.ast.elem_type) - 1;
1130 return !isOneLineArrayType(tree, array_type, rbracket) or
1131 becomesMultilineExpr(tree, array_type.ast.elem_type);
1132 },
1133 .array_access => {
1134 const lhs, const rhs = tree.nodeData(node).node_and_node;
1135 const lbracket = tree.firstToken(rhs) - 1;
1136 const rbracket = tree.lastToken(rhs) + 1;
1137 return !tree.tokensOnSameLine(lbracket, rbracket) or
1138 becomesMultilineExpr(tree, lhs) or
1139 becomesMultilineExpr(tree, rhs);
1140 },
1141 .call_one,
1142 .call,
1143 .builtin_call_two,
1144 .builtin_call,
1145 .array_init_one,
1146 .array_init_dot_two,
1147 .array_init_dot,
1148 .array_init,
1149 .struct_init_one,
1150 .struct_init_dot_two,
1151 .struct_init_dot,
1152 .struct_init,
1153 => |tag| {
1154 var buf: [2]Ast.Node.Index = undefined;
1155 const opt_lhs: Ast.Node.OptionalIndex, const items = switch (tag) {
1156 .call_one, .call => blk: {
1157 const full = tree.fullCall(buf[0..1], node).?;
1158 break :blk .{ full.ast.fn_expr.toOptional(), full.ast.params };
1159 },
1160 .builtin_call_two, .builtin_call => .{ .none, tree.builtinCallParams(&buf, node).? },
1161 .array_init_one,
1162 .array_init_dot_two,
1163 .array_init_dot,
1164 .array_init,
1165 => blk: {
1166 const full = tree.fullArrayInit(&buf, node).?;
1167 break :blk .{ full.ast.type_expr, full.ast.elements };
1168 },
1169 .struct_init_one,
1170 .struct_init_dot_two,
1171 .struct_init_dot,
1172 .struct_init,
1173 => blk: {
1174 const full = tree.fullStructInit(&buf, node).?;
1175 break :blk .{ full.ast.type_expr, full.ast.fields };
1176 },
1177 else => unreachable,
1178 };
1179 if (opt_lhs.unwrap()) |lhs| {
1180 if (becomesMultilineExpr(tree, lhs))
1181 return true;
1182 }
1183 for (items) |expr| {
1184 if (becomesMultilineExpr(tree, expr))
1185 return true;
1186 }
1187 return false;
1188 },
1189 .assign_destructure => {
1190 const full = tree.assignDestructure(node);
1191 for (full.ast.variables) |expr| {
1192 if (becomesMultilineExpr(tree, expr))
1193 return true;
1194 }
1195 return becomesMultilineExpr(tree, full.ast.value_expr);
1196 },
1197 .ptr_type_aligned,
1198 .ptr_type_sentinel,
1199 .ptr_type,
1200 .ptr_type_bit_range,
1201 => {
1202 const full = tree.fullPtrType(node).?;
1203 return becomesMultilineExpr(tree, full.ast.child_type) or
1204 optBecomesMultilineExpr(tree, full.ast.sentinel) or
1205 optBecomesMultilineExpr(tree, full.ast.align_node) or
1206 optBecomesMultilineExpr(tree, full.ast.addrspace_node) or
1207 optBecomesMultilineExpr(tree, full.ast.bit_range_start) or
1208 optBecomesMultilineExpr(tree, full.ast.bit_range_end);
1209 },
1210 .slice_open,
1211 .slice,
1212 .slice_sentinel,
1213 => {
1214 const full = tree.fullSlice(node).?;
1215 return becomesMultilineExpr(tree, full.ast.sliced) or
1216 becomesMultilineExpr(tree, full.ast.start) or
1217 optBecomesMultilineExpr(tree, full.ast.end) or
1218 optBecomesMultilineExpr(tree, full.ast.sentinel);
1219 },
1220 .@"comptime",
1221 .@"nosuspend",
1222 .@"suspend",
1223 .@"resume",
1224 .bit_not,
1225 .bool_not,
1226 .negation,
1227 .negation_wrap,
1228 .optional_type,
1229 .address_of,
1230 .deref,
1231 .@"try",
1232 => return becomesMultilineExpr(tree, tree.nodeData(node).node),
1233 .@"return" => return optBecomesMultilineExpr(tree, tree.nodeData(node).opt_node),
1234 .field_access,
1235 .unwrap_optional,
1236 .grouped_expression,
1237 => return becomesMultilineExpr(tree, tree.nodeData(node).node_and_token[0]),
1238 .add,
1239 .add_wrap,
1240 .add_sat,
1241 .array_cat,
1242 .array_mult,
1243 .bang_equal,
1244 .bit_and,
1245 .bit_or,
1246 .shl,
1247 .shl_sat,
1248 .shr,
1249 .bit_xor,
1250 .bool_and,
1251 .bool_or,
1252 .div,
1253 .equal_equal,
1254 .greater_or_equal,
1255 .greater_than,
1256 .less_or_equal,
1257 .less_than,
1258 .merge_error_sets,
1259 .mod,
1260 .mul,
1261 .mul_wrap,
1262 .mul_sat,
1263 .sub,
1264 .sub_wrap,
1265 .sub_sat,
1266 .@"orelse",
1267 .@"catch",
1268 .error_union,
1269 .assign,
1270 .assign_bit_and,
1271 .assign_bit_or,
1272 .assign_shl,
1273 .assign_shl_sat,
1274 .assign_shr,
1275 .assign_bit_xor,
1276 .assign_div,
1277 .assign_sub,
1278 .assign_sub_wrap,
1279 .assign_sub_sat,
1280 .assign_mod,
1281 .assign_add,
1282 .assign_add_wrap,
1283 .assign_add_sat,
1284 .assign_mul,
1285 .assign_mul_wrap,
1286 .assign_mul_sat,
1287 => {
1288 const lhs, const rhs = tree.nodeData(node).node_and_node;
1289 return becomesMultilineExpr(tree, lhs) or becomesMultilineExpr(tree, rhs);
1290 },
1291 .@"break", .@"continue" => {
1292 const opt_expr = tree.nodeData(node).opt_token_and_opt_node[1];
1293 return optBecomesMultilineExpr(tree, opt_expr);
1294 },
1295 .anyframe_type => return becomesMultilineExpr(tree, tree.nodeData(node).token_and_node[1]),
1296 .@"errdefer",
1297 .@"defer",
1298 .for_range,
1299 .switch_range,
1300 .switch_case_one,
1301 .switch_case_inline_one,
1302 .switch_case,
1303 .switch_case_inline,
1304 .asm_output,
1305 .asm_input,
1306 .fn_decl,
1307 .container_field,
1308 .container_field_init,
1309 .container_field_align,
1310 .root,
1311 .global_var_decl,
1312 .local_var_decl,
1313 .simple_var_decl,
1314 .aligned_var_decl,
1315 .test_decl,
1316 => unreachable,
1317 }972 }
973 n += pattern.len * splat;
974
975 w.end = 0;
976 return n;
1318}977}
1319978
1320fn isOneLineArrayType(979fn rendersMultiline(r: *const Render, node: Ast.Node.Index) error{OutOfMemory}!bool {
1321 tree: Ast,980 var no_nl_buf: [64]u8 = undefined;
1322 array_type: Ast.full.ArrayType,981 var no_nl_w: Writer = .{
1323 rbracket: Ast.TokenIndex,982 .vtable = &.{ .drain = drainNoNewline },
1324) bool {983 .buffer = &no_nl_buf,
1325 return tree.tokensOnSameLine(array_type.ast.lbracket, rbracket) and984 };
1326 !becomesMultilineExpr(tree, array_type.ast.elem_count) and985
1327 !optBecomesMultilineExpr(tree, array_type.ast.sentinel);986 if (r.ais.disabled_offset != null) return true;
987 var sub_ais: AutoIndentingStream = .init(r.gpa, &no_nl_w, r.ais.indent_delta);
988 defer sub_ais.deinit();
989 // The following are needed to make sure isLineOverIndented is correct
990 sub_ais.indent_count = r.ais.indent_count;
991 sub_ais.applied_indent = r.ais.applied_indent;
992 sub_ais.current_line_empty = r.ais.current_line_empty;
993
994 var sub_r: Render = .{
995 .gpa = r.gpa,
996 .ais = &sub_ais,
997 .tree = r.tree,
998 .fixups = r.fixups,
999 };
1000
1001 renderExpression(&sub_r, node, .none) catch |e| return switch (e) {
1002 error.OutOfMemory => return error.OutOfMemory,
1003 error.WriteFailed => return true,
1004 };
1005 if (sub_ais.disabled_offset != null) return true;
1006 if (std.mem.indexOfScalar(u8, no_nl_w.buffered(), '\n') != null) {
1007 return true;
1008 }
1009
1010 return false;
1328}1011}
13291012
1330fn renderArrayType(1013fn renderArrayType(
...@@ -1335,7 +1018,9 @@ fn renderArrayType(...@@ -1335,7 +1018,9 @@ fn renderArrayType(
1335 const tree = r.tree;1018 const tree = r.tree;
1336 const ais = r.ais;1019 const ais = r.ais;
1337 const rbracket = tree.firstToken(array_type.ast.elem_type) - 1;1020 const rbracket = tree.firstToken(array_type.ast.elem_type) - 1;
1338 const one_line = isOneLineArrayType(tree, array_type, rbracket);1021 const one_line = tree.tokensOnSameLine(array_type.ast.lbracket, rbracket) and
1022 !try rendersMultiline(r, array_type.ast.elem_count) and
1023 (if (array_type.ast.sentinel.unwrap()) |s| !try rendersMultiline(r, s) else true);
1339 const inner_space = if (one_line) Space.none else Space.newline;1024 const inner_space = if (one_line) Space.none else Space.newline;
1340 try ais.pushIndent(.normal);1025 try ais.pushIndent(.normal);
1341 try renderToken(r, array_type.ast.lbracket, inner_space); // lbracket1026 try renderToken(r, array_type.ast.lbracket, inner_space); // lbracket
...@@ -2524,6 +2209,10 @@ fn renderArrayInit(...@@ -2524,6 +2209,10 @@ fn renderArrayInit(
2524 try renderSpace(&sub_r, after_expr, tokenSliceForRender(tree, after_expr).len, .none);2209 try renderSpace(&sub_r, after_expr, tokenSliceForRender(tree, after_expr).len, .none);
25252210
2526 buf.clearRetainingCapacity();2211 buf.clearRetainingCapacity();
2212 // The following are needed to make sure isLineOverIndented is not influenced by
2213 // the previous element.
2214 sub_ais.indent_count = 0;
2215 sub_ais.applied_indent = 0;
2527 }2216 }
2528 }2217 }
25292218
lib/std/zig/parser_test.zig+29
...@@ -6930,6 +6930,35 @@ test "zig fmt: cast builtins are not reordered with comments" {...@@ -6930,6 +6930,35 @@ test "zig fmt: cast builtins are not reordered with comments" {
6930 );6930 );
6931}6931}
69326932
6933test "zig fmt: inner over-indented if expressions becoming multiline" {
6934 try testTransform(
6935 \\const a = (b or
6936 \\c) and [if (d) {}]T; // If the if-statement is kept on the same line it becomes multiline
6937 \\const a = (b or
6938 \\c)[if (d) {}]; // If the if-statement is kept on the same line it becomes multiline
6939 \\const a = .{a, b, (c or
6940 \\d), if (d) {}, e, f, g,};
6941 \\
6942 ,
6943 \\const a = (b or
6944 \\ c) and [
6945 \\ if (d) {}
6946 \\]T; // If the if-statement is kept on the same line it becomes multiline
6947 \\const a = (b or
6948 \\ c)[
6949 \\ if (d) {}
6950 \\]; // If the if-statement is kept on the same line it becomes multiline
6951 \\const a = .{
6952 \\ a, b,
6953 \\ (c or
6954 \\ d),
6955 \\ if (d) {}, e,
6956 \\ f, g,
6957 \\};
6958 \\
6959 );
6960}
6961
6933test "recovery: top level" {6962test "recovery: top level" {
6934 try testError(6963 try testError(
6935 \\test "" {inline}6964 \\test "" {inline}