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

1#!/usr/bin/env python3 

2""" 

3Shared parsing utilities for the C to PlantUML converter. 

4 

5Centralizes small but widely used helpers so parser/tokenizer code can 

6delegate to a single source of truth, reducing duplication and divergence. 

7""" 

8 

9from __future__ import annotations 

10 

11import re 

12from typing import List, Optional 

13 

14 

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 

23 

24 

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 

37 

38 

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 

45 

46 

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 

54 

55 

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. 

58 

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 

64 

65 if start_pos >= len(tokens) or tokens[start_pos].type != TokenType.LBRACE: 

66 return None 

67 

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 

76 

77 return pos - 1 if depth == 0 else None 

78 

79 

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. 

82 

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 

88 

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 

104 

105 

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) 

112 

113 

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 

118 

119 

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. 

123 

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 

138 

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 

169 

170 return base_type, name 

171