Coverage for src/c2puml/core/parse_utils.py: 74%
92 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-14 18:23 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-14 18:23 +0000
1#!/usr/bin/env python3
2"""
3Shared parsing utilities for the C to PlantUML converter.
5Centralizes small but widely used helpers so parser/tokenizer code can
6delegate to a single source of truth, reducing duplication and divergence.
7"""
9from __future__ import annotations
11import re
12from typing import List, Optional
15def clean_type_string(type_str: str) -> str:
16 """Clean type string by removing newlines and normalizing whitespace."""
17 if not type_str:
18 return type_str
19 cleaned = type_str.replace('\n', ' ')
20 cleaned = re.sub(r"\s+", " ", cleaned)
21 cleaned = cleaned.strip()
22 return cleaned
25def clean_value_string(value_str: str) -> str:
26 """Clean value string by removing excessive whitespace and newlines."""
27 if not value_str:
28 return value_str
29 cleaned = value_str.replace('\n', ' ')
30 cleaned = re.sub(r"\s+", " ", cleaned)
31 cleaned = cleaned.strip()
32 cleaned = re.sub(r"\s*\{\s*", "{", cleaned)
33 cleaned = re.sub(r"\s*\}\s*", "}", cleaned)
34 cleaned = re.sub(r"\s*,\s*", ", ", cleaned)
35 cleaned = re.sub(r"\s*&\s*", "&", cleaned)
36 return cleaned
39def fix_array_bracket_spacing(type_str: str) -> str:
40 """Fix spacing around array brackets in type strings."""
41 type_str = clean_type_string(type_str)
42 type_str = re.sub(r"\s*\[\s*", "[", type_str)
43 type_str = re.sub(r"\s*\]\s*", "]", type_str)
44 return type_str
47def fix_pointer_spacing(type_str: str) -> str:
48 """Fix spacing around pointer asterisks in type strings."""
49 # Fix double pointer spacing: "type * *" -> "type **"
50 type_str = re.sub(r"\*\s+\*", "**", type_str)
51 # Fix triple pointer spacing: "type * * *" -> "type ***"
52 type_str = re.sub(r"\*\s+\*\s+\*", "***", type_str)
53 return type_str
56def find_matching_brace(tokens, start_pos: int) -> Optional[int]:
57 """Find a matching closing brace in a token list starting at an opening brace.
59 Mirrors StructureFinder._find_matching_brace to allow reuse in other
60 components without duplicating logic.
61 """
62 # Import locally to avoid circular dependencies at module import time
63 from .parser_tokenizer import TokenType
65 if start_pos >= len(tokens) or tokens[start_pos].type != TokenType.LBRACE:
66 return None
68 depth = 1
69 pos = start_pos + 1
70 while pos < len(tokens) and depth > 0:
71 if tokens[pos].type == TokenType.LBRACE:
72 depth += 1
73 elif tokens[pos].type == TokenType.RBRACE:
74 depth -= 1
75 pos += 1
77 return pos - 1 if depth == 0 else None
80def collect_array_dimensions_from_tokens(tokens, start_index: int) -> tuple[list[str], int]:
81 """Collect one or more array dimension groups starting at start_index.
83 Expects tokens[start_index] to be a LBRACKET. Returns (dims, next_index)
84 where dims is a list of strings (each the content between brackets), and
85 next_index is the index after the last closing bracket.
86 """
87 from .parser_tokenizer import TokenType
89 dims: list[str] = []
90 i = start_index
91 while i < len(tokens) and tokens[i].type == TokenType.LBRACKET:
92 j = i + 1
93 content_parts: list[str] = []
94 while j < len(tokens) and tokens[j].type != TokenType.RBRACKET:
95 if tokens[j].type not in (TokenType.WHITESPACE, TokenType.COMMENT, TokenType.NEWLINE):
96 content_parts.append(tokens[j].value)
97 j += 1
98 # Join with spaces to preserve readability for expressions
99 dim_str = " ".join(content_parts).strip()
100 dims.append(dim_str)
101 # Move past the closing bracket if present
102 i = j + 1 if j < len(tokens) and tokens[j].type == TokenType.RBRACKET else j
103 return dims, i
106def join_type_with_dims(base_type: str, dims: list[str]) -> str:
107 """Append collected array dimensions to a base type and normalize spacing."""
108 if not dims:
109 return base_type
110 type_with_dims = base_type + "".join(f"[{d}]" for d in dims)
111 return fix_array_bracket_spacing(type_with_dims)
114def normalize_dim_value(dim: str) -> str:
115 """Normalize numeric dimension tokens like 5U/6UL to plain digits; keep expressions as-is."""
116 m = re.match(r"\s*(\d+)", dim)
117 return m.group(1) if m else dim
120def normalize_type_and_name_for_arrays(base_type: str, name: str) -> tuple[str, str]:
121 """Normalize cases where array dimensions are attached to the name or
122 accidentally glued to the type.
124 Handles typical forms:
125 - name carries dims: base_type, name="var[2U][3]" -> (base_type[2][3], "var")
126 - type accidentally includes name+dims (parser edge):
127 base_type="T var[2", name="U" -> (T[2], "var")
128 - type already has dims: base_type="T[2]", name="var" -> unchanged
129 """
130 # First, the straightforward case: dimensions present on the name
131 if name and '[' in name:
132 dims = re.findall(r"\[(.*?)\]", name)
133 if dims:
134 dims = [normalize_dim_value(d) for d in dims]
135 clean_name = re.split(r"\[", name, 1)[0].strip()
136 type_with_dims = join_type_with_dims(base_type, dims)
137 return type_with_dims, clean_name
139 # If name doesn't have dims but base_type looks like it ends with
140 # "identifier [ <expr>" (possibly with a split numeric suffix like 'U' in name),
141 # try to pull dims off the end of base_type and extract the real name.
142 # This guards against tokenizer/parse edge cases in some codebases.
143 trailing_dim_match = re.search(r"\[\s*([^\]]*)\s*$", base_type)
144 if trailing_dim_match:
145 # Split base_type into prefix (before name), name candidate, and leftover before '['
146 before_bracket = base_type[: trailing_dim_match.start()].rstrip()
147 # The candidate variable name is the last identifier in before_bracket
148 m_name = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\s*$", before_bracket)
149 if m_name:
150 var_name_candidate = m_name.group(1)
151 # True type is everything before the candidate name
152 true_base_type = before_bracket[: m_name.start()].rstrip()
153 # Normalize the dimension value; if the current 'name' is just a numeric suffix (e.g. 'U', 'UL'),
154 # append it to the dim string for normalization purposes
155 dim_raw = (trailing_dim_match.group(1) or "").strip()
156 suffix = name.strip() if name and re.fullmatch(r"[uUlL]+", name.strip()) else ""
157 # If we have a split numeric suffix like 'U', reconstruct two dims:
158 # first without suffix, second with suffix preserved -> e.g., [2][2U]
159 if var_name_candidate and dim_raw:
160 dims_out: list[str] = []
161 # First dimension without suffix normalization in source code often is separate;
162 # we normalize numeric tokens (2U -> 2) only for the first copy.
163 dims_out.append(normalize_dim_value(dim_raw))
164 if suffix:
165 dims_out.append(f"{dim_raw}{suffix}")
166 fixed_type = join_type_with_dims(true_base_type or base_type, dims_out)
167 fixed_name = var_name_candidate
168 return fixed_type, fixed_name
170 return base_type, name