6565
6666--- Extracts path handling multiline attributes
6767function 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
368419function 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