Skip to content

fix(python): locate places a decorator-line position on the decorated callable - #412

Merged
rahlk merged 3 commits into
release/2.0from
fix/issue-408-locate-decorator-line
Sep 16, 2026
Merged

rahlk merged 3 commits into
release/2.0from
fix/issue-408-locate-decorator-line

Conversation

@rahlk

@rahlk rahlk commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #408. Stacked on #411 (the 1.5.3 bump) so the two CHANGELOG.md entries share one rc.8 section; retarget to release/2.0 once #411 merges.

PyCallable.start_line is the def line (ast.FunctionDef.lineno) and the AST puts decorators above it, so a position on @http.route(...) sat inside no callable span and locate reported module_scope. Both backends now read where the decorator is applied:

  • local: _find_innermost admits the callable whose decorators[].span starts at or before the line and above its def (_decorator_covers);
  • Neo4j: _LOCATE_QUERY gains one EXISTS disjunct over PY_DECORATED_BY.start_line, which codeanalyzer-python 1.5.2 emits.

Ranking is unchanged (def-based width), so a decorator on a nested callable resolves to the nested one. Not changed, and pinned by tests: a blank line with no decorator over it stays module_scope (no nearest-callable fallback); a class decorator stays module_scope; an analysis or graph from 1.5.1 or earlier, which records no decorator span, answers exactly as before and raises nothing.

Tests. The shared locate fixture gains cached (two decorators), outer.helper (decorator on a nested callable inside the enclosing span), old (decorator recorded without a span) and a @dataclass class. Fourteen new tests over both backends, parity included; the fake driver's responder evaluates the disjunct only when the statement names PY_DECORATED_BY and only over edges that carry start_line, so dropping either fails the suite.

Measured. odoo-slim-19 re-emitted by codeanalyzer-python 1.5.3 at level 1 (1,626 modules, 15,549 callables, 5,615 decorator edges), 40 positions (20 decorator lines, 20 body lines), median of 5, same graph both ways:

statement time placed decorator lines placed
old 71.0 ms 20/40 0/20
new 70.2 ms 40/40 20/20

The plan still seeks pysymbol_id (NodeUniqueIndexSeekByRange); the disjunct runs as SelectOrSemiApply over the seek's rows. The number is recorded above _LOCATE_QUERY.

Verification. tests/analysis/python offline: 496 passed, 168 skipped (all live-Neo4j tiers). Rest of the offline suite: 1298 passed, 236 skipped. docs/agent-api-reference.md gains a decorator-line row in the locate outcomes table.

Siblings checked. TypeScript's callable span starts at the first decorator line (measured with 1.6.0: start_line 4 for a method declared on line 6) and Java's at the first annotation line (measured with 3.3.3: span start [4, 5] for a method declared on line 6), so neither backend has this gap.

… callable (#408)

PyCallable.start_line is the def line (ast.FunctionDef.lineno) and the
AST puts decorators above it, so a position on @http.route(...) sat in
no callable span and fell through to module_scope. Both backends now
read where the decorator is applied: _find_innermost admits a callable
whose decorators[].span starts at or before the line and above its def;
_LOCATE_QUERY gains one EXISTS disjunct over PY_DECORATED_BY.start_line,
which codeanalyzer-python 1.5.2 emits. An older graph or analysis carries
no span there, the comparison is null, and the position stays module
scope. Ranking is unchanged (def-based width), so a decorator on a
nested callable resolves to the nested one. A class decorator is still
module scope, pinned. Fixture gains cached/outer.helper/old/Config;
fourteen new tests, parity included.
odoo-slim-19 re-emitted by codeanalyzer-python 1.5.3, 40 positions,
median of 5, same graph both ways: 71.0 ms placing 20/40 before the
PY_DECORATED_BY disjunct, 70.2 ms placing 40/40 with it; the plan still
seeks pysymbol_id.
@github-actions github-actions Bot added the fix Bug fixes label Sep 16, 2026
@rahlk

rahlk commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Live tier on the 1.5.3 graph (cldk-408-it, bolt 7698, odoo-slim-19 re-emitted at level 1): test_e2e_neo4j_live.py, test_bounded_enumeration.py, test_resolve.py, test_neo4j_self_loops.py.

branch result
release/2.0 (same graph, via a worktree) 8 failed, 100 passed, 17 skipped
this branch 8 failed, 100 passed, 17 skipped, identical failure set

Every locate live test passes on both. The eight failures pre-exist and are about the graph, not this change:

  • six are the level-1 emit: no PY_RESOLVES_TO, no formal_in ports, so resolve_value raises SelectorNotInGraph and the resolution-edge probes find nothing;
  • test_locate_parity_documented_module_source_divergence's live twin pins the stale module_source_unavailable divergence that Incorporate codeanalyzer-python v1.5.2 #396 tracks — :PyModule.source is present on this graph (1,626 of 1,626);
  • test_get_config_keys_and_config_uses_line_up: get_config_uses() came back empty on a graph that carries PY_USES_CONFIG edges. Not touched here; worth its own look.

@rahlk
rahlk changed the base branch from chore/issue-410-codeanalyzer-python-1.5.3 to release/2.0 September 16, 2026 08:20
@rahlk
rahlk merged commit 69aa675 into release/2.0 Sep 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant