Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.4] - 2026-05-01

### Added
- **Line number storage** - Functions, methods, and classes now store their source code line numbers
- Added `line_number` field to `FunctionInfo` and `ClassInfo` models
- AST extractor captures `lineno` attribute from AST nodes
- Graph loader stores `line_number` property in Neo4j for Function, Method, and Class nodes
- Parameter complexity quality rule now returns line numbers in violation output
- Enables "navigate to source" functionality for quality rule violations
- **User Outcome**: Users can navigate directly to violations in their code
- All 289 unit tests passing

## [0.8.3] - 2026-04-14

### Changed
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,5 +422,5 @@ Review these documents to understand patterns and best practices:

---

**Last Updated**: 2026-04-14
**Current Version**: 0.8.3
**Last Updated**: 2026-05-01
**Current Version**: 0.8.4
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mapper (Application Mapper)

![Version](https://img.shields.io/badge/version-0.8.3-blue.svg)
![Version](https://img.shields.io/badge/version-0.8.4-blue.svg)
![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/ydkadri/9501806ed5eac873dd324bc606c6dd79/raw/mapper-tests.json&cacheSeconds=300)
![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/ydkadri/9501806ed5eac873dd324bc606c6dd79/raw/mapper-coverage.json&cacheSeconds=300)
![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mapper"
version = "0.8.3"
version = "0.8.4"
description = "Mapper (Application Mapper) - AST-based Python code analyzer with Neo4j graph storage"
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -93,7 +93,7 @@ addopts = [
testpaths = ["tests"]

[tool.bumpversion]
current_version = "0.8.3"
current_version = "0.8.4"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
Expand Down
2 changes: 1 addition & 1 deletion src/mapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Public modules for programmatic access
from mapper import analyser, graph, graph_loader

__version__ = "0.8.3"
__version__ = "0.8.4"

__all__ = [
# Version
Expand Down
2 changes: 2 additions & 0 deletions src/mapper/ast_parser/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def _extract_class(self, node: ast.ClassDef) -> models.ClassInfo:
return models.ClassInfo(
name=node.name,
is_public=self._is_public(node.name),
line_number=node.lineno,
docstring=ast.get_docstring(node),
bases=bases,
decorators=decorators,
Expand Down Expand Up @@ -189,6 +190,7 @@ def _extract_function(self, node: ast.FunctionDef) -> models.FunctionInfo:
return models.FunctionInfo(
name=node.name,
is_public=self._is_public(node.name),
line_number=node.lineno,
docstring=ast.get_docstring(node),
parameters=parameters,
return_type=return_type,
Expand Down
2 changes: 2 additions & 0 deletions src/mapper/ast_parser/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class FunctionInfo:

name: str
is_public: bool
line_number: int | None = None
docstring: str | None = None
parameters: list[ParameterInfo] = attrs.field(factory=list)
return_type: str | None = None
Expand All @@ -124,6 +125,7 @@ class ClassInfo:

name: str
is_public: bool
line_number: int | None = None
docstring: str | None = None
bases: list[str] = attrs.field(factory=list)
decorators: list[DecoratorInfo] = attrs.field(factory=list)
Expand Down
4 changes: 4 additions & 0 deletions src/mapper/graph_loader/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ def _create_class_node(self, class_info: ast_parser.models.ClassInfo, fqn: str)
"package": self.package_name,
"is_public": class_info.is_public,
}
if class_info.line_number is not None:
properties["line_number"] = class_info.line_number
if class_info.docstring:
properties["docstring"] = class_info.docstring
if class_info.bases:
Expand Down Expand Up @@ -296,6 +298,8 @@ def _create_function_node(
"package": self.package_name,
"is_public": func_info.is_public,
}
if func_info.line_number is not None:
properties["line_number"] = func_info.line_number
if func_info.docstring:
properties["docstring"] = func_info.docstring
if func_info.return_type:
Expand Down
2 changes: 1 addition & 1 deletion src/mapper/quality/rules/param_complexity.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def run(
RETURN m.path as file_path,
collect({
function: f.name,
line: null,
line: f.line_number,
param_count: param_count
}) as violations
ORDER BY file_path
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/ast_parser/test_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,56 @@ class _PrivateClass:

init_method = next(m for m in public_class.methods if m.name == "__init__")
assert init_method.is_public is True

def test_line_number_extraction(self):
"""Test that line numbers are captured for functions and classes."""
code = textwrap.dedent('''
"""Module docstring."""

def first_function():
"""First function on line 4."""
pass

def second_function():
"""Second function on line 7."""
pass

class FirstClass:
"""First class on line 11."""
pass

class SecondClass:
"""Second class on line 15."""

def method_one(self):
"""Method on line 18."""
pass

def method_two(self):
"""Method on line 22."""
pass
''')

extractor = ast_parser.ASTExtractor(code, "module.py")
result = extractor.extract()

# Check function line numbers
first_func = next(f for f in result.functions if f.name == "first_function")
assert first_func.line_number == 4

second_func = next(f for f in result.functions if f.name == "second_function")
assert second_func.line_number == 8

# Check class line numbers
first_class = next(c for c in result.classes if c.name == "FirstClass")
assert first_class.line_number == 12

second_class = next(c for c in result.classes if c.name == "SecondClass")
assert second_class.line_number == 16

# Check method line numbers
method_one = next(m for m in second_class.methods if m.name == "method_one")
assert method_one.line_number == 19

method_two = next(m for m in second_class.methods if m.name == "method_two")
assert method_two.line_number == 23
87 changes: 87 additions & 0 deletions tests/unit/graph_loader/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,93 @@ def test_load_with_external_base_class(self):
assert mock_connection.create_node.call_count >= 2 # module + class
# Relationship creation depends on whether base class exists in graph

def test_load_with_line_numbers(self):
"""Test that line numbers are stored in Neo4j nodes."""
mock_connection = Mock()
loader = graph_loader.GraphLoader(mock_connection, package_name="test-pkg")

module_info = ast_parser.models.ModuleInfo(path="test.py", name="test")

# Function with line number
func_info = ast_parser.models.FunctionInfo(
name="my_function",
is_public=True,
line_number=42,
docstring="Function on line 42",
)

# Class with line number
class_info = ast_parser.models.ClassInfo(
name="MyClass",
is_public=True,
line_number=100,
docstring="Class on line 100",
methods=[
ast_parser.models.FunctionInfo(
name="my_method",
is_public=True,
line_number=105,
docstring="Method on line 105",
)
],
)

extraction = ast_parser.models.ExtractionResult(
module=module_info,
functions=[func_info],
classes=[class_info],
)

loader.load_extraction(extraction)

# Module + Class + Method + Function = 4 nodes (classes processed before functions)
assert mock_connection.create_node.call_count == 4

# Verify class node has line_number (call 1 - first after module)
class_call = mock_connection.create_node.call_args_list[1]
class_props = class_call[0][1]
assert class_props["name"] == "MyClass"
assert class_props["line_number"] == 100

# Verify method node has line_number (call 2 - inside class)
method_call = mock_connection.create_node.call_args_list[2]
method_props = method_call[0][1]
assert method_props["name"] == "my_method"
assert method_props["line_number"] == 105

# Verify function node has line_number (call 3 - functions processed after classes)
func_call = mock_connection.create_node.call_args_list[3]
func_props = func_call[0][1]
assert func_props["name"] == "my_function"
assert func_props["line_number"] == 42

def test_load_without_line_numbers(self):
"""Test that missing line numbers (None) are handled gracefully."""
mock_connection = Mock()
loader = graph_loader.GraphLoader(mock_connection, package_name="test-pkg")

module_info = ast_parser.models.ModuleInfo(path="test.py", name="test")

# Function without line number (backwards compatibility)
func_info = ast_parser.models.FunctionInfo(
name="my_function",
is_public=True,
line_number=None,
)

extraction = ast_parser.models.ExtractionResult(
module=module_info,
functions=[func_info],
)

loader.load_extraction(extraction)

# Verify function node was created without line_number property
func_call = mock_connection.create_node.call_args_list[1]
func_props = func_call[0][1]
assert func_props["name"] == "my_function"
assert "line_number" not in func_props # Should not include None values


class TestGraphLoaderBatch:
"""Tests for batch loading operations."""
Expand Down
Loading