Coverage for src/c2puml/core/parser.py: 79%

912 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-14 18:23 +0000

1#!/usr/bin/env python3 

2""" 

3Parser module for C to PlantUML converter - Step 1: Parse C code files and generate model.json 

4""" 

5import logging 

6from pathlib import Path 

7from typing import TYPE_CHECKING, Dict, List, Optional, Set 

8 

9from ..models import Enum, EnumValue, Field, FileModel, ProjectModel, Struct 

10from .parser_tokenizer import ( 

11 CTokenizer, 

12 StructureFinder, 

13 TokenType, 

14 find_enum_values, 

15 find_struct_fields, 

16) 

17from .preprocessor import PreprocessorManager 

18from .parser_anonymous_processor import AnonymousTypedefProcessor 

19from ..utils import detect_file_encoding 

20import re 

21from .parse_utils import ( 

22 clean_type_string, 

23 clean_value_string, 

24 fix_array_bracket_spacing, 

25 fix_pointer_spacing, 

26 collect_array_dimensions_from_tokens, 

27 join_type_with_dims, 

28 normalize_dim_value, 

29) 

30 

31INCLUDE_FILENAME_RE = re.compile(r'[<"\']([^>\'\"]+)[>\'\"]') 

32 

33if TYPE_CHECKING: 

34 from ..config import Config 

35 from ..models import Alias, Enum, Field, Function, Struct, Union 

36 

37 

38class CParser: 

39 """C/C++ parser for extracting structural information from source code using tokenization""" 

40 

41 def __init__(self): 

42 self.logger = logging.getLogger(__name__) 

43 self.tokenizer = CTokenizer() 

44 self.preprocessor = PreprocessorManager() 

45 

46 def parse_project( 

47 self, source_folder: str, recursive_search: bool = True, config: "Config" = None 

48 ) -> ProjectModel: 

49 """Parse a C/C++ project and return a model""" 

50 # Enhanced source path validation 

51 if not source_folder or not isinstance(source_folder, str): 

52 raise ValueError(f"Source folder must be a non-empty string, got: {type(source_folder)}") 

53 

54 if not source_folder.strip(): 

55 raise ValueError("Source folder cannot be empty or whitespace") 

56 

57 try: 

58 source_folder_path = Path(source_folder).resolve() 

59 except (OSError, RuntimeError) as e: 

60 raise ValueError(f"Failed to resolve source folder path '{source_folder}': {e}") 

61 

62 if not source_folder_path.exists(): 

63 # Provide helpful error message with suggestions 

64 error_msg = f"Source folder not found: {source_folder_path}" 

65 

66 # Check if it's a relative path issue 

67 if not Path(source_folder).is_absolute(): 

68 current_dir = Path.cwd() 

69 error_msg += f"\nCurrent working directory: {current_dir}" 

70 error_msg += f"\nTried to resolve relative path: {source_folder}" 

71 

72 # Check if parent directory exists 

73 parent_dir = source_folder_path.parent 

74 if parent_dir.exists(): 

75 error_msg += f"\nParent directory exists: {parent_dir}" 

76 # List contents of parent directory 

77 try: 

78 contents = [item.name for item in parent_dir.iterdir() if item.is_dir()] 

79 if contents: 

80 error_msg += f"\nAvailable directories in parent: {', '.join(contents[:10])}" 

81 if len(contents) > 10: 

82 error_msg += f" (and {len(contents) - 10} more)" 

83 except (OSError, PermissionError): 

84 error_msg += "\nCannot list parent directory contents (permission denied)" 

85 else: 

86 error_msg += f"\nParent directory does not exist: {parent_dir}" 

87 

88 raise ValueError(error_msg) 

89 

90 if not source_folder_path.is_dir(): 

91 raise ValueError(f"Source folder must be a directory, got: {source_folder_path} (is_file: {source_folder_path.is_file()})") 

92 

93 # Check if directory is readable 

94 try: 

95 source_folder_path.iterdir() 

96 except PermissionError: 

97 raise ValueError(f"Permission denied accessing source folder: {source_folder_path}") 

98 except OSError as e: 

99 raise ValueError(f"Error accessing source folder '{source_folder_path}': {e}") 

100 

101 self.logger.info("Parsing project: %s", source_folder_path) 

102 

103 # Find all C/C++ files in the project 

104 try: 

105 all_c_files = self._find_c_files(source_folder_path, recursive_search) 

106 except OSError as e: 

107 raise ValueError(f"Error searching for C/C++ files in '{source_folder_path}': {e}") 

108 

109 self.logger.info("Found %d C/C++ files", len(all_c_files)) 

110 

111 # Apply file filtering based on configuration 

112 c_files = [] 

113 if config: 

114 for file_path in all_c_files: 

115 if config._should_include_file(file_path.name): 

116 c_files.append(file_path) 

117 self.logger.debug( 

118 "Included file after filtering: %s", file_path.name 

119 ) 

120 else: 

121 self.logger.debug( 

122 "Excluded file after filtering: %s", file_path.name 

123 ) 

124 else: 

125 c_files = all_c_files 

126 

127 self.logger.info("After filtering: %d C/C++ files", len(c_files)) 

128 

129 # Parse each file using filename as key for simplified tracking 

130 files = {} 

131 failed_files = [] 

132 

133 for file_path in c_files: 

134 try: 

135 # Use relative path for tracking and filename as key 

136 relative_path = str(file_path.relative_to(source_folder_path)) 

137 file_model = self.parse_file(file_path, relative_path) 

138 

139 # Use filename as key (filenames are guaranteed to be unique) 

140 if file_model.name in files: 

141 raise RuntimeError( 

142 f"Duplicate filename detected: '{file_model.name}' from '{file_path}'. " 

143 f"Already seen from '{files[file_model.name].file_path}'." 

144 ) 

145 files[file_model.name] = file_model 

146 

147 self.logger.debug("Successfully parsed: %s", relative_path) 

148 

149 except (OSError, ValueError) as e: 

150 self.logger.warning("Failed to parse %s: %s", file_path, e) 

151 failed_files.append(str(file_path)) 

152 

153 if failed_files: 

154 error_msg = ( 

155 f"Failed to parse {len(failed_files)} files: {failed_files}. " 

156 "Stopping model processing." 

157 ) 

158 self.logger.error(error_msg) 

159 raise RuntimeError(error_msg) 

160 

161 model = ProjectModel( 

162 project_name=source_folder_path.name, 

163 source_folder=str(source_folder_path), 

164 files=files, 

165 ) 

166 

167 # Update all uses fields across the entire project 

168 model.update_uses_fields() 

169 

170 self.logger.info("Parsing complete. Parsed %d files successfully.", len(files)) 

171 return model 

172 

173 def parse_file(self, file_path: Path, relative_path: str) -> FileModel: 

174 """Parse a single C/C++ file and return a file model using tokenization""" 

175 self.logger.debug("Parsing file: %s", file_path) 

176 

177 # Detect encoding 

178 encoding = self._detect_encoding(file_path) 

179 

180 # Read file content 

181 with open(file_path, "r", encoding=encoding) as f: 

182 content = f.read() 

183 

184 # Tokenize the content 

185 tokens = self.tokenizer.tokenize(content) 

186 self.logger.debug("Tokenized file into %d tokens", len(tokens)) 

187 

188 # Process preprocessor directives 

189 self.preprocessor.add_defines_from_content(tokens) 

190 processed_tokens = self.preprocessor.process_file(tokens) 

191 self.logger.debug( 

192 "Preprocessor processed %d tokens -> %d tokens", 

193 len(tokens), 

194 len(processed_tokens), 

195 ) 

196 

197 # Filter out whitespace and comments for structure finding 

198 filtered_tokens = self.tokenizer.filter_tokens(processed_tokens) 

199 structure_finder = StructureFinder(filtered_tokens) 

200 

201 # Parse different structures using tokenizer 

202 structs = self._parse_structs_with_tokenizer(processed_tokens, structure_finder) 

203 enums = self._parse_enums_with_tokenizer(processed_tokens, structure_finder) 

204 unions = self._parse_unions_with_tokenizer(processed_tokens, structure_finder) 

205 functions = self._parse_functions_with_tokenizer( 

206 processed_tokens, structure_finder 

207 ) 

208 aliases = self._parse_aliases_with_tokenizer(processed_tokens) 

209 

210 # "uses" fields will be updated when we have the full project model 

211 

212 # Map typedef names to anonymous structs/enums/unions if needed 

213 # This logic will be handled by typedef_relations instead 

214 

215 file_model = FileModel( 

216 file_path=str(file_path), 

217 structs=structs, 

218 enums=enums, 

219 unions=unions, 

220 functions=functions, 

221 globals=self._parse_globals_with_tokenizer(processed_tokens), 

222 includes=self._parse_includes_with_tokenizer(processed_tokens), 

223 macros=self._parse_macros_with_tokenizer(processed_tokens), 

224 aliases=aliases, 

225 # Tag names are now stored in struct/enum/union objects 

226 ) 

