From 995507a6a46af79e74b4f501e4006efc9e6b06ba Mon Sep 17 00:00:00 2001 From: Ram Nadella Date: Sat, 19 Jul 2025 16:36:37 -0400 Subject: [PATCH 1/3] fix: correct off-by-one column offset in ruff parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pylight/src/parser/ruff.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylight/src/parser/ruff.rs b/pylight/src/parser/ruff.rs index 6d25b40..c1b7551 100644 --- a/pylight/src/parser/ruff.rs +++ b/pylight/src/parser/ruff.rs @@ -101,8 +101,8 @@ impl<'a> SymbolExtractor<'a> { let location = self .source_code .source_location(offset.into(), ruff_source_file::PositionEncoding::Utf8); - // Both line and column are 1-based in Ruff - (location.line.get(), location.character_offset.get()) + // Ruff returns 1-based line and column, but we need 0-based column for compatibility + (location.line.get(), location.character_offset.get() - 1) } } From 5e9c0141b81761fe9d5b5c474387c475f2ca5d0a Mon Sep 17 00:00:00 2001 From: Ram Nadella Date: Sat, 19 Jul 2025 16:39:05 -0400 Subject: [PATCH 2/3] test: add column position verification for both parsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pylight/src/parser/tests.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pylight/src/parser/tests.rs b/pylight/src/parser/tests.rs index 574c8ed..19860cd 100644 --- a/pylight/src/parser/tests.rs +++ b/pylight/src/parser/tests.rs @@ -125,4 +125,37 @@ class MyClass: .iter() .any(|s| s.name == "value" && s.kind == SymbolKind::Method)); } + + #[test] + fn test_column_positions() { + use crate::parser::{create_parser, ParserBackend}; + + // Test both parser backends + for backend in [ParserBackend::TreeSitter, ParserBackend::Ruff] { + let parser = create_parser(backend).unwrap(); + + // Test function column position + let code = "def my_func():\n pass"; + let symbols = parser.parse_file(Path::new("test.py"), code).unwrap(); + assert_eq!(symbols.len(), 1); + assert_eq!(symbols[0].name, "my_func"); + assert_eq!(symbols[0].line, 1); + assert_eq!(symbols[0].column, 4, "Function name should start at column 4 (0-based) for parser {:?}", backend); + + // Test class column position + let code = "class MyClass:\n pass"; + let symbols = parser.parse_file(Path::new("test.py"), code).unwrap(); + assert_eq!(symbols.len(), 1); + assert_eq!(symbols[0].name, "MyClass"); + assert_eq!(symbols[0].line, 1); + assert_eq!(symbols[0].column, 6, "Class name should start at column 6 (0-based) for parser {:?}", backend); + + // Test indented method column position + let code = "class MyClass:\n def my_method(self):\n pass"; + let symbols = parser.parse_file(Path::new("test.py"), code).unwrap(); + let method = symbols.iter().find(|s| s.name == "my_method").unwrap(); + assert_eq!(method.line, 2); + assert_eq!(method.column, 8, "Method name should start at column 8 (0-based) for parser {:?}", backend); + } + } } From f3a17a1a185aabf7d1b7b003eb6eeb3f55f26d6d Mon Sep 17 00:00:00 2001 From: Ram Nadella Date: Sat, 19 Jul 2025 16:41:21 -0400 Subject: [PATCH 3/3] style: apply cargo fmt to test file --- pylight/src/parser/tests.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pylight/src/parser/tests.rs b/pylight/src/parser/tests.rs index 19860cd..607d0de 100644 --- a/pylight/src/parser/tests.rs +++ b/pylight/src/parser/tests.rs @@ -129,33 +129,45 @@ class MyClass: #[test] fn test_column_positions() { use crate::parser::{create_parser, ParserBackend}; - + // Test both parser backends for backend in [ParserBackend::TreeSitter, ParserBackend::Ruff] { let parser = create_parser(backend).unwrap(); - + // Test function column position let code = "def my_func():\n pass"; let symbols = parser.parse_file(Path::new("test.py"), code).unwrap(); assert_eq!(symbols.len(), 1); assert_eq!(symbols[0].name, "my_func"); assert_eq!(symbols[0].line, 1); - assert_eq!(symbols[0].column, 4, "Function name should start at column 4 (0-based) for parser {:?}", backend); - + assert_eq!( + symbols[0].column, 4, + "Function name should start at column 4 (0-based) for parser {:?}", + backend + ); + // Test class column position let code = "class MyClass:\n pass"; let symbols = parser.parse_file(Path::new("test.py"), code).unwrap(); assert_eq!(symbols.len(), 1); assert_eq!(symbols[0].name, "MyClass"); assert_eq!(symbols[0].line, 1); - assert_eq!(symbols[0].column, 6, "Class name should start at column 6 (0-based) for parser {:?}", backend); - + assert_eq!( + symbols[0].column, 6, + "Class name should start at column 6 (0-based) for parser {:?}", + backend + ); + // Test indented method column position let code = "class MyClass:\n def my_method(self):\n pass"; let symbols = parser.parse_file(Path::new("test.py"), code).unwrap(); let method = symbols.iter().find(|s| s.name == "my_method").unwrap(); assert_eq!(method.line, 2); - assert_eq!(method.column, 8, "Method name should start at column 8 (0-based) for parser {:?}", backend); + assert_eq!( + method.column, 8, + "Method name should start at column 8 (0-based) for parser {:?}", + backend + ); } } }