Skip to content

Commit de74bc2

Browse files
authored
Merge pull request #49 from zerochae/feature/fastapi-multiline-support
feat: Complete FastAPI multiline support and resolve DotNet parsing artifacts
2 parents ba44ad7 + 751ada7 commit de74bc2

10 files changed

Lines changed: 1043 additions & 142 deletions

File tree

TODO.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# TODO - endpoint.nvim
2+
3+
## Comment Filtering Enhancement
4+
5+
### Problem Description
6+
**Issue**: endpoint.nvim currently detects commented-out code as valid endpoints across all frameworks.
7+
8+
**Examples of problematic detection:**
9+
```python
10+
# FastAPI - These commented lines show up in results
11+
# @router.delete(
12+
# "/{product_id}",
13+
# status_code=status.HTTP_204_NO_CONTENT
14+
# )
15+
# Results in: DELETE /{product_id} (should be ignored)
16+
```
17+
18+
```csharp
19+
// C# - These commented lines show up in results
20+
// [HttpGet("users/{id}")]
21+
// public async Task<User> GetUser(int id) { }
22+
// Results in: GET /users/{id} (should be ignored)
23+
```
24+
25+
```typescript
26+
// NestJS - These commented lines show up in results
27+
// @Get('users/:id')
28+
// async getUser(@Param('id') id: string) { }
29+
// Results in: GET /users/:id (should be ignored)
30+
```
31+
32+
**Root cause**: ripgrep searches for patterns (e.g., `@router.get`, `[HttpGet]`, `@Get`) without considering if they're commented out.
33+
34+
### Current Implementation Status
35+
- **DotNet parser**: ✅ Has `_is_commented_code()` function that filters out commented attributes (`//` and `/* */`)
36+
- **All other parsers**: ❌ No comment filtering implemented yet
37+
38+
**DotNet implementation location**: `lua/endpoint/parser/dotnet_parser.lua:_is_commented_code()`
39+
40+
### Proposed Solution
41+
Implement user-configurable comment filtering system:
42+
43+
#### Phase 1: Extend Comment Filtering to All Frameworks
44+
- [ ] **FastAPI**: Add Python comment filtering (`#`)
45+
- [ ] **NestJS**: Add TypeScript comment filtering (`//`, `/* */`)
46+
- [ ] **Ktor**: Add Kotlin comment filtering (`//`, `/* */`)
47+
- [ ] **Spring**: Add Java comment filtering (`//`, `/* */`)
48+
- [ ] **Symfony**: Add PHP comment filtering (`//`, `/* */`, `#`)
49+
- [ ] **Express**: Add JavaScript comment filtering (`//`, `/* */`)
50+
- [ ] **Rails**: Add Ruby comment filtering (`#`)
51+
52+
#### Phase 2: Create User Configuration Option
53+
Add configuration option to control comment filtering behavior:
54+
55+
```lua
56+
-- User config example
57+
{
58+
comment_filtering = {
59+
enabled = true, -- Default: ignore commented endpoints
60+
per_language = {
61+
python = true, -- FastAPI, Django
62+
typescript = true, -- NestJS, Express-TS
63+
javascript = true, -- Express
64+
csharp = true, -- DotNet
65+
java = true, -- Spring, Servlet
66+
kotlin = true, -- Ktor
67+
php = true, -- Symfony
68+
ruby = true -- Rails
69+
}
70+
}
71+
}
72+
```
73+
74+
#### Phase 3: Implementation Details
75+
1. **Base comment detection patterns** by language:
76+
- Python: `^\\s*#`
77+
- JavaScript/TypeScript: `^\\s*//`, `^\\s*/\\*`
78+
- C#/Java/Kotlin: `^\\s*//`, `^\\s*/\\*`
79+
- PHP: `^\\s*//`, `^\\s*/\\*`, `^\\s*#`
80+
- Ruby: `^\\s*#`
81+
82+
2. **Utility function** in base Parser class:
83+
```lua
84+
function Parser:is_commented_line(line, language)
85+
-- Language-specific comment detection
86+
end
87+
```
88+
89+
3. **Configuration integration**:
90+
- Read from user config
91+
- Apply filters before endpoint creation
92+
- Fallback to enabled=true for better UX
93+
94+
### Use Cases
95+
- **Default behavior**: Clean results without commented code (recommended for most users)
96+
- **Development mode**: Include commented code for debugging (when temporarily commenting endpoints)
97+
- **Legacy code analysis**: See all patterns including commented ones (for codebase analysis)
98+
- **Team preferences**: Some teams might want to see commented endpoints as "potential future endpoints"
99+
100+
### Benefits
101+
- **User choice**: Flexible configuration per project/language needs
102+
- **Clean results**: Better default experience - no confusion from dead code
103+
- **Debugging support**: Option to see all matches when needed for troubleshooting
104+
- **Consistent behavior**: Same logic across all frameworks (FastAPI, NestJS, Spring, etc.)
105+
106+
### Technical Notes
107+
- **Implementation approach**: Extend each parser's `parse_content()` or `is_content_valid_for_parsing()` method
108+
- **Pattern detection**: Check if the line containing the endpoint pattern starts with language-specific comment syntax
109+
- **Configuration integration**: Hook into existing user config system in endpoint.nvim
110+
- **Performance impact**: Minimal - just additional regex check per matched line
111+
- **Backward compatibility**: Default to filtering enabled, so existing users get cleaner results
112+
113+
### Related Files to Modify
114+
- `lua/endpoint/parser/fastapi_parser.lua` - Add Python comment filtering
115+
- `lua/endpoint/parser/nestjs_parser.lua` - Add TypeScript/JavaScript comment filtering
116+
- `lua/endpoint/parser/spring_parser.lua` - Add Java comment filtering
117+
- `lua/endpoint/parser/ktor_parser.lua` - Add Kotlin comment filtering
118+
- `lua/endpoint/parser/symfony_parser.lua` - Add PHP comment filtering
119+
- `lua/endpoint/parser/express_parser.lua` - Add JavaScript comment filtering
120+
- `lua/endpoint/parser/rails_parser.lua` - Add Ruby comment filtering
121+
- User config system - Add comment filtering configuration options
122+
123+
---
124+
125+
*Created: 2024-09-24*
126+
*Context: Discovered during FastAPI multiline testing - commented endpoints were appearing in results*
127+
*Priority: Medium*
128+
*Estimated effort: 2-3 sessions*
129+
*Status: Planning phase - DotNet implementation exists as reference*

