Coverage for src/c2puml/core/transformer.py: 62%
792 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""""""
4import json
5import logging
6import re
7from pathlib import Path
8from typing import Any, Callable, Dict, List, Optional, Pattern, Set, Tuple, Union as TypingUnion
9from collections import deque
11from ..models import (
12 Alias,
13 Enum,
14 EnumValue,
15 Field,
16 FileModel,
17 Function,
18 IncludeRelation,
19 ProjectModel,
20 Struct,
21 Union,
22)
25class Transformer:
26 """"""
28 def __init__(self) -> None:
29 self.logger = logging.getLogger(__name__)
31 def transform(
32 self, model_file: str, config_file: str, output_file: Optional[str] = None
33 ) -> str:
34 """"""
35 self.logger.info("Step 2: Transforming model: %s", model_file)
37 model = self._load_model(model_file)
38 config = self._load_config(config_file)
40 transformed_model = self._apply_transformations(model, config)
42 output_path = output_file or model_file
43 self._save_model(transformed_model, output_path)
45 self.logger.info("Step 2 complete! Transformed model saved to: %s", output_path)
46 return output_path
48 def _load_model(self, model_file: str) -> ProjectModel:
49 """"""
50 model_path = Path(model_file)
51 if not model_path.exists():
52 raise FileNotFoundError(f"Model file not found: {model_file}")
54 try:
55 model = ProjectModel.load(model_file)
56 self.logger.debug("Loaded model with %d files", len(model.files))
57 return model
58 except Exception as e:
59 raise ValueError(f"Failed to load model from {model_file}: {e}") from e
61 def _load_config(self, config_file: str) -> Dict[str, Any]:
62 """"""
63 config_path = Path(config_file)
64 if not config_path.exists():
65 raise FileNotFoundError(f"Configuration file not found: {config_file}")
67 try:
68 with open(config_file, "r", encoding="utf-8") as f:
69 config = json.load(f)
71 self.logger.debug("Loaded configuration from: %s", config_file)
72 return config
74 except Exception as e:
75 raise ValueError(
76 f"Failed to load configuration from {config_file}: {e}"
77 ) from e
79 def _apply_transformations(
80 self, model: ProjectModel, config: Dict[str, Any]
81 ) -> ProjectModel:
82 """"""
83 self.logger.info("Applying transformations to model")
85 if "file_filters" in config:
86 model = self._apply_file_filters(model, config["file_filters"])
88 config = self._ensure_backward_compatibility(config)
90 model = self._apply_transformation_containers(model, config)
92 if self._should_process_include_relations(config):
93 model = self._process_include_relations_simplified(model, config)
95 self.logger.info(
96 "Transformations complete. Model now has %d files", len(model.files)
97 )
98 return model
100 def _apply_transformation_containers(
101 self, model: ProjectModel, config: Dict[str, Any]
102 ) -> ProjectModel:
103 """"""
104 transformation_containers = self._discover_transformation_containers(config)
106 if not transformation_containers:
107 return model
109 for container_name, transformation_config in transformation_containers:
110 self.logger.info("Applying transformation container: %s", container_name)
111 model = self._apply_single_transformation_container(
112 model, transformation_config, container_name
113 )
114 self._log_model_state_after_container(model, container_name)
116 return model
118 def _log_model_state_after_container(
119 self, model: ProjectModel, container_name: str
120 ) -> None:
121 """"""
122 total_elements = sum(
123 len(file_model.structs) + len(file_model.enums) + len(file_model.unions) +
124 len(file_model.functions) + len(file_model.globals) + len(file_model.macros) +
125 len(file_model.aliases)
126 for file_model in model.files.values()
127 )
128 self.logger.info(
129 "After %s: model contains %d files with %d total elements",
130 container_name, len(model.files), total_elements
131 )
133 def _should_process_include_relations(self, config: Dict[str, Any]) -> bool:
134 """"""
135 if config.get("include_depth", 1) > 1:
136 return True
138 if "file_specific" in config:
139 for file_config in config["file_specific"].values():
140 if file_config.get("include_depth", 1) > 1:
141 return True
143 return False
145 def _discover_transformation_containers(self, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
146 """"""
147 transformation_containers = [
148 (key, value)
149 for key, value in config.items()
150 if key.startswith("transformations") and isinstance(value, dict)
151 ]
153 transformation_containers.sort(key=lambda x: x[0])
155 self.logger.info(
156 "Discovered %d transformation containers: %s",
157 len(transformation_containers),
158 [name for name, _ in transformation_containers]
159 )
161 return transformation_containers
163 def _ensure_backward_compatibility(self, config: Dict[str, Any]) -> Dict[str, Any]:
164 """"""
165 config = config.copy()
167 if self._is_legacy_transformation_format(config):
168 self.logger.info("Converting legacy 'transformations' format to container format")
169 old_transformations = config.pop("transformations")
170 config["transformations_00_default"] = old_transformations
171 self.logger.debug("Converted to container: transformations_00_default")
173 return config
175 def _is_legacy_transformation_format(self, config: Dict[str, Any]) -> bool:
176 """"""
177 return (
178 "transformations" in config and
179 not any(key.startswith("transformations_") for key in config.keys())
180 )
182 def _apply_single_transformation_container(
183 self,
184 model: ProjectModel,
185 transformation_config: Dict[str, Any],
186 container_name: str
187 ) -> ProjectModel:
188 """"""
189 self.logger.debug("Processing transformation container: %s", container_name)
191 target_files = self._get_target_files(model, transformation_config)
193 model = self._apply_remove_operations(model, transformation_config, target_files, container_name)
194 model = self._apply_rename_operations(model, transformation_config, target_files, container_name)
195 model = self._apply_add_operations(model, transformation_config, target_files, container_name)
197 return model
199 def _get_target_files(
200 self, model: ProjectModel, transformation_config: Dict[str, Any]
201 ) -> Set[str]:
202 """"""
203 selected_files = transformation_config.get("file_selection", [])
205 if not isinstance(selected_files, list):
206 selected_files = []
207 self.logger.warning("Invalid file_selection format, must be a list, defaulting to empty list")
209 if not selected_files:
210 target_files = set(model.files.keys())
211 self.logger.debug("No file selection specified, applying to all %d files", len(target_files))
212 else:
213 target_files = self._match_files_by_patterns(model, selected_files)
214 self.logger.debug(
215 "File selection patterns %s matched %d files: %s",
216 selected_files, len(target_files), list(target_files)
217 )
219 return target_files
221 def _match_files_by_patterns(
222 self, model: ProjectModel, patterns: List[str]
223 ) -> Set[str]:
224 """"""
225 target_files = set()
226 for pattern in patterns:
227 for file_path in model.files.keys():
228 if self._matches_pattern(file_path, pattern):
229 target_files.add(file_path)
230 return target_files
232 def _apply_remove_operations(
233 self,
234 model: ProjectModel,
235 transformation_config: Dict[str, Any],
236 target_files: Set[str],
237 container_name: str
238 ) -> ProjectModel:
239 """"""
240 if "remove" not in transformation_config:
241 return model
243 self.logger.debug("Applying remove operations for container: %s", container_name)
245 removed_typedef_names = self._collect_typedef_names_for_removal(
246 model, transformation_config["remove"], target_files
247 )
249 model = self._apply_removals(model, transformation_config["remove"], target_files)
251 if removed_typedef_names:
252 self.logger.debug("Calling type reference cleanup for container: %s", container_name)
253 self._cleanup_type_references_by_names(model, removed_typedef_names)
255 return model
257 def _apply_rename_operations(
258 self,
259 model: ProjectModel,
260 transformation_config: Dict[str, Any],
261 target_files: Set[str],
262 container_name: str
263 ) -> ProjectModel:
264 """"""
265 if "rename" not in transformation_config:
266 return model
268 self.logger.debug("Applying rename operations for container: %s", container_name)
269 return self._apply_renaming(model, transformation_config["rename"], target_files)
271 def _apply_add_operations(
272 self,
273 model: ProjectModel,
274 transformation_config: Dict[str, Any],
275 target_files: Set[str],
276 container_name: str
277 ) -> ProjectModel:
278 """"""
279 if "add" not in transformation_config:
280 return model
282 self.logger.debug("Applying add operations for container: %s", container_name)
283 return self._apply_additions(model, transformation_config["add"], target_files)
285 def _collect_typedef_names_for_removal(
286 self,
287 model: ProjectModel,
288 remove_config: Dict[str, Any],
289 target_files: Set[str]
290 ) -> Set[str]:
291 """"""
292 removed_typedef_names = set()
294 if "typedef" not in remove_config:
295 return removed_typedef_names
297 typedef_patterns = remove_config["typedef"]
298 compiled_patterns = self._compile_patterns(typedef_patterns)
300 if not compiled_patterns:
301 return removed_typedef_names
303 for file_path in target_files:
304 if file_path in model.files:
305 file_model = model.files[file_path]
306 for alias_name in file_model.aliases.keys():
307 if self._matches_any_pattern(alias_name, compiled_patterns):
308 removed_typedef_names.add(alias_name)
310 self.logger.debug("Pre-identified typedefs for removal: %s", list(removed_typedef_names))
311 return removed_typedef_names
313 def _process_include_relations_simplified(
314 self, model: ProjectModel, config: Dict[str, Any]
315 ) -> ProjectModel:
316 """"""
317 global_include_depth = config.get("include_depth", 1)
318 file_specific_config = config.get("file_specific", {})
319 include_filter_local_only = config.get("include_filter_local_only", False)
320 always_show_includes = config.get("always_show_includes", False)
322 self.logger.info(
323 "Processing includes with simplified depth-based approach (global_depth=%d)",
324 global_include_depth
325 )
327 for file_model in model.files.values():
328 file_model.include_relations = []
330 file_map = {}
331 for file_model in model.files.values():
332 filename = Path(file_model.name).name
333 file_map[filename] = file_model
335 c_files = sorted([
336 fm for fm in model.files.values() if fm.name.endswith(".c")
337 ], key=lambda fm: fm.name)
339 for root_file in c_files:
340 self._process_root_c_file_includes(
341 root_file, file_map, global_include_depth, file_specific_config, include_filter_local_only, always_show_includes
342 )
344 return model
346 def _process_root_c_file_includes(
347 self,
348 root_file: FileModel,
349 file_map: Dict[str, FileModel],
350 global_include_depth: int,
351 file_specific_config: Dict[str, Any],
352 include_filter_local_only: bool,
353 always_show_includes: bool
354 ) -> None:
355 """"""
356 root_filename = Path(root_file.name).name
358 include_depth = global_include_depth
359 include_filters = []
361 if root_filename in file_specific_config:
362 file_config = file_specific_config[root_filename]
363 include_depth = file_config.get("include_depth", global_include_depth)
364 include_filters = file_config.get("include_filter", [])
366 if include_filter_local_only:
367 local_header_pattern = f"^{Path(root_filename).stem}\\.h$"
368 if local_header_pattern not in include_filters:
369 include_filters.append(local_header_pattern)
371 if include_depth <= 1:
372 self.logger.debug(
373 "Skipping include processing for %s (depth=%d)",
374 root_filename, include_depth
375 )
376 return
378 compiled_filters = []
379 if include_filters:
380 try:
381 compiled_filters = [re.compile(pattern) for pattern in include_filters]
382 self.logger.debug(
383 "Compiled %d filter patterns for %s",
384 len(compiled_filters), root_filename
385 )
386 except re.error as e:
387 self.logger.warning(
388 "Invalid regex pattern for %s: %s", root_filename, e
389 )
391 self.logger.debug(
392 "Processing includes for root C file %s (depth=%d, filters=%d)",
393 root_filename, include_depth, len(compiled_filters)
394 )
396 processed_files = set()
397 try:
398 root_file.placeholder_headers.clear()
399 except Exception:
400 root_file.placeholder_headers = set()
402 current_level = [root_file]
404 for depth in range(1, include_depth + 1):
405 next_level = []
407 self.logger.debug(
408 "Processing depth %d for %s (%d files at current level)",
409 depth, root_filename, len(current_level)
410 )
412 for current_file in sorted(current_level, key=lambda fm: Path(fm.name).name):
413 current_filename = Path(current_file.name).name
415 if current_filename in processed_files:
416 continue
417 processed_files.add(current_filename)
419 for include_name in sorted(current_file.includes):
420 filtered_out_by_patterns = False
421 if compiled_filters:
422 if not any(pattern.search(include_name) for pattern in compiled_filters):
423 if always_show_includes:
424 filtered_out_by_patterns = True
425 self.logger.debug(
426 "Include %s filtered by patterns at depth %d for %s, but will be shown as placeholder",
427 include_name, depth, root_filename
428 )
429 # Intentionally do not continue here; still add relation and mark placeholder
430 else:
431 self.logger.debug(
432 "Filtered out include %s at depth %d for %s",
433 include_name, depth, root_filename
434 )
435 continue
437 if include_name not in file_map:
438 self.logger.debug(
439 "Include %s not found in project files (depth %d, root %s)",
440 include_name, depth, root_filename
441 )
442 continue
444 if include_name == current_filename:
445 self.logger.debug(
446 "Skipping self-reference %s at depth %d for %s",
447 include_name, depth, root_filename
448 )
449 continue
451 existing_relation = any(
452 rel.source_file == current_filename and rel.included_file == include_name
453 for rel in root_file.include_relations
454 )
456 if existing_relation:
457 self.logger.debug(
458 "Skipping duplicate relation %s -> %s for %s",
459 current_filename, include_name, root_filename
460 )
461 continue
463 relation = IncludeRelation(
464 source_file=current_filename,
465 included_file=include_name,
466 depth=depth
467 )
468 root_file.include_relations.append(relation)
470 self.logger.debug(
471 "Added include relation: %s -> %s (depth %d) for root %s",
472 current_filename, include_name, depth, root_filename
473 )
475 if filtered_out_by_patterns:
476 try:
477 root_file.placeholder_headers.add(include_name)
478 except Exception:
479 root_file.placeholder_headers = {include_name}
480 continue
482 included_file = file_map[include_name]
483 if included_file not in next_level and include_name not in processed_files:
484 next_level.append(included_file)
486 current_level = sorted(next_level, key=lambda fm: Path(fm.name).name)
488 if not current_level:
489 self.logger.debug(
490 "No more files to process at depth %d for %s",
491 depth + 1, root_filename
492 )
493 break
495 self.logger.debug(
496 "Completed include processing for %s: %d relations generated",
497 root_filename, len(root_file.include_relations)
498 )
500 def _apply_file_filters(
501 self, model: ProjectModel, filters: Dict[str, Any]
502 ) -> ProjectModel:
503 """"""
504 include_patterns = self._compile_patterns(filters.get("include", []))
505 exclude_patterns = self._compile_patterns(filters.get("exclude", []))
507 if not include_patterns and not exclude_patterns:
508 return model
510 filtered_files = {}
511 for file_path, file_model in model.files.items():
512 if self._should_include_file(file_path, include_patterns, exclude_patterns):
513 filtered_files[file_path] = file_model
515 model.files = filtered_files
516 self.logger.debug(
517 "User file filtering: %d files after filtering", len(model.files)
518 )
519 return model
521 def _apply_include_filters(
522 self, model: ProjectModel, include_filters: Dict[str, List[str]]
523 ) -> ProjectModel:
524 """"""
525 self.logger.info(
526 "Applying include filters for %d root files", len(include_filters)
527 )
529 compiled_filters = {}
530 for root_file, patterns in include_filters.items():
531 try:
532 compiled_filters[root_file] = [
533 re.compile(pattern) for pattern in patterns
534 ]
535 self.logger.debug(
536 "Compiled %d patterns for root file: %s", len(patterns), root_file
537 )
538 except re.error as e:
539 self.logger.warning(
540 "Invalid regex pattern for root file %s: %s", root_file, e
541 )
542 continue
544 if not compiled_filters:
545 self.logger.warning(
546 "No valid include filters found, skipping include filtering"
547 )
548 return model
550 header_to_root = self._create_header_to_root_mapping(model)
552 for file_path, file_model in model.files.items():
553 root_file = self._find_root_file_with_mapping(
554 file_path, file_model, header_to_root
555 )
557 if root_file in compiled_filters:
558 self._filter_include_relations(
559 file_model, compiled_filters[root_file], root_file
560 )
562 return model
564 def _create_header_to_root_mapping(self, model: ProjectModel) -> Dict[str, str]:
565 """"""
566 header_to_root = {}
567 c_files = []
568 for file_path, file_model in model.files.items():
569 if file_model.name.endswith(".c"):
570 header_to_root[file_model.name] = file_model.name
571 c_files.append(file_model.name)
572 for file_path, file_model in model.files.items():
573 if not file_model.name.endswith(".c"):
574 header_base_name = Path(file_model.name).stem
575 matching_c_file = header_base_name + ".c"
577 if matching_c_file in [Path(c_file).name for c_file in c_files]:
578 header_to_root[file_model.name] = matching_c_file
579 else:
580 including_c_files = []
581 for c_file_path, c_file_model in model.files.items():
582 if (c_file_model.name.endswith(".c") and
583 file_model.name in c_file_model.includes):
584 including_c_files.append(c_file_model.name)
586 if including_c_files:
587 header_to_root[file_model.name] = including_c_files[0]
588 else:
589 if c_files:
590 header_to_root[file_model.name] = c_files[0]
591 return header_to_root
593 def _find_root_file_with_mapping(
594 self, file_path: str, file_model: FileModel, header_to_root: Dict[str, str]
595 ) -> str:
596 """"""
597 if file_model.name.endswith(".c"):
598 return file_model.name
599 return header_to_root.get(file_model.name, file_model.name)
601 def _find_root_file(self, file_path: str, file_model: FileModel) -> str:
602 """"""
603 filename = Path(file_path).name
604 if filename.endswith(".c"):
605 return filename
606 base_name = Path(file_path).stem
607 if base_name and not filename.startswith("."):
608 return base_name + ".c"
609 return filename
611 def _filter_include_relations(
612 self, file_model: FileModel, patterns: List[re.Pattern], root_file: str
613 ) -> None:
614 """"""
615 self.logger.debug(
616 "Filtering include_relations for file %s (root: %s)", file_model.name, root_file
617 )
619 original_relations_count = len(file_model.include_relations)
620 filtered_relations: List[IncludeRelation] = []
622 for relation in file_model.include_relations:
623 if self._matches_any_pattern(relation.included_file, patterns):
624 filtered_relations.append(relation)
625 else:
626 self.logger.debug(
627 "Filtered out include relation: %s -> %s (root: %s)",
628 relation.source_file,
629 relation.included_file,
630 root_file,
631 )
632 try:
633 file_model.placeholder_headers.add(relation.included_file)
634 except Exception:
635 file_model.placeholder_headers = {relation.included_file}
637 file_model.include_relations = filtered_relations
639 self.logger.debug(
640 "Include filtering for %s: relations %d->%d (includes preserved)",
641 file_model.name,
642 original_relations_count,
643 len(file_model.include_relations),
644 )
646 def _matches_any_pattern(self, text: str, patterns: List[Pattern[str]]) -> bool:
647 """"""
648 return any(pattern.search(text) for pattern in patterns)
650 def _matches_pattern(self, text: str, pattern: str) -> bool:
651 """"""
652 try:
653 return bool(re.search(pattern, text))
654 except re.error as e:
655 self.logger.warning("Invalid regex pattern '%s': %s", pattern, e)
656 return False
658 # Removed unused _apply_model_transformations (legacy API)
660 def _apply_renaming(
661 self, model: ProjectModel, rename_config: Dict[str, Any], target_files: Set[str]
662 ) -> ProjectModel:
663 """"""
664 self.logger.debug(
665 "Applying renaming transformations to %d files", len(target_files)
666 )
668 for file_path in target_files:
669 if file_path in model.files:
670 file_model = model.files[file_path]
671 self.logger.debug("Applying renaming to file: %s", file_path)
672 self._apply_file_level_renaming(file_model, rename_config)
674 if "files" in rename_config:
675 model = self._rename_files(model, rename_config["files"], target_files)
677 return model
679 def _apply_file_level_renaming(
680 self, file_model: FileModel, rename_config: Dict[str, Any]
681 ) -> None:
682 """"""
683 rename_operations = [
684 ("typedef", self._rename_typedefs),
685 ("functions", self._rename_functions),
686 ("macros", self._rename_macros),
687 ("globals", self._rename_globals),
688 ("includes", self._rename_includes),
689 ("structs", self._rename_structs),
690 ("enums", self._rename_enums),
691 ("unions", self._rename_unions),
692 ]
694 for config_key, rename_method in rename_operations:
695 if config_key in rename_config:
696 rename_method(file_model, rename_config[config_key])
698 def _cleanup_type_references(
699 self, model: ProjectModel, removed_typedef_patterns: List[str], target_files: Set[str]
700 ) -> None:
701 """"""
702 self.logger.debug("Starting type reference cleanup with patterns: %s, target_files: %s",
703 removed_typedef_patterns, list(target_files))
705 if not removed_typedef_patterns:
706 self.logger.debug("No typedef patterns to clean up")
707 return
709 compiled_patterns = self._compile_patterns(removed_typedef_patterns)
710 if not compiled_patterns:
711 self.logger.debug("No valid compiled patterns")
712 return
714 removed_types = set()
716 for file_path in target_files:
717 if file_path in model.files:
718 file_model = model.files[file_path]
720 for alias_name in list(file_model.aliases.keys()):
721 if self._matches_any_pattern(alias_name, compiled_patterns):
722 removed_types.add(alias_name)
723 self.logger.debug("Found removed typedef: %s in file %s", alias_name, file_path)
725 self.logger.debug("Total removed types identified: %s", list(removed_types))
727 cleaned_count = 0
728 for file_path, file_model in model.files.items():
729 file_cleaned = 0
731 for func in file_model.functions:
732 if func.return_type and self._contains_removed_type(func.return_type, removed_types):
733 old_type = func.return_type
734 func.return_type = self._remove_type_references(func.return_type, removed_types)
735 if func.return_type != old_type:
736 file_cleaned += 1
737 self.logger.debug(
738 "Cleaned return type '%s' -> '%s' in function %s",
739 old_type, func.return_type, func.name
740 )
742 for param in func.parameters:
743 if param.type and self._contains_removed_type(param.type, removed_types):
744 old_type = param.type
745 param.type = self._remove_type_references(param.type, removed_types)
746 if param.type != old_type:
747 file_cleaned += 1
748 self.logger.debug(
749 "Cleaned parameter type '%s' -> '%s' for parameter %s",
750 old_type, param.type, param.name
751 )
753 for global_var in file_model.globals:
754 if global_var.type and self._contains_removed_type(global_var.type, removed_types):
755 old_type = global_var.type
756 global_var.type = self._remove_type_references(global_var.type, removed_types)
757 if global_var.type != old_type:
758 file_cleaned += 1
759 self.logger.debug(
760 "Cleaned global variable type '%s' -> '%s' for %s",
761 old_type, global_var.type, global_var.name
762 )
764 for struct in file_model.structs.values():
765 for field in struct.fields:
766 if field.type and self._contains_removed_type(field.type, removed_types):
767 old_type = field.type
768 field.type = self._remove_type_references(field.type, removed_types)
769 if field.type != old_type:
770 file_cleaned += 1
771 self.logger.debug(
772 "Cleaned struct field type '%s' -> '%s' for %s.%s",
773 old_type, field.type, struct.name, field.name
774 )
776 cleaned_count += file_cleaned
778 if cleaned_count > 0:
779 self.logger.info(
780 "Cleaned %d type references to removed typedefs: %s",
781 cleaned_count, list(removed_types)
782 )
784 def _contains_removed_type(self, type_str: str, removed_types: Set[str]) -> bool:
785 """"""
786 if not type_str or not removed_types:
787 return False
789 for removed_type in removed_types:
790 if removed_type in type_str:
791 return True
792 return False
794 def _remove_type_references(self, type_str: str, removed_types: Set[str]) -> str:
795 """"""
796 if not type_str or not removed_types:
797 return type_str
799 cleaned_type = type_str
800 for removed_type in removed_types:
801 if removed_type in cleaned_type:
802 cleaned_type = cleaned_type.replace(removed_type, "void")
804 cleaned_type = " ".join(cleaned_type.split())
805 return cleaned_type
807 def _cleanup_type_references_by_names(
808 self, model: ProjectModel, removed_typedef_names: Set[str]
809 ) -> None:
810 """"""
811 if not removed_typedef_names:
812 self.logger.debug("No removed typedef names provided")
813 return
815 self.logger.debug("Cleaning type references for removed typedefs: %s", list(removed_typedef_names))
817 cleaned_count = 0
818 for file_path, file_model in model.files.items():
819 file_cleaned = 0
821 for func in file_model.functions:
822 if func.return_type and self._contains_removed_type(func.return_type, removed_typedef_names):
823 old_type = func.return_type
824 func.return_type = self._remove_type_references(func.return_type, removed_typedef_names)
825 if func.return_type != old_type:
826 file_cleaned += 1
827 self.logger.debug(
828 "Cleaned return type '%s' -> '%s' in function %s",
829 old_type, func.return_type, func.name
830 )
832 for param in func.parameters:
833 if param.type and self._contains_removed_type(param.type, removed_typedef_names):
834 old_type = param.type
835 param.type = self._remove_type_references(param.type, removed_typedef_names)
836 if param.type != old_type:
837 file_cleaned += 1
838 self.logger.debug(
839 "Cleaned parameter type '%s' -> '%s' for parameter %s",
840 old_type, param.type, param.name
841 )
843 for global_var in file_model.globals:
844 if global_var.type and self._contains_removed_type(global_var.type, removed_typedef_names):
845 old_type = global_var.type
846 global_var.type = self._remove_type_references(global_var.type, removed_typedef_names)
847 if global_var.type != old_type:
848 file_cleaned += 1
849 self.logger.debug(
850 "Cleaned global variable type '%s' -> '%s' for %s",
851 old_type, global_var.type, global_var.name
852 )
854 for struct in file_model.structs.values():
855 for field in struct.fields:
856 if field.type and self._contains_removed_type(field.type, removed_typedef_names):
857 old_type = field.type
858 field.type = self._remove_type_references(field.type, removed_typedef_names)
859 if field.type != old_type:
860 file_cleaned += 1
861 self.logger.debug(
862 "Cleaned struct field type '%s' -> '%s' for %s.%s",
863 old_type, field.type, struct.name, field.name
864 )
866 cleaned_count += file_cleaned
867 if file_cleaned > 0:
868 self.logger.debug("Cleaned %d type references in file %s", file_cleaned, file_path)
870 if cleaned_count > 0:
871 self.logger.info(
872 "Cleaned %d type references to removed typedefs: %s",
873 cleaned_count, list(removed_typedef_names)
874 )
875 else:
876 self.logger.debug("No type references found to clean up")
878 def _update_type_references_for_renames(self, file_model: FileModel, typedef_renames: Dict[str, str]) -> None:
879 """"""
880 updated_count = 0
882 for func in file_model.functions:
883 if func.return_type:
884 old_type = func.return_type
885 new_type = self._update_type_string_for_renames(func.return_type, typedef_renames)
886 if new_type != old_type:
887 func.return_type = new_type
888 updated_count += 1
889 self.logger.debug(
890 "Updated return type '%s' -> '%s' in function %s",
891 old_type, new_type, func.name
892 )
894 for param in func.parameters:
895 if param.type:
896 old_type = param.type
897 new_type = self._update_type_string_for_renames(param.type, typedef_renames)
898 if new_type != old_type:
899 param.type = new_type
900 updated_count += 1
901 self.logger.debug(
902 "Updated parameter type '%s' -> '%s' for parameter %s in function %s",
903 old_type, new_type, param.name, func.name
904 )
906 for global_var in file_model.globals:
907 if global_var.type:
908 old_type = global_var.type
909 new_type = self._update_type_string_for_renames(global_var.type, typedef_renames)
910 if new_type != old_type:
911 global_var.type = new_type
912 updated_count += 1
913 self.logger.debug(
914 "Updated global variable type '%s' -> '%s' for %s",
915 old_type, new_type, global_var.name
916 )
918 for struct in file_model.structs.values():
919 for field in struct.fields:
920 if field.type:
921 old_type = field.type
922 new_type = self._update_type_string_for_renames(field.type, typedef_renames)
923 if new_type != old_type:
924 field.type = new_type
925 updated_count += 1
926 self.logger.debug(
927 "Updated struct field type '%s' -> '%s' for %s.%s",
928 old_type, new_type, struct.name, field.name
929 )
931 for union in file_model.unions.values():
932 for field in union.fields:
933 if field.type:
934 old_type = field.type
935 new_type = self._update_type_string_for_renames(field.type, typedef_renames)
936 if new_type != old_type:
937 field.type = new_type
938 updated_count += 1
939 self.logger.debug(
940 "Updated union field type '%s' -> '%s' for %s.%s",
941 old_type, new_type, union.name, field.name
942 )
944 if updated_count > 0:
945 self.logger.info(
946 "Updated %d type references for renamed typedefs in %s: %s",
947 updated_count, file_model.name, typedef_renames
948 )
950 def _update_type_string_for_renames(self, type_str: str, typedef_renames: Dict[str, str]) -> str:
951 """"""
952 if not type_str or not typedef_renames:
953 return type_str
955 updated_type = type_str
956 for old_name, new_name in typedef_renames.items():
957 pattern = r'\b' + re.escape(old_name) + r'\b'
958 updated_type = re.sub(pattern, new_name, updated_type)
960 return updated_type
962 def _rename_dict_elements(
963 self,
964 elements_dict: Dict[str, Any],
965 patterns_map: Dict[str, str],
966 create_renamed_element: Callable[[str, Any], Any],
967 element_type: str,
968 file_name: str
969 ) -> Dict[str, Any]:
970 """"""
971 original_count = len(elements_dict)
972 seen_names = set()
973 deduplicated_elements = {}
975 for name, element in elements_dict.items():
976 new_name = self._apply_rename_patterns(name, patterns_map)
978 if new_name in seen_names:
979 self.logger.debug(
980 "Deduplicating %s: removing duplicate '%s' (renamed from '%s')",
981 element_type, new_name, name
982 )
983 continue
985 seen_names.add(new_name)
987 updated_element = element if new_name == name else create_renamed_element(new_name, element)
988 deduplicated_elements[new_name] = updated_element
990 removed_count = original_count - len(deduplicated_elements)
991 if removed_count > 0:
992 self.logger.info(
993 "Renamed %ss in %s, removed %d duplicates", element_type, file_name, removed_count
994 )
996 return deduplicated_elements
998 def _rename_list_elements(
999 self,
1000 elements_list: List[Any],
1001 patterns_map: Dict[str, str],
1002 get_element_name: Callable[[Any], str],
1003 create_renamed_element: Callable[[str, Any], Any],
1004 element_type: str,
1005 file_name: str
1006 ) -> List[Any]:
1007 """"""
1008 original_count = len(elements_list)
1009 seen_names = set()
1010 deduplicated_elements = []
1012 for element in elements_list:
1013 name = get_element_name(element)
1014 new_name = self._apply_rename_patterns(name, patterns_map)
1016 if new_name in seen_names:
1017 self.logger.debug(
1018 "Deduplicating %s: removing duplicate '%s' (renamed from '%s')",
1019 element_type, new_name, name
1020 )
1021 continue
1023 seen_names.add(new_name)
1025 updated_element = element if new_name == name else create_renamed_element(new_name, element)
1026 deduplicated_elements.append(updated_element)
1028 removed_count = original_count - len(deduplicated_elements)
1029 if removed_count > 0:
1030 self.logger.info(
1031 "Renamed %ss in %s, removed %d duplicates", element_type, file_name, removed_count
1032 )
1034 return deduplicated_elements
1036 def _apply_rename_patterns(self, original_name: str, patterns_map: Dict[str, str]) -> str:
1037 """"""
1038 for pattern, replacement in patterns_map.items():
1039 try:
1040 new_name = re.sub(pattern, replacement, original_name)
1041 if new_name != original_name:
1042 self.logger.debug(
1043 "Renamed '%s' to '%s' using pattern '%s'",
1044 original_name, new_name, pattern
1045 )
1046 return new_name
1047 except re.error as e:
1048 self.logger.warning(
1049 "Invalid regex pattern '%s': %s", pattern, e
1050 )
1051 continue
1053 return original_name
1055 def _rename_typedefs(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1056 """"""
1057 if not patterns_map:
1058 return
1060 typedef_renames = {}
1062 def create_renamed_alias(name: str, alias: Alias) -> Alias:
1063 return Alias(name, alias.original_type, alias.uses)
1065 for old_name in file_model.aliases:
1066 new_name = self._apply_rename_patterns(old_name, patterns_map)
1067 if new_name != old_name:
1068 typedef_renames[old_name] = new_name
1070 file_model.aliases = self._rename_dict_elements(
1071 file_model.aliases, patterns_map, create_renamed_alias, "typedef", file_model.name
1072 )
1074 if typedef_renames:
1075 self._update_type_references_for_renames(file_model, typedef_renames)
1077 def _rename_functions(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1078 """"""
1079 if not patterns_map:
1080 return
1082 def get_function_name(func: Function) -> str:
1083 return func.name
1085 def create_renamed_function(name: str, func: Function) -> Function:
1086 return Function(name, func.return_type, func.parameters, func.is_static, func.is_declaration, func.is_inline)
1088 file_model.functions = self._rename_list_elements(
1089 file_model.functions, patterns_map, get_function_name,
1090 create_renamed_function, "function", file_model.name
1091 )
1093 def _rename_macros(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1094 """"""
1095 if not patterns_map:
1096 return
1098 def get_macro_name(macro: str) -> str:
1099 import re
1100 if macro.startswith("#define "):
1101 match = re.search(r"#define\s+([A-Za-z_][A-Za-z0-9_]*)", macro)
1102 if match:
1103 return match.group(1)
1104 return macro
1106 def create_renamed_macro(name: str, macro: str) -> str:
1107 import re
1108 if macro.startswith("#define "):
1109 pattern = r"(#define\s+)([A-Za-z_][A-Za-z0-9_]*)(\s*\([^)]*\))?(.*)?"
1110 match = re.match(pattern, macro)
1111 if match:
1112 define_part = match.group(1)
1113 params = match.group(3) or ""
1114 rest = match.group(4) or ""
1115 return f"{define_part}{name}{params}{rest}"
1116 return macro
1118 file_model.macros = self._rename_list_elements(
1119 file_model.macros, patterns_map, get_macro_name,
1120 create_renamed_macro, "macro", file_model.name
1121 )
1123 def _rename_globals(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1124 """"""
1125 if not patterns_map:
1126 return
1128 def get_global_name(global_var: Field) -> str:
1129 return global_var.name
1131 def create_renamed_global(name: str, global_var: Field) -> Field:
1132 return Field(name, global_var.type)
1134 file_model.globals = self._rename_list_elements(
1135 file_model.globals, patterns_map, get_global_name,
1136 create_renamed_global, "global", file_model.name
1137 )
1139 def _rename_includes(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1140 """"""
1141 if not patterns_map:
1142 return
1144 file_model.includes = self._rename_set_elements(
1145 file_model.includes, patterns_map, "include", file_model.name
1146 )
1148 file_model.include_relations = self._rename_include_relations(
1149 file_model.include_relations, patterns_map
1150 )
1152 def _rename_set_elements(
1153 self,
1154 elements_set: Set[str],
1155 patterns_map: Dict[str, str],
1156 element_type: str,
1157 file_name: str
1158 ) -> Set[str]:
1159 """"""
1160 original_count = len(elements_set)
1161 seen_names = set()
1162 deduplicated_elements = set()
1164 for element in elements_set:
1165 new_name = self._apply_rename_patterns(element, patterns_map)
1167 if new_name in seen_names:
1168 self.logger.debug(
1169 "Deduplicating %s: removing duplicate '%s' (renamed from '%s')",
1170 element_type, new_name, element
1171 )
1172 continue
1174 seen_names.add(new_name)
1175 deduplicated_elements.add(new_name)
1177 removed_count = original_count - len(deduplicated_elements)
1178 if removed_count > 0:
1179 self.logger.info(
1180 "Renamed %ss in %s, removed %d duplicates", element_type, file_name, removed_count
1181 )
1183 return deduplicated_elements
1185 def _rename_include_relations(
1186 self, relations: List[IncludeRelation], patterns_map: Dict[str, str]
1187 ) -> List[IncludeRelation]:
1188 """"""
1189 updated_relations = []
1190 for relation in relations:
1191 new_included_file = self._apply_rename_patterns(relation.included_file, patterns_map)
1192 updated_relation = IncludeRelation(
1193 relation.source_file,
1194 new_included_file,
1195 relation.depth
1196 )
1197 updated_relations.append(updated_relation)
1198 return updated_relations
1200 def _rename_structs(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1201 """"""
1202 if not patterns_map:
1203 return
1205 def create_renamed_struct(name: str, struct: Struct) -> Struct:
1206 return Struct(name, struct.fields)
1208 file_model.structs = self._rename_dict_elements(
1209 file_model.structs, patterns_map, create_renamed_struct, "struct", file_model.name
1210 )
1212 def _rename_enums(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1213 """"""
1214 if not patterns_map:
1215 return
1217 def create_renamed_enum(name: str, enum: Enum) -> Enum:
1218 return Enum(name, enum.values)
1220 file_model.enums = self._rename_dict_elements(
1221 file_model.enums, patterns_map, create_renamed_enum, "enum", file_model.name
1222 )
1224 def _rename_unions(self, file_model: FileModel, patterns_map: Dict[str, str]) -> None:
1225 """"""
1226 if not patterns_map:
1227 return
1229 def create_renamed_union(name: str, union: Union) -> Union:
1230 return Union(name, union.fields)
1232 file_model.unions = self._rename_dict_elements(
1233 file_model.unions, patterns_map, create_renamed_union, "union", file_model.name
1234 )
1236 def _rename_files(self, model: ProjectModel, patterns_map: Dict[str, str], target_files: Set[str]) -> ProjectModel:
1237 """"""
1238 if not patterns_map:
1239 return model
1241 updated_files = {}
1242 file_rename_map: Dict[str, str] = {}
1244 for file_path, file_model in model.files.items():
1245 if file_path in target_files:
1246 new_file_path = self._apply_rename_patterns(file_path, patterns_map)
1248 if new_file_path != file_path:
1249 file_model.name = new_file_path
1250 file_rename_map[Path(file_path).name] = Path(new_file_path).name
1251 self.logger.debug("Renamed file: %s -> %s", file_path, new_file_path)
1253 updated_files[new_file_path] = file_model
1254 else:
1255 updated_files[file_path] = file_model
1257 model.files = updated_files
1259 if file_rename_map:
1260 for fm in model.files.values():
1261 if fm.includes:
1262 new_includes: Set[str] = set()
1263 for inc in fm.includes:
1264 inc_new = file_rename_map.get(inc, self._apply_rename_patterns(inc, patterns_map))
1265 new_includes.add(inc_new)
1266 fm.includes = new_includes
1268 if fm.include_relations:
1269 for rel in fm.include_relations:
1270 src_new = file_rename_map.get(rel.source_file, self._apply_rename_patterns(rel.source_file, patterns_map))
1271 inc_new = file_rename_map.get(rel.included_file, self._apply_rename_patterns(rel.included_file, patterns_map))
1272 rel.source_file = src_new
1273 rel.included_file = inc_new
1275 return model
1277 def _apply_additions(
1278 self, model: ProjectModel, add_config: Dict[str, Any], target_files: Set[str]
1279 ) -> ProjectModel:
1280 """"""
1281 self.logger.debug(
1282 "Applying addition transformations to %d files", len(target_files)
1283 )
1285 for file_path in target_files:
1286 if file_path in model.files:
1287 self.logger.debug("Applying additions to file: %s", file_path)
1289 return model
1291 def _apply_removals(
1292 self, model: ProjectModel, remove_config: Dict[str, Any], target_files: Set[str]
1293 ) -> ProjectModel:
1294 """"""
1295 self.logger.debug(
1296 "Applying removal transformations to %d files", len(target_files)
1297 )
1299 for file_path in target_files:
1300 if file_path in model.files:
1301 file_model = model.files[file_path]
1302 self.logger.debug("Applying removals to file: %s", file_path)
1303 self._apply_file_level_removals(file_model, remove_config)
1305 return model
1307 def _apply_file_level_removals(
1308 self, file_model: FileModel, remove_config: Dict[str, Any]
1309 ) -> None:
1310 """"""
1311 removal_operations = [
1312 ("typedef", self._remove_typedefs),
1313 ("functions", self._remove_functions),
1314 ("macros", self._remove_macros),
1315 ("globals", self._remove_globals),
1316 ("includes", self._remove_includes),
1317 ("structs", self._remove_structs),
1318 ("enums", self._remove_enums),
1319 ("unions", self._remove_unions),
1320 ]
1322 for config_key, removal_method in removal_operations:
1323 if config_key in remove_config:
1324 removal_method(file_model, remove_config[config_key])
1326 def _remove_dict_elements(
1327 self,
1328 elements_dict: Dict[str, Any],
1329 patterns: List[str],
1330 element_type: str,
1331 file_name: str
1332 ) -> Dict[str, Any]:
1333 """"""
1334 if not patterns:
1335 return elements_dict
1337 original_count = len(elements_dict)
1338 compiled_patterns = self._compile_patterns(patterns)
1340 filtered_elements = {}
1341 for name, element in elements_dict.items():
1342 if not self._matches_any_pattern(name, compiled_patterns):
1343 filtered_elements[name] = element
1344 else:
1345 self.logger.debug("Removed %s: %s", element_type, name)
1347 removed_count = original_count - len(filtered_elements)
1348 if removed_count > 0:
1349 self.logger.info(
1350 "Removed %d %ss from %s", removed_count, element_type, file_name
1351 )
1353 return filtered_elements
1355 def _remove_list_elements(
1356 self,
1357 elements_list: List[Any],
1358 patterns: List[str],
1359 get_element_name: Callable[[Any], str],
1360 element_type: str,
1361 file_name: str
1362 ) -> List[Any]:
1363 """"""
1364 if not patterns:
1365 return elements_list
1367 original_count = len(elements_list)
1368 compiled_patterns = self._compile_patterns(patterns)
1370 filtered_elements = []
1371 for element in elements_list:
1372 name = get_element_name(element)
1373 if not self._matches_any_pattern(name, compiled_patterns):
1374 filtered_elements.append(element)
1375 else:
1376 self.logger.debug("Removed %s: %s", element_type, name)
1378 removed_count = original_count - len(filtered_elements)
1379 if removed_count > 0:
1380 self.logger.info(
1381 "Removed %d %ss from %s", removed_count, element_type, file_name
1382 )
1384 return filtered_elements
1386 def _remove_typedefs(self, file_model: FileModel, patterns: List[str]) -> None:
1387 """"""
1388 file_model.aliases = self._remove_dict_elements(
1389 file_model.aliases, patterns, "typedef", file_model.name
1390 )
1392 def _remove_functions(self, file_model: FileModel, patterns: List[str]) -> None:
1393 """"""
1394 def get_function_name(func: Function) -> str:
1395 return func.name
1397 file_model.functions = self._remove_list_elements(
1398 file_model.functions, patterns, get_function_name, "function", file_model.name
1399 )
1401 def _remove_macros(self, file_model: FileModel, patterns: List[str]) -> None:
1402 """"""
1403 def get_macro_name(macro: str) -> str:
1404 import re
1405 if macro.startswith("#define "):
1406 match = re.search(r"#define\s+([A-Za-z_][A-Za-z0-9_]*)", macro)
1407 if match:
1408 return match.group(1)
1409 return macro
1411 file_model.macros = self._remove_list_elements(
1412 file_model.macros, patterns, get_macro_name, "macro", file_model.name
1413 )
1415 def _remove_globals(self, file_model: FileModel, patterns: List[str]) -> None:
1416 """"""
1417 def get_global_name(global_var: Field) -> str:
1418 return global_var.name
1420 file_model.globals = self._remove_list_elements(
1421 file_model.globals, patterns, get_global_name, "global variable", file_model.name
1422 )
1424 def _remove_includes(self, file_model: FileModel, patterns: List[str]) -> None:
1425 """"""
1426 if not patterns:
1427 return
1429 original_count = len(file_model.includes)
1430 compiled_patterns = self._compile_patterns(patterns)
1432 filtered_includes = set()
1433 for include in file_model.includes:
1434 if not self._matches_any_pattern(include, compiled_patterns):
1435 filtered_includes.add(include)
1436 else:
1437 self.logger.debug("Removed include: %s", include)
1439 file_model.includes = filtered_includes
1440 removed_count = original_count - len(file_model.includes)
1442 if removed_count > 0:
1443 self._remove_matching_include_relations(file_model, compiled_patterns, removed_count)
1445 def _remove_matching_include_relations(
1446 self, file_model: FileModel, compiled_patterns: List[Pattern[str]], removed_includes_count: int
1447 ) -> None:
1448 """"""
1449 original_relations_count = len(file_model.include_relations)
1450 filtered_relations = []
1452 for relation in file_model.include_relations:
1453 if not self._matches_any_pattern(relation.included_file, compiled_patterns):
1454 filtered_relations.append(relation)
1455 else:
1456 self.logger.debug("Removed include relation: %s -> %s",
1457 relation.source_file, relation.included_file)
1459 file_model.include_relations = filtered_relations
1460 removed_relations_count = original_relations_count - len(file_model.include_relations)
1462 self.logger.info(
1463 "Removed %d includes and %d include relations from %s",
1464 removed_includes_count, removed_relations_count, file_model.name
1465 )
1467 def _remove_structs(self, file_model: FileModel, patterns: List[str]) -> None:
1468 """"""
1469 file_model.structs = self._remove_dict_elements(
1470 file_model.structs, patterns, "struct", file_model.name
1471 )
1473 def _remove_enums(self, file_model: FileModel, patterns: List[str]) -> None:
1474 """"""
1475 file_model.enums = self._remove_dict_elements(
1476 file_model.enums, patterns, "enum", file_model.name
1477 )
1479 def _remove_unions(self, file_model: FileModel, patterns: List[str]) -> None:
1480 """"""
1481 file_model.unions = self._remove_dict_elements(
1482 file_model.unions, patterns, "union", file_model.name
1483 )
1485 def _should_include_file(
1486 self,
1487 file_path: str,
1488 include_patterns: List[Pattern[str]],
1489 exclude_patterns: List[Pattern[str]],
1490 ) -> bool:
1491 """"""
1492 if include_patterns:
1493 if not any(pattern.search(file_path) for pattern in include_patterns):
1494 return False
1496 if exclude_patterns:
1497 if any(pattern.search(file_path) for pattern in exclude_patterns):
1498 return False
1500 return True
1502 def _compile_patterns(self, patterns: List[str]) -> List[Pattern[str]]:
1503 """"""
1504 compiled_patterns: List[Pattern[str]] = []
1505 for pattern in patterns:
1506 try:
1507 compiled_patterns.append(re.compile(pattern))
1508 except re.error as e:
1509 self.logger.warning("Invalid regex pattern '%s': %s", pattern, e)
1510 return compiled_patterns
1512 # Removed unused _filter_dict (not used in current pipeline)
1514 # Removed unused _filter_list (not used in current pipeline)
1516 # Removed unused _dict_to_file_model (no callers in current code)
1518 def _save_model(self, model: ProjectModel, output_file: str) -> None:
1519 """"""
1520 try:
1521 model.save(output_file)
1522 self.logger.debug("Model saved to: %s", output_file)
1523 except Exception as e:
1524 raise ValueError(f"Failed to save model to {output_file}: {e}") from e