227 

228 # Process anonymous typedefs after initial parsing 

229 anonymous_processor = AnonymousTypedefProcessor() 

230 anonymous_processor.process_file_model(file_model) 

231 

232 return file_model 

233 

234 def _parse_structs_with_tokenizer( 

235 self, tokens, structure_finder 

236 ) -> Dict[str, "Struct"]: 

237 """Parse struct definitions using tokenizer""" 

238 

239 structs = {} 

240 struct_infos = structure_finder.find_structs() 

241 

242 for start_pos, end_pos, struct_name in struct_infos: 

243 # Need to map back to original token positions 

244 # Find the original token positions by looking at line/column info 

245 original_start = self._find_original_token_pos( 

246 tokens, structure_finder.tokens, start_pos 

247 ) 

248 original_end = self._find_original_token_pos( 

249 tokens, structure_finder.tokens, end_pos 

250 ) 

251 

252 if original_start is not None and original_end is not None: 

253 # Extract field information from original token range 

254 field_tuples = find_struct_fields(tokens, original_start, original_end) 

255 

256 # Convert to Field objects 

257 fields = [] 

258 for field_name, field_type in field_tuples: 

259 try: 

260 fields.append(Field(field_name, field_type)) 

261 except ValueError as e: 

262 self.logger.warning( 

263 "Error creating field %s: %s", field_name, e 

264 ) 

265 

266 # For anonymous structs, use a special key that can be mapped later 

267 if not struct_name: 

268 struct_name = "__anonymous_struct__" 

269 

270 # Extract tag name if this is a typedef struct 

271 tag_name = "" 

272 if struct_name and not struct_name.startswith("__anonymous"): 

273 # Check if this struct has a typedef 

274 tag_name = self._extract_tag_name_for_struct(tokens, struct_name) 

275 

276 # Only register non-empty struct names here; anonymous will be created by the anonymous processor 

277 if struct_name: 

278 structs[struct_name] = Struct( 

279 struct_name, fields, tag_name=tag_name, uses=[] 

280 ) 

281 self.logger.debug( 

282 "Parsed struct: %s with %d fields", struct_name, len(fields) 

283 ) 

284 

285 return structs 

286 

287 def _parse_enums_with_tokenizer( 

288 self, tokens, structure_finder 

289 ) -> Dict[str, "Enum"]: 

290 """Parse enum definitions using tokenizer""" 

291 enums = {} 

292 enum_infos = structure_finder.find_enums() 

293 

294 for start_pos, end_pos, enum_name in enum_infos: 

295 # Need to map back to original token positions 

296 original_start = self._find_original_token_pos( 

297 tokens, structure_finder.tokens, start_pos 

298 ) 

299 original_end = self._find_original_token_pos( 

300 tokens, structure_finder.tokens, end_pos 

301 ) 

302 

303 if original_start is not None and original_end is not None: 

304 # Extract enum values from original token range 

305 value_strs = find_enum_values(tokens, original_start, original_end) 

306 values = [] 

307 for v in value_strs: 

308 if "=" in v: 

309 name, val = v.split("=", 1) 

310 name = name.strip() 

311 val = val.strip() 

312 if name: # Only add if name is not empty 

313 values.append(EnumValue(name=name, value=val)) 

314 else: 

315 name = v.strip() 

316 if name: # Only add if name is not empty 

317 values.append(EnumValue(name=name)) 

318 

319 # For anonymous enums, use a special key that can be mapped later 

320 if not enum_name: 

321 enum_name = "__anonymous_enum__" 

322 

323 # Extract tag name if this is a typedef enum 

324 tag_name = "" 

325 if enum_name and not enum_name.startswith("__anonymous"): 

326 # Check if this enum has a typedef 

327 tag_name = self._extract_tag_name_for_enum(tokens, enum_name) 

328 

329 enums[enum_name] = Enum(enum_name, values, tag_name=tag_name) 

330 self.logger.debug( 

331 "Parsed enum: %s with %d values", enum_name, len(values) 

332 ) 

333 

334 return enums 

335 

336 def _parse_unions_with_tokenizer( 

337 self, tokens, structure_finder 

338 ) -> Dict[str, "Union"]: 

339 """Parse union definitions using tokenizer""" 

340 from ..models import Field, Union 

341 

342 unions = {} 

343 union_infos = structure_finder.find_unions() 

344 

345 for start_pos, end_pos, union_name in union_infos: 

346 # Need to map back to original token positions 

347 original_start = self._find_original_token_pos( 

348 tokens, structure_finder.tokens, start_pos 

349 ) 

350 original_end = self._find_original_token_pos( 

351 tokens, structure_finder.tokens, end_pos 

352 ) 

353 

354 if original_start is not None and original_end is not None: 

355 # Extract field information from original token range 

356 field_tuples = find_struct_fields(tokens, original_start, original_end) 

357 

358 # Convert to Field objects 

359 fields = [] 

360 for field_name, field_type in field_tuples: 

361 try: 

362 fields.append(Field(field_name, field_type)) 

363 except ValueError as e: 

364 self.logger.warning( 

365 "Error creating union field %s: %s", field_name, e 

366 ) 

367 

368 # For anonymous unions, use a special key that can be mapped later 

369 if not union_name: 

370 union_name = "__anonymous_union__" 

371 

372 # Extract tag name if this is a typedef union 

373 tag_name = "" 

374 if union_name and not union_name.startswith("__anonymous"): 

375 # Check if this union has a typedef 

376 tag_name = self._extract_tag_name_for_union(tokens, union_name) 

377 

378 unions[union_name] = Union( 

379 union_name, fields, tag_name=tag_name, uses=[] 

380 ) 

381 self.logger.debug( 

382 "Parsed union: %s with %d fields", union_name, len(fields) 

383 ) 

384 

385 return unions 

386 

387 def _parse_functions_with_tokenizer( 

388 self, tokens, structure_finder 

389 ) -> List["Function"]: 

390 """Parse function declarations/definitions using tokenizer""" 

391 from ..models import Function 

392 

393 functions = [] 

394 function_infos = structure_finder.find_functions() 

395 

396 for ( 

397 start_pos, 

398 end_pos, 

399 func_name, 

400 return_type, 

401 is_declaration, 

402 is_inline, 

403 ) in function_infos: 

404 # Map back to original token positions to parse parameters 

405 original_start = self._find_original_token_pos( 

406 tokens, structure_finder.tokens, start_pos 

407 ) 

408 original_end = self._find_original_token_pos( 

409 tokens, structure_finder.tokens, end_pos 

410 ) 

411 

412 parameters = [] 

413 if original_start is not None and original_end is not None: 

414 # Parse parameters from the token range 

415 parameters = self._parse_function_parameters( 

416 tokens, original_start, original_end, func_name 

417 ) 

418 

419 try: 

420 # Create function with declaration flag 

421 function = Function(func_name, return_type, parameters) 

422 # Add custom attributes to track if this is a declaration and if it's inline 

423 function.is_declaration = is_declaration 

424 function.is_inline = is_inline 

425 functions.append(function) 

426 self.logger.debug( 

427 f"Parsed function: {func_name} with {len(parameters)} parameters (declaration: {is_declaration}, inline: {is_inline})" 

428 ) 

429 except Exception as e: 

430 self.logger.warning("Error creating function %s: %s", func_name, e) 

431 

432 return functions 

433 

434 def _parse_globals_with_tokenizer(self, tokens) -> List["Field"]: 

435 """Parse global variables using tokenizer""" 

436 from ..models import Field 

437 

438 globals_list = [] 

439 

440 i = 0 

441 while i < len(tokens): 

442 # Skip preprocessor directives, comments, etc. 

443 if tokens[i].type in [ 

444 TokenType.INCLUDE, 

445 TokenType.DEFINE, 

446 TokenType.COMMENT, 

447 TokenType.WHITESPACE, 

448 TokenType.NEWLINE, 

449 ]: 

450 i += 1 

451 continue 

452 

453 # Skip preprocessor directives but keep their content 

454 if tokens[i].type == TokenType.PREPROCESSOR: 

455 i = self._skip_preprocessor_directives(tokens, i) 

456 continue 

457 

458 # Skip function definitions (look for parentheses) 

459 if self._looks_like_function(tokens, i): 

460 i = self._skip_function(tokens, i) 

461 continue 

462 

463 # Skip struct/enum/union definitions 

464 if tokens[i].type in [ 

465 TokenType.STRUCT, 

466 TokenType.ENUM, 

467 TokenType.UNION, 

468 TokenType.TYPEDEF, 

469 ]: 

470 i = self._skip_structure_definition(tokens, i) 

471 continue 

472 