lua/endpoint/parser/dotnet_parser.lua

Lines changed: 98 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,55 @@ end
6565

6666
---Extracts path handling multiline attributes
6767
function DotNetParser:_extract_path_multiline(file_path, start_line, content)
68-
-- First try single line extraction
69-
local path = self:_extract_path_single_line(content, file_path, start_line)
68+
-- Clean the input content first - this removes ripgrep artifacts
69+
local clean_content = self:_clean_multiline_content(content)
70+
71+
-- First try single line extraction from cleaned content
72+
local path = self:_extract_path_single_line(clean_content, file_path, start_line)
7073
if path then
7174
return path, nil -- Single line, no end_line
7275
end
7376

74-
-- If it's a multiline attribute, read the file to find the complete attribute
75-
if self:_is_multiline_attribute(content) then
76-
local file = io.open(file_path, "r")
77-
if not file then
78-
return nil, nil
77+
-- For bare HTTP attributes like [HttpGet], look for nearby [Route(...)] in the file
78+
local file = io.open(file_path, "r")
79+
if not file then
80+
return nil, nil
81+
end
82+
83+
local lines = {}
84+
for line in file:lines() do
85+
table.insert(lines, line)
86+
end
87+
file:close()
88+
89+
-- Check if this is a bare HTTP attribute (like [HttpGet] without path)
90+
if clean_content:match "^%s*%[Http%w+%]%s*$" then
91+
-- Look for [Route(...)] in the next few lines
92+
local extracted_path = nil
93+
local attribute_end_line = nil
94+
95+
for i = start_line, math.min(start_line + 5, #lines) do
96+
local line = lines[i]
97+
if line then
98+
local route_match = line:match '%[Route%(%s*"([^"]*)"'
99+
if route_match then
100+
extracted_path = route_match
101+
attribute_end_line = i
102+
break
103+
end
104+
end
79105
end
80106

81-
local lines = {}
82-
for line in file:lines() do
83-
table.insert(lines, line)
107+
-- Return the path from Route attribute
108+
if extracted_path ~= nil then -- Allow empty string paths
109+
return extracted_path, attribute_end_line
84110
end
85-
file:close()
111+
end
86112

113+
-- If it's a multiline attribute with parentheses, read the file to find the complete attribute
114+
if self:_is_multiline_attribute(clean_content) then
87115
-- Read the next few lines to find the complete attribute
88-
local multiline_content = content
116+
local multiline_content = clean_content
89117
local extracted_path = nil
90118
local attribute_end_line = nil
91119

@@ -99,8 +127,14 @@ function DotNetParser:_extract_path_multiline(file_path, start_line, content)
99127
extracted_path = self:_extract_path_single_line(multiline_content, file_path, start_line)
100128
end
101129

102-
-- If we hit closing parenthesis followed by closing bracket, this is the end
103-
if next_line:match "%s*%)%s*$" then
130+
-- Look for attribute closing: )] (more specific than just ))
131+
if next_line:match "%s*%)%s*%]%s*$" then
132+
attribute_end_line = i
133+
break
134+
end
135+
136+
-- Also check for single line with )] anywhere
137+
if next_line:match "%)%]" then
104138
attribute_end_line = i
105139
break
106140
end
@@ -227,8 +261,9 @@ function DotNetParser:parse_content(content, file_path, line_number, column)
227261
}
228262

