|
|
import os |
|
|
import tempfile |
|
|
import shutil |
|
|
import zipfile |
|
|
import tarfile |
|
|
from pathlib import Path |
|
|
from typing import Dict, List, Optional, Tuple |
|
|
|
|
|
|
|
|
def _extract_zip(path: Path) -> str: |
|
|
temp_dir = tempfile.mkdtemp() |
|
|
with zipfile.ZipFile(path, 'r') as zip_ref: |
|
|
zip_ref.extractall(temp_dir) |
|
|
return temp_dir |
|
|
|
|
|
|
|
|
def _extract_tgz(path: Path) -> str: |
|
|
temp_dir = tempfile.mkdtemp() |
|
|
with tarfile.open(path, 'r:gz') as tar_ref: |
|
|
tar_ref.extractall(temp_dir) |
|
|
return temp_dir |
|
|
|
|
|
|
|
|
def prepare_input_path(path: str) -> str: |
|
|
"""Handles different input types: directories, files, zip or tgz archives.""" |
|
|
path_obj = Path(path) |
|
|
if path_obj.is_dir(): |
|
|
return str(path_obj) |
|
|
|
|
|
if path_obj.suffix == '.zip': |
|
|
return _extract_zip(path_obj) |
|
|
elif path_obj.suffix in {'.tgz', '.tar.gz'}: |
|
|
return _extract_tgz(path_obj) |
|
|
elif path_obj.is_file(): |
|
|
|
|
|
temp_dir = tempfile.mkdtemp() |
|
|
shutil.copy(path_obj, temp_dir) |
|
|
return temp_dir |
|
|
else: |
|
|
raise ValueError(f"Unsupported path type or extension: {path}") |
|
|
|
|
|
|
|
|
def file_path_to_module_path(file_path: str) -> str: |
|
|
""" |
|
|
Convert a file path to a module path by replacing path separators with dots |
|
|
and removing the file extension. |
|
|
|
|
|
Examples: |
|
|
path/to/repo/python_script.py -> path.to.repo.python_script |
|
|
src/utils/helper.py -> src.utils.helper |
|
|
module.py -> module |
|
|
|
|
|
Args: |
|
|
file_path: File path string |
|
|
|
|
|
Returns: |
|
|
Module path with dots instead of slashes |
|
|
""" |
|
|
|
|
|
normalized = file_path.replace('\\', '/').replace(os.sep, '/') |
|
|
|
|
|
|
|
|
without_ext = os.path.splitext(normalized)[0] |
|
|
|
|
|
|
|
|
module_path = without_ext.replace('/', '.') |
|
|
|
|
|
return module_path |
|
|
|
|
|
|
|
|
def generate_entity_aliases(entity_name: str, file_path: str) -> list: |
|
|
""" |
|
|
Generate all possible aliases for an entity based on its name and file path. |
|
|
|
|
|
For example, if a file 'path/to/repo/python_script.py' defines 'Class_1', |
|
|
the aliases would be: |
|
|
- Class_1 (simple name) |
|
|
- path.to.repo.python_script.Class_1 (fully qualified from file path) |
|
|
|
|
|
For C++ namespaced entities like 'math::Calculator': |
|
|
- math::Calculator (fully qualified name) |
|
|
- Calculator (unqualified name, for use with 'using namespace') |
|
|
- math.calculator.math::Calculator (module-based fully qualified) |
|
|
|
|
|
For temporary paths like '.tmp.tmptqky4yk4..pyinstaller.run_astropy_tests.pos': |
|
|
- pos (simple name) |
|
|
- .run_astropy_tests.pos (progressive path removal) |
|
|
- pyinstaller.run_astropy_tests.pos (further removal) |
|
|
- .tmp.tmptqky4yk4..pyinstaller.run_astropy_tests.pos (full path) |
|
|
|
|
|
Args: |
|
|
entity_name: The name of the entity (e.g., 'Class_1', 'my_function', 'math::Calculator') |
|
|
file_path: The file path where the entity is defined |
|
|
|
|
|
Returns: |
|
|
List of alias strings |
|
|
""" |
|
|
aliases = [] |
|
|
|
|
|
|
|
|
aliases.append(entity_name) |
|
|
|
|
|
|
|
|
if '::' in entity_name: |
|
|
|
|
|
unqualified_name = entity_name.split('::')[-1] |
|
|
if unqualified_name != entity_name: |
|
|
aliases.append(unqualified_name) |
|
|
|
|
|
|
|
|
module_path = file_path_to_module_path(file_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fully_qualified = f"{module_path}.{entity_name}" |
|
|
|
|
|
|
|
|
|
|
|
components = module_path.split('.') |
|
|
|
|
|
|
|
|
def is_temp_component(component: str) -> bool: |
|
|
"""Check if a path component looks like a temporary directory.""" |
|
|
if not component: |
|
|
return True |
|
|
|
|
|
if component.startswith('tmp') and len(component) > 3: |
|
|
return True |
|
|
if component.startswith('.tmp'): |
|
|
return True |
|
|
|
|
|
if len(component) > 8 and component.replace('_', '').replace('-', '').isalnum(): |
|
|
|
|
|
if sum(c.islower() for c in component) > len(component) / 2: |
|
|
if sum(c.isdigit() for c in component) > 2: |
|
|
return True |
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
clean_components = [] |
|
|
for component in components: |
|
|
if not is_temp_component(component): |
|
|
clean_components.append(component) |
|
|
|
|
|
|
|
|
if clean_components: |
|
|
for i in range(1, len(clean_components) + 1): |
|
|
|
|
|
partial_path = '.'.join(clean_components[-i:]) |
|
|
partial_alias = f".{partial_path}.{entity_name}" |
|
|
if partial_alias != entity_name and partial_alias not in aliases: |
|
|
aliases.append(partial_alias) |
|
|
|
|
|
|
|
|
if i == len(clean_components): |
|
|
no_dot_alias = f"{partial_path}.{entity_name}" |
|
|
if no_dot_alias != entity_name and no_dot_alias not in aliases: |
|
|
aliases.append(no_dot_alias) |
|
|
|
|
|
|
|
|
if fully_qualified != entity_name and fully_qualified not in aliases: |
|
|
aliases.append(fully_qualified) |
|
|
|
|
|
return aliases |
|
|
|
|
|
|
|
|
def normalize_include_path(include_path: str) -> str: |
|
|
""" |
|
|
Normalize an include path from #include directive to a module-like path. |
|
|
|
|
|
Examples: |
|
|
<vector> -> vector |
|
|
<iostream> -> iostream |
|
|
"myheader.h" -> myheader |
|
|
"utils/helper.h" -> utils.helper |
|
|
<boost/algorithm/string.hpp> -> boost.algorithm.string |
|
|
|
|
|
Args: |
|
|
include_path: The include path from #include directive |
|
|
|
|
|
Returns: |
|
|
Normalized module-like path |
|
|
""" |
|
|
|
|
|
path = include_path.strip('<>"') |
|
|
|
|
|
|
|
|
module_path = file_path_to_module_path(path) |
|
|
|
|
|
return module_path |
|
|
|
|
|
|
|
|
def build_entity_alias_map(entities: Dict[str, Dict]) -> Dict[str, str]: |
|
|
""" |
|
|
Build a mapping from all entity aliases to their canonical entity names. |
|
|
This allows quick lookup when matching called entities to their definitions. |
|
|
|
|
|
Args: |
|
|
entities: Dictionary of entity info keyed by canonical entity name |
|
|
|
|
|
Returns: |
|
|
Dictionary mapping alias -> canonical entity name |
|
|
""" |
|
|
alias_map = {} |
|
|
|
|
|
for entity_name, info in entities.items(): |
|
|
|
|
|
alias_map[entity_name] = entity_name |
|
|
|
|
|
|
|
|
aliases = info.get('aliases', []) |
|
|
for alias in aliases: |
|
|
if alias and alias not in alias_map: |
|
|
alias_map[alias] = entity_name |
|
|
|
|
|
return alias_map |
|
|
|
|
|
|
|
|
def resolve_entity_call(called_name: str, alias_map: Dict[str, str], |
|
|
imports: List[str] = None) -> Optional[str]: |
|
|
""" |
|
|
Resolve a called entity name to its canonical definition using aliases. |
|
|
|
|
|
This handles cases like: |
|
|
- Direct call: 'MyClass' -> 'MyClass' |
|
|
- Qualified call: 'module.MyClass' -> 'MyClass' (if alias exists) |
|
|
- Imported call: 'helper' -> 'utils.helper' (if imported) |
|
|
- Simple name to qualified: 'Calculator' -> 'utils::Calculator' |
|
|
|
|
|
Args: |
|
|
called_name: The name of the called entity |
|
|
alias_map: Mapping from aliases to canonical entity names |
|
|
imports: List of import paths (optional, for context) |
|
|
|
|
|
Returns: |
|
|
Canonical entity name if found, None otherwise |
|
|
""" |
|
|
|
|
|
if not called_name or not called_name.strip(): |
|
|
return None |
|
|
|
|
|
|
|
|
if called_name in alias_map: |
|
|
return alias_map[called_name] |
|
|
|
|
|
|
|
|
if imports: |
|
|
for import_path in imports: |
|
|
|
|
|
qualified = f"{import_path}.{called_name}" |
|
|
if qualified in alias_map: |
|
|
return alias_map[qualified] |
|
|
|
|
|
|
|
|
qualified_cpp = f"{import_path}::{called_name}" |
|
|
if qualified_cpp in alias_map: |
|
|
return alias_map[qualified_cpp] |
|
|
|
|
|
|
|
|
|
|
|
simple_name = extract_simple_name(called_name) |
|
|
candidates = [] |
|
|
|
|
|
for alias, canonical in alias_map.items(): |
|
|
alias_simple = extract_simple_name(alias) |
|
|
|
|
|
if alias_simple == simple_name: |
|
|
candidates.append(canonical) |
|
|
|
|
|
|
|
|
if len(candidates) == 1: |
|
|
return candidates[0] |
|
|
|
|
|
|
|
|
|
|
|
if len(candidates) > 1: |
|
|
return min(candidates, key=lambda x: len(x)) |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
def extract_simple_name(qualified_name: str) -> str: |
|
|
""" |
|
|
Extract the simple name from a qualified name. |
|
|
|
|
|
Examples: |
|
|
'namespace::MyClass' -> 'MyClass' |
|
|
'module.MyClass' -> 'MyClass' |
|
|
'MyClass' -> 'MyClass' |
|
|
|
|
|
Args: |
|
|
qualified_name: Fully or partially qualified name |
|
|
|
|
|
Returns: |
|
|
Simple name without namespace/module prefix |
|
|
""" |
|
|
|
|
|
if '::' in qualified_name: |
|
|
return qualified_name.split('::')[-1] |
|
|
|
|
|
|
|
|
if '.' in qualified_name: |
|
|
return qualified_name.split('.')[-1] |
|
|
|
|
|
return qualified_name |
|
|
|
|
|
|