473 # Skip if we're inside a struct definition (look for opening brace) 

474 if i > 0 and tokens[i - 1].type == TokenType.LBRACE: 

475 # We're inside a struct, skip until closing brace 

476 brace_count = 1 

477 j = i 

478 while j < len(tokens) and brace_count > 0: 

479 if tokens[j].type == TokenType.LBRACE: 

480 brace_count += 1 

481 elif tokens[j].type == TokenType.RBRACE: 

482 brace_count -= 1 

483 j += 1 

484 i = j 

485 continue 

486 

487 # Skip macros and other preprocessor content 

488 if tokens[i].type == TokenType.DEFINE: 

489 # Skip the entire macro content (multi-line macros are now merged) 

490 i += 1 

491 continue 

492 

493 # Additional check: skip if we're inside any brace block (struct, function, etc.) 

494 brace_count = 0 

495 j = i - 1 

496 while j >= 0: 

497 if tokens[j].type == TokenType.RBRACE: 

498 brace_count += 1 

499 elif tokens[j].type == TokenType.LBRACE: 

500 brace_count -= 1 

501 if brace_count < 0: 

502 # We're inside a brace block, skip this token 

503 i += 1 

504 break 

505 j -= 1 

506 else: 

507 # Not inside a brace block, proceed with global variable parsing 

508 global_info = self._parse_global_variable(tokens, i) 

509 if global_info: 

510 var_name, var_type, var_value = global_info 

511 # Only add if it looks like a real global variable (not a fragment) 

512 if ( 

513 var_name 

514 and var_name.strip() 

515 and var_type 

516 and var_type.strip() 

517 and not var_name.startswith("#") 

518 and len(var_type) < 200 

519 and not var_type.startswith("\\") 

520 and not var_name.startswith("\\") 

521 and "\\" not in var_type 

522 and "\\" not in var_name 

523 ): 

524 try: 

525 # Additional validation before creating Field 

526 stripped_name = var_name.strip() 

527 stripped_type = var_type.strip() 

528 if stripped_name and stripped_type: 

529 globals_list.append( 

530 Field( 

531 name=stripped_name, 

532 type=stripped_type, 

533 value=var_value, 

534 ) 

535 ) 

536 self.logger.debug( 

537 f"Parsed global: {stripped_name} : {stripped_type}" 

538 ) 

539 except Exception as e: 

540 self.logger.warning( 

541 f"Error creating global field {var_name}: {e}" 

542 ) 

543 i = self._skip_to_semicolon(tokens, i) 

544 else: 

545 i += 1 

546 

547 return globals_list 

548 

549 def _parse_includes_with_tokenizer(self, tokens) -> List[str]: 

550 """Parse #include directives using tokenizer""" 

551 includes = [] 

552 

553 for token in tokens: 

554 if token.type == TokenType.INCLUDE: 

555 match = INCLUDE_FILENAME_RE.search(token.value) 

556 if match: 

557 includes.append(match.group(1)) 

558 

559 return includes 

560 

561 def _parse_macros_with_tokenizer(self, tokens) -> List[str]: 

562 """Parse macro definitions using tokenizer""" 

563 macros = [] 

564 

565 for token in tokens: 

566 if token.type == TokenType.DEFINE: 

567 # Store the full macro definition for display flexibility 

568 # e.g., "#define PI 3.14159" -> "#define PI 3.14159" 

569 # e.g., "#define MIN(a, b) ((a) < (b) ? (a) : (b))" -> "#define MIN(a, b) ((a) < (b) ? (a) : (b))" 

570 macro_definition = token.value.strip() 

571 if macro_definition not in macros: 

572 macros.append(macro_definition) 

573 

574 return macros 

575 

576 def _parse_aliases_with_tokenizer(self, tokens) -> Dict[str, "Alias"]: 

577 """Parse type aliases (primitive or derived typedefs) using tokenizer""" 

578 from ..models import Alias 

579 

580 aliases = {} 

581 

582 i = 0 

583 while i < len(tokens): 

584 if tokens[i].type == TokenType.TYPEDEF: 

585 # Found typedef, parse it 

586 typedef_info = self._parse_single_typedef(tokens, i) 

587 if typedef_info: 

588 typedef_name, original_type = typedef_info 

589 

590 # Only include if it's NOT a struct/enum/union typedef 

591 if original_type not in ["struct", "enum", "union"]: 

592 aliases[typedef_name] = Alias( 

593 name=typedef_name, original_type=original_type, uses=[] 

594 ) 

595 

596 i += 1 

597 

598 return aliases 

599 

600 # _parse_typedef_relations_with_tokenizer method removed - tag names are now in struct/enum/union 

601 

602 def _extract_tag_name_for_struct(self, tokens, struct_name: str) -> str: 

603 """Extract tag name for a struct if it has a typedef""" 

604 i = 0 

605 while i < len(tokens): 

606 if tokens[i].type == TokenType.TYPEDEF: 

607 typedef_info = self._parse_single_typedef(tokens, i) 

608 if typedef_info: 

609 typedef_name, original_type = typedef_info 

610 if original_type == "struct" and typedef_name == struct_name: 

611 # Extract the tag name from the typedef 

612 return self._extract_tag_name_from_typedef(tokens, i) 

613 i += 1 

614 return "" 

615 

616 def _extract_tag_name_for_enum(self, tokens, enum_name: str) -> str: 

617 """Extract tag name for an enum if it has a typedef""" 

618 i = 0 

619 while i < len(tokens): 

620 if tokens[i].type == TokenType.TYPEDEF: 

621 typedef_info = self._parse_single_typedef(tokens, i) 

622 if typedef_info: 

623 typedef_name, original_type = typedef_info 

624 if original_type == "enum" and typedef_name == enum_name: 

625 # Extract the tag name from the typedef 

626 return self._extract_tag_name_from_typedef(tokens, i) 

627 i += 1 

628 return "" 

629 

630 def _extract_tag_name_for_union(self, tokens, union_name: str) -> str: 

631 """Extract tag name for a union if it has a typedef""" 

632 i = 0 

633 while i < len(tokens): 

634 if tokens[i].type == TokenType.TYPEDEF: 

635 typedef_info = self._parse_single_typedef(tokens, i) 

636 if typedef_info: 

637 typedef_name, original_type = typedef_info 

638 if original_type == "union" and typedef_name == union_name: 

639 # Extract the tag name from the typedef 

640 return self._extract_tag_name_from_typedef(tokens, i) 

641 i += 1 

642 return "" 

643 

644 def _extract_non_primitive_types( 

645 self, type_str: str, available_types: Set[str] 

646 ) -> List[str]: 

647 """Extract non-primitive type names from a type string that exist in available_types""" 

648 # Define primitive types 

649 primitive_types = { 

650 "void", 

651 "char", 

652 "short", 

653 "int", 

654 "long", 

655 "float", 

656 "double", 

657 "signed", 

658 "unsigned", 

659 "const", 

660 "volatile", 

661 "static", 

662 "extern", 

663 "auto", 

664 "register", 

665 "inline", 

666 "restrict", 

667 "size_t", 

668 "ptrdiff_t", 

669 "int8_t", 

670 "int16_t", 

671 "int32_t", 

672 "int64_t", 

673 "uint8_t", 

674 "uint16_t", 

675 "uint32_t", 

676 "uint64_t", 

677 "intptr_t", 

678 "uintptr_t", 

679 "bool", 

680 "true", 

681 "false", 

682 "NULL", 

683 "nullptr", 

684 } 

685 

686 # Remove common C keywords and operators 

687 import re 

688 

689 # Split by common delimiters and operators 

690 parts = re.split(r"[\[\]\(\)\{\}\s\*&,;]", type_str) 

691 

692 # Extract potential type names that exist in available_types 

693 types = [] 

694 for part in parts: 

695 part = part.strip() 

696 if part and len(part) > 1 and part not in primitive_types: 

697 # Check if it looks like a type name (starts with letter, contains letters/numbers/underscores) 

698 if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", part): 

699 # Only include if it exists in available_types 

700 if part in available_types: 

701 types.append(part) 

702 

703 return list(set(types)) # Remove duplicates 

704 

705 def _find_c_files( 

706 self, source_folder_path: Path, recursive_search: bool 

707 ) -> List[Path]: 

708 """Find all C/C++ files in the source folder""" 

709 c_extensions = {".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hxx"} 

710 files = [] 

711 

712 self.logger.debug("Searching for files with extensions: %s", c_extensions) 

713 

714 try: 

715 if recursive_search: 

716 for ext in c_extensions: 

717 try: 

718 files.extend(source_folder_path.rglob(f"*{ext}")) 

719 except (OSError, PermissionError) as e: 

720 self.logger.warning("Error during recursive search for %s files: %s", ext, e) 

721 # Continue with other extensions 

722 else: 

723 for ext in c_extensions: 

724 try: 

