diff --git a/.c3fmt b/.c3fmt index 94c583b..e0d0e4d 100644 --- a/.c3fmt +++ b/.c3fmt @@ -7,8 +7,9 @@ max_line_length: 120 brace_style: ALLMAN else_on_newline: true +newline_at_eof: false spaces_before_trailing_comment: 1 align_assignments: true -align_comments: true \ No newline at end of file +align_comments: true diff --git a/README.md b/README.md index 7ce8a06..08f7de5 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,11 @@ You can look at [.c3fmt](.c3fmt) for the default configuration. | `max_line_length` | Maximum line length before wrapping. | `120` | | `brace_style` | The brace style to use: `ALLMAN` or `K&R`. | `ALLMAN` | | `else_on_newline` | Whether to put `else` on a new line. | `true` | +| `newline_at_eof` | Ensure formatted output ends with a newline. | `false` | | `spaces_before_trailing_comment` | The number of space inserted before trailing comments | `1` | | `align_assignments` | Align `=` and `=>` in consecutive declarations/assignments. | `true` | | `align_comments` | Align trailing comments in consecutive lines. | `true` | +| `align_string_concat` | Align wrapped adjacent string literals under the first literal's column (otherwise indent one level). | `false` | ## Building @@ -149,4 +151,3 @@ The test suite includes: ## Known issues *(none yet)* - diff --git a/src/config.c3 b/src/config.c3 index a2add97..a09d971 100644 --- a/src/config.c3 +++ b/src/config.c3 @@ -17,11 +17,13 @@ struct Config BraceStyle brace_style; bool else_on_newline; + bool newline_at_eof; uint spaces_before_trailing_comment; bool align_assignments; bool align_comments; + bool align_string_concat; } fn Config default_config() @@ -34,9 +36,11 @@ fn Config default_config() .max_line_length = 120, .brace_style = ALLMAN, .else_on_newline = true, + .newline_at_eof = false, .spaces_before_trailing_comment = 1, .align_assignments = true, .align_comments = true, + .align_string_concat = false, }; } @@ -118,12 +122,16 @@ fn Config? parse_from_file(String file_path) => @pool() } case "else_on_newline": conf.else_on_newline = val.to_bool()!; + case "newline_at_eof": + conf.newline_at_eof = val.to_bool()!; case "spaces_before_trailing_comment": conf.spaces_before_trailing_comment = (uint)val.to_int()!; case "align_assignments": conf.align_assignments = val.to_bool()!; case "align_comments": conf.align_comments = val.to_bool()!; + case "align_string_concat": + conf.align_string_concat = val.to_bool()!; default: return INVALID_CONFIG~; @@ -131,4 +139,4 @@ fn Config? parse_from_file(String file_path) => @pool() } return conf; -} \ No newline at end of file +} diff --git a/src/formatter.c3 b/src/formatter.c3 index 3845b74..249a9da 100644 --- a/src/formatter.c3 +++ b/src/formatter.c3 @@ -449,27 +449,55 @@ fn bool C3Fmt.has_trailing_comma(&self, TSNode node) return false; } -fn bool is_alignable_block_node(TSNode node) +fn bool is_declaration_wrapper(TSNode node) { String type = node.type(); - if (type == "declaration_stmt" || type == "global_declaration") + return type == "declaration_stmt" || type == "global_declaration"; +} + +fn TSNode declaration_target(TSNode node) +{ + if (!is_declaration_wrapper(node)) return node; + + for (uint i = 0; i < ts::node_named_child_count(node); ++i) { - if (ts::node_named_child_count(node) == 0) return false; - node = ts::node_named_child(node, 0); - type = node.type(); + TSNode child = ts::node_named_child(node, i); + if (child.type() == "doc_comment" || child.is_comment()) continue; + return child; } - return type == "const_declaration" || type == "var_declaration" || (type == "declaration" && node.start_point_row() == node.end_point_row()) || type == "enum_constant"; + return (TSNode){}; } -fn TSNode find_assignment_op(TSNode node) +fn void C3Fmt.process_declaration_prefix(&self, TSNode node, TSNode target) { - TSNode target = node; - String type = node.type(); - if (type == "declaration_stmt" || type == "global_declaration") + if (!is_declaration_wrapper(node)) return; + + for (uint i = 0; i < ts::node_child_count(node); ++i) { - if (ts::node_named_child_count(node) == 0) return (TSNode){}; - target = ts::node_named_child(node, 0); + TSNode child = ts::node_child(node, i); + if (ts::node_eq(child, target)) return; + if (ts::node_is_named(child)) self.process_node(child); } +} + +fn bool is_alignable_block_node(TSNode node) +{ + node = declaration_target(node); + if (ts::node_is_null(node)) return false; + String type = node.type(); + if (type == "const_declaration" || type == "var_declaration" || type == "enum_constant") return true; + if (type != "declaration") return false; + if (node.start_point_row() == node.end_point_row()) return true; + if (!node.has_child_with_field("right")) return false; + String right_type = node.get_child_with_field("right").type(); + return right_type == "binary_expr" || right_type == "ternary_expr" || right_type == "elvis_orelse_expr"; +} + +fn TSNode find_assignment_op(TSNode node) +{ + TSNode target = declaration_target(node); + if (ts::node_is_null(target)) return (TSNode){}; + String type = target.type(); for (uint i = 0; i < ts::node_child_count(target); ++i) { @@ -506,6 +534,17 @@ fn void C3Fmt.process_assignment_left(&self, TSNode target, TSNode op) } } +fn void C3Fmt.process_alignment_probe_node(&self, TSNode child) +{ + if (child.type() == "declaration_stmt" || child.type() == "global_declaration") + { + self.process_composite(child, append_newline: false); + return; + } + + self.process_node(child, false); +} + fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint end_index) { usz max_op_col = 0; @@ -525,7 +564,7 @@ fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint if (!ts::node_is_null(op)) { // Format up to the '=' - TSNode target = (child.type() == "declaration_stmt" || child.type() == "global_declaration") ? ts::node_named_child(child, 0) : child; + TSNode target = declaration_target(child); self.process_assignment_left(target, op); max_op_col = max(max_op_col, self.current_column()); } @@ -533,15 +572,7 @@ fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint // Reset and format whole node without trailing newline to find comment col while (self.buf.len() > start_buf_len) (void)self.pop_token(); - if (child.type() == "declaration_stmt" || child.type() == "global_declaration") - { - // We need to NOT append newline here - self.process_composite(child, append_newline: false); - } - else - { - self.process_node(child, false); - } + self.process_alignment_probe_node(child); if (i + 1 < named_child_count) { @@ -557,6 +588,41 @@ fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint } } + if (self.config.max_line_length > 0 && max_op_col > 0) + { + for (uint i = start_index; i < end_index; ++i) + { + TSNode child = ts::node_named_child(parent, i); + if (child.is_comment()) continue; + + sz start_buf_len = self.buf.len(); + TSNode op = find_assignment_op(child); + if (ts::node_is_null(op)) continue; + + TSNode target = declaration_target(child); + TSNode right = (target.type() == "macro_declaration" || !target.has_child_with_field("right")) + ? (TSNode){} + : target.get_child_with_field("right"); + if (ts::node_is_null(right) || right.type() != "call_expr") continue; + + self.process_assignment_left(target, op); + usz op_col = self.current_column(); + while (self.buf.len() > start_buf_len) (void)self.pop_token(); + + self.process_alignment_probe_node(child); + usz line_col = self.current_column(); + while (self.buf.len() > start_buf_len) (void)self.pop_token(); + + if (max_op_col > op_col + && line_col <= self.config.max_line_length + && line_col + (max_op_col - op_col) > self.config.max_line_length) + { + max_op_col = 0; + break; + } + } + } + if (max_op_col > 80) { max_op_col = 0; } @@ -587,9 +653,11 @@ fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint if (!ts::node_is_null(op) && max_op_col > 0) { - TSNode target = (child.type() == "declaration_stmt" || child.type() == "global_declaration") ? ts::node_named_child(child, 0) : child; + TSNode target = declaration_target(child); TSNode right = (target.type() == "macro_declaration" || !target.has_child_with_field("right")) ? (TSNode){} : target.get_child_with_field("right"); + self.process_declaration_prefix(child, target); + // Format until '=' self.process_assignment_left(target, op); @@ -610,7 +678,7 @@ fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint TSNode decl = ts::node_named_child(parent, m); TSNode op2 = find_assignment_op(decl); if (!ts::node_is_null(op2)) { - TSNode t2 = (decl.type() == "declaration_stmt" || decl.type() == "global_declaration") ? ts::node_named_child(decl, 0) : decl; + TSNode t2 = declaration_target(decl); TSNode r2 = (t2.type() == "macro_declaration" || !t2.has_child_with_field("right")) ? (TSNode){} : t2.get_child_with_field("right"); // For macros, the 'right' side is the body, but it starts with => if (ts::node_is_null(r2) && t2.type() == "macro_declaration") r2 = op2; @@ -648,9 +716,14 @@ fn void C3Fmt.process_aligned_block(&self, TSNode parent, uint start_index, uint } // Semicolon of declaration_stmt if any - if (child.type() == "declaration_stmt" || child.type() == "global_declaration") { - for (uint s = 1; s < ts::node_child_count(child); ++s) { + if (is_declaration_wrapper(child)) { + bool found_target = false; + for (uint s = 0; s < ts::node_child_count(child); ++s) { TSNode sc = ts::node_child(child, s); + if (!found_target) { + if (ts::node_eq(sc, target)) found_target = true; + continue; + } if (ts::node_is_named(sc)) { if (sc.is_comment()) { self.buf.push(token::space()); @@ -899,6 +972,55 @@ fn void C3Fmt.process_composite( } } +<* + Format a sequence of adjacent string/bytes literals (implicit concatenation). + Each literal after the first becomes a wrap point, so an over-long concatenation + is broken with one literal per line, keeping the first on the opening line. + @param node : "The string_expr or bytes_expr node" +*> +fn void C3Fmt.process_concat_literals(&self, TSNode node) +{ + // Comments between literals are rare; fall back to the plain layout for them. + for (uint i = 0; i < ts::node_child_count(node); ++i) + { + if (ts::node_child(node, i).is_comment()) + { + self.process_composite(node); + return; + } + } + + if (!self.wrapping_enabled) + { + self.process_composite(node); + return; + } + + usz id = self.begin_wrap_group(2); + + // When aligning, continuation literals line up under the first literal's + // column; otherwise they hang one indent level below. + bool align = self.config.align_string_concat; + usz align_col = align ? self.current_column() : 0; + + for (uint i = 0; i < ts::node_child_count(node); ++i) + { + TSNode child = ts::node_child(node, i); + + if (i > 0) + { + self.buf.space(); + self.buf.wrap_point({ .indent = align ? 0 : 1, .group_id = id }); + if (align) + { + self.buf.push(token::align(align_col)); + } + } + + self.process_node(child); + } +} + <* Rawly format node sequentially, without appending any separator between them. Useful for nodes like field accessor ("my_struct.my_field"). @@ -1777,6 +1899,8 @@ fn void C3Fmt.process_node( } } bool is_multi = start_row != node.end_point().row; + usz id = 0; + bool has_const = false; for (uint i = 0; i < ts::node_child_count(node); ++i) { TSNode child = ts::node_child(node, i); @@ -1795,6 +1919,11 @@ fn void C3Fmt.process_node( { self.buf.newline(); self.indent(); + id = self.begin_wrap_group(2); + } + else + { + id = self.begin_wrap_group(2); } continue; } @@ -1803,7 +1932,6 @@ fn void C3Fmt.process_node( { if (is_multi) { - self.buf.newline(); self.dedent(); } self.buf.text(";"); @@ -1822,6 +1950,18 @@ fn void C3Fmt.process_node( } } + if (type == "const_ident" && self.wrapping_enabled) + { + if (!is_multi || has_const) + { + self.buf.wrap_point({ + .indent = is_multi ? 0 : 1, + .group_id = id + }); + } + has_const = true; + } + if (child.is_comment()) { String comment_str = self.extract_comment_string(child); @@ -2460,8 +2600,10 @@ fn void C3Fmt.process_node( case "doc_comment_contract_descriptor": self.buf.text(node.get_text(self.source_code)); - case "bytes_expr": // TODO idk what this is, a space separated list of bytes_literal ? - case "string_expr": // same... + case "bytes_expr": // space separated list of bytes_literal + case "string_expr": // adjacent string literals (implicit concatenation) + self.process_concat_literals(node); + case "lengthof": case "$eval": case "$stringify": diff --git a/src/printer.c3 b/src/printer.c3 index 5ab0986..bbe611a 100644 --- a/src/printer.c3 +++ b/src/printer.c3 @@ -246,6 +246,12 @@ fn void C3Fmt.print(&self) default: } } + + String output = self.dstr_buf.str_view(); + if (self.config.newline_at_eof && output.len > 0 && output[^1] != '\n') + { + self.dstr_buf.append('\n'); + } } fn void C3Fmt.print_indent(&self) @@ -567,4 +573,4 @@ fn bool TokenBuffer.gobble_last_newline(&self) } return false; -} \ No newline at end of file +} diff --git a/test/corpus.c3 b/test/corpus.c3 index 3551b74..2a0a91d 100644 --- a/test/corpus.c3 +++ b/test/corpus.c3 @@ -68,12 +68,47 @@ fn void comments() @test { test("comments", "test/corpus/comments.c3", "test/corpus/comments_f.c3", conf); } +fn void string_concat() @test { + Config conf = c3fmt::default_config(); + conf.max_line_length = 80; + test("string_concat", "test/corpus/string_concat.c3", "test/corpus/string_concat_f.c3", conf); +} + +fn void string_concat_align() @test { + Config conf = c3fmt::default_config(); + conf.max_line_length = 80; + conf.align_string_concat = true; + test("string_concat_align", "test/corpus/string_concat.c3", "test/corpus/string_concat_align_f.c3", conf); +} + fn void alignment() @test { Config conf = c3fmt::default_config(); conf.max_line_length = 180; test("alignment", "test/corpus/alignment.c3", "test/corpus/alignment_f.c3", conf); } +fn void alignment_wrap_stability() @test { + test("alignment_wrap_stability", "test/corpus/alignment_wrap_stability.c3", "test/corpus/alignment_wrap_stability_f.c3"); +} + +fn void newline_at_eof() @test { + String input = "fn void main() {}"; + + Config conf = c3fmt::default_config(); + assert(!conf.newline_at_eof); + + String without_newline = c3fmt::format_string(mem, input, conf, check_semantic: true)!!; + defer without_newline.free(mem); + assert(without_newline.len > 0); + assert(without_newline[^1] != '\n'); + + conf.newline_at_eof = true; + String with_newline = c3fmt::format_string(mem, input, conf, check_semantic: true)!!; + defer with_newline.free(mem); + assert(with_newline.len > 0); + assert(with_newline[^1] == '\n'); +} + fn void indent_leak() @test { test("indent_leak", "test/corpus/indent_leak.c3", "test/corpus/indent_leak_f.c3"); } diff --git a/test/corpus/alignment.c3 b/test/corpus/alignment.c3 index 038439c..fe855fe 100644 --- a/test/corpus/alignment.c3 +++ b/test/corpus/alignment.c3 @@ -8,3 +8,11 @@ const int[2][] SAUCER_LINES = { {6,0}, {6,1}, {6,2}, {6,3}, {6,4}, {6,5}, {7,0}, {7,1}, {7,2}, {7,3}, {7,4}, {7,5} }; + +<* + Constants in this group should align even when the first declaration has a + doc comment. +*> +const int FIRST = 1; +const int SECOND_LONGER = 2; +const int THIRD = 3; diff --git a/test/corpus/alignment_f.c3 b/test/corpus/alignment_f.c3 index 95a7e61..435acbb 100644 --- a/test/corpus/alignment_f.c3 +++ b/test/corpus/alignment_f.c3 @@ -4,3 +4,11 @@ const int[2][] SAUCER_LINES = { { 6, 0 }, { 6, 1 }, { 6, 2 }, { 6, 3 }, { 6, 4 }, { 6, 5 }, { 7, 0 }, { 7, 1 }, { 7, 2 }, { 7, 3 }, { 7, 4 }, { 7, 5 } }; + +<* + Constants in this group should align even when the first declaration has a + doc comment. +*> +const int FIRST = 1; +const int SECOND_LONGER = 2; +const int THIRD = 3; diff --git a/test/corpus/alignment_wrap_stability.c3 b/test/corpus/alignment_wrap_stability.c3 new file mode 100644 index 0000000..7a2bebf --- /dev/null +++ b/test/corpus/alignment_wrap_stability.c3 @@ -0,0 +1,6 @@ +fn void update_record(Source* source) +{ + ExtremelyLongComputationContext* context = (ExtremelyLongComputationContext*)source.context; + Result result = build_record(source.primary_value, context, source.secondary_value, source.tertiary_value); + (void)result; +} diff --git a/test/corpus/alignment_wrap_stability_f.c3 b/test/corpus/alignment_wrap_stability_f.c3 new file mode 100644 index 0000000..7a2bebf --- /dev/null +++ b/test/corpus/alignment_wrap_stability_f.c3 @@ -0,0 +1,6 @@ +fn void update_record(Source* source) +{ + ExtremelyLongComputationContext* context = (ExtremelyLongComputationContext*)source.context; + Result result = build_record(source.primary_value, context, source.secondary_value, source.tertiary_value); + (void)result; +} diff --git a/test/corpus/string_concat.c3 b/test/corpus/string_concat.c3 new file mode 100644 index 0000000..bef7418 --- /dev/null +++ b/test/corpus/string_concat.c3 @@ -0,0 +1,10 @@ +module x; + +const String SHORT = "ab" "cd"; + +const String LONG = "aaaaaaaaaaaaaaaaaaaaaaaa" "bbbbbbbbbbbbbbbbbbbbbbbb" "cccccccccccccccccccccccc" "dddddddddddddddddddddddd"; + +fn void f() +{ + String s = "one one one one one one one" "two two two two two two two" "three three three three three"; +} diff --git a/test/corpus/string_concat_align_f.c3 b/test/corpus/string_concat_align_f.c3 new file mode 100644 index 0000000..2292014 --- /dev/null +++ b/test/corpus/string_concat_align_f.c3 @@ -0,0 +1,15 @@ +module x; + +const String SHORT = "ab" "cd"; + +const String LONG = "aaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccc" + "dddddddddddddddddddddddd"; + +fn void f() +{ + String s = "one one one one one one one" + "two two two two two two two" + "three three three three three"; +} diff --git a/test/corpus/string_concat_f.c3 b/test/corpus/string_concat_f.c3 new file mode 100644 index 0000000..cd052f1 --- /dev/null +++ b/test/corpus/string_concat_f.c3 @@ -0,0 +1,15 @@ +module x; + +const String SHORT = "ab" "cd"; + +const String LONG = "aaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbb" + "cccccccccccccccccccccccc" + "dddddddddddddddddddddddd"; + +fn void f() +{ + String s = "one one one one one one one" + "two two two two two two two" + "three three three three three"; +} diff --git a/test/corpus/wrapping.c3 b/test/corpus/wrapping.c3 index 07e92be..bb423f2 100644 --- a/test/corpus/wrapping.c3 +++ b/test/corpus/wrapping.c3 @@ -26,4 +26,29 @@ Pipeline a = { .offset = "a very long string that will go beyond 80 words",// Todo: Add CameraData offset here .range = "a very long string that will go beyond 80 words" } -}; \ No newline at end of file +}; + +faultdef SAMPLE_ALPHA, SAMPLE_BETA, SAMPLE_GAMMA, SAMPLE_DELTA, SAMPLE_EPSILON, SAMPLE_ZETA, SAMPLE_ETA, SAMPLE_THETA, SAMPLE_IOTA; + +fn void wrap_assignment_expr() +{ + ParserState* state = (ParserState*)raw; + WorkItem* item = (WorkItem*)state.owner; + Handler handler = item.handler; + bool paused = (state.flags & SAMPLE_FLAG_DISABLED) != 0; + bool ready = !state.error_code; + ulong total_count = ready ? compute_total_count(context, state, item) : 0; + bool should_continue = ready && !paused && (long)item.interval > 0 && total_count <= (ulong)item.remaining_count; +} + +fn void wrap_assignment_expr_stable() +{ + ParserState* state = (ParserState*)raw; + WorkItem* item = (WorkItem*)state.owner; + Handler handler = item.handler; + bool paused = (state.flags & SAMPLE_FLAG_DISABLED) != 0; + bool ready = !state.error_code; + ulong total_count = ready ? compute_total_count(context, state, item) : 0; + bool should_continue = ready && !paused && (long)item.interval > 0 + && total_count <= (ulong)item.remaining_count; +} diff --git a/test/corpus/wrapping_f.c3 b/test/corpus/wrapping_f.c3 index 873c1f9..628e3e0 100644 --- a/test/corpus/wrapping_f.c3 +++ b/test/corpus/wrapping_f.c3 @@ -38,3 +38,40 @@ Pipeline a = { .range = "a very long string that will go beyond 80 words" } }; + +faultdef + SAMPLE_ALPHA, + SAMPLE_BETA, + SAMPLE_GAMMA, + SAMPLE_DELTA, + SAMPLE_EPSILON, + SAMPLE_ZETA, + SAMPLE_ETA, + SAMPLE_THETA, + SAMPLE_IOTA; + +fn void wrap_assignment_expr() +{ + ParserState* state = (ParserState*)raw; + WorkItem* item = (WorkItem*)state.owner; + Handler handler = item.handler; + bool paused = (state.flags & SAMPLE_FLAG_DISABLED) != 0; + bool ready = !state.error_code; + ulong total_count = ready ? compute_total_count(context, state, item) + : 0; + bool should_continue = ready && !paused && (long)item.interval > 0 + && total_count <= (ulong)item.remaining_count; +} + +fn void wrap_assignment_expr_stable() +{ + ParserState* state = (ParserState*)raw; + WorkItem* item = (WorkItem*)state.owner; + Handler handler = item.handler; + bool paused = (state.flags & SAMPLE_FLAG_DISABLED) != 0; + bool ready = !state.error_code; + ulong total_count = ready ? compute_total_count(context, state, item) + : 0; + bool should_continue = ready && !paused && (long)item.interval > 0 + && total_count <= (ulong)item.remaining_count; +}