Skip to content

refactor: extract shared Astroid utilities into core module#52

Open
nnennandukwe wants to merge 1 commit into
mainfrom
feature/41-core-module
Open

refactor: extract shared Astroid utilities into core module#52
nnennandukwe wants to merge 1 commit into
mainfrom
feature/41-core-module

Conversation

@nnennandukwe
Copy link
Copy Markdown
Owner

Summary

Closes #41

Extracts reusable Astroid-based AST analysis utilities from the performance profiler into a new shared workshop_mcp.core module, enabling new code quality tools to leverage the same infrastructure without duplicating parsing logic.

What changed

  • New core/ module with four files:

    • ast_utils.py — shared dataclasses (FunctionInfo, LoopInfo, ImportInfo, CallInfo, ClassInfo) and extraction functions (parse_source, extract_functions, extract_classes, extract_imports, extract_calls, get_source_segment)
    • inference.pyinfer_callable() and get_qualified_name() for Astroid type inference
    • schema.py — unified CodeIssue dataclass and Severity enum for cross-tool issue reporting
    • __init__.py — public API re-exports
  • Refactored performance_profiler/ast_analyzer.py to delegate to core utilities instead of implementing its own extraction logic (~210 lines removed, replaced with imports from core)

  • Backward-compatible re-exports in performance_profiler/__init__.py — added CallInfo, FunctionInfo, ImportInfo, LoopInfo to __all__ so existing imports from workshop_mcp.performance_profiler continue to work

  • 39 new tests in tests/test_core.py covering all core utilities, dataclass construction, inference helpers, and backward compatibility verification