725 files.extend(source_folder_path.glob(f"*{ext}")) 

726 except (OSError, PermissionError) as e: 

727 self.logger.warning("Error during search for %s files: %s", ext, e) 

728 # Continue with other extensions 

729 except Exception as e: 

730 raise OSError(f"Failed to search for C/C++ files in '{source_folder_path}': {e}") 

731 

732 # Filter out hidden files and common exclude patterns 

733 filtered_files = [] 

734 exclude_patterns = {".git", "__pycache__", "node_modules", ".vscode", ".idea"} 

735 

736 for file_path in files: 

737 try: 

738 # Skip hidden files and directories 

739 if any(part.startswith(".") for part in file_path.parts): 

740 continue 

741 

742 # Skip common exclude patterns 

743 if any(pattern in file_path.parts for pattern in exclude_patterns): 

744 continue 

745 

746 # Verify the file is actually accessible 

747 if not file_path.exists(): 

748 self.logger.debug("Skipping non-existent file: %s", file_path) 

749 continue 

750 

751 if not file_path.is_file(): 

752 self.logger.debug("Skipping non-file item: %s", file_path) 

753 continue 

754 

755 filtered_files.append(file_path) 

756 except (OSError, PermissionError) as e: 

757 self.logger.warning("Error accessing file %s: %s", file_path, e) 

758 # Skip files we can't access 

759 continue 

760 

761 self.logger.debug("Found %d C/C++ files after filtering", len(filtered_files)) 

762 return sorted(filtered_files) 

763 

764 def _detect_encoding(self, file_path: Path) -> str: 

765 """Detect file encoding with platform-aware fallbacks""" 

766 return detect_file_encoding(file_path) 

767 

768 def _find_original_token_pos(self, all_tokens, filtered_tokens, filtered_pos): 

769 """Find the position in all_tokens that corresponds to filtered_tokens[filtered_pos]""" 

770 if filtered_pos >= len(filtered_tokens): 

771 return None 

772 

773 target_token = filtered_tokens[filtered_pos] 

774 

775 # Search for the token in all_tokens by line and column 

776 for i, token in enumerate(all_tokens): 

777 if ( 

778 token.line == target_token.line 

779 and token.column == target_token.column 

780 and token.value == target_token.value 

781 ): 

782 return i 

783 

784 return None 

785 

786 def _parse_single_typedef(self, tokens, start_pos): 

787 """Parse a single typedef starting at the given position""" 

788 # Skip 'typedef' keyword 

789 pos = start_pos + 1 

790 

791 # Skip whitespace and comments 

792 while pos < len(tokens) and tokens[pos].type in [ 

793 TokenType.WHITESPACE, 

794 TokenType.COMMENT, 

795 ]: 

796 pos += 1 

797 

798 if pos >= len(tokens): 

799 return None 

800 

801 # Check if it's a struct/enum/union typedef 

802 if tokens[pos].type in [TokenType.STRUCT, TokenType.ENUM, TokenType.UNION]: 

803 # Look ahead to see if this complex type is immediately followed by a function-pointer declarator 

804 # Pattern to detect: ... } ( * name ) ( ... ) 

805 look = pos 

806 # Find the matching closing brace of the outer struct/union/enum 

807 if tokens[look].type in [TokenType.STRUCT, TokenType.ENUM, TokenType.UNION]: 

808 # Advance to the opening brace 

809 while look < len(tokens) and tokens[look].type != TokenType.LBRACE: 

810 look += 1 

811 if look < len(tokens) and tokens[look].type == TokenType.LBRACE: 

812 brace_count = 1 

813 look += 1 

814 while look < len(tokens) and brace_count > 0: 

815 if tokens[look].type == TokenType.LBRACE: 

816 brace_count += 1 

817 elif tokens[look].type == TokenType.RBRACE: 

818 brace_count -= 1 

819 look += 1 

820 # Now 'look' is token after the closing brace 

821 j = look 

822 # Skip whitespace/comments 

823 while j < len(tokens) and tokens[j].type in [TokenType.WHITESPACE, TokenType.COMMENT, TokenType.NEWLINE]: 

824 j += 1 

825 # Detect function-pointer declarator: ( * IDENT ) ( 

826 if ( 

827 j + 4 < len(tokens) 

828 and tokens[j].type == TokenType.LPAREN 

829 and tokens[j + 1].type == TokenType.ASTERISK 

830 and tokens[j + 2].type == TokenType.IDENTIFIER 

831 and tokens[j + 3].type == TokenType.RPAREN 

832 and tokens[j + 4].type == TokenType.LPAREN 

833 ): 

834 typedef_name = tokens[j + 2].value 

835 # Collect the full typedef original type up to the semicolon, preserving parentheses/brackets spacing 

836 k = pos 

837 formatted: list[str] = [] 

838 while k < len(tokens) and tokens[k].type != TokenType.SEMICOLON: 

839 t = tokens[k] 

840 if t.type in [TokenType.LPAREN, TokenType.RPAREN, TokenType.LBRACKET, TokenType.RBRACKET]: 

841 formatted.append(t.value) 

842 elif formatted and formatted[-1] not in ["(", ")", "[", "]"]: 

843 # Prepend space before non-bracket tokens when previous isn't a bracket 

844 formatted.append(" " + t.value) 

845 else: 

846 formatted.append(t.value) 

847 k += 1 

848 original_type = "".join(formatted) 

849 # Clean excessive whitespace inside type 

850 original_type = self._clean_type_string(original_type) 

851 return (typedef_name, original_type) 

852 # Fallback to standard complex typedef parsing 

853 return self._parse_complex_typedef(tokens, pos) 

854 

855 # Collect all non-whitespace/comment tokens until semicolon 

856 # But handle nested structures properly 

857 all_tokens = [] 

858 brace_count = 0 

859 paren_count = 0 

860 

861 while pos < len(tokens): 

862 token = tokens[pos] 

863 

864 # Track nested braces and parentheses 

865 if token.type == TokenType.LBRACE: 

866 brace_count += 1 

867 elif token.type == TokenType.RBRACE: 

868 brace_count -= 1 

869 elif token.type == TokenType.LPAREN: 

870 paren_count += 1 

871 elif token.type == TokenType.RPAREN: 

872 paren_count -= 1 

873 elif token.type == TokenType.SEMICOLON: 

874 # Only treat semicolon as end if we're not inside nested structures 

875 # For function pointer typedefs, we need to be outside the parameter list parentheses 

876 if brace_count == 0 and paren_count == 0: 

877 # We're outside any nested structures and parentheses 

878 break 

879 

880 if token.type not in [TokenType.WHITESPACE, TokenType.COMMENT]: 

881 all_tokens.append(token) 

882 pos += 1 

883 

884 if len(all_tokens) < 2: 

885 return None 

886 

887 # Function pointer typedef: typedef ret (*name)(params); 

888 for i in range(len(all_tokens) - 3): 

889 if ( 

890 all_tokens[i].type 

891 in [ 

892 TokenType.IDENTIFIER, 

893 TokenType.INT, 

894 TokenType.VOID, 

895 TokenType.CHAR, 

896 TokenType.FLOAT, 

897 TokenType.DOUBLE, 

898 TokenType.LONG, 

899 TokenType.SHORT, 

900 TokenType.UNSIGNED, 

901 TokenType.SIGNED, 

902 ] 

903 and all_tokens[i + 1].type == TokenType.LPAREN 

904 and all_tokens[i + 2].type == TokenType.ASTERISK 

905 and all_tokens[i + 3].type == TokenType.IDENTIFIER 

906 ): 

907 # Check if this is followed by a parameter list 

908 if i + 4 < len(all_tokens) and all_tokens[i + 4].type == TokenType.RPAREN: 

909 if i + 5 < len(all_tokens) and all_tokens[i + 5].type == TokenType.LPAREN: 

910 # This is a function pointer with parameters - skip this pattern and use the complex logic 

911 break 

912 

913 # Simple function pointer typedef without complex parameters 

914 typedef_name = all_tokens[i + 3].value 

915 # Fix: Properly format function pointer type - preserve spaces between tokens but not around parentheses 

916 formatted_tokens = [] 

917 for j, token in enumerate(all_tokens): 

918 if token.type in [TokenType.LPAREN, TokenType.RPAREN]: 

919 # Don't add spaces around parentheses 

920 formatted_tokens.append(token.value) 

921 elif j > 0 and all_tokens[j - 1].type not in [ 

922 TokenType.LPAREN, 

923 TokenType.RPAREN, 

924 ]: 

925 # Add space before token if previous token wasn't a parenthesis 

926 formatted_tokens.append(" " + token.value) 

927 else: 

928 # No space before token 

929 formatted_tokens.append(token.value) 

930 original_type = "".join(formatted_tokens) 

931 return (typedef_name, original_type) 

932 

