diff --git a/compiler/fory_compiler/generators/base.py b/compiler/fory_compiler/generators/base.py index 9647af5b2d..dcc10a15a9 100644 --- a/compiler/fory_compiler/generators/base.py +++ b/compiler/fory_compiler/generators/base.py @@ -206,3 +206,185 @@ def get_license_header(self, comment_prefix: str = "//") -> str: return "\n".join( f"{comment_prefix} {line}" if line else comment_prefix for line in lines ) + + def wrap_line( + self, + line: str, + max_width: int = 80, + indent: str = "", + continuation_indent: str = None, + ) -> List[str]: + """Wrap a long line into multiple lines with max_width characters. + + Args: + line: The line to wrap + max_width: Maximum width per line (default 80) + indent: The indentation to preserve for the first line + continuation_indent: Extra indentation for continuation lines. + If None, uses indent + 4 spaces. + + Returns: + List of wrapped lines + """ + if continuation_indent is None: + continuation_indent = indent + " " + + # If line is already short enough, return as is + if len(line) <= max_width: + return [line] + + # Don't wrap C++ preprocessor directives (macros) + stripped = line.lstrip() + if stripped.startswith("#"): + return [line] + + # Don't wrap comment lines (license headers, etc.) + if ( + stripped.startswith("//") + or stripped.startswith("/*") + or stripped.startswith("*") + or stripped.startswith("#") + ): + return [line] + + # Extract the leading indent + leading_spaces = line[: len(line) - len(line.lstrip())] + + # Get the content without indent + content = line[len(leading_spaces) :] + + # If it's still too short after considering indent, don't wrap + if len(leading_spaces) + len(content) <= max_width: + return [line] + + # Find good break points (prefer breaking at spaces, commas, operators) + result = [] + current = content + first_line = True + + while len(leading_spaces) + len(current) > max_width: + # Calculate available width + if first_line: + available = max_width - len(leading_spaces) + else: + available = max_width - len(continuation_indent) + + if available <= 0: + # Can't wrap reasonably, return original + return [line] + + # Find the best break point + break_point = -1 + + # Look for break points in order of preference + search_text = current[:available] + + # Try to break at common delimiters (working backwards) + for delimiter in [ + ", ", + " && ", + " || ", + " + ", + " - ", + " * ", + " / ", + " = ", + " ", + ",", + ]: + idx = search_text.rfind(delimiter) + if idx > 0: + break_point = idx + len(delimiter) + break + + # If no good break point found, just break at max width + if break_point <= 0: + break_point = available + + # Add the line segment + if first_line: + result.append(leading_spaces + current[:break_point].rstrip()) + first_line = False + else: + result.append(continuation_indent + current[:break_point].rstrip()) + + # Continue with the rest + current = current[break_point:].lstrip() + + # Add the remaining content + if current: + if first_line: + result.append(leading_spaces + current) + else: + result.append(continuation_indent + current) + + return result if result else [line] + + def wrap_lines( + self, lines: List[str], max_width: int = 80, preserve_blank: bool = True + ) -> List[str]: + """Wrap multiple lines, handling each line's indentation. + + Args: + lines: List of lines to wrap + max_width: Maximum width per line + preserve_blank: If True, preserve blank lines as-is + + Returns: + List of wrapped lines + """ + result = [] + for line in lines: + if preserve_blank and not line.strip(): + result.append(line) + else: + result.extend(self.wrap_line(line, max_width)) + return result + + def format_long_line( + self, + prefix: str, + content: str, + suffix: str = "", + max_width: int = 80, + continuation_indent: str = " ", + ) -> List[str]: + """Format a long line with semantic understanding. + + This is for semantic line wrapping where we know the structure. + Use this when generating code to avoid breaking in wrong places. + + Args: + prefix: The prefix part (e.g., "public class ") + content: The main content that might be long + suffix: The suffix part (e.g., " {") + max_width: Maximum line width + continuation_indent: Indentation for continuation lines + + Returns: + List of formatted lines + + Example: + format_long_line("public class ", "VeryLongClassName", " {") + => ["public class VeryLongClassName {"] # if fits + => ["public class", " VeryLongClassName {"] # if too long + """ + full_line = prefix + content + suffix + if len(full_line) <= max_width: + return [full_line] + + # If content alone with suffix fits on next line + if len(continuation_indent + content + suffix) <= max_width: + return [prefix.rstrip(), continuation_indent + content + suffix] + + # Content is too long even on its own line + # Put each part on separate lines + lines = [prefix.rstrip()] + if len(continuation_indent + content) <= max_width: + lines.append(continuation_indent + content) + else: + # Content itself needs wrapping + lines.append(continuation_indent + content) + if suffix: + lines.append(suffix) + return lines diff --git a/compiler/fory_compiler/generators/go.py b/compiler/fory_compiler/generators/go.py index 34ee3410ac..95cf034b33 100644 --- a/compiler/fory_compiler/generators/go.py +++ b/compiler/fory_compiler/generators/go.py @@ -199,6 +199,49 @@ def message_has_unions(self, message: Message) -> bool: PrimitiveKind.ANY: "any", } + def wrap_line( + self, + line: str, + max_width: int = 80, + indent: str = "", + continuation_indent: str = None, + ) -> List[str]: + """Override base wrap_line to handle Go-specific syntax. + + Go has specific constructs that should not be wrapped: + 1. Function signatures - must not split between params and return type + 2. Struct field tags - backtick strings must not be split + 3. Single-line function bodies - must not split "{ return ... }" + """ + if continuation_indent is None: + continuation_indent = indent + " " + + # If line is already short enough, return as is + if len(line) <= max_width: + return [line] + + stripped = line.lstrip() + + # Don't wrap comments + if ( + stripped.startswith("//") + or stripped.startswith("/*") + or stripped.startswith("*") + ): + return [line] + + # Don't wrap lines with backticks (struct tags) + if "`" in line: + return [line] + + # Don't wrap function definitions (including one-liners like "func Foo() Type { return ... }") + # Detect by checking if line starts with "func " and contains "{" + if stripped.startswith("func ") and "{" in line: + return [line] + + # For all other cases, use base class wrapping + return super().wrap_line(line, max_width, indent, continuation_indent) + def generate(self) -> List[GeneratedFile]: """Generate Go files for the schema.""" files = [] @@ -521,7 +564,16 @@ def generate_union( lines.append(")") lines.append("") - lines.append(f"type {type_name} struct {{") + # Type declaration with semantic wrapping for Go + type_decl = f"type {type_name} struct {{" + if len(type_decl) > 80: + lines.extend( + self.format_long_line( + "type ", type_name, " struct {", continuation_indent="\t" + ) + ) + else: + lines.append(type_decl) lines.append(f"\tcase_ {case_type}") lines.append("\tvalue any") lines.append("}") @@ -583,15 +635,13 @@ def generate_union( lines.append(f"\tcase {case_type}{case_name}:") lines.append(f"\t\tv, ok := u.value.({case_type_name})") lines.append("\t\tif !ok {") - lines.append( - f'\t\t\treturn fmt.Errorf("corrupted {type_name}: case={case_name} but invalid value")' - ) + lines.append(f'\t\t\treturn fmt.Errorf("corrupted {type_name}: " +') + lines.append(f'\t\t\t\t"case={case_name} but invalid value")') lines.append("\t\t}") if case_type_name.startswith("*"): lines.append("\t\tif v == nil {") - lines.append( - f'\t\t\treturn fmt.Errorf("corrupted {type_name}: case={case_name} but nil value")' - ) + lines.append(f'\t\t\treturn fmt.Errorf("corrupted {type_name}: " +') + lines.append(f'\t\t\t\t"case={case_name} but nil value")') lines.append("\t\t}") lines.append(f"\t\tif visitor.{case_name} != nil {{") lines.append(f"\t\t\treturn visitor.{case_name}(v)") @@ -861,6 +911,7 @@ def generate_field( if tags: tag_str = ",".join(tags) + # Concatenate parts to ensure single line in output lines.append(f'{field_name} {go_type} `fory:"{tag_str}"`') else: lines.append(f"{field_name} {go_type}") diff --git a/compiler/fory_compiler/generators/java.py b/compiler/fory_compiler/generators/java.py index 11c479e10d..1bb3729448 100644 --- a/compiler/fory_compiler/generators/java.py +++ b/compiler/fory_compiler/generators/java.py @@ -441,8 +441,9 @@ def generate_message_file(self, message: Message) -> GeneratedFile: lines.append(f"import {imp};") lines.append("") - # Class declaration - lines.append(f"public class {message.name} {{") + # Class declaration with semantic line wrapping + class_lines = self.format_long_line("public class ", message.name, " {") + lines.extend(class_lines) # Generate nested enums as static inner classes for nested_enum in message.nested_enums: @@ -669,7 +670,8 @@ def generate_union_class( lines.append(f"{ind} return {type_id_expr};") lines.append(f"{ind} default:") lines.append( - f'{ind} throw new IllegalStateException("Unknown {union.name} case id: " + caseId);' + f'{ind} throw new IllegalStateException("Unknown " + ' + f'"{union.name} case id: " + caseId);' ) lines.append(f"{ind} }}") lines.append(f"{ind} }}") @@ -705,7 +707,8 @@ def generate_union_class( lines.append(f"{ind} return {case_enum}.{case_enum_name};") lines.append(f"{ind} default:") lines.append( - f'{ind} throw new IllegalStateException("Unknown {union.name} case id: " + index);' + f'{ind} throw new IllegalStateException("Unknown " + ' + f'"{union.name} case id: " + index);' ) lines.append(f"{ind} }}") lines.append(f"{ind} }}") @@ -1074,14 +1077,28 @@ def generate_getter_setter(self, field: Field) -> List[str]: field_name = self.to_camel_case(field.name) pascal_name = self.to_pascal_case(field.name) - # Getter - lines.append(f"public {java_type} get{pascal_name}() {{") + # Getter with semantic line wrapping + getter_sig = f"public {java_type} get{pascal_name}()" + if len(getter_sig) > 80: + getter_lines = self.format_long_line( + "public ", f"{java_type} get{pascal_name}()", " {" + ) + else: + getter_lines = [f"{getter_sig} {{"] + lines.extend(getter_lines) lines.append(f" return {field_name};") lines.append("}") lines.append("") - # Setter - lines.append(f"public void set{pascal_name}({java_type} {field_name}) {{") + # Setter with semantic line wrapping + setter_sig = f"public void set{pascal_name}({java_type} {field_name})" + if len(setter_sig) > 80: + setter_lines = self.format_long_line( + "public void ", f"set{pascal_name}({java_type} {field_name})", " {" + ) + else: + setter_lines = [f"{setter_sig} {{"] + lines.extend(setter_lines) lines.append(f" this.{field_name} = {field_name};") lines.append("}") lines.append("") diff --git a/compiler/fory_compiler/generators/python.py b/compiler/fory_compiler/generators/python.py index 8e70cae0f8..7d96d714f2 100644 --- a/compiler/fory_compiler/generators/python.py +++ b/compiler/fory_compiler/generators/python.py @@ -141,6 +141,64 @@ class PythonGenerator(BaseGenerator): PrimitiveKind.ANY: "None", } + def wrap_line( + self, + line: str, + max_width: int = 80, + indent: str = "", + continuation_indent: str = None, + ) -> List[str]: + """Override base wrap_line to handle Python-specific syntax. + + Python has specific constructs that should not be wrapped: + 1. Assignment statements - must not split between variable and value + 2. Conditional statements with logical operators (and, or, not) + 3. Raise statements with string literals + """ + if continuation_indent is None: + continuation_indent = indent + " " + + # If line is already short enough, return as is + if len(line) <= max_width: + return [line] + + stripped = line.lstrip() + + # Don't wrap comments + if stripped.startswith("#"): + return [line] + + # Don't wrap raise statements (they often have long string literals) + if stripped.startswith("raise "): + return [line] + + # Don't wrap assignment statements (lines containing " = " at the statement level) + # This prevents splitting like: _threadsafe_fory = pyfory.ThreadSafeFory(...) + # Only check for simple assignments (not comparisons or keyword args) + if ( + " = " in line + and not line.strip().startswith("if ") + and not line.strip().startswith("elif ") + and not line.strip().startswith("while ") + ): + # Check if this looks like a simple assignment (has = but not == or <= or >=) + # and the = comes before any opening parentheses + eq_pos = line.find(" = ") + paren_pos = line.find("(") + if eq_pos > 0 and (paren_pos < 0 or eq_pos < paren_pos): + # This is likely an assignment statement, don't wrap it + return [line] + + # Don't wrap conditional statements (if/elif/while) with logical operators + # This prevents splitting like: if condition and not\n isinstance(...) + if stripped.startswith(("if ", "elif ", "while ")) and ( + " and " in line or " or " in line + ): + return [line] + + # For all other cases, use base class wrapping + return super().wrap_line(line, max_width, indent, continuation_indent) + def safe_name(self, name: str) -> str: """Return a Python-safe identifier.""" if keyword.iskeyword(name): @@ -615,7 +673,14 @@ def generate_field( else: field_default = f"{default_expr}{trailing_comment}" - lines.append(f"{field_name}: {python_type} = {field_default}") + # Build field line with semantic wrapping for Python + field_line = f"{field_name}: {python_type} = {field_default}" + if len(field_line) > 80: + # Use Python line continuation with proper indentation + lines.append(f"{field_name}: {python_type} = \\") + lines.append(f" {field_default}") + else: + lines.append(field_line) return lines