Skip to content
Open
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
39 changes: 28 additions & 11 deletions code_review_graph/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3885,17 +3885,17 @@ def _emit_spring_injections(
) -> None:
"""Emit INJECTS edges for Spring DI injection points in a Java class.

Handles three patterns:
Handles four patterns:
- @Autowired / @Inject / @Resource field injection
- @Autowired constructor injection
- Lombok @RequiredArgsConstructor / @AllArgsConstructor with final fields
- Lombok @RequiredArgsConstructor: final + @NonNull non-static fields
- Lombok @AllArgsConstructor: all non-static fields
"""
if language != "java":
return

has_lombok_constructor = any(
a in _LOMBOK_CONSTRUCTOR_ANNOTATIONS for a in class_annotations
)
has_required_args = "RequiredArgsConstructor" in class_annotations
has_all_args = "AllArgsConstructor" in class_annotations
qualified_source = self._qualify(class_name, file_path, None)

# Find the class body
Expand All @@ -3906,7 +3906,7 @@ def _emit_spring_injections(
if member.type == "field_declaration":
self._emit_spring_field_injection(
member, qualified_source, file_path,
edges, has_lombok_constructor,
edges, has_required_args, has_all_args,
)
elif member.type == "constructor_declaration":
self._emit_spring_constructor_injection(
Expand All @@ -3919,9 +3919,15 @@ def _emit_spring_field_injection(
qualified_source: str,
file_path: str,
edges: list[EdgeInfo],
has_lombok_constructor: bool,
has_required_args: bool,
has_all_args: bool,
) -> None:
"""Emit an INJECTS edge for a single field_declaration if injection applies."""
"""Emit an INJECTS edge for a single field_declaration if injection applies.

Lombok semantics:
- @RequiredArgsConstructor: final fields + @NonNull-annotated fields (non-static)
- @AllArgsConstructor: all non-static fields regardless of final/@NonNull
"""
field_annotations: list[str] = []
has_final = False
has_static = False
Expand Down Expand Up @@ -3967,12 +3973,23 @@ def _emit_spring_field_injection(
return

has_inject_annotation = any(a in _SPRING_INJECT_ANNOTATIONS for a in field_annotations)
is_lombok_injected = has_lombok_constructor and has_final
has_non_null = "NonNull" in field_annotations

if not has_inject_annotation and not is_lombok_injected:
# @RequiredArgsConstructor: final + @NonNull fields
is_required_injected = has_required_args and (has_final or has_non_null)
# @AllArgsConstructor: every non-static field (static already filtered above)
is_all_injected = has_all_args

if not has_inject_annotation and not is_required_injected and not is_all_injected:
return

injection_type = "field" if has_inject_annotation else "constructor_lombok"
if has_inject_annotation:
injection_type = "field"
elif is_all_injected and not is_required_injected:
injection_type = "constructor_lombok_all"
else:
injection_type = "constructor_lombok"

extra: dict = {"injection_type": injection_type}
if field_name:
extra["field_name"] = field_name
Expand Down
20 changes: 20 additions & 0 deletions tests/fixtures/SpringDI.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.RequiredArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.NonNull;

// Plain interface — not a Spring bean
public interface OrderRepository {
Expand Down Expand Up @@ -63,6 +65,24 @@ public AuditLogger(OrderRepository orderRepository) {
public void log(String msg) {}
}

// @Service with @RequiredArgsConstructor — mixes final and @NonNull fields
@Service
@RequiredArgsConstructor
class PaymentService {
private final OrderRepository orderRepository; // final — injected
@NonNull
private NotificationService notificationService; // @NonNull non-final — injected
private String tag = "PaymentService"; // plain non-final — NOT injected
}

// @Component with @AllArgsConstructor — all non-static fields regardless of final
@Component
@AllArgsConstructor
class ReportService {
private OrderRepository orderRepository; // non-final — injected by @AllArgs
private NotificationService notificationService; // non-final — injected by @AllArgs
}

// @Configuration with @Bean factory methods
@Configuration
class AppConfig {
Expand Down
34 changes: 30 additions & 4 deletions tests/test_multilang.py
Original file line number Diff line number Diff line change
Expand Up @@ -2118,13 +2118,39 @@ def test_autowired_constructor_source_is_class(self):
sources = {e.source for e in injects}
assert any("AuditLogger" in s for s in sources)

def test_required_args_nonnull_field_injected(self):
"""@RequiredArgsConstructor should inject @NonNull non-final fields."""
injects = [e for e in self.edges if e.kind == "INJECTS"]
lombok_edges = [e for e in injects
if e.extra.get("injection_type") == "constructor_lombok"]
payment_targets = {e.target for e in lombok_edges if "PaymentService" in e.source}
assert "OrderRepository" in payment_targets
assert "NotificationService" in payment_targets

def test_required_args_plain_field_not_injected(self):
"""@RequiredArgsConstructor must NOT inject plain non-final, non-@NonNull fields."""
injects = [e for e in self.edges if e.kind == "INJECTS"]
payment_targets = {e.target for e in injects if "PaymentService" in e.source}
assert "String" not in payment_targets

def test_all_args_injects_all_non_static_fields(self):
"""@AllArgsConstructor should inject all non-static fields including non-final."""
injects = [e for e in self.edges if e.kind == "INJECTS"]
all_args_edges = [e for e in injects
if e.extra.get("injection_type") == "constructor_lombok_all"]
report_targets = {e.target for e in all_args_edges if "ReportService" in e.source}
assert "OrderRepository" in report_targets
assert "NotificationService" in report_targets

def test_total_injects_edge_count(self):
"""Sanity check: total INJECTS edges matches known injection points."""
injects = [e for e in self.edges if e.kind == "INJECTS"]
# NotificationService: 1 field
# OrderService: 2 lombok (orderRepository + notificationService)
# AuditLogger: 1 constructor
assert len(injects) >= 4
# NotificationService: 1 @Autowired field
# OrderService: 2 lombok (2 final fields)
# PaymentService: 2 lombok (1 final + 1 @NonNull)
# AuditLogger: 1 @Autowired constructor
# ReportService: 2 @AllArgsConstructor (2 non-final fields)
assert len(injects) >= 8

def test_field_name_stored_in_injects_extra(self):
"""INJECTS edges must carry extra.field_name for the resolver."""
Expand Down