Skip to content

Commit 216a926

Browse files
ram-nadellaclaude
andauthored
Fix off-by-one column offset in ruff parser (#25)
* fix: correct off-by-one column offset in ruff parser The ruff parser returns 1-based column positions while the codebase expects 0-based positions (matching tree-sitter's behavior). This fix subtracts 1 from the column position to convert from 1-based to 0-based indexing. This ensures symbol positions correctly point to the first character of function/class names rather than one character into the name. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add column position verification for both parsers Added comprehensive test to verify that both tree-sitter and ruff parsers report consistent column positions for function and class names. This test ensures the fix for the ruff parser's off-by-one column issue is working correctly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * style: apply cargo fmt to test file --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc1959f commit 216a926

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

pylight/src/parser/ruff.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ impl<'a> SymbolExtractor<'a> {
101101
let location = self
102102
.source_code
103103
.source_location(offset.into(), ruff_source_file::PositionEncoding::Utf8);
104-
// Both line and column are 1-based in Ruff
105-
(location.line.get(), location.character_offset.get())
104+
// Ruff returns 1-based line and column, but we need 0-based column for compatibility
105+
(location.line.get(), location.character_offset.get() - 1)
106106
}
107107
}
108108

pylight/src/parser/tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,49 @@ class MyClass:
125125
.iter()
126126
.any(|s| s.name == "value" && s.kind == SymbolKind::Method));
127127
}
128+
129+
#[test]
130+
fn test_column_positions() {
131+
use crate::parser::{create_parser, ParserBackend};
132+
133+
// Test both parser backends
134+
for backend in [ParserBackend::TreeSitter, ParserBackend::Ruff] {
135+
let parser = create_parser(backend).unwrap();
136+
137+
// Test function column position
138+
let code = "def my_func():\n pass";
139+
let symbols = parser.parse_file(Path::new("test.py"), code).unwrap();
140+
assert_eq!(symbols.len(), 1);
141+
assert_eq!(symbols[0].name, "my_func");
142+
assert_eq!(symbols[0].line, 1);
143+
assert_eq!(
144+
symbols[0].column, 4,
145+
"Function name should start at column 4 (0-based) for parser {:?}",
146+
backend
147+
);
148+
149+
// Test class column position
150+
let code = "class MyClass:\n pass";
151+
let symbols = parser.parse_file(Path::new("test.py"), code).unwrap();
152+
assert_eq!(symbols.len(), 1);
153+
assert_eq!(symbols[0].name, "MyClass");
154+
assert_eq!(symbols[0].line, 1);
155+
assert_eq!(
156+
symbols[0].column, 6,
157+
"Class name should start at column 6 (0-based) for parser {:?}",
158+
backend
159+
);
160+
161+
// Test indented method column position
162+
let code = "class MyClass:\n def my_method(self):\n pass";
163+
let symbols = parser.parse_file(Path::new("test.py"), code).unwrap();
164+
let method = symbols.iter().find(|s| s.name == "my_method").unwrap();
165+
assert_eq!(method.line, 2);
166+
assert_eq!(
167+
method.column, 8,
168+
"Method name should start at column 8 (0-based) for parser {:?}",
169+
backend
170+
);
171+
}
172+
}
128173
}

0 commit comments

Comments
 (0)