933 # Complex function pointer typedef: typedef ret (*name)(complex_params); 

934 # This handles cases where the function pointer has complex parameters that span multiple tokens 

935 if len(all_tokens) >= 6: 

936 # Look for pattern: type ( * name ) ( ... ) 

937 for i in range(len(all_tokens) - 5): 

938 if ( 

939 all_tokens[i].type 

940 in [ 

941 TokenType.IDENTIFIER, 

942 TokenType.INT, 

943 TokenType.VOID, 

944 TokenType.CHAR, 

945 TokenType.FLOAT, 

946 TokenType.DOUBLE, 

947 TokenType.LONG, 

948 TokenType.SHORT, 

949 TokenType.UNSIGNED, 

950 TokenType.SIGNED, 

951 ] 

952 and all_tokens[i + 1].type == TokenType.LPAREN 

953 and all_tokens[i + 2].type == TokenType.ASTERISK 

954 and all_tokens[i + 3].type == TokenType.IDENTIFIER 

955 and all_tokens[i + 4].type == TokenType.RPAREN 

956 and all_tokens[i + 5].type == TokenType.LPAREN 

957 ): 

958 # Find the closing parenthesis for the parameter list 

959 paren_count = 1 

960 param_end = i + 6 

961 while param_end < len(all_tokens) and paren_count > 0: 

962 if all_tokens[param_end].type == TokenType.LPAREN: 

963 paren_count += 1 

964 elif all_tokens[param_end].type == TokenType.RPAREN: 

965 paren_count -= 1 

966 param_end += 1 

967 

968 if paren_count == 0: 

969 typedef_name = all_tokens[i + 3].value 

970 # Format the complete typedef properly 

971 formatted_tokens = [] 

972 for j, token in enumerate(all_tokens): 

973 if token.type in [TokenType.LPAREN, TokenType.RPAREN]: 

974 # Don't add spaces around parentheses 

975 formatted_tokens.append(token.value) 

976 elif j > 0 and all_tokens[j - 1].type not in [ 

977 TokenType.LPAREN, 

978 TokenType.RPAREN, 

979 ]: 

980 # Add space before token if previous token wasn't a parenthesis 

981 formatted_tokens.append(" " + token.value) 

982 else: 

983 # No space before token 

984 formatted_tokens.append(token.value) 

985 original_type = "".join(formatted_tokens) 

986 return (typedef_name, original_type) 

987 

988 # Array typedef: typedef type name[size]; 

989 for i in range(len(all_tokens)): 

990 if ( 

991 all_tokens[i].type == TokenType.LBRACKET 

992 and i > 0 

993 and all_tokens[i - 1].type == TokenType.IDENTIFIER 

994 ): 

995 typedef_name = all_tokens[i - 1].value 

996 # Fix: Properly format array type - preserve spaces between tokens but not around brackets 

997 formatted_tokens = [] 

998 for j, token in enumerate(all_tokens): 

999 if token.type in [TokenType.LBRACKET, TokenType.RBRACKET]: 

1000 # Don't add spaces around brackets 

1001 formatted_tokens.append(token.value) 

1002 elif j > 0 and all_tokens[j - 1].type not in [ 

1003 TokenType.LBRACKET, 

1004 TokenType.RBRACKET, 

1005 ]: 

1006 # Add space before token if previous token wasn't a bracket 

1007 formatted_tokens.append(" " + token.value) 

1008 else: 

1009 # No space before token 

1010 formatted_tokens.append(token.value) 

1011 original_type = "".join(formatted_tokens) 

1012 return (typedef_name, original_type) 

1013 

1014 # Pointer typedef: typedef type * name; 

1015 for i in range(len(all_tokens) - 2): 

1016 if ( 

1017 all_tokens[i].type == TokenType.ASTERISK 

1018 and all_tokens[i + 1].type == TokenType.IDENTIFIER 

1019 ): 

1020 typedef_name = all_tokens[i + 1].value 

1021 # Fix: Properly format pointer type - preserve spaces between tokens 

1022 formatted_tokens = [] 

1023 for j, token in enumerate(all_tokens): 

1024 if j > 0: 

1025 # Add space before token 

1026 formatted_tokens.append(" " + token.value) 

1027 else: 

1028 # No space before first token 

1029 formatted_tokens.append(token.value) 

1030 original_type = "".join(formatted_tokens) 

1031 return (typedef_name, original_type) 

1032 

1033 # Basic typedef: the last token is the typedef name, everything else is the type 

1034 typedef_name = all_tokens[-1].value 

1035 type_tokens = all_tokens[:-1] 

1036 original_type = " ".join(t.value for t in type_tokens) 

1037 original_type = self._clean_type_string(original_type) 

1038 original_type = self._fix_array_bracket_spacing(original_type) 

1039 return (typedef_name, original_type) 

1040 

1041 def _parse_complex_typedef(self, tokens, start_pos): 

1042 """Parse complex typedef (struct/enum/union)""" 

1043 # Parse complex typedefs with proper structure detection 

1044 

1045 # Find the typedef name by looking for the pattern after the closing brace 

1046 brace_count = 0 

1047 pos = start_pos 

1048 

1049 # Find opening brace 

1050 while pos < len(tokens) and tokens[pos].type != TokenType.LBRACE: 

1051 pos += 1 

1052 

1053 if pos >= len(tokens): 

1054 return None 

1055 

1056 # Skip to closing brace 

1057 brace_count = 1 

1058 pos += 1 

1059 

1060 while pos < len(tokens) and brace_count > 0: 

1061 if tokens[pos].type == TokenType.LBRACE: 

1062 brace_count += 1 

1063 elif tokens[pos].type == TokenType.RBRACE: 

1064 brace_count -= 1 

1065 pos += 1 

1066 

1067 if brace_count > 0: 

1068 return None 

1069 

1070 # Find typedef name after closing brace 

1071 while pos < len(tokens) and tokens[pos].type in [ 

1072 TokenType.WHITESPACE, 

1073 TokenType.COMMENT, 

1074 ]: 

1075 pos += 1 

1076 

1077 if pos < len(tokens) and tokens[pos].type == TokenType.IDENTIFIER: 

1078 typedef_name = tokens[pos].value 

1079 struct_type = tokens[start_pos].value # struct/enum/union 

1080 return (typedef_name, struct_type) 

1081 

1082 return None 

1083 

1084 def _extract_tag_name_from_typedef(self, tokens, start_pos): 

1085 """Extract the tag name from a typedef like 'typedef struct TagName { ... } TypedefName;'""" 

1086 # Skip 'typedef' keyword 

1087 pos = start_pos + 1 

1088 

1089 # Skip whitespace and comments 

1090 while pos < len(tokens) and tokens[pos].type in [ 

1091 TokenType.WHITESPACE, 

1092 TokenType.COMMENT, 

1093 ]: 

1094 pos += 1 

1095 

1096 if pos >= len(tokens): 

1097 return "" 

1098 

1099 # Check if it's a struct/enum/union 

1100 if tokens[pos].type not in [TokenType.STRUCT, TokenType.ENUM, TokenType.UNION]: 

1101 return "" 

1102 

1103 # Skip struct/enum/union keyword 

1104 pos += 1 

1105 

1106 # Skip whitespace and comments 

1107 while pos < len(tokens) and tokens[pos].type in [ 

1108 TokenType.WHITESPACE, 

1109 TokenType.COMMENT, 

1110 ]: 

1111 pos += 1 

1112 

1113 # Look for tag name (identifier before opening brace) 

1114 if pos < len(tokens) and tokens[pos].type == TokenType.IDENTIFIER: 

1115 tag_name = tokens[pos].value 

1116 return tag_name 

1117 

1118 return "" 

1119 

1120 def _looks_like_function(self, tokens, start_pos): 

1121 """Check if the token sequence starting at start_pos looks like a function""" 

1122 # Look ahead for parentheses within a reasonable distance 

1123 for i in range(start_pos, min(start_pos + 10, len(tokens))): 

1124 if tokens[i].type == TokenType.LPAREN: 

1125 return True 

1126 if tokens[i].type in [ 

1127 TokenType.SEMICOLON, 

1128 TokenType.LBRACE, 

1129 TokenType.RBRACE, 

1130 ]: 

1131 return False 

1132 return False 

1133 

1134 def _skip_function(self, tokens, start_pos): 

1135 """Skip over a function definition or declaration""" 

1136 # Find the end (either semicolon for declaration or closing brace for definition) 

1137 i = start_pos 

1138 brace_count = 0 

1139 paren_count = 0 

1140 

1141 while i < len(tokens): 

1142 if tokens[i].type == TokenType.LPAREN: 

1143 paren_count += 1 

1144 elif tokens[i].type == TokenType.RPAREN: 

1145 paren_count -= 1 

1146 elif tokens[i].type == TokenType.LBRACE: 

1147 brace_count += 1 

