Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bad_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
THRESHOLD_VALUE = 5
MIN_X_VALUE = 5
MAX_Y_VALUE = 10
ITERATION_LIMIT = 100
MIN_ITERATION_VALUE = 10
MAX_ITERATION_VALUE = 5
def do_something_crazy(x: int, y: int) -> int:
"""
This function performs a series of operations based on the input values x and y.
It checks if x is greater than the threshold value and y is less than the max y value.
If the conditions are met, it iterates over a range of numbers and prints a message.
Finally, it returns the sum of x and y.
"""
if x > THRESHOLD_VALUE:
if y < MAX_Y_VALUE:
for i in range(ITERATION_LIMIT):
print("doing something", x)
return x + y
do_something_crazy(10, 5)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +1 to +19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix formatting/newline so pre-commit passes.

This file is currently failing black and end-of-file-fixer in CI; please run pre-commit formatting on the file before merge.
As per coding guidelines: Use black formatter with target-version set to py38, py39, py310, py311; Flake8 linting must use max-line-length of 100.

🧰 Tools
🪛 GitHub Actions: Pre-commit

[error] 1-1: pre-commit hook 'end-of-file-fixer' failed (exit code 1): files were modified by this hook (Fixing bad_code.py).


[error] 1-1: pre-commit hook 'black' failed: reformatted bad_code.py.

🪛 Ruff (0.15.10)

[warning] 16-16: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bad_code.py` around lines 1 - 19, Run the project pre-commit hooks (or run
black with target-version py38,py39,py310,py311 and end-of-file-fixer) on this
file to fix formatting and ensure a single trailing newline at EOF; specifically
format the module-level constants (THRESHOLD_VALUE, MIN_X_VALUE, MAX_Y_VALUE,
ITERATION_LIMIT, MIN_ITERATION_VALUE, MAX_ITERATION_VALUE) and the
do_something_crazy function to match Black's style, and ensure no lines exceed
100 characters for flake8 compliance before committing.

159 changes: 126 additions & 33 deletions refactron/analysis/symbol_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the unused Set import.

Pre-commit is already failing on Line 12 with F401.

Suggested fix
-from typing import Any, Dict, List, Optional, Set
+from typing import Any, Dict, List, Optional
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from typing import Any, Dict, List, Optional, Set
from typing import Any, Dict, List, Optional
🧰 Tools
🪛 GitHub Actions: Pre-commit

[error] 12-12: flake8: F401 'typing.Set' imported but unused

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/analysis/symbol_table.py` at line 12, The import list in
symbol_table.py includes an unused symbol "Set" causing an F401 pre-commit
failure; remove "Set" from the from typing import line (the import that
currently reads "from typing import Any, Dict, List, Optional, Set") so only the
actually used types (Any, Dict, List, Optional) remain, then run the pre-commit
checks to verify the F401 is resolved.


from refactron.core.inference import InferenceEngine

Expand Down Expand Up @@ -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)

@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]
Comment on lines +95 to +112

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Rebuild exports for shadowed names when removing a file.

remove_file() deletes the winning export entry for each removed symbol name, but it never restores another global symbol with the same name from the remaining files. After deleting or reanalyzing one module, resolve_reference() can incorrectly lose a still-existing cross-file export.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/analysis/symbol_table.py` around lines 95 - 112, remove_file
currently deletes export entries whose Symbol.file_path points to the removed
file but doesn't restore a different global symbol with the same name, causing
resolve_reference to lose valid cross-file exports; update remove_file to, for
each export name removed, scan remaining self.symbols (use self._normalize_path
and the Symbol.name / Symbol.file_path attributes) to find an alternative symbol
with the same name and set self.exports[name] to that symbol (or pop only if
none found), ensuring file_metadata and symbols cleanup remains unchanged.


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
Expand All @@ -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)


Expand All @@ -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)

# 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)
Comment on lines +172 to +176

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only refresh file_metadata after a successful analysis.

Line 176 runs even when _analyze_file() failed, because _analyze_file() swallows the exception at Lines 226-230. That marks the file as up to date, so later incremental builds skip it and its symbols stay missing until the file changes again.

Suggested fix
         for file_path in python_files:
             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._analyze_file(abs_path):
+                    self._update_file_metadata(abs_path, path_str)
...
-    def _analyze_file(self, file_path: Path) -> None:
+    def _analyze_file(self, file_path: Path) -> bool:
         """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")
+            return True
         except Exception as e:
             logger.warning(f"Failed to build symbol table for {path_str}: {e}")
+            return False

Also applies to: 223-230

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/analysis/symbol_table.py` around lines 172 - 176, The file metadata
is updated even when _analyze_file() fails (it swallows exceptions), causing
files to be marked up-to-date with missing symbols; change the logic so
_update_file_metadata(path) runs only after a successful analysis: either make
_analyze_file(abs_path) return a success boolean (or re-raise errors) and call
_update_file_metadata(abs_path, path_str) only when that returns True, or let
_analyze_file propagate exceptions and wrap the caller to update metadata in the
try block after a successful call; apply the same fix to the other occurrence
that wraps _analyze_file() (the block covering lines ~223-230), keeping the
symbol_table.remove_file(path_str) behavior unchanged.


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")
Comment on lines +183 to +200

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Give file_metadata a typed shape before returning these comparisons.

The current Dict[str, Any] metadata is what causes the return stat.st_mtime != metadata.get("mtime") path to fail mypy with no-any-return. Pull the fields into typed locals or model file_metadata with a TypedDict/dataclass before comparing.

As per coding guidelines, refactron/**/*.py: Type annotations are required in refactron/ with mypy disallow_untyped_defs = true enabled.

🧰 Tools
🪛 GitHub Actions: Pre-commit

[error] 198-200: mypy: Returning Any from function declared to return 'bool' [no-any-return]

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/analysis/symbol_table.py` around lines 183 - 200, The metadata dict
returned from symbol_table.file_metadata is untyped which causes mypy failures
in _has_file_changed; change the model to a typed shape (e.g., a TypedDict or
dataclass for the per-file metadata) or cast/pull fields into explicitly typed
locals before comparisons in _has_file_changed; update the type of
symbol_table.file_metadata to use that TypedDict/dataclass and in
_has_file_changed extract typed locals like size, sha256, and mtime (with proper
Optional[type] annotations) and then perform the size/hash/mtime comparisons
(still using _calculate_hash when sha256 exists) so mypy no-any-return errors
are resolved.

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 ""

# 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
)
Expand Down Expand Up @@ -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."""
Expand All @@ -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:
Expand All @@ -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

Expand Down
Loading
Loading