229263
-- Add end_line_number if multiline
264+
-- Note: Highlighter does (end_line - 1), so we add +1 to include the closing bracket
230265
if end_line_number then
231-
endpoint.end_line_number = end_line_number
266+
endpoint.end_line_number = end_line_number + 1
232267
end
233268

234269
table.insert(endpoints, endpoint)
@@ -316,21 +351,33 @@ function DotNetParser:_clean_multiline_content(content)
316351
-- Aggressively clean multiline ripgrep artifacts
317352
local clean_content = content
318353

319-
-- Strategy 1: Look for attribute patterns and stop at their natural end
354+
-- Strategy 1: Look for HTTP method + Route attribute combinations first
355+
356+
-- Pattern 1: [HttpX] followed by [Route(...)] - preserve both
357+
local http_and_route = clean_content:match("(%[Http%w+%]%s*%[Route%([^%)]*%)%])")
358+
if http_and_route then
359+
return http_and_route
360+
end
320361

321-
-- Pattern 1: [HttpX("path")] - find the closing of the attribute
322-
local attr_with_path = clean_content:match("(%[Http%w+%([^%)]*%))")
362+
-- Pattern 2: [HttpX("path")] - single attribute with path
363+
local attr_with_path = clean_content:match("(%[Http%w+%([^%)]*%)%])")
323364
if attr_with_path then
324365
return attr_with_path
325366
end
326367