1148 elif tokens[i].type == TokenType.RBRACE: 

1149 brace_count -= 1 

1150 if brace_count == 0 and paren_count == 0: 

1151 return i + 1 

1152 elif ( 

1153 tokens[i].type == TokenType.SEMICOLON 

1154 and paren_count == 0 

1155 and brace_count == 0 

1156 ): 

1157 return i + 1 

1158 i += 1 

1159 

1160 return i 

1161 

1162 def _skip_structure_definition(self, tokens, start_pos): 

1163 """Skip over struct/enum/union/typedef definition""" 

1164 i = start_pos 

1165 brace_count = 0 

1166 

1167 while i < len(tokens): 

1168 if tokens[i].type == TokenType.LBRACE: 

1169 brace_count += 1 

1170 elif tokens[i].type == TokenType.RBRACE: 

1171 brace_count -= 1 

1172 if brace_count == 0: 

1173 # Continue until semicolon 

1174 while i < len(tokens) and tokens[i].type != TokenType.SEMICOLON: 

1175 i += 1 

1176 return i + 1 if i < len(tokens) else i 

1177 elif tokens[i].type == TokenType.SEMICOLON and brace_count == 0: 

1178 return i + 1 

1179 i += 1 

1180 

1181 return i 

1182 

1183 def _parse_global_variable(self, tokens, start_pos): 

1184 """Parse a global variable declaration starting at start_pos""" 

1185 # Look for pattern: [static/extern] type name [= value]; 

1186 i = start_pos 

1187 collected_tokens = [] 

1188 

1189 # Collect tokens until semicolon 

1190 while i < len(tokens) and tokens[i].type != TokenType.SEMICOLON: 

1191 if tokens[i].type not in [TokenType.WHITESPACE, TokenType.COMMENT]: 

1192 collected_tokens.append(tokens[i]) 

1193 i += 1 

1194 

1195 if len(collected_tokens) < 2: 

1196 return None 

1197 

1198 # Skip modifiers 

1199 start_idx = 0 

1200 while start_idx < len(collected_tokens) and collected_tokens[ 

1201 start_idx 

1202 ].type in [TokenType.STATIC, TokenType.EXTERN, TokenType.CONST]: 

1203 start_idx += 1 

1204 

1205 # Check if there's an assignment 

1206 assign_idx = None 

1207 for j in range(start_idx, len(collected_tokens)): 

1208 if collected_tokens[j].type == TokenType.ASSIGN: 

1209 assign_idx = j 

1210 break 

1211 

1212 # Extract variable name and type 

1213 if assign_idx is not None: 

1214 # Has assignment: type name = value or type name[size] = value 

1215 if assign_idx > start_idx + 1: 

1216 # Check if this is an array declaration with assignment 

1217 bracket_idx = None 

1218 for j in range(assign_idx - 1, start_idx, -1): 

1219 if collected_tokens[j].type == TokenType.RBRACKET: 

1220 bracket_idx = j 

1221 break 

1222 

1223 if bracket_idx is not None: 

1224 # Array declaration with assignment: find the identifier before the first '[' 

1225 # First, find the matching '[' for this last bracket 

1226 for j in range(bracket_idx - 1, start_idx, -1): 

1227 if collected_tokens[j].type == TokenType.LBRACKET: 

1228 # Found the first '[' of the trailing bracket groups; now find the identifier before it 

1229 for k in range(j - 1, start_idx, -1): 

1230 if collected_tokens[k].type == TokenType.IDENTIFIER: 

1231 var_name = collected_tokens[k].value 

1232 type_tokens = collected_tokens[start_idx:k] 

1233 # Format base type preserving spaces 

1234 formatted_type = [] 

1235 for idx, token in enumerate(type_tokens): 

1236 if idx > 0: 

1237 formatted_type.append(" " + token.value) 

1238 else: 

1239 formatted_type.append(token.value) 

1240 base_type = "".join(formatted_type) 

1241 # Collect all trailing [size] groups between name and '=' using shared helper 

1242 dims, _n = collect_array_dimensions_from_tokens(collected_tokens[:assign_idx], k + 1) 

1243 dims = [normalize_dim_value(d) for d in dims] 

1244 var_type = join_type_with_dims(base_type, dims) 

1245 var_type = self._clean_type_string(var_type) 

1246 value_tokens = collected_tokens[assign_idx + 1 :] 

1247 var_value = " ".join(t.value for t in value_tokens) 

1248 var_value = self._clean_value_string(var_value) 

1249 return (var_name, var_type, var_value) 

1250 break 

1251 else: 

1252 # Regular assignment: type name = value 

1253 var_name = collected_tokens[assign_idx - 1].value 

1254 type_tokens = collected_tokens[start_idx : assign_idx - 1] 

1255 value_tokens = collected_tokens[assign_idx + 1 :] 

1256 var_type = " ".join(t.value for t in type_tokens) 

1257 var_type = self._clean_type_string(var_type) 

1258 var_type = self._fix_array_bracket_spacing(var_type) 

1259 var_value = " ".join(t.value for t in value_tokens) 

1260 # Clean the value string to remove excessive whitespace and newlines 

1261 var_value = self._clean_value_string(var_value) 

1262 return (var_name, var_type, var_value) 

1263 else: 

1264 # No assignment: type name or type name[size][size]... 

1265 if len(collected_tokens) > start_idx + 1: 

1266 # Check if this is an array declaration 

1267 bracket_idx = None 

1268 for j in range(len(collected_tokens) - 1, start_idx, -1): 

1269 if collected_tokens[j].type == TokenType.RBRACKET: 

1270 bracket_idx = j 

1271 break 

1272 

1273 if bracket_idx is not None: 

1274 # Array declaration: find the identifier before the first '[' and collect all dims 

1275 for j in range(bracket_idx - 1, start_idx, -1): 

1276 if collected_tokens[j].type == TokenType.LBRACKET: 

1277 # Found the first '[' of the trailing bracket groups; look for identifier before it 

1278 for k in range(j - 1, start_idx, -1): 

1279 if collected_tokens[k].type == TokenType.IDENTIFIER: 

1280 var_name = collected_tokens[k].value 

1281 type_tokens = collected_tokens[start_idx:k] 

1282 # Format base type preserving spaces 

1283 formatted_type = [] 

1284 for idx2, token in enumerate(type_tokens): 

1285 if idx2 > 0: 

1286 formatted_type.append(" " + token.value) 

1287 else: 

1288 formatted_type.append(token.value) 

1289 base_type = "".join(formatted_type) 

1290 # Collect all trailing [size] groups after the name using shared helper 

1291 dims, _n = collect_array_dimensions_from_tokens(collected_tokens, k + 1) 

1292 dims = [normalize_dim_value(d) for d in dims] 

1293 var_type = join_type_with_dims(base_type, dims) 

1294 var_type = self._clean_type_string(var_type) 

1295 return (var_name, var_type, None) 

1296 break 

1297 else: 

1298 # Regular variable: last token is the name 

1299 var_name = collected_tokens[-1].value 

1300 type_tokens = collected_tokens[start_idx:-1] 

1301 var_type = " ".join(t.value for t in type_tokens) 

1302 var_type = self._clean_type_string(var_type) 

1303 var_type = self._fix_array_bracket_spacing(var_type) 

1304 return (var_name, var_type, None) 

1305 

1306 return None 

1307 

1308 def _skip_to_semicolon(self, tokens, start_pos): 

1309 """Skip to the next semicolon""" 

1310 i = start_pos 

1311 while i < len(tokens) and tokens[i].type != TokenType.SEMICOLON: 

1312 i += 1 

1313 return i + 1 if i < len(tokens) else i 

1314 

1315 def _skip_preprocessor_directives(self, tokens, start_pos): 

1316 """Skip preprocessor directives but keep their content for parsing""" 

1317 # This method is deprecated - use the PreprocessorManager instead 

1318 i = start_pos 

1319 while i < len(tokens) and tokens[i].type == TokenType.PREPROCESSOR: 

1320 # Skip the preprocessor directive itself 

1321 i += 1 

1322 return i 

1323 

1324 def _parse_function_parameters(self, tokens, start_pos, end_pos, func_name): 

1325 """Parse function parameters from token range""" 

1326 

1327 parameters = [] 

1328 

1329 # Find the opening parenthesis for the function 

1330 paren_start = None 

1331 paren_end = None 

1332 

1333 for i in range(start_pos, min(end_pos + 1, len(tokens))): 

1334 if tokens[i].type == TokenType.IDENTIFIER and tokens[i].value == func_name: 

1335 # Look for opening parenthesis after function name 

1336 for j in range(i + 1, min(end_pos + 1, len(tokens))): 

1337 if tokens[j].type == TokenType.LPAREN: 

1338 paren_start = j 

1339 break 

