-
Notifications
You must be signed in to change notification settings - Fork 4
Perf/symbol table cache invalidation #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,13 @@ | |
| Maps classes, functions, variables, and their relationships across the codebase. | ||
| """ | ||
|
|
||
| import hashlib | ||
| import json | ||
| import logging | ||
| from dataclasses import dataclass, field | ||
| from enum import Enum | ||
| from pathlib import Path | ||
| from typing import Any, Dict, List, Optional | ||
| from typing import Any, Dict, List, Optional, Set | ||
|
|
||
| from refactron.core.inference import InferenceEngine | ||
|
|
||
|
|
@@ -61,30 +62,59 @@ class SymbolTable: | |
| symbols: Dict[str, Dict[str, Dict[str, Symbol]]] = field(default_factory=dict) | ||
| # Map: global_name -> Symbol (for easy cross-file lookup of exports) | ||
| exports: Dict[str, Symbol] = field(default_factory=dict) | ||
| # Map: file_path -> { "mtime": float, "size": int, "sha256": str } | ||
| file_metadata: Dict[str, Dict[str, Any]] = field(default_factory=dict) | ||
|
|
||
|
Comment on lines
+65
to
67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify Any-typed metadata declaration
rg -n 'file_metadata:\s*Dict\[str,\s*Dict\[str,\s*Any\]\]' refactron/analysis/symbol_table.py
# Verify bool-returning function currently returns expressions based on Any lookups
rg -n 'return current_hash != stored_hash|return stat\.st_mtime != metadata\.get\("mtime"\)' \
refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib Length of output: 264 🏁 Script executed: # View the import statements and file_metadata declaration (around line 66)
head -70 refactron/analysis/symbol_table.py | tail -25Repository: Refactron-ai/Refactron_lib Length of output: 1035 🏁 Script executed: # View the function containing the problematic return statements (around lines 176-189)
sed -n '170,195p' refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib Length of output: 1005 🏁 Script executed: # Check Python version targets and type annotations in the file
head -20 refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib Length of output: 533 🏁 Script executed: # Check if TypedDict is already imported or if we need typing_extensions
grep -n "TypedDict\|typing_extensions" refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check Python version targets in project configuration
fd -e pyproject.toml -e setup.py -e setup.cfg | head -3 | xargs grep -l "python_requires\|target-version" 2>/dev/null | head -1 | xargs cat | grep -A5 -B5 "python\|version" | head -30Repository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Find and read pyproject.toml to check Python version targets
find . -name "pyproject.toml" -type f | head -1 | xargs cat | grep -A10 "python" | head -15Repository: Refactron-ai/Refactron_lib Length of output: 679 🏁 Script executed: # Also verify the complete context of the problematic lines one more time with broader view
sed -n '176,195p' refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib Length of output: 775 Tighten
Suggested fix-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, TypedDict
+
+
+class FileMetadata(TypedDict, total=False):
+ mtime: float
+ size: int
+ sha256: str
@@
- file_metadata: Dict[str, Dict[str, Any]] = field(default_factory=dict)
+ file_metadata: Dict[str, FileMetadata] = field(default_factory=dict)
@@
- metadata = self.symbol_table.file_metadata[file_path_str]
+ metadata: FileMetadata = self.symbol_table.file_metadata[file_path_str]
@@
- stored_hash = metadata.get("sha256")
- if stored_hash:
+ stored_hash = metadata.get("sha256")
+ if isinstance(stored_hash, str) and stored_hash:
current_hash = self._calculate_hash(file_path)
- return current_hash != stored_hash
+ return bool(current_hash != stored_hash)
@@
- return stat.st_mtime != metadata.get("mtime")
+ stored_mtime = metadata.get("mtime")
+ return bool(stored_mtime is None or stat.st_mtime != stored_mtime)Required to satisfy 🤖 Prompt for AI Agents |
||
| @staticmethod | ||
| def _normalize_path(path: str) -> str: | ||
| """Standardize path format for consistent keys/storage.""" | ||
| return Path(path).resolve().as_posix() | ||
|
|
||
| def add_symbol(self, symbol: Symbol) -> None: | ||
| """Add a symbol to the table.""" | ||
| if symbol.file_path not in self.symbols: | ||
| self.symbols[symbol.file_path] = {} | ||
| path = self._normalize_path(symbol.file_path) | ||
| # Ensure the symbol itself stores the normalized path | ||
| symbol.file_path = path | ||
|
|
||
| if path not in self.symbols: | ||
| self.symbols[path] = {} | ||
|
|
||
| if symbol.scope not in self.symbols[symbol.file_path]: | ||
| self.symbols[symbol.file_path][symbol.scope] = {} | ||
| if symbol.scope not in self.symbols[path]: | ||
| self.symbols[path][symbol.scope] = {} | ||
|
|
||
| self.symbols[symbol.file_path][symbol.scope][symbol.name] = symbol | ||
| self.symbols[path][symbol.scope][symbol.name] = symbol | ||
|
|
||
| # Track global exports (top-level functions and classes) | ||
| if symbol.scope == "global" and symbol.type in ( | ||
| SymbolType.CLASS, | ||
| SymbolType.FUNCTION, | ||
| SymbolType.VARIABLE, | ||
| ): | ||
| # Key by module path + name? Or just name for now? | ||
| # Using simple name collision strategy for MVP | ||
| self.exports[symbol.name] = symbol | ||
|
|
||
| def remove_file(self, file_path: str) -> None: | ||
| """Remove all symbols and metadata associated with a file.""" | ||
| norm_path = self._normalize_path(file_path) | ||
|
|
||
| if norm_path in self.symbols: | ||
| del self.symbols[norm_path] | ||
|
|
||
| # Remove from exports | ||
| names_to_remove = [ | ||
| name | ||
| for name, sym in self.exports.items() | ||
| if self._normalize_path(sym.file_path) == norm_path | ||
| ] | ||
| for name in names_to_remove: | ||
| self.exports.pop(name, None) | ||
|
|
||
| if norm_path in self.file_metadata: | ||
| del self.file_metadata[norm_path] | ||
|
|
||
| def get_symbol(self, file_path: str, name: str, scope: str = "global") -> Optional[Symbol]: | ||
| """Retrieve a symbol.""" | ||
| return self.symbols.get(file_path, {}).get(scope, {}).get(name) | ||
| norm_path = self._normalize_path(file_path) | ||
| return self.symbols.get(norm_path, {}).get(scope, {}).get(name) | ||
|
|
||
| def resolve_reference( | ||
| self, name: str, current_file: str, current_scope: str | ||
|
|
@@ -106,8 +136,7 @@ def resolve_reference( | |
| if file_global: | ||
| return file_global | ||
|
|
||
| # 3. Cross-file exports (Naive implementation) | ||
| # TODO: Enhance this with proper import resolution | ||
| # 3. Cross-file exports | ||
| return self.exports.get(name) | ||
|
|
||
|
|
||
|
|
@@ -120,42 +149,94 @@ def __init__(self, cache_dir: Optional[Path] = None): | |
| self.inference_engine = InferenceEngine() | ||
|
|
||
| def build_for_project(self, project_root: Path) -> SymbolTable: | ||
| """Scan project and build symbol table.""" | ||
| """Scan project and build symbol table incrementally.""" | ||
| if self.cache_dir: | ||
| cached = self._load_cache() | ||
| if cached: | ||
| # TODO: Implement incremental update logic here | ||
| return cached | ||
| cached_table = self._load_cache() | ||
| if cached_table: | ||
| self.symbol_table = cached_table | ||
|
|
||
| python_files = list(project_root.rglob("*.py")) | ||
| current_file_paths = {fp.resolve().as_posix() for fp in python_files} | ||
|
|
||
| # 1. Remove deleted files | ||
| cached_files = list(self.symbol_table.file_metadata.keys()) | ||
| for cached_path in cached_files: | ||
| if cached_path not in current_file_paths: | ||
| logger.debug(f"Removing deleted file from symbol table: {cached_path}") | ||
| self.symbol_table.remove_file(cached_path) | ||
|
|
||
|
Comment on lines
+161
to
+167
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deleted-file cleanup misses stale symbols when old cache lacks Line 151 derives Suggested fix- cached_files = list(self.symbol_table.file_metadata.keys())
+ cached_files = set(self.symbol_table.file_metadata.keys()) | set(
+ self.symbol_table.symbols.keys()
+ )
for cached_path in cached_files:
if cached_path not in current_file_paths:
logger.debug(f"Removing deleted file from symbol table: {cached_path}")
self.symbol_table.remove_file(cached_path)🤖 Prompt for AI Agents |
||
| # 2. Analyze new or modified files | ||
| for file_path in python_files: | ||
| self._analyze_file(file_path) | ||
| abs_path = file_path.resolve() | ||
| path_str = abs_path.as_posix() | ||
| if self._has_file_changed(abs_path, path_str): | ||
| logger.debug(f"Analyzing changed file: {path_str}") | ||
| self.symbol_table.remove_file(path_str) | ||
| self._analyze_file(abs_path) | ||
| self._update_file_metadata(abs_path, path_str) | ||
|
|
||
| if self.cache_dir: | ||
| self._save_cache() | ||
|
|
||
| return self.symbol_table | ||
|
|
||
| def _analyze_file(self, file_path: Path) -> None: | ||
| """Analyze a single file and populate symbols.""" | ||
| def _has_file_changed(self, file_path: Path, file_path_str: str) -> bool: | ||
| """Check if file has changed since last analysis.""" | ||
| if file_path_str not in self.symbol_table.file_metadata: | ||
| return True | ||
|
|
||
| metadata = self.symbol_table.file_metadata[file_path_str] | ||
| try: | ||
| stat = file_path.stat() | ||
| if stat.st_size != metadata.get("size"): | ||
| return True | ||
|
|
||
| # Authoritative check: compare SHA-256 hashes | ||
| stored_hash = metadata.get("sha256") | ||
| if stored_hash: | ||
| current_hash = self._calculate_hash(file_path) | ||
| return current_hash != stored_hash | ||
|
|
||
| return stat.st_mtime != metadata.get("mtime") | ||
| except Exception: | ||
| return True | ||
|
|
||
| def _calculate_hash(self, file_path: Path) -> str: | ||
| """Calculate SHA-256 hash of file content.""" | ||
| try: | ||
| # We use astroid for better inference capabilities later | ||
| tree = self.inference_engine.parse_file(str(file_path)) | ||
| return hashlib.sha256(file_path.read_bytes()).hexdigest() | ||
| except Exception: | ||
| return "" | ||
|
Comment on lines
+204
to
+209
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not persist empty SHA-256 values; it weakens rollback-safe invalidation. If Suggested fix-def _calculate_hash(self, file_path: Path) -> str:
+def _calculate_hash(self, file_path: Path) -> Optional[str]:
@@
- except Exception:
- return ""
+ except OSError as e:
+ logger.warning(f"Failed to hash {file_path}: {e}")
+ return None
@@
- self.symbol_table.file_metadata[file_path_str] = {
+ file_hash = self._calculate_hash(file_path)
+ if not file_hash:
+ self.symbol_table.file_metadata.pop(file_path_str, None)
+ return
+ self.symbol_table.file_metadata[file_path_str] = {
"mtime": stat.st_mtime,
"size": stat.st_size,
- "sha256": self._calculate_hash(file_path),
+ "sha256": file_hash,
}Also applies to: 206-210 🧰 Tools🪛 Ruff (0.15.9)[warning] 199-199: Do not catch blind exception: (BLE001) 🤖 Prompt for AI Agents |
||
|
|
||
| # Walk the tree | ||
| self._visit_node(tree, str(file_path), "global") | ||
| def _update_file_metadata(self, file_path: Path, path_str: str) -> None: | ||
| """Update file metadata in symbol table.""" | ||
| try: | ||
| stat = file_path.stat() | ||
| self.symbol_table.file_metadata[path_str] = { | ||
| "mtime": stat.st_mtime, | ||
| "size": stat.st_size, | ||
| "sha256": self._calculate_hash(file_path), | ||
| } | ||
| except Exception as e: | ||
| logger.warning(f"Failed to update metadata for {path_str}: {e}") | ||
|
|
||
| def _analyze_file(self, file_path: Path) -> None: | ||
| """Analyze a single file and populate symbols.""" | ||
| path_str = file_path.resolve().as_posix() | ||
| try: | ||
| tree = self.inference_engine.parse_file(path_str) | ||
| self._visit_node(tree, path_str, "global") | ||
| except Exception as e: | ||
| logger.warning(f"Failed to build symbol table for {file_path}: {e}") | ||
| logger.warning(f"Failed to build symbol table for {path_str}: {e}") | ||
|
|
||
| def _visit_node(self, node: Any, file_path: str, scope: str) -> None: | ||
| """Recursive node visitor.""" | ||
| import astroid.nodes as nodes | ||
|
|
||
| new_scope = scope | ||
|
|
||
| if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)): | ||
| # Register the definition itself in the CURRENT scope | ||
| # Recognize both FunctionDef and AsyncFunctionDef | ||
| if isinstance(node, (nodes.ClassDef, nodes.FunctionDef, nodes.AsyncFunctionDef)): | ||
| symbol_type = ( | ||
| SymbolType.CLASS if isinstance(node, nodes.ClassDef) else SymbolType.FUNCTION | ||
| ) | ||
|
|
@@ -192,9 +273,8 @@ def _visit_node(self, node: Any, file_path: str, scope: str) -> None: | |
| self.symbol_table.add_symbol(symbol) | ||
|
|
||
| # Recurse children | ||
| if hasattr(node, "get_children"): | ||
| for child in node.get_children(): | ||
| self._visit_node(child, file_path, new_scope) | ||
| for child in node.get_children(): | ||
| self._visit_node(child, file_path, new_scope) | ||
|
|
||
| def _save_cache(self) -> None: | ||
| """Save symbol table to cache.""" | ||
|
|
@@ -214,6 +294,7 @@ def _save_cache(self) -> None: | |
| for f, scopes in self.symbol_table.symbols.items() | ||
| }, | ||
| "exports": {n: sym.to_dict() for n, sym in self.symbol_table.exports.items()}, | ||
| "file_metadata": self.symbol_table.file_metadata, | ||
| } | ||
|
|
||
| with open(cache_file, "w") as f: | ||
|
|
@@ -238,15 +319,27 @@ def _load_cache(self) -> Optional[SymbolTable]: | |
|
|
||
| # Reconstruct symbols | ||
| for f_path, scopes in data.get("symbols", {}).items(): | ||
| table.symbols[f_path] = {} | ||
| # Normalize path on load just in case | ||
| norm_f_path = SymbolTable._normalize_path(f_path) | ||
| table.symbols[norm_f_path] = {} | ||
| for scope_name, names in scopes.items(): | ||
| table.symbols[f_path][scope_name] = {} | ||
| table.symbols[norm_f_path][scope_name] = {} | ||
| for name, sym_data in names.items(): | ||
| table.symbols[f_path][scope_name][name] = Symbol.from_dict(sym_data) | ||
| sym = Symbol.from_dict(sym_data) | ||
| sym.file_path = norm_f_path | ||
| table.symbols[norm_f_path][scope_name][name] = sym | ||
|
|
||
| # Reconstruct exports | ||
| for name, sym_data in data.get("exports", {}).items(): | ||
| table.exports[name] = Symbol.from_dict(sym_data) | ||
| sym = Symbol.from_dict(sym_data) | ||
| sym.file_path = SymbolTable._normalize_path(sym.file_path) | ||
| table.exports[name] = sym | ||
|
|
||
| # Reconstruct metadata | ||
| file_metadata = data.get("file_metadata", {}) | ||
| table.file_metadata = { | ||
| SymbolTable._normalize_path(k): v for k, v in file_metadata.items() | ||
| } | ||
|
|
||
| return table | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| Provides capabilities to infer types, values, and resolve symbols. | ||
| """ | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
| from typing import Any, List, Optional | ||
|
|
||
| import astroid | ||
|
|
@@ -28,10 +30,67 @@ def parse_string(code: str, module_name: str = "") -> nodes.Module: | |
| @staticmethod | ||
| def parse_file(file_path: str) -> nodes.Module: | ||
| """Parse a file into an astroid node tree.""" | ||
| builder = astroid.builder.AstroidBuilder(astroid.MANAGER) | ||
| with open(file_path, "r", encoding="utf-8") as f: | ||
| code = f.read() | ||
| return builder.string_build(code, modname=file_path) | ||
| # Use canonical path (resolved and posix-style for consistency) | ||
| abs_path = Path(file_path).resolve().as_posix() | ||
| manager = astroid.MANAGER | ||
|
|
||
| # Aggressively clear cache for this file to ensure fresh AST | ||
| # Try both resolved and absolute paths to handle symlinks and normalization differences | ||
| raw_abs = os.path.abspath(file_path) | ||
| manager.astroid_cache.pop(abs_path, None) | ||
| manager.astroid_cache.pop(raw_abs, None) | ||
| manager.astroid_cache.pop(file_path, None) | ||
|
|
||
| # 2. Find and clear by module name if it exists in caches | ||
| file_to_mod = getattr(manager, "file_to_module_cache", {}) | ||
| # Some versions use _mod_file_cache | ||
| if not file_to_mod: | ||
| file_to_mod = getattr(manager, "_mod_file_cache", {}) | ||
|
|
||
| modname = ( | ||
| file_to_mod.get(abs_path) or file_to_mod.get(raw_abs) or file_to_mod.get(file_path) | ||
| ) | ||
| if modname: | ||
| manager.astroid_cache.pop(modname, None) | ||
|
|
||
| # 3. Exhaustive search in astroid_cache for any module pointing to this file | ||
| for key, val in list(manager.astroid_cache.items()): | ||
| if hasattr(val, "file") and val.file: | ||
| val_path = Path(val.file).resolve().as_posix() | ||
| if val_path == abs_path or val_path == raw_abs.replace("\\", "/"): | ||
| manager.astroid_cache.pop(key, None) | ||
|
|
||
| # 4. Clear the mappings themselves | ||
| for attr in ("file_to_module_cache", "_mod_file_cache"): | ||
| cache = getattr(manager, attr, None) | ||
| if isinstance(cache, dict): | ||
| cache.pop(abs_path, None) | ||
| cache.pop(raw_abs, None) | ||
| cache.pop(file_path, None) | ||
|
|
||
| # 5. Read file and parse directly to bypass astroid's file cache | ||
| try: | ||
| with open(abs_path, "r", encoding="utf-8") as f: | ||
| code = f.read() | ||
|
|
||
| # Resolve module name to keep astroid's state consistent | ||
| modname = "" | ||
| try: | ||
| from astroid import modutils | ||
|
|
||
| modname = modutils.modname_from_path(abs_path) | ||
| except Exception: | ||
| pass | ||
|
|
||
| # Use string_build via parse to avoid manager.ast_from_file's internal caching | ||
| return astroid.parse(code, module_name=modname, path=abs_path) | ||
|
Comment on lines
+77
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "inference.py" | head -20Repository: Refactron-ai/Refactron_lib Length of output: 99 🏁 Script executed: # Look for refactron directory structure
fd -type f "inference\.py" | grep refactronRepository: Refactron-ai/Refactron_lib Length of output: 240 🏁 Script executed: # Get the file location and content
ls -la refactron/core/inference.py 2>/dev/null && wc -l refactron/core/inference.pyRepository: Refactron-ai/Refactron_lib Length of output: 178 🏁 Script executed: cat -n refactron/core/inference.py | sed -n '65,100p'Repository: Refactron-ai/Refactron_lib Length of output: 1757 🏁 Script executed: # Also check the full function signature and any related context
cat -n refactron/core/inference.py | sed -n '1,90p'Repository: Refactron-ai/Refactron_lib Length of output: 4199 🏁 Script executed: # Check if BackupManager is imported or used in this file
grep -n "BackupManager\|validate_backup_integrity" refactron/core/inference.pyRepository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check the broader context - what calls parse_file and expects?
rg -n "parse_file" --type pyRepository: Refactron-ai/Refactron_lib Length of output: 2087 🏁 Script executed: # Look at modutils to understand what modname_from_path returns
rg -A 5 "modutils.modname_from_path" --type pyRepository: Refactron-ai/Refactron_lib Length of output: 518 🏁 Script executed: # Check how symbol_table.py uses the result from parse_file()
cat -n refactron/analysis/symbol_table.py | sed -n '240,270p'Repository: Refactron-ai/Refactron_lib Length of output: 1510 🏁 Script executed: # Look at the test cases to understand expected behavior
cat -n tests/test_rag_parser.py | sed -n '84,110p'Repository: Refactron-ai/Refactron_lib Length of output: 1352 🏁 Script executed: # Check if there are any files with relative imports that would be problematic
rg "from \.\." --type py | head -20Repository: Refactron-ai/Refactron_lib Length of output: 208 🏁 Script executed: # Search for any relative import issues, bugs, or TODOs related to module_name
rg "relative import|package context" --type py -iRepository: Refactron-ai/Refactron_lib Length of output: 694 🏁 Script executed: # Check for any test failures or issues related to module_name or modutils
rg "modname|module_name" refactron/core/inference.py -B 2 -A 2Repository: Refactron-ai/Refactron_lib Length of output: 1297 🏁 Script executed: # Look for git history or comments about this change
git log --oneline -20 -- refactron/core/inference.py 2>/dev/null || echo "Git history unavailable"Repository: Refactron-ai/Refactron_lib Length of output: 176 🏁 Script executed: # Check if there are any conftest or fixture files that test relative imports
rg "from \.\." tests/ --type py | head -5Repository: Refactron-ai/Refactron_lib Length of output: 208 🏁 Script executed: # Look at the test for relative import detection
cat -n tests/test_analyzer_coverage_supplement.py | grep -A 10 "Test relative import"Repository: Refactron-ai/Refactron_lib Length of output: 414 🏁 Script executed: # Check what dependency_analyzer does with relative imports
cat -n refactron/analyzers/dependency_analyzer.py | sed -n '1,50p'Repository: Refactron-ai/Refactron_lib Length of output: 2084 🏁 Script executed: # Search for any error logs or issues with module inference
rg "Failed to.*infer|infer.*failed" --type py -iRepository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check if there's documentation about astroid's parse function behavior
rg "astroid.parse" refactron/ --type py -B 2 -A 2Repository: Refactron-ai/Refactron_lib Length of output: 859 🏁 Script executed: # Check if there are actual test cases that would fail with empty module_name
rg "from \.\." tests/ --type py -B 2 -A 2Repository: Refactron-ai/Refactron_lib Length of output: 435 🏁 Script executed: # Look at the actual test for relative import detection more closely
cat -n tests/test_analyzer_coverage_supplement.py | sed -n '195,220p'Repository: Refactron-ai/Refactron_lib Length of output: 965 🏁 Script executed: # Check if symbol_table.py actually needs to resolve relative imports
rg "infer\|relative import\|package" refactron/analysis/symbol_table.py --type py | head -20Repository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Look for any actual issues in tests related to module resolution
rg "modname|module_name" tests/ --type pyRepository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check if modutils.modname_from_path can actually fail, and in what scenarios
python3 << 'EOF'
try:
from astroid import modutils
# Test with a random path
result = modutils.modname_from_path("/some/random/path/not/in/sys/path.py")
print(f"Result for path not in sys.path: {result}")
except Exception as e:
print(f"Exception type: {type(e).__name__}, message: {e}")
EOFRepository: Refactron-ai/Refactron_lib Length of output: 173 🏁 Script executed: # Check the actual astroid documentation or source comments about module_name and path
python3 << 'EOF'
import inspect
import astroid
# Get the signature and docstring of astroid.parse
sig = inspect.signature(astroid.parse)
print("astroid.parse signature:")
print(sig)
print("\nDocstring:")
print(astroid.parse.__doc__)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 599 🏁 Script executed: # Look for any actual reported issues or TODOs in the code about this
rg "TODO|FIXME|BUG|XXX" refactron/core/inference.py --type pyRepository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check if there are any test failures or skip markers related to this
rg "@pytest.mark.skip|@pytest.mark.xfail|@unittest.skip" tests/ --type py | grep -i "import\|module\|inference"Repository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check what version of astroid is being used and what functions are available
python3 << 'EOF'
import astroid
print(f"Astroid version: {astroid.__version__}")
print(f"modutils attributes:")
from astroid import modutils
print([attr for attr in dir(modutils) if 'modname' in attr.lower() or 'path' in attr.lower()])
EOFRepository: Refactron-ai/Refactron_lib Length of output: 486 🏁 Script executed: # Check if there's a requirements file or setup.py that specifies astroid version
cat setup.py pyproject.toml requirements.txt 2>/dev/null | grep -A 2 -B 2 "astroid" | head -20Repository: Refactron-ai/Refactron_lib Length of output: 228 🏁 Script executed: # Look at what the code is actually importing from modutils
rg "from astroid import modutils|from astroid.modutils import" --type pyRepository: Refactron-ai/Refactron_lib Length of output: 142 🏁 Script executed: # Check if there's any version compatibility handling in the codebase
rg "astroid.__version__|astroid.version" --type pyRepository: Refactron-ai/Refactron_lib Length of output: 195 🏁 Script executed: # Verify the actual modpath_from_file function and how it works
python3 << 'EOF'
from astroid import modutils
import inspect
print("modpath_from_file signature:")
print(inspect.signature(modutils.modpath_from_file))
print("\nDocstring:")
print(modutils.modpath_from_file.__doc__)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 715 🏁 Script executed: # Test modpath_from_file to see what it returns
python3 << 'EOF'
from astroid import modutils
try:
result = modutils.modpath_from_file("/some/random/path.py")
print(f"modpath_from_file result: {result}")
except Exception as e:
print(f"Exception: {type(e).__name__}: {e}")
# Try with an actual file
import tempfile
import os
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
temp_path = f.name
try:
result = modutils.modpath_from_file(temp_path)
print(f"modpath_from_file for temp file: {result}")
except Exception as e:
print(f"Exception for temp file: {type(e).__name__}: {e}")
finally:
os.unlink(temp_path)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 544 🏁 Script executed: # Check if modname_from_path was available in older astroid versions
git log -p --all -S "modname_from_path" -- "*.py" | head -50 2>/dev/null || echo "Not found in git history"Repository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check the actual error handling in parse_file - what exceptions are expected?
cat -n refactron/core/inference.py | sed -n '71,95p'Repository: Refactron-ai/Refactron_lib Length of output: 1226 🏁 Script executed: # Test if astroid can resolve relative imports with empty module_name but valid path
python3 << 'EOF'
import astroid
import tempfile
import os
from pathlib import Path
# Create a test package structure
temp_dir = tempfile.mkdtemp()
pkg_dir = os.path.join(temp_dir, "mypackage")
os.makedirs(pkg_dir)
# Create __init__.py
with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
f.write("# init\n")
# Create utils.py
with open(os.path.join(pkg_dir, "utils.py"), "w") as f:
f.write("def helper(): return 42\n")
# Create test.py with relative import
test_file = os.path.join(pkg_dir, "test.py")
with open(test_file, "w") as f:
f.write("""from . import utils
def main():
return utils.helper()
""")
# Test 1: Parse with empty module_name
print("Test 1: Parse with empty module_name and path")
with open(test_file) as f:
code = f.read()
try:
module1 = astroid.parse(code, module_name="", path=test_file)
print(f" Success. Module name: '{module1.name}'")
# Try to infer the import
for node in module1.nodes_of_class(astroid.ImportFrom):
print(f" Found ImportFrom: {node.as_string()}")
except Exception as e:
print(f" Error: {e}")
# Test 2: Parse with correct module_name
print("\nTest 2: Parse with correct module_name='mypackage.test' and path")
try:
module2 = astroid.parse(code, module_name="mypackage.test", path=test_file)
print(f" Success. Module name: '{module2.name}'")
for node in module2.nodes_of_class(astroid.ImportFrom):
print(f" Found ImportFrom: {node.as_string()}")
except Exception as e:
print(f" Error: {e}")
# Cleanup
import shutil
shutil.rmtree(temp_dir)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 646 🏁 Script executed: # Check what the correct way to get module name from file path should be
python3 << 'EOF'
from astroid import modutils
import os
# Show the correct usage of modpath_from_file
print("Correct function: modpath_from_file()")
print("It returns a list of module path components")
print("\nExample of how to convert modpath to modname:")
modpath = ["mypackage", "test"]
modname = ".".join(modpath)
print(f" modpath: {modpath}")
print(f" modname: {modname}")
EOFRepository: Refactron-ai/Refactron_lib Length of output: 257 🏁 Script executed: # Test if inference actually works with empty module_name for relative imports
python3 << 'EOF'
import astroid
import tempfile
import os
from pathlib import Path
# Create a test package structure
temp_dir = tempfile.mkdtemp()
pkg_dir = os.path.join(temp_dir, "mypackage")
os.makedirs(pkg_dir)
# Create __init__.py
with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
f.write("# init\n")
# Create utils.py
with open(os.path.join(pkg_dir, "utils.py"), "w") as f:
f.write("def helper(): return 42\n")
# Create test.py with relative import
test_file = os.path.join(pkg_dir, "test.py")
with open(test_file) as f:
code = f.read()
# Parse with empty module_name (current behavior)
print("Test: Inference with empty module_name")
module1 = astroid.parse(code, module_name="", path=test_file)
print(f"Module name: '{module1.name}'")
# Try to infer the imported symbol
for node in module1.nodes_of_class(astroid.nodes.ImportFrom):
print(f"ImportFrom node: level={node.level}, names={node.names}")
try:
# Try to infer what 'utils' refers to
inferred = list(node.infer_name("utils", None))
print(f" Inferred 'utils': {inferred}")
except Exception as e:
print(f" Inference failed: {type(e).__name__}: {e}")
print("\n---\n")
# Parse with correct module_name (desired behavior)
print("Test: Inference with correct module_name='mypackage.test'")
module2 = astroid.parse(code, module_name="mypackage.test", path=test_file)
print(f"Module name: '{module2.name}'")
for node in module2.nodes_of_class(astroid.nodes.ImportFrom):
print(f"ImportFrom node: level={node.level}, names={node.names}")
try:
inferred = list(node.infer_name("utils", None))
print(f" Inferred 'utils': {inferred}")
except Exception as e:
print(f" Inference failed: {type(e).__name__}: {e}")
# Cleanup
import shutil
shutil.rmtree(temp_dir)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 236 🏁 Script executed: # Check if symbol_table.py actually needs to infer relative imports
cat -n refactron/analysis/symbol_table.py | sed -n '1,50p'Repository: Refactron-ai/Refactron_lib Length of output: 1841 🏁 Script executed: # Check if there are any actual test failures when running tests
rg "test.*parse_file|test.*inference" tests/ --type py -lRepository: Refactron-ai/Refactron_lib Length of output: 94 🏁 Script executed: # Fix the test - create the file first, then read it
python3 << 'EOF'
import astroid
import tempfile
import os
from pathlib import Path
# Create a test package structure
temp_dir = tempfile.mkdtemp()
pkg_dir = os.path.join(temp_dir, "mypackage")
os.makedirs(pkg_dir)
# Create __init__.py
with open(os.path.join(pkg_dir, "__init__.py"), "w") as f:
f.write("# init\n")
# Create utils.py
with open(os.path.join(pkg_dir, "utils.py"), "w") as f:
f.write("def helper(): return 42\n")
# Create test.py with relative import - BEFORE reading it
test_file = os.path.join(pkg_dir, "test.py")
with open(test_file, "w") as f:
f.write("""from . import utils
def main():
return utils.helper()
""")
# NOW read it
with open(test_file) as f:
code = f.read()
# Parse with empty module_name (current behavior)
print("Test: Inference with empty module_name")
module1 = astroid.parse(code, module_name="", path=test_file)
print(f"Module name: '{module1.name}'")
# Try to infer the imported symbol
for node in module1.nodes_of_class(astroid.nodes.ImportFrom):
print(f"ImportFrom node: level={node.level}, names={node.names}")
try:
inferred = list(node.infer_name("utils", None))
print(f" Inferred 'utils': {inferred}")
except Exception as e:
print(f" Inference failed: {type(e).__name__}: {e}")
print("\n---\n")
# Parse with correct module_name (desired behavior)
print("Test: Inference with correct module_name='mypackage.test'")
module2 = astroid.parse(code, module_name="mypackage.test", path=test_file)
print(f"Module name: '{module2.name}'")
for node in module2.nodes_of_class(astroid.nodes.ImportFrom):
print(f"ImportFrom node: level={node.level}, names={node.names}")
try:
inferred = list(node.infer_name("utils", None))
print(f" Inferred 'utils': {inferred}")
except Exception as e:
print(f" Inference failed: {type(e).__name__}: {e}")
# Cleanup
import shutil
shutil.rmtree(temp_dir)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 490 🏁 Script executed: # Check what the test_rag_parser tests actually do
cat -n tests/test_rag_parser.py | sed -n '84,120p'Repository: Refactron-ai/Refactron_lib Length of output: 1826 🏁 Script executed: # Look for any test that calls InferenceEngine.parse_file directly
rg "InferenceEngine\|parse_file" tests/ --type py -B 2 -A 5 | head -50Repository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check if there are any actual test failures by examining test output or fixtures
cat -n tests/test_rag_parser.py | sed -n '1,50p'Repository: Refactron-ai/Refactron_lib Length of output: 1516 🏁 Script executed: # Look at what parse_file returns and how it's used in RAG parser
cat -n refactron/rag/parser.py | sed -n '185,210p'Repository: Refactron-ai/Refactron_lib Length of output: 1234 🏁 Script executed: # The key question: does symbol_table need correct module_name for its use case?
cat -n refactron/analysis/symbol_table.py | sed -n '240,280p'Repository: Refactron-ai/Refactron_lib Length of output: 2045 🏁 Script executed: # Verify if modname_from_path is truly non-existent or if it's from a different import
rg "modname_from_path" . --type pyRepository: Refactron-ai/Refactron_lib Length of output: 162 🏁 Script executed: # Check if there's maybe a compatibility layer or if this is just dead code
rg "def modname_from_path" . --type pyRepository: Refactron-ai/Refactron_lib Length of output: 52 🏁 Script executed: # Check the git commit history for this specific function
git log --oneline -p refactron/core/inference.py | grep -A 10 -B 5 "modname_from_path" | head -40Repository: Refactron-ai/Refactron_lib Length of output: 731 🏁 Script executed: # Check if maybe there was an older version of astroid that had this function
python3 << 'EOF'
# See if there's any way to resolve module name correctly from a path
from astroid import modutils
import os
import sys
test_path = "/some/nonexistent/path/mymodule.py"
print("Available functions in modutils related to module/path:")
for attr in sorted(dir(modutils)):
if not attr.startswith('_'):
print(f" - {attr}")
EOFRepository: Refactron-ai/Refactron_lib Length of output: 1044 🏁 Script executed: # Check if the code even runs - try to import and use it
python3 << 'EOF'
import sys
sys.path.insert(0, '.')
try:
from refactron.core.inference import InferenceEngine
print("Import successful")
# Try to use parse_file
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write("x = 1\n")
f.flush()
try:
result = InferenceEngine.parse_file(f.name)
print(f"parse_file succeeded: {result}")
except Exception as e:
print(f"parse_file failed: {type(e).__name__}: {e}")
finally:
import os
os.unlink(f.name)
except Exception as e:
print(f"Import failed: {e}")
EOFRepository: Refactron-ai/Refactron_lib Length of output: 341 🏁 Script executed: # Final verification: Check if symbol_table actually needs correct module_name
# by checking what operations it does on the parsed tree
cat -n refactron/analysis/symbol_table.py | sed -n '264,320p'Repository: Refactron-ai/Refactron_lib Length of output: 2504 🏁 Script executed: # Verify the actual impact: does empty module_name cause problems for inference in practice?
python3 << 'EOF'
import astroid
import tempfile
import os
# Create a simple test
temp_dir = tempfile.mkdtemp()
test_file = os.path.join(temp_dir, "test.py")
with open(test_file, "w") as f:
f.write("""
class MyClass:
def method(self):
return 42
def my_function():
pass
""")
# Parse with empty module_name (current broken behavior)
with open(test_file) as f:
code = f.read()
module = astroid.parse(code, module_name="", path=test_file)
print(f"Module name: '{module.name}'")
print(f"Module file: {module.file}")
print(f"Classes: {[c.name for c in module.nodes_of_class(astroid.nodes.ClassDef)]}")
print(f"Functions: {[f.name for f in module.nodes_of_class(astroid.nodes.FunctionDef)]}")
# Can we visit the nodes like symbol_table does?
for node in module.body:
if isinstance(node, (astroid.nodes.ClassDef, astroid.nodes.FunctionDef)):
print(f"Node: {node.name}, line: {node.lineno}")
import shutil
shutil.rmtree(temp_dir)
EOFRepository: Refactron-ai/Refactron_lib Length of output: 231 Fix the non-existent function call and properly resolve module names. The code calls With empty Instead of silently falling back to empty module name, either:
🧰 Tools🪛 GitHub Actions: CI[error] Command failed with exit code 1: flake8 refactron --count --select=E9,F63,F7,F82 --show-source --statistics 🪛 Ruff (0.15.9)[error] 82-83: (S110) [warning] 82-82: Do not catch blind exception: (BLE001) 🤖 Prompt for AI Agents |
||
| except (OSError, UnicodeDecodeError): | ||
| # Fallback to manager if manual read fails | ||
| try: | ||
| return manager.ast_from_file(abs_path) | ||
| except Exception as e: | ||
| # Fallback for virtual/non-existent files if needed | ||
| raise ValueError(f"Failed to parse {abs_path}: {e}") | ||
|
|
||
| @staticmethod | ||
| def infer_node(node: nodes.NodeNG, context: Optional[InferenceContext] = None) -> List[Any]: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.