327-
-- Pattern 2: [HttpX] - bare attribute
368+
-- Pattern 3: [HttpX] - bare attribute (check if Route follows)
328369
local bare_attr = clean_content:match("(%[Http%w+%])")
329370
if bare_attr then
371+
-- Look for Route attribute that might follow
372+
local remaining = clean_content:sub(clean_content:find("%[Http%w+%]") + #bare_attr)
373+
local route_part = remaining:match("^%s*(%[Route%([^%)]*%)%])")
374+
if route_part then
375+
return bare_attr .. " " .. route_part
376+
end
330377
return bare_attr
331378
end
332379

333-
-- Pattern 3: [Route("path")] - find the closing of the route attribute
380+
-- Pattern 4: [Route("path")] only - find the closing of the route attribute
334381
local route_attr = clean_content:match("(%[Route%([^%)]*%)%])")
335382
if route_attr then
336383
return route_attr
@@ -340,11 +387,15 @@ function DotNetParser:_clean_multiline_content(content)
340387

341388
-- Cut at first occurrence of patterns that indicate we've gone too far
342389
local cut_patterns = {
343-
"%] %[", -- Multiple attributes on separate lines
344-
"%]%s*public", -- Reached method definition
390+
"%]%s*public", -- Reached class or method definition
345391
"%]%s*{", -- Reached method body
346-
"%] %w+", -- Reached other attributes like [Authorize]
347-
"%)%] %[", -- End of one attribute, start of another
392+
"%]%s*private", -- Reached private method
393+
"%]%s*protected", -- Reached protected method
394+
"%)%]%s*public", -- Route attribute followed by class definition
395+
"%)%].*public%s+class", -- Route attribute followed by class
396+
"%]%s*public%s+async", -- Reached async method definition
397+
"%]%s*public%s+.*Task", -- Reached method returning Task
398+
"%]%s*public%s+.*ActionResult", -- Reached method returning ActionResult
348399
}
349400

350401
for _, pattern in ipairs(cut_patterns) do
@@ -368,8 +419,13 @@ end
368419
function DotNetParser:_contains_unwanted_artifacts(content)
369420
-- Only filter obvious artifacts, let the improved regex patterns do the work
370421

371-
-- Filter out content that contains multiple attributes (ripgrep artifact)
422+
-- Filter out content that contains multiple attributes BUT allow HTTP method + Route combinations
372423
if content:match "%]%s*%[" then
424+
-- Allow [HttpX] followed by [Route(...)]
425+
if content:match "%[Http%w+%]%s*%[Route%(" then
426+
return false
427+
end
428+
-- Reject other multiple attribute patterns
373429
return true
374430
end
375431

@@ -591,24 +647,34 @@ function DotNetParser:_extract_route_info(content, file_path, line_number)
591647
if cleaned_content:match "%[Http(%w+)%]$" then
592648
method_from_content = cleaned_content:match "%[Http(%w+)%]"
593649

594-
-- Look for [Route(...)] in surrounding lines
595-
for i = math.max(1, line_number - 5), math.min(#lines, line_number + 5) do
650+
-- Look for [Route(...)] in next few lines only (method-level routes come after HTTP attributes)
651+
for i = line_number, math.min(#lines, line_number + 3) do
596652
local line = lines[i]
597653
if line and line:match "%[Route%(" then
598-
-- Read multiple lines to get complete Route attribute
654+
-- First try simple single line match
655+
local route_path = line:match "%[Route%([^%)]*[\"']([^\"']*)[\"']"
656+
if route_path ~= nil then -- Allow empty strings
657+
return method_from_content, route_path
658+
end
659+
660+
-- If not found, try multiline (but only read until ])
599661
local route_content = line
600-
for j = i + 1, math.min(i + 5, #lines) do
662+
for j = i + 1, math.min(i + 3, #lines) do -- Only read next 3 lines max
601663
local next_line = lines[j]
602664
if next_line then
665+
-- Stop if we hit a method definition or other attribute
666+
if next_line:match "public%s+" or next_line:match "%[%w+" then
667+
break
668+
end
603669
route_content = route_content .. " " .. next_line:gsub("^%s+", ""):gsub("%s+$", "")
604-
if next_line:match "%s*%)%s*$" then
670+
if next_line:match "%s*%)%s*%]%s*$" then -- Look for complete )] ending
605671
break
606672
end
607673
end
608674
end
609675

610-
local route_path = route_content:match "%[Route%([^%)]*[\"']([^\"']+)[\"']"
611-
if route_path then
676+
route_path = route_content:match "%[Route%([^%)]*[\"']([^\"']*)[\"']"
677+
if route_path ~= nil then -- Allow empty strings
612678
return method_from_content, route_path
613679
end
614680
end

0 commit comments

Comments
 (0)