1340 elif tokens[j].type not in [ 

1341 TokenType.WHITESPACE, 

1342 TokenType.COMMENT, 

1343 ]: 

1344 break 

1345 break 

1346 

1347 if paren_start is None: 

1348 return parameters 

1349 

1350 # Find matching closing parenthesis 

1351 paren_depth = 1 

1352 for i in range(paren_start + 1, min(end_pos + 1, len(tokens))): 

1353 if tokens[i].type == TokenType.LPAREN: 

1354 paren_depth += 1 

1355 elif tokens[i].type == TokenType.RPAREN: 

1356 paren_depth -= 1 

1357 if paren_depth == 0: 

1358 paren_end = i 

1359 break 

1360 

1361 if paren_end is None: 

1362 return parameters 

1363 

1364 # Parse parameter tokens between parentheses 

1365 param_tokens = [] 

1366 for i in range(paren_start + 1, paren_end): 

1367 if tokens[i].type not in [TokenType.WHITESPACE, TokenType.COMMENT, TokenType.NEWLINE]: 

1368 param_tokens.append(tokens[i]) 

1369 

1370 # If no parameters or just "void", return empty list 

1371 if not param_tokens or ( 

1372 len(param_tokens) == 1 and param_tokens[0].value == "void" 

1373 ): 

1374 return parameters 

1375 

1376 # Split parameters by commas, but handle function pointers correctly 

1377 current_param = [] 

1378 paren_depth = 0 

1379 for token in param_tokens: 

1380 if token.type == TokenType.LPAREN: 

1381 paren_depth += 1 

1382 elif token.type == TokenType.RPAREN: 

1383 paren_depth -= 1 

1384 elif token.type == TokenType.COMMA and paren_depth == 0: 

1385 # Only split on commas that are not inside parentheses 

1386 if current_param: 

1387 param = self._parse_single_parameter(current_param) 

1388 if param: 

1389 parameters.append(param) 

1390 current_param = [] 

1391 continue 

1392 

1393 current_param.append(token) 

1394 

1395 # Handle last parameter 

1396 if current_param: 

1397 param = self._parse_single_parameter(current_param) 

1398 if param: 

1399 parameters.append(param) 

1400 

1401 return parameters 

1402 

1403 def _parse_single_parameter(self, param_tokens): 

1404 """Parse a single function parameter from tokens""" 

1405 from ..models import Field 

1406 

1407 if not param_tokens: 

1408 return None 

1409 

1410 # Handle variadic parameters (three consecutive dots) 

1411 if len(param_tokens) == 3 and all(t.value == "." for t in param_tokens): 

1412 return Field(name="...", type="...") 

1413 

1414 # Handle variadic parameters (single ... token) 

1415 if len(param_tokens) == 1 and param_tokens[0].value == "...": 

1416 return Field(name="...", type="...") 

1417 

1418 # Handle function pointer parameters: type (*name)(params) 

1419 if len(param_tokens) >= 5: 

1420 # Look for pattern: type ( * name ) ( params ) 

1421 for i in range(len(param_tokens) - 4): 

1422 if ( 

1423 param_tokens[i].type == TokenType.LPAREN 

1424 and param_tokens[i + 1].type == TokenType.ASTERISK 

1425 and param_tokens[i + 2].type == TokenType.IDENTIFIER 

1426 and param_tokens[i + 3].type == TokenType.RPAREN 

1427 and param_tokens[i + 4].type == TokenType.LPAREN 

1428 ): 

1429 # Found function pointer pattern 

1430 func_name = param_tokens[i + 2].value 

1431 

1432 # Find the closing parenthesis for the parameter list 

1433 paren_count = 1 

1434 param_end = i + 5 

1435 while param_end < len(param_tokens) and paren_count > 0: 

1436 if param_tokens[param_end].type == TokenType.LPAREN: 

1437 paren_count += 1 

1438 elif param_tokens[param_end].type == TokenType.RPAREN: 

1439 paren_count -= 1 

1440 param_end += 1 

1441 

1442 if paren_count == 0: 

1443 # Extract the type (everything before the function pointer) 

1444 type_tokens = param_tokens[:i] 

1445 param_type = " ".join(t.value for t in type_tokens) 

1446 

1447 # Extract the function pointer part 

1448 func_ptr_tokens = param_tokens[i:param_end] 

1449 func_ptr_type = " ".join(t.value for t in func_ptr_tokens) 

1450 

1451 # Combine type and function pointer 

1452 full_type = (param_type + " " + func_ptr_type).strip() 

1453 

1454 # Fix array bracket spacing 

1455 full_type = self._fix_array_bracket_spacing(full_type) 

1456 

1457 return Field(name=func_name, type=full_type) 

1458 else: 

1459 # Incomplete function pointer - try to reconstruct 

1460 type_tokens = param_tokens[:i] 

1461 param_type = " ".join(t.value for t in type_tokens) 

1462 func_ptr_tokens = param_tokens[i:] 

1463 func_ptr_type = " ".join(t.value for t in func_ptr_tokens) 

1464 full_type = (param_type + " " + func_ptr_type).strip() 

1465 full_type = self._fix_array_bracket_spacing(full_type) 

1466 return Field(name=func_name, type=full_type) 

1467 

1468 # Also look for pattern: type ( * name ) ( params ) with spaces 

1469 for i in range(len(param_tokens) - 4): 

1470 if ( 

1471 param_tokens[i].type == TokenType.LPAREN 

1472 and param_tokens[i + 1].type == TokenType.ASTERISK 

1473 and param_tokens[i + 2].type == TokenType.IDENTIFIER 

1474 and param_tokens[i + 3].type == TokenType.RPAREN 

1475 and param_tokens[i + 4].type == TokenType.LPAREN 

1476 ): 

1477 # Found function pointer pattern 

1478 func_name = param_tokens[i + 2].value 

1479 

1480 # Find the closing parenthesis for the parameter list 

1481 paren_count = 1 

1482 param_end = i + 5 

1483 while param_end < len(param_tokens) and paren_count > 0: 

1484 if param_tokens[param_end].type == TokenType.LPAREN: 

1485 paren_count += 1 

1486 elif param_tokens[param_end].type == TokenType.RPAREN: 

1487 paren_count -= 1 

1488 param_end += 1 

1489 

1490 if paren_count == 0: 

1491 # Extract the type (everything before the function pointer) 

1492 type_tokens = param_tokens[:i] 

1493 param_type = " ".join(t.value for t in type_tokens) 

1494 

1495 # Extract the function pointer part 

1496 func_ptr_tokens = param_tokens[i:param_end] 

1497 func_ptr_type = " ".join(t.value for t in func_ptr_tokens) 

1498 

1499 # Combine type and function pointer 

1500 full_type = (param_type + " " + func_ptr_type).strip() 

1501 

1502 # Fix array bracket spacing 

1503 full_type = self._fix_array_bracket_spacing(full_type) 

1504 

1505 return Field(name=func_name, type=full_type) 

1506 else: 

1507 # Incomplete function pointer - try to reconstruct 

1508 type_tokens = param_tokens[:i] 

1509 param_type = " ".join(t.value for t in type_tokens) 

1510 func_ptr_tokens = param_tokens[i:] 

1511 func_ptr_type = " ".join(t.value for t in func_ptr_tokens) 

1512 full_type = (param_type + " " + func_ptr_type).strip() 

1513 full_type = self._fix_array_bracket_spacing(full_type) 

1514 return Field(name=func_name, type=full_type) 

1515 

1516 # For parameters like "int x" or "const char *name" or "char* argv[]" 

1517 if len(param_tokens) >= 2: 

1518 # Check if the last token is a closing bracket (array parameter) 

1519 if param_tokens[-1].type == TokenType.RBRACKET: 

1520 # Find the opening bracket to get the array size 

1521 bracket_start = None 

1522 for i in range(len(param_tokens) - 1, -1, -1): 

1523 if param_tokens[i].type == TokenType.LBRACKET: 

1524 bracket_start = i 

1525 break 

1526 

1527 if bracket_start is not None: 

1528 # Extract the parameter name (last identifier before the opening bracket) 

1529 param_name = None 

1530 for i in range(bracket_start - 1, -1, -1): 

1531 if param_tokens[i].type == TokenType.IDENTIFIER: 

1532 param_name = param_tokens[i].value 

1533 break 

1534 

1535 if param_name: 

1536 # Extract the type (everything before the parameter name) 

1537 type_tokens = param_tokens[:i] 

1538 param_type = " ".join(t.value for t in type_tokens) 

1539 

1540 # Add the array brackets to the type 

1541 array_size = "" 

1542 if bracket_start + 1 < len(param_tokens) - 1: 

1543 # There's content between brackets 

1544 array_content = param_tokens[bracket_start + 1:-1] 

1545 array_size = " ".join(t.value for t in array_content) 

1546 

1547 param_type = param_type + "[" + array_size + "]" 