Additions beyond the original issue

  • ClassInfo dataclass and extract_classes() — not in the original issue spec but needed by the complexity analyzer (feat: Add complexity analyzer tool #44) for class-level metrics
  • Backward-compatibility re-exports from performance_profiler/__init__.py — ensures no breakage of existing imports
  • Identity tests verifying core.FunctionInfo is performance_profiler.ast_analyzer.FunctionInfo (same class, not a copy)

What was NOT included from the issue

The original issue proposed a broader tools/ directory restructure and several additional utility functions (walk_ast(), find_calls_in_node(), is_inside_loop(), is_method_of(), etc.). These were intentionally deferred — the current extraction covers exactly what the downstream tools (#44, #45) need. The patterns.py pattern-matching helpers were not extracted since the performance profiler's pattern matching is specific to performance checking.

Test plan

  • All 161 existing + new tests pass (poetry run pytest)
  • ruff check and ruff format --check pass
  • Pre-commit hooks pass (ruff, trailing whitespace, detect-secrets, conventional commits)
  • Backward compatibility: imports from workshop_mcp.performance_profiler.ast_analyzer still resolve to the same classes
  • No server.py changes — this is pure infrastructure

Note: This is the first branch in a chain. Merge this before #44 and #45.

main ← PR #41 (this) ← PR #44 ← PR #45

🤖 Generated with Claude Code

Moves shared AST parsing dataclasses (FunctionInfo, LoopInfo, ImportInfo,
CallInfo, ClassInfo) and utility functions (parse_source, extract_functions,
extract_classes, extract_imports, extract_calls, get_source_segment) into
a new workshop_mcp.core module. The performance_profiler.ast_analyzer now
delegates to core, with backward-compatible re-exports to avoid breaking
existing imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Extract shared Astroid utilities into core module for code reuse

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Extract shared Astroid AST utilities into new core module
• Enable code reuse across multiple analysis tools
• Add ClassInfo dataclass and extract_classes() function
• Maintain backward compatibility with existing imports
• Add 39 comprehensive tests for core utilities
Diagram
flowchart LR
  A["performance_profiler<br/>ast_analyzer.py"] -->|"delegates to"| B["core<br/>ast_utils.py"]
  A -->|"uses"| C["core<br/>inference.py"]
  B -->|"exports"| D["FunctionInfo<br/>LoopInfo<br/>ImportInfo<br/>CallInfo<br/>ClassInfo"]
  C -->|"exports"| E["infer_callable<br/>get_qualified_name"]
  F["core<br/>schema.py"] -->|"exports"| G["CodeIssue<br/>Severity"]
  H["performance_profiler<br/>__init__.py"] -->|"re-exports"| D
  I["tests<br/>test_core.py"] -->|"validates"| B
  I -->|"validates"| C
  I -->|"validates"| G
Loading

Grey Divider

File Changes

1. src/workshop_mcp/core/__init__.py ✨ Enhancement +35/-0

Core module public API initialization

• New module initialization file
• Exports all public APIs from submodules
• Provides unified interface for core utilities
• Includes 14 items in __all__ for explicit public API

src/workshop_mcp/core/init.py


2. src/workshop_mcp/core/ast_utils.py ✨ Enhancement +317/-0

Core AST parsing utilities and dataclasses

• Defines 5 dataclasses: FunctionInfo, LoopInfo, ImportInfo, CallInfo, ClassInfo
• Implements 6 public functions: parse_source(), extract_functions(), extract_classes(),
 extract_imports(), extract_calls(), get_source_segment()
• Includes 6 internal helper functions for AST traversal and metadata extraction
• Handles both sync and async function definitions, decorators, type annotations

src/workshop_mcp/core/ast_utils.py


3. src/workshop_mcp/core/inference.py ✨ Enhancement +41/-0

Astroid type inference utilities

• Implements infer_callable() for type inference using Astroid
• Implements get_qualified_name() for extracting qualified names
• Handles inference errors gracefully with try-except blocks
• Provides fallback to None when inference fails

src/workshop_mcp/core/inference.py


View more (4)
4. src/workshop_mcp/core/schema.py ✨ Enhancement +29/-0

Unified code issue schema and severity levels

• Defines Severity enum with 4 levels: INFO, WARNING, ERROR, CRITICAL
• Defines CodeIssue dataclass for unified issue reporting
• Includes 9 fields with optional suggestion, code_snippet, function_name, end_line, column
• Provides common schema for all analysis tools

src/workshop_mcp/core/schema.py


5. src/workshop_mcp/performance_profiler/__init__.py ✨ Enhancement +5/-1

Backward-compatible dataclass re-exports

• Adds backward-compatible re-exports of 4 dataclasses
• Imports CallInfo, FunctionInfo, ImportInfo, LoopInfo from ast_analyzer
• Adds these classes to __all__ for public API
• Maintains existing exports of ASTAnalyzer, PerformanceChecker, etc.

src/workshop_mcp/performance_profiler/init.py


6. src/workshop_mcp/performance_profiler/ast_analyzer.py Refactoring +22/-209

Refactor to delegate to core utilities

• Removes 4 dataclass definitions (moved to core)
• Removes ~210 lines of extraction logic (delegated to core)
• Imports dataclasses and functions from workshop_mcp.core
• Simplifies get_functions(), get_imports(), get_calls() to use core utilities
• Refactors _infer_callable_name() to delegate to infer_callable()
• Keeps loop extraction logic in _extract_loops_recursive() (not yet extracted)
• Adds __all__ for explicit public API

src/workshop_mcp/performance_profiler/ast_analyzer.py


7. tests/test_core.py 🧪 Tests +388/-0

Comprehensive tests for core utilities

• Adds 39 new tests covering all core utilities
• Tests parse_source(), extract_functions(), extract_classes(), extract_imports(),
 extract_calls(), get_source_segment()
• Tests CodeIssue dataclass and Severity enum
• Tests inference utilities infer_callable() and get_qualified_name()
• Includes backward compatibility tests verifying class identity
• Validates that core and profiler export identical classes

tests/test_core.py


Grey Divider

Qodo Logo


🛠️ Relevant configurations:


These are the relevant configurations for this tool:

[config]

is_auto_command: True
is_new_pr: True
model_reasoning: vertex_ai/gemini-2.5-pro
model: gpt-5.2-2025-12-11
model_turbo: anthropic/claude-haiku-4-5-20251001
fallback_models: ['anthropic/claude-sonnet-4-5-20250929', 'bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0']
second_model_for_exhaustive_mode: o4-mini
git_provider: github
publish_output: True
publish_output_no_suggestions: True
publish_output_progress: True
verbosity_level: 0
publish_logs: False
debug_mode: False
use_wiki_settings_file: True
use_repo_settings_file: True
use_global_settings_file: True
use_global_wiki_settings_file: False
disable_auto_feedback: False
ai_timeout: 150
response_language: en-US
clone_repo_instead_of_fetch: True
always_clone: False
add_repo_metadata: True
clone_repo_time_limit: 300
publish_inline_comments_fallback_batch_size: 5
publish_inline_comments_fallback_sleep_time: 2
max_model_tokens: 32000
custom_model_max_tokens: -1
patch_extension_skip_types: ['.md', '.txt']
extra_allowed_extensions: []
allow_dynamic_context: True
allow_forward_dynamic_context: True
max_extra_lines_before_dynamic_context: 12
patch_extra_lines_before: 5
patch_extra_lines_after: 1
ai_handler: litellm
cli_mode: False
TRIAL_GIT_ORG_MAX_INVOKES_PER_MONTH: 30
TRIAL_RATIO_CLOSE_TO_LIMIT: 0.8
invite_only_mode: False
enable_request_access_msg_on_new_pr: False
check_also_invites_field: False
calculate_context: True
disable_checkboxes: False
output_relevant_configurations: True
large_patch_policy: clip
seed: -1
temperature: 0.2
allow_dynamic_context_ab_testing: False
choose_dynamic_context_ab_testing_ratio: 0.5
ignore_pr_title: ['^\\[Auto\\]', '^Auto']
ignore_pr_target_branches: []
ignore_pr_source_branches: []
ignore_pr_labels: []
ignore_ticket_labels: []
allow_only_specific_folders: []
ignore_pr_authors: []
ignore_repositories: []
ignore_language_framework: []
enable_ai_metadata: True
present_reasoning: True
max_tickets: 10
max_tickets_chars: 8000
prevent_any_approval: False
enable_comment_approval: False
enable_auto_approval: False
auto_approve_for_low_review_effort: -1
auto_approve_for_no_suggestions: False
ensure_ticket_compliance: False
new_diff_format: True
new_diff_format_add_external_references: True
tasks_queue_ttl_from_dequeue_in_seconds: 900
enable_custom_labels: False

[pr_description]

publish_labels: False
add_original_user_description: True
generate_ai_title: False
extra_instructions: 
enable_pr_type: True
final_update_message: True
enable_help_text: False
enable_help_comment: False
bring_latest_tag: False
enable_pr_diagram: True
publish_description_as_comment: False
publish_description_as_comment_persistent: True
enable_semantic_files_types: True
collapsible_file_list: adaptive
collapsible_file_list_threshold: 8
inline_file_summary: False
use_description_markers: False
include_generated_by_header: True
enable_large_pr_handling: True
max_ai_calls: 4
auto_create_ticket: False

@qodo-code-review
Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (5)

Grey Divider


Action required

1. Missing core/patterns.py 📎 Requirement gap ✓ Correctness
Description
• The shared workshop_mcp.core package does not include a patterns.py module, even though the
  checklist requires it for centralized AST pattern helpers.
• This prevents other tools from reusing common call/loop/context detection logic and keeps generic
  helpers tied to performance_profiler.
Code

src/workshop_mcp/core/init.py[R1-17]

+"""Core utilities for AST analysis shared across all code quality tools."""
+
+from .ast_utils import (
+    CallInfo,
+    ClassInfo,
+    FunctionInfo,
+    ImportInfo,
+    LoopInfo,
+    extract_calls,
+    extract_classes,
+    extract_functions,
+    extract_imports,
+    get_source_segment,
+    parse_source,
+)
+from .inference import get_qualified_name, infer_callable
+from .schema import CodeIssue, Severity
Evidence
Compliance requires a core/patterns.py module with pattern helpers, but workshop_mcp.core only
imports ast_utils, inference, and schema, while the only patterns.py present is still under
performance_profiler. This indicates the required core module was not created/extracted as
specified.

Create shared core module for Astroid utilities (core/ package and modules)
Extract pattern helpers into core.patterns
src/workshop_mcp/core/init.py[1-17]
src/workshop_mcp/performance_profiler/init.py[5-7]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces `workshop_mcp.core` but does not include the required `core/patterns.py` module and helpers. The checklist requires these helpers to be centralized in core for reuse.

## Issue Context
The current codebase still relies on `workshop_mcp.performance_profiler.patterns`, which keeps generic helpers coupled to the performance profiler.

## Fix Focus Areas
- src/workshop_mcp/core/__init__.py[1-35]
- src/workshop_mcp/performance_profiler/__init__.py[5-7]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. walk_ast() not implemented 📎 Requirement gap ✓ Correctness
Description
• The checklist requires core.ast_utils to provide walk_ast() as a reusable traversal utility,
  but the module currently only exposes parse/extract helpers.
• Downstream tools that need generic AST traversal will still have to implement their own walking
  logic, defeating the purpose of shared infrastructure.
Code

src/workshop_mcp/core/ast_utils.py[R72-197]

+def parse_source(source_code: str, file_path: str | None = None) -> astroid.Module:
+    """Parse Python source code into an Astroid AST module.
+
+    Args:
+        source_code: Python source code as a string.
+        file_path: Optional file path for error reporting.
+
+    Returns:
+        Parsed Astroid Module node.
+
+    Raises:
+        SyntaxError: If the source code contains syntax errors.
+    """
+    try:
+        return astroid.parse(source_code, path=file_path)
+    except astroid.AstroidSyntaxError as e:
+        raise SyntaxError(str(e)) from e
+
+
+def extract_functions(tree: astroid.Module) -> list[FunctionInfo]:
+    """Extract all function definitions from an Astroid AST.
+
+    Args:
+        tree: Parsed Astroid Module node.
+
+    Returns:
+        List of FunctionInfo objects containing function metadata.
+    """
+    functions = []
+    for node in tree.nodes_of_class((astroid.FunctionDef, astroid.AsyncFunctionDef)):
+        func_info = _extract_function_info(node)
+        functions.append(func_info)
+    return functions
+
+
+def extract_classes(tree: astroid.Module) -> list[ClassInfo]:
+    """Extract all class definitions from an Astroid AST.
+
+    Args:
+        tree: Parsed Astroid Module node.
+
+    Returns:
+        List of ClassInfo objects containing class metadata.
+    """
+    classes = []
+    for node in tree.nodes_of_class(astroid.ClassDef):
+        class_info = _extract_class_info(node)
+        classes.append(class_info)
+    return classes
+
+
+def extract_imports(tree: astroid.Module) -> list[ImportInfo]:
+    """Extract all import statements from an Astroid AST.
+
+    Args:
+        tree: Parsed Astroid Module node.
+
+    Returns:
+        List of ImportInfo objects containing import metadata.
+    """
+    imports = []
+
+    # Handle regular imports
+    for node in tree.nodes_of_class(astroid.Import):
+        for name, alias in node.names:
+            import_info = ImportInfo(
+                module=name,
+                names=[name],
+                line_number=node.lineno,
+                is_from_import=False,
+                aliases={name: alias} if alias else {},
+                resolved_module=None,
+            )
+            imports.append(import_info)
+
+    # Handle from imports
+    for node in tree.nodes_of_class(astroid.ImportFrom):
+        if node.modname:
+            aliases = {}
+            names = []
+            for name, alias in node.names:
+                names.append(name)
+                if alias:
+                    aliases[name] = alias
+
+            import_info = ImportInfo(
+                module=node.modname,
+                names=names,
+                line_number=node.lineno,
+                is_from_import=True,
+                aliases=aliases,
+                resolved_module=None,
+            )
+            imports.append(import_info)
+
+    return imports
+
+
+def extract_calls(tree: astroid.Module) -> list[CallInfo]:
+    """Extract all function calls from an Astroid AST.
+
+    Args:
+        tree: Parsed Astroid Module node.
+
+    Returns:
+        List of CallInfo objects containing call metadata.
+    """
+    calls: list[CallInfo] = []
+    _extract_calls_recursive(tree, calls, None, False, False)
+    return calls
+
+
+def get_source_segment(source_code: str, line_start: int, line_end: int) -> str:
+    """Get a segment of source code by line numbers.
+
+    Args:
+        source_code: Full source code string.
+        line_start: Starting line number (1-indexed).
+        line_end: Ending line number (1-indexed, inclusive).
+
+    Returns:
+        Source code segment as a string.
+    """
+    lines = source_code.splitlines()
+    return "\n".join(lines[line_start - 1 : line_end])
+
Evidence
Compliance item 8 explicitly requires walk_ast() in core.ast_utils.py. The module defines
parse_source(), extract_functions(), extract_classes(), extract_imports(),
extract_calls(), and get_source_segment(), but no walk_ast() function is present.

Extract AST utilities into core.ast_utils (parse/extract/walk)
src/workshop_mcp/core/ast_utils.py[72-197]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`walk_ast()` is required by the checklist but is missing from `src/workshop_mcp/core/ast_utils.py`.

## Issue Context
Downstream tools may need a standard AST traversal utility for consistency and reuse.

## Fix Focus Areas
- src/workshop_mcp/core/ast_utils.py[72-197]
- src/workshop_mcp/core/__init__.py[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. is_method_of() missing 📎 Requirement gap ✓ Correctness
Description
• The shared inference module does not implement is_method_of(), which the checklist requires as
  part of reusable inference helpers.
• Without it, tools needing method/class relationship checks must re-implement logic or depend on
  performance-profiler-specific code.
Code

src/workshop_mcp/core/inference.py[R1-41]

+"""Astroid type inference utilities."""
+
+import astroid
+
+
+def infer_callable(node: astroid.NodeNG) -> str | None:
+    """Try to infer the fully qualified name of a callable.
+
+    Uses Astroid's inference engine to resolve what a function actually is,
+    providing fully qualified names when possible.
+
+    Args:
+        node: An Astroid node representing a callable expression.
+
+    Returns:
+        Fully qualified name string, or None if inference fails.
+    """
+    try:
+        inferred = next(node.infer(), None)
+        if inferred and hasattr(inferred, "qname"):
+            return inferred.qname()
+    except (astroid.InferenceError, StopIteration):
+        pass
+    return None
+
+
+def get_qualified_name(node: astroid.NodeNG) -> str | None:
+    """Get the qualified name for an Astroid node if available.
+
+    Args:
+        node: An Astroid node.
+
+    Returns:
+        Qualified name string, or None if not available.
+    """
+    if hasattr(node, "qname"):
+        try:
+            return node.qname()
+        except Exception:  # noqa: S110
+            return None
+    return None
Evidence
Compliance item 9 requires core.inference to include infer_callable(), get_qualified_name(),
and is_method_of(). The file currently defines only the first two.

Extract inference utilities into core.inference
src/workshop_mcp/core/inference.py[1-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`is_method_of()` is required by the checklist but is not present in `src/workshop_mcp/core/inference.py`.

## Issue Context
This helper is needed for reusable call/target analysis across tools.

## Fix Focus Areas
- src/workshop_mcp/core/inference.py[1-41]
- src/workshop_mcp/core/__init__.py[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
4. CodeIssue missing file 📎 Requirement gap ✓ Correctness
Description
• The unified CodeIssue schema does not match the required ticket fields: it lacks the required
  file field and makes column optional while the checklist specifies it as part of the schema.
• This breaks the goal of consistent, cross-tool issue output and may force tools to create their
  own per-tool schemas or add ad-hoc fields.
Code

src/workshop_mcp/core/schema.py[R16-29]

+@dataclass
+class CodeIssue:
+    """Represents a code issue detected by any analysis tool."""
+
+    tool: str
+    category: str
+    severity: Severity
+    message: str
+    line: int
+    suggestion: str | None = None
+    code_snippet: str | None = None
+    function_name: str | None = None
+    end_line: int | None = None
+    column: int | None = None
Evidence
Compliance item 11 specifies CodeIssue must include (at minimum) `tool, category, severity,
message, file, line, column, suggestion, code_snippet. The implemented CodeIssue omits file` and
adds non-specified fields (function_name, end_line) while making column optional.

Implement unified issue output schema in core.schema (CodeIssue dataclass)
src/workshop_mcp/core/schema.py[16-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CodeIssue` in `core/schema.py` does not match the required unified issue schema (missing `file`, and field set differs from the required list).

## Issue Context
Downstream tools depend on a consistent schema across tools, so the dataclass fields must match the checklist acceptance criteria.

## Fix Focus Areas
- src/workshop_mcp/core/schema.py[16-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Tests lack type hints 📘 Rule violation ✓ Correctness
Description
• New test functions were added without parameter/return type annotations (e.g., missing -> None),
  violating the requirement that all functions must have type hints.
• This reduces consistency across the codebase and weakens static analysis benefits that the project
  expects from universal type annotations.
Code

tests/test_core.py[R21-33]

+class TestParseSource:
+    """Test parse_source utility."""
+
+    def test_parse_valid_code(self):
+        """Test parsing valid Python source code."""
+        tree = parse_source("def hello(): pass")
+        assert tree is not None
+
+    def test_parse_empty_source(self):
+        """Test parsing empty source code."""
+        tree = parse_source("")
+        assert tree is not None
+
Evidence
Compliance item 27 requires all functions to have type hints. The added test methods are defined
without return type annotations (and consistently throughout the new test file).

CLAUDE.md
tests/test_core.py[21-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new tests in `tests/test_core.py` define many functions without type hints, which violates the project requirement that all functions have type annotations.

## Issue Context
Pytest tests can still be annotated (commonly with `-&gt; None`) without impacting test execution.

## Fix Focus Areas
- tests/test_core.py[21-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Profiler not using core.schema 📎 Requirement gap ✓ Correctness
Description
• The performance profiler package still imports its own Severity and issue structures from
  workshop_mcp.performance_profiler.patterns rather than using
  workshop_mcp.core.schema/core.patterns as required by the checklist.
• This keeps the profiler partially coupled to its old internal infrastructure and undermines the
  goal of shared, unified tooling primitives.
Code

src/workshop_mcp/performance_profiler/init.py[R5-7]

+from .ast_analyzer import ASTAnalyzer, CallInfo, FunctionInfo, ImportInfo, LoopInfo
from .patterns import IssueCategory, PerformanceIssue, Severity
from .performance_checker import PerformanceChecker
Evidence
Compliance item 12 requires the performance profiler to use
core.ast_utils/core.inference/core.patterns/core.schema for extracted capabilities. The profiler
still imports from its local patterns module and does not use core.schema (or core.patterns,
which is also missing).

Refactor performance profiler to use core module utilities
src/workshop_mcp/performance_profiler/init.py[5-7]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Performance profiler code still relies on `performance_profiler.patterns` for issue/severity concepts instead of using the extracted core modules as required.

## Issue Context
This PR introduces `workshop_mcp.core.schema` but the profiler is not yet using it, leaving schema duplication/inconsistency.

## Fix Focus Areas
- src/workshop_mcp/performance_profiler/__init__.py[5-19]
- src/workshop_mcp/core/schema.py[1-29]
- src/workshop_mcp/core/__init__.py[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Unvalidated source bounds 🐞 Bug ✓ Correctness
Description
get_source_segment() documents 1-indexed inclusive line bounds but performs no validation;
  invalid inputs (e.g., line_start<=0, line_end<line_start) can silently return incorrect snippets
  due to Python slicing semantics.
• This can mislead downstream issue reporting since snippets are embedded into reported issues
  (e.g., PerformanceChecker includes code_snippet from get_source_segment).
• Add explicit bounds validation (or safe clamping) to prevent silent corruption of issue context.
Code

src/workshop_mcp/core/ast_utils.py[R184-196]

+def get_source_segment(source_code: str, line_start: int, line_end: int) -> str:
+    """Get a segment of source code by line numbers.
+
+    Args:
+        source_code: Full source code string.
+        line_start: Starting line number (1-indexed).
+        line_end: Ending line number (1-indexed, inclusive).
+
+    Returns:
+        Source code segment as a string.
+    """
+    lines = source_code.splitlines()
+    return "\n".join(lines[line_start - 1 : line_end])
Evidence
The implementation slices without validating the documented 1-indexed contract, so invalid bounds
won’t error and can produce misleading output. Downstream, the performance checker relies on these
snippets when constructing issues, so incorrect slicing produces incorrect issue context.

src/workshop_mcp/core/ast_utils.py[184-196]
src/workshop_mcp/performance_profiler/performance_checker.py[80-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`workshop_mcp.core.ast_utils.get_source_segment()` claims to accept 1-indexed inclusive line bounds but does not validate them. Python list slicing will silently accept invalid indices (e.g., 0 or negative) and return the wrong lines, which can mislead issue reports that include code snippets.

## Issue Context
Downstream code (e.g., `PerformanceChecker`) uses `get_source_segment()` to embed snippets into reported issues. If a caller ever passes invalid bounds (from AST metadata quirks or future tools), the snippet can be wrong without any signal.

## Fix Focus Areas
- src/workshop_mcp/core/ast_utils.py[184-196]
- src/workshop_mcp/performance_profiler/performance_checker.py[80-119]

## Suggested change
- Add checks like:
 - `if line_start &lt; 1: raise ValueError(...)`
 - `if line_end &lt; line_start: raise ValueError(...)`
 - `if line_end &gt; len(lines): raise ValueError(...)`
- Add a couple of unit tests for invalid bounds (start=0, end&lt;start, end&gt;EOF) to ensure the behavior is explicit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

8. Local import in traversal 🐞 Bug ➹ Performance
Description
_extract_calls_recursive() performs a from .inference import infer_callable inside the
  per-call processing path.
• While Python caches imports (so this is not a huge runtime cost), executing the import statement
  repeatedly for many call nodes is unnecessary overhead and reduces readability.
• Move the import to module scope (or pass a callable) to keep the traversal tight and predictable.
Code

src/workshop_mcp/core/ast_utils.py[R290-296]

+    if isinstance(node, astroid.Call):
+        function_name = _get_call_name(node.func)
+        if function_name:
+            from .inference import infer_callable
+
+            inferred_callable = infer_callable(node.func)
+
Evidence
The call-extraction recursion runs over the full AST and triggers this import statement for each
call node with a resolved name. This function is the source of CallInfo objects used by
PerformanceChecker, so it sits on a frequently executed analysis path.

src/workshop_mcp/core/ast_utils.py[290-296]
src/workshop_mcp/performance_profiler/performance_checker.py[80-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_extract_calls_recursive()` imports `infer_callable` inside the per-call block. Even though imports are cached, repeating the import statement is unnecessary in a traversal that may visit many call nodes.

## Issue Context
`extract_calls()` is used to build `CallInfo` lists consumed by performance checks. Keeping the traversal body minimal helps both clarity and efficiency.

## Fix Focus Areas
- src/workshop_mcp/core/ast_utils.py[271-309]

## Suggested change
- Move `from .inference import infer_callable` to the top of `ast_utils.py` (module scope).
- Remove the inner import.
- Ensure this does not introduce circular imports (it should not, since `inference.py` only imports `astroid`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +1 to +17
"""Core utilities for AST analysis shared across all code quality tools."""

from .ast_utils import (
CallInfo,
ClassInfo,
FunctionInfo,
ImportInfo,
LoopInfo,
extract_calls,
extract_classes,
extract_functions,
extract_imports,
get_source_segment,
parse_source,
)
from .inference import get_qualified_name, infer_callable
from .schema import CodeIssue, Severity
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.

Action required

1. Missing core/patterns.py 📎 Requirement gap ✓ Correctness

• The shared workshop_mcp.core package does not include a patterns.py module, even though the
  checklist requires it for centralized AST pattern helpers.
• This prevents other tools from reusing common call/loop/context detection logic and keeps generic
  helpers tied to performance_profiler.
Agent Prompt
## Issue description
The PR introduces `workshop_mcp.core` but does not include the required `core/patterns.py` module and helpers. The checklist requires these helpers to be centralized in core for reuse.

## Issue Context
The current codebase still relies on `workshop_mcp.performance_profiler.patterns`, which keeps generic helpers coupled to the performance profiler.

## Fix Focus Areas
- src/workshop_mcp/core/__init__.py[1-35]
- src/workshop_mcp/performance_profiler/__init__.py[5-7]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +72 to +197
def parse_source(source_code: str, file_path: str | None = None) -> astroid.Module:
"""Parse Python source code into an Astroid AST module.

Args:
source_code: Python source code as a string.
file_path: Optional file path for error reporting.

Returns:
Parsed Astroid Module node.

Raises:
SyntaxError: If the source code contains syntax errors.
"""
try:
return astroid.parse(source_code, path=file_path)
except astroid.AstroidSyntaxError as e:
raise SyntaxError(str(e)) from e


def extract_functions(tree: astroid.Module) -> list[FunctionInfo]:
"""Extract all function definitions from an Astroid AST.

Args:
tree: Parsed Astroid Module node.

Returns:
List of FunctionInfo objects containing function metadata.
"""
functions = []
for node in tree.nodes_of_class((astroid.FunctionDef, astroid.AsyncFunctionDef)):
func_info = _extract_function_info(node)
functions.append(func_info)
return functions


def extract_classes(tree: astroid.Module) -> list[ClassInfo]:
"""Extract all class definitions from an Astroid AST.

Args:
tree: Parsed Astroid Module node.

Returns:
List of ClassInfo objects containing class metadata.
"""
classes = []
for node in tree.nodes_of_class(astroid.ClassDef):
class_info = _extract_class_info(node)
classes.append(class_info)
return classes


def extract_imports(tree: astroid.Module) -> list[ImportInfo]:
"""Extract all import statements from an Astroid AST.

Args:
tree: Parsed Astroid Module node.

Returns:
List of ImportInfo objects containing import metadata.
"""
imports = []

# Handle regular imports
for node in tree.nodes_of_class(astroid.Import):
for name, alias in node.names:
import_info = ImportInfo(
module=name,
names=[name],
line_number=node.lineno,
is_from_import=False,
aliases={name: alias} if alias else {},
resolved_module=None,
)
imports.append(import_info)

# Handle from imports
for node in tree.nodes_of_class(astroid.ImportFrom):
if node.modname:
aliases = {}
names = []
for name, alias in node.names:
names.append(name)
if alias:
aliases[name] = alias

import_info = ImportInfo(
module=node.modname,
names=names,
line_number=node.lineno,
is_from_import=True,
aliases=aliases,
resolved_module=None,
)
imports.append(import_info)

return imports


def extract_calls(tree: astroid.Module) -> list[CallInfo]:
"""Extract all function calls from an Astroid AST.

Args:
tree: Parsed Astroid Module node.

Returns:
List of CallInfo objects containing call metadata.
"""
calls: list[CallInfo] = []
_extract_calls_recursive(tree, calls, None, False, False)
return calls


def get_source_segment(source_code: str, line_start: int, line_end: int) -> str:
"""Get a segment of source code by line numbers.

Args:
source_code: Full source code string.
line_start: Starting line number (1-indexed).
line_end: Ending line number (1-indexed, inclusive).

Returns:
Source code segment as a string.
"""
lines = source_code.splitlines()
return "\n".join(lines[line_start - 1 : line_end])

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.

Action required

2. walk_ast() not implemented 📎 Requirement gap ✓ Correctness

• The checklist requires core.ast_utils to provide walk_ast() as a reusable traversal utility,
  but the module currently only exposes parse/extract helpers.
• Downstream tools that need generic AST traversal will still have to implement their own walking
  logic, defeating the purpose of shared infrastructure.
Agent Prompt
## Issue description
`walk_ast()` is required by the checklist but is missing from `src/workshop_mcp/core/ast_utils.py`.

## Issue Context
Downstream tools may need a standard AST traversal utility for consistency and reuse.

## Fix Focus Areas
- src/workshop_mcp/core/ast_utils.py[72-197]
- src/workshop_mcp/core/__init__.py[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1 to +41
"""Astroid type inference utilities."""

import astroid


def infer_callable(node: astroid.NodeNG) -> str | None:
"""Try to infer the fully qualified name of a callable.

Uses Astroid's inference engine to resolve what a function actually is,
providing fully qualified names when possible.

Args:
node: An Astroid node representing a callable expression.

Returns:
Fully qualified name string, or None if inference fails.
"""
try:
inferred = next(node.infer(), None)
if inferred and hasattr(inferred, "qname"):
return inferred.qname()
except (astroid.InferenceError, StopIteration):
pass
return None


def get_qualified_name(node: astroid.NodeNG) -> str | None:
"""Get the qualified name for an Astroid node if available.

Args:
node: An Astroid node.

Returns:
Qualified name string, or None if not available.
"""
if hasattr(node, "qname"):
try:
return node.qname()
except Exception: # noqa: S110
return None
return None
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.

Action required

3. is_method_of() missing 📎 Requirement gap ✓ Correctness

• The shared inference module does not implement is_method_of(), which the checklist requires as
  part of reusable inference helpers.
• Without it, tools needing method/class relationship checks must re-implement logic or depend on
  performance-profiler-specific code.
Agent Prompt
## Issue description
`is_method_of()` is required by the checklist but is not present in `src/workshop_mcp/core/inference.py`.

## Issue Context
This helper is needed for reusable call/target analysis across tools.

## Fix Focus Areas
- src/workshop_mcp/core/inference.py[1-41]
- src/workshop_mcp/core/__init__.py[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +16 to +29
@dataclass
class CodeIssue:
"""Represents a code issue detected by any analysis tool."""

tool: str
category: str
severity: Severity
message: str
line: int
suggestion: str | None = None
code_snippet: str | None = None
function_name: str | None = None
end_line: int | None = None
column: int | None = None
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.

Action required

4. codeissue missing file 📎 Requirement gap ✓ Correctness

• The unified CodeIssue schema does not match the required ticket fields: it lacks the required
  file field and makes column optional while the checklist specifies it as part of the schema.
• This breaks the goal of consistent, cross-tool issue output and may force tools to create their
  own per-tool schemas or add ad-hoc fields.
Agent Prompt
## Issue description
`CodeIssue` in `core/schema.py` does not match the required unified issue schema (missing `file`, and field set differs from the required list).

## Issue Context
Downstream tools depend on a consistent schema across tools, so the dataclass fields must match the checklist acceptance criteria.

## Fix Focus Areas
- src/workshop_mcp/core/schema.py[16-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests/test_core.py
Comment on lines +21 to +33
class TestParseSource:
"""Test parse_source utility."""

def test_parse_valid_code(self):
"""Test parsing valid Python source code."""
tree = parse_source("def hello(): pass")
assert tree is not None

def test_parse_empty_source(self):
"""Test parsing empty source code."""
tree = parse_source("")
assert tree is not None

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.

Action required

5. Tests lack type hints 📘 Rule violation ✓ Correctness

• New test functions were added without parameter/return type annotations (e.g., missing -> None),
  violating the requirement that all functions must have type hints.
• This reduces consistency across the codebase and weakens static analysis benefits that the project
  expects from universal type annotations.
Agent Prompt
## Issue description
The new tests in `tests/test_core.py` define many functions without type hints, which violates the project requirement that all functions have type annotations.

## Issue Context
Pytest tests can still be annotated (commonly with `-> None`) without impacting test execution.

## Fix Focus Areas
- tests/test_core.py[21-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +5 to 7
from .ast_analyzer import ASTAnalyzer, CallInfo, FunctionInfo, ImportInfo, LoopInfo
from .patterns import IssueCategory, PerformanceIssue, Severity
from .performance_checker import PerformanceChecker
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.

Action required

6. Profiler not using core.schema 📎 Requirement gap ✓ Correctness

• The performance profiler package still imports its own Severity and issue structures from
  workshop_mcp.performance_profiler.patterns rather than using
  workshop_mcp.core.schema/core.patterns as required by the checklist.
• This keeps the profiler partially coupled to its old internal infrastructure and undermines the
  goal of shared, unified tooling primitives.
Agent Prompt
## Issue description
Performance profiler code still relies on `performance_profiler.patterns` for issue/severity concepts instead of using the extracted core modules as required.

## Issue Context
This PR introduces `workshop_mcp.core.schema` but the profiler is not yet using it, leaving schema duplication/inconsistency.

## Fix Focus Areas
- src/workshop_mcp/performance_profiler/__init__.py[5-19]
- src/workshop_mcp/core/schema.py[1-29]
- src/workshop_mcp/core/__init__.py[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +184 to +196
def get_source_segment(source_code: str, line_start: int, line_end: int) -> str:
"""Get a segment of source code by line numbers.

Args:
source_code: Full source code string.
line_start: Starting line number (1-indexed).
line_end: Ending line number (1-indexed, inclusive).

Returns:
Source code segment as a string.
"""
lines = source_code.splitlines()
return "\n".join(lines[line_start - 1 : line_end])
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.

Action required

7. Unvalidated source bounds 🐞 Bug ✓ Correctness

get_source_segment() documents 1-indexed inclusive line bounds but performs no validation;
  invalid inputs (e.g., line_start<=0, line_end<line_start) can silently return incorrect snippets
  due to Python slicing semantics.
• This can mislead downstream issue reporting since snippets are embedded into reported issues
  (e.g., PerformanceChecker includes code_snippet from get_source_segment).
• Add explicit bounds validation (or safe clamping) to prevent silent corruption of issue context.
Agent Prompt
## Issue description
`workshop_mcp.core.ast_utils.get_source_segment()` claims to accept 1-indexed inclusive line bounds but does not validate them. Python list slicing will silently accept invalid indices (e.g., 0 or negative) and return the wrong lines, which can mislead issue reports that include code snippets.

## Issue Context
Downstream code (e.g., `PerformanceChecker`) uses `get_source_segment()` to embed snippets into reported issues. If a caller ever passes invalid bounds (from AST metadata quirks or future tools), the snippet can be wrong without any signal.

## Fix Focus Areas
- src/workshop_mcp/core/ast_utils.py[184-196]
- src/workshop_mcp/performance_profiler/performance_checker.py[80-119]

## Suggested change
- Add checks like:
  - `if line_start < 1: raise ValueError(...)`
  - `if line_end < line_start: raise ValueError(...)`
  - `if line_end > len(lines): raise ValueError(...)`
- Add a couple of unit tests for invalid bounds (start=0, end<start, end>EOF) to ensure the behavior is explicit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: extract shared Astroid utilities into core module

1 participant