1548 

1549 # Fix array bracket spacing 

1550 param_type = self._fix_array_bracket_spacing(param_type) 

1551 

1552 return Field(name=param_name, type=param_type) 

1553 else: 

1554 # Regular parameter: last token is the parameter name 

1555 param_name = param_tokens[-1].value 

1556 type_tokens = param_tokens[:-1] 

1557 param_type = " ".join(t.value for t in type_tokens) 

1558 

1559 # Fix array bracket spacing and pointer spacing 

1560 param_type = self._fix_array_bracket_spacing(param_type) 

1561 param_type = self._fix_pointer_spacing(param_type) 

1562 

1563 # Handle unnamed parameters (just type) 

1564 if param_name in [ 

1565 "void", 

1566 "int", 

1567 "char", 

1568 "float", 

1569 "double", 

1570 "long", 

1571 "short", 

1572 "unsigned", 

1573 "signed", 

1574 ]: 

1575 # This is just a type without a name 

1576 return Field(name="unnamed", type=param_type + " " + param_name) 

1577 

1578 # Additional validation before creating Field 

1579 if param_name and param_name.strip() and param_type and param_type.strip(): 

1580 return Field(name=param_name.strip(), type=param_type.strip()) 

1581 else: 

1582 # Fallback for invalid parameters - try to reconstruct the full parameter 

1583 full_param = " ".join(t.value for t in param_tokens) 

1584 full_param = self._fix_array_bracket_spacing(full_param) 

1585 if full_param.strip(): 

1586 return Field(name="unnamed", type=full_param.strip()) 

1587 else: 

1588 return Field(name="unnamed", type="unknown") 

1589 elif len(param_tokens) == 1: 

1590 # Single token - might be just type (like "void") or name 

1591 token_value = param_tokens[0].value 

1592 if token_value in [ 

1593 "void", 

1594 "int", 

1595 "char", 

1596 "float", 

1597 "double", 

1598 "long", 

1599 "short", 

1600 "unsigned", 

1601 "signed", 

1602 ]: 

1603 return Field(name="unnamed", type=token_value) 

1604 else: 

1605 # If we can't determine the type, use the token value as type 

1606 if token_value and token_value.strip(): 

1607 return Field(name="unnamed", type=token_value.strip()) 

1608 else: 

1609 return Field(name="unnamed", type="unknown") 

1610 

1611 return None 

1612 

1613 def _fix_array_bracket_spacing(self, type_str: str) -> str: 

1614 """Fix spacing around array brackets in type strings""" 

1615 return fix_array_bracket_spacing(type_str) 

1616 

1617 def _fix_pointer_spacing(self, type_str: str) -> str: 

1618 """Fix spacing around pointer asterisks in type strings""" 

1619 return fix_pointer_spacing(type_str) 

1620 

1621 def _clean_type_string(self, type_str: str) -> str: 

1622 """Clean type string by removing newlines and normalizing whitespace""" 

1623 return clean_type_string(type_str) 

1624 

1625 def _clean_value_string(self, value_str: str) -> str: 

1626 """Clean value string by removing excessive whitespace and newlines""" 

1627 return clean_value_string(value_str) 

1628 

1629 # _get_timestamp helper removed as unused to reduce surface area 

1630 

1631 

1632class Parser: 

1633 """Main parser class for Step 1: Parse C code files and generate model.json""" 

1634 

1635 def __init__(self): 

1636 self.c_parser = CParser() 

1637 self.logger = logging.getLogger(__name__) 

1638 

1639 def parse( 

1640 self, 

1641 source_folders: "List[str]", 

1642 output_file: str = "model.json", 

1643 recursive_search: bool = True, 

1644 config: "Config" = None, 

1645 ) -> str: 

1646 """Parse C/C++ projects and generate model.json 

1647 

1648 Args: 

1649 source_folders: List of source folder directories within the project 

1650 output_file: Path to the output model.json file 

1651 recursive_search: Whether to search subdirectories recursively 

1652 config: Configuration object for filtering and processing 

1653 

1654 Returns: 

1655 Path to the generated model.json file 

1656 """ 

1657 # Enhanced validation for source_folders 

1658 if not isinstance(source_folders, list): 

1659 raise TypeError(f"source_folders must be a list of strings, got: {type(source_folders)}") 

1660 

1661 if not source_folders: 

1662 raise ValueError("At least one source folder must be provided") 

1663 

1664 # Validate all items are strings and not empty 

1665 for i, folder in enumerate(source_folders): 

1666 if not isinstance(folder, str): 

1667 raise TypeError(f"All source folders must be strings, got {type(folder)} at index {i}: {folder}") 

1668 if not folder.strip(): 

1669 raise ValueError(f"Source folder at index {i} cannot be empty or whitespace: {repr(folder)}") 

1670 

1671 self.logger.info( 

1672 f"Step 1: Parsing C/C++ project with {len(source_folders)} source folders" 

1673 ) 

1674 

1675 # Get project name from config or use default 

1676 project_name = ( 

1677 getattr(config, "project_name", "C_Project") if config else "C_Project" 

1678 ) 

1679 

1680 # Parse each source folder and combine results 

1681 all_files = {} 

1682 total_structs = 0 

1683 total_enums = 0 

1684 total_functions = 0 

1685 failed_folders = [] 

1686 

1687 for i, source_folder in enumerate(source_folders): 

1688 self.logger.info( 

1689 f"Parsing source folder {i+1}/{len(source_folders)}: {source_folder}" 

1690 ) 

1691 

1692 try: 

1693 # Parse the individual source folder 

1694 model = self.c_parser.parse_project( 

1695 source_folder, recursive_search, config 

1696 ) 

1697 

1698 all_files.update(model.files) 

1699 

1700 # Update totals 

1701 total_structs += sum(len(f.structs) for f in model.files.values()) 

1702 total_enums += sum(len(f.enums) for f in model.files.values()) 

1703 total_functions += sum(len(f.functions) for f in model.files.values()) 

1704 

1705 self.logger.info( 

1706 f"Successfully parsed source folder {source_folder}: {len(model.files)} files" 

1707 ) 

1708 

1709 except Exception as e: 

1710 self.logger.error( 

1711 "Failed to parse source folder %s: %s", source_folder, e 

1712 ) 

1713 failed_folders.append((source_folder, str(e))) 

1714 

1715 # If this is the only source folder, re-raise the error 

1716 if len(source_folders) == 1: 

1717 raise 

1718 

1719 # For multiple source folders, continue with others but log the failure 

1720 self.logger.warning( 

1721 "Continuing with other source folders despite failure in %s", source_folder 

1722 ) 

1723 

1724 # If all source folders failed, raise an error 

1725 if failed_folders and len(failed_folders) == len(source_folders): 

1726 error_msg = "All source folders failed to parse:\n" 

1727 for folder, error in failed_folders: 

1728 error_msg += f" - {folder}: {error}\n" 

1729 raise RuntimeError(error_msg) 

1730 

1731 # If some folders failed, log a warning 

1732 if failed_folders: 

1733 self.logger.warning( 

1734 f"Failed to parse {len(failed_folders)} out of {len(source_folders)} source folders" 

1735 ) 

1736 

1737 # Create combined project model 

1738 combined_model = ProjectModel( 

1739 project_name=project_name, 

1740 source_folder=( 

1741 ",".join(source_folders) 

1742 if len(source_folders) > 1 

1743 else source_folders[0] 

1744 ), 

1745 files=all_files, 

1746 ) 

1747 

1748 # Update all uses fields across the entire combined project 

1749 combined_model.update_uses_fields() 

1750 

1751 # Save combined model to JSON file 

1752 try: 

1753 combined_model.save(output_file) 

1754 except Exception as e: 

1755 raise RuntimeError(f"Failed to save model to {output_file}: {e}") from e 

1756 

1757 # Step 1.5: Verify model sanity 

1758 self.logger.info("Step 1.5: Verifying model sanity...") 

1759 from .verifier import ModelVerifier 

1760 

1761 verifier = ModelVerifier() 

1762 is_valid, issues = verifier.verify_model(combined_model) 

1763 

1764 if not is_valid: 

1765 self.logger.warning( 

1766 f"Model verification found {len(issues)} issues - model may contain parsing errors" 

1767 ) 

1768 # Continue processing but warn about potential issues 

1769 else: 

1770 self.logger.info("Model verification passed - all values look sane") 

1771 

1772 self.logger.info("Step 1 complete! Model saved to: %s", output_file) 

1773 self.logger.info( 

1774 f"Found {len(all_files)} total files across {len(source_folders)} source folder(s)" 

1775 ) 

1776 

1777 # Print summary 

1778 self.logger.info( 

1779 f"Summary: {total_structs} structs, {total_enums} enums, " 

1780 f"{total_functions} functions" 

1781 ) 

1782 

1783 return output_file