Skip to content

Commit c91dc9b

Browse files
authored
Merge pull request #44 from zerochae/feature/ktor-multiline-complete
feat: implement comprehensive Ktor multiline routing support
2 parents be438cb + f4b6014 commit c91dc9b

4 files changed

Lines changed: 166 additions & 14 deletions

File tree

check.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
- 멀티라인 서치
22
## spring ✅ (멀티라인 어노테이션 지원 추가)
3-
## ktor
3+
## ktor ✅ (멀티라인 라우팅 지원 추가)
44
## rails ✅
55
## servlet ❌
66
## symfony ❌

lua/endpoint/core/Parser.lua

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,26 @@ function Parser:combine_paths(base_path, endpoint_path)
4747
return base_path
4848
end
4949

50+
-- Special case: both paths are root "/"
51+
if base_path == "/" and endpoint_path == "/" then
52+
return "/"
53+
end
54+
5055
-- Remove trailing slash from base and leading slash from endpoint
51-
base_path = base_path:gsub("/$", "")
52-
endpoint_path = endpoint_path:gsub("^/", "")
56+
local clean_base = base_path:gsub("/$", "")
57+
local clean_endpoint = endpoint_path:gsub("^/", "")
5358

5459
-- Handle root endpoint case
55-
if endpoint_path == "" then
56-
return base_path
60+
if clean_endpoint == "" then
61+
return clean_base == "" and "/" or clean_base
62+
end
63+
64+
-- If base becomes empty after removing trailing slash, it was root
65+
if clean_base == "" then
66+
return "/" .. clean_endpoint
5767
end
5868

59-
return base_path .. "/" .. endpoint_path
69+
return clean_base .. "/" .. clean_endpoint
6070
end
6171

6272
---Parses content to extract endpoint information (unified implementation)

lua/endpoint/parser/ktor_parser.lua

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,24 @@ function KtorParser:extract_base_path(file_path, line_number)
2525
end
2626

2727
---Extracts endpoint path from Ktor routing content
28-
function KtorParser:extract_endpoint_path(content)
28+
function KtorParser:extract_endpoint_path(content, file_path, line_number)
29+
-- Use multiline extraction for better accuracy
30+
if file_path and line_number then
31+
local path, end_line = self:_extract_path_multiline(file_path, line_number, content)
32+
if path then
33+
-- Store end_line_number for highlighting
34+
self._last_end_line_number = end_line
35+
return path
36+
end
37+
end
38+
39+
-- Fallback to single line extraction
40+
self._last_end_line_number = nil
41+
return self:_extract_path_single_line(content)
42+
end
43+
44+
---Extracts path from single line content
45+
function KtorParser:_extract_path_single_line(content)
2946
-- Handle multiline patterns by normalizing whitespace
3047
local normalized_content = content:gsub("%s+", " "):gsub("[\r\n]+", " ")
3148

@@ -56,6 +73,102 @@ function KtorParser:extract_endpoint_path(content)
5673
return nil
5774
end
5875

76+
---Extracts path handling multiline routing definitions
77+
function KtorParser:_extract_path_multiline(file_path, start_line, content)
78+
-- First try single line extraction
79+
local path = self:_extract_path_single_line(content)
80+
if path then
81+
return path, nil -- Single line, no end_line
82+
end
83+
84+
-- If it's a multiline routing definition, read the file to find the path
85+
if self:_is_multiline_routing(content) then
86+
local file = io.open(file_path, "r")
87+
if not file then
88+
return nil, nil
89+
end
90+
91+
local lines = {}
92+
for line in file:lines() do
93+
table.insert(lines, line)
94+
end
95+
file:close()
96+
97+
-- Read the next few lines to find the path parameter
98+
local multiline_content = content
99+
for i = start_line + 1, math.min(start_line + 5, #lines) do
100+
local next_line = lines[i]
101+
if next_line then
102+
multiline_content = multiline_content .. " " .. next_line:gsub("^%s+", ""):gsub("%s+$", "")
103+
104+
-- Try to extract path from accumulated content
105+
local extracted_path = self:_extract_path_single_line(multiline_content)
106+
if extracted_path then
107+
return extracted_path, i -- Return path and end line number
108+
end
109+
110+
-- If we hit closing parenthesis followed by opening brace, stop and return end line
111+
if next_line:match "%s*%)%s*{" then
112+
-- Try one more time to extract path before stopping
113+
local final_path = self:_extract_path_single_line(multiline_content)
114+
return final_path, i
115+
end
116+
end
117+
end
118+
end
119+
120+
return nil, nil
121+
end
122+
123+
---Checks if routing definition spans multiple lines
124+
function KtorParser:_is_multiline_routing(content)
125+
-- Check if content has HTTP method followed by opening parenthesis but no closing parenthesis and path
126+
return content:match "^%s*%w+%s*%(%s*$" or content:match "^%s*%w+%s*%($"
127+
end
128+
129+
---Override parse_content to add end_line_number for multiline endpoints
130+
function KtorParser:parse_content(content, file_path, line_number, column)
131+
if not self:is_content_valid_for_parsing(content) then
132+
return nil
133+
end
134+
135+
-- Use the 4 core methods to create endpoint
136+
local base_path = self:extract_base_path(file_path, line_number)
137+
local endpoint_path = self:extract_endpoint_path(content, file_path, line_number)
138+
local method = self:extract_method(content)
139+
140+
if not endpoint_path or not method then
141+
return nil
142+
end
143+
144+
-- Combine paths
145+
local full_path = self:combine_paths(base_path, endpoint_path)
146+
147+
-- Create endpoint entry
148+
local endpoint = {
149+
method = method:upper(),
150+
endpoint_path = full_path,
151+
file_path = file_path,
152+
line_number = line_number,
153+
column = column,
154+
display_value = method:upper() .. " " .. full_path,
155+
confidence = self:get_parsing_confidence(content),
156+
tags = { "api" },
157+
metadata = self:create_metadata("endpoint", {
158+
base_path = base_path,
159+
raw_endpoint_path = endpoint_path,
160+
}, content),
161+
}
162+
163+
-- Add end_line_number if multiline
164+
if self._last_end_line_number then
165+
endpoint.end_line_number = self._last_end_line_number
166+
self._last_end_line_number = nil -- Clean up
167+
end
168+
169+
return endpoint
170+
end
171+
59172
---Extracts HTTP method from Ktor routing content
60173
function KtorParser:extract_method(content)
61174
-- Handle multiline patterns by normalizing whitespace
@@ -151,6 +264,8 @@ function KtorParser:_extract_base_paths_from_file(file_path, target_line)
151264
-- Track nesting level and extract route paths
152265
local bracket_depth = 0
153266
local route_stack = {}
267+
local multiline_route_buffer = nil
268+
local multiline_route_start_depth = nil
154269

155270
for i = 1, target_line - 1 do
156271
local line = lines[i]
@@ -159,13 +274,36 @@ function KtorParser:_extract_base_paths_from_file(file_path, target_line)
159274
local _, open_count = line:gsub("{", "")
160275
local _, close_count = line:gsub("}", "")
161276

162-
-- Check for route("path") declarations
163-
local route_path = line:match 'route%s*%("([^"]+)"%)'
164-
if not route_path then
165-
route_path = line:match "route%s*%('([^']+)'%)"
166-
end
167-
if route_path then
168-
table.insert(route_stack, { path = route_path, depth = bracket_depth })
277+
-- Handle multiline route detection
278+
if multiline_route_buffer then
279+
-- We're in a multiline route, look for the path
280+
multiline_route_buffer = multiline_route_buffer .. " " .. line:gsub("^%s+", ""):gsub("%s+$", "")
281+
local route_path = multiline_route_buffer:match 'route%s*%(%s*"([^"]+)"%s*%)'
282+
if not route_path then
283+
route_path = multiline_route_buffer:match "route%s*%(%s*'([^']+)'%s*%)"
284+
end
285+
if route_path then
286+
table.insert(route_stack, { path = route_path, depth = multiline_route_start_depth })
287+
multiline_route_buffer = nil
288+
multiline_route_start_depth = nil
289+
elseif line:match "%)" then
290+
-- Found closing paren but no path, reset
291+
multiline_route_buffer = nil
292+
multiline_route_start_depth = nil
293+
end
294+
else
295+
-- Check for single line route("path") declarations
296+
local route_path = line:match 'route%s*%("([^"]+)"%)'
297+
if not route_path then
298+
route_path = line:match "route%s*%('([^']+)'%)"
299+
end
300+
if route_path then
301+
table.insert(route_stack, { path = route_path, depth = bracket_depth })
302+
elseif line:match "route%s*%(" then
303+
-- Found start of multiline route
304+
multiline_route_buffer = line
305+
multiline_route_start_depth = bracket_depth
306+
end
169307
end
170308

171309
bracket_depth = bracket_depth + open_count - close_count

meta/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@
251251
---@field private _is_valid_http_method fun(self: endpoint.KtorParser, method?: string): boolean
252252
---@field private _get_full_path fun(self: endpoint.KtorParser, path: string, file_path?: string, line_number?: number): string
253253
---@field private _extract_base_paths_from_file fun(self: endpoint.KtorParser, file_path: string, target_line: number): string[]
254+
---@field private _extract_path_single_line fun(self: endpoint.KtorParser, content: string): string|nil
255+
---@field private _extract_path_multiline fun(self: endpoint.KtorParser, file_path: string, start_line: number, content: string): string|nil, number|nil
256+
---@field private _is_multiline_routing fun(self: endpoint.KtorParser, content: string): boolean
257+
---@field private _last_end_line_number number|nil
254258
---@field private _looks_like_incomplete_ktor_routing fun(self: endpoint.KtorParser, content: string): boolean
255259
---@field private _get_extended_routing_content fun(self: endpoint.KtorParser, initial_content: string, file_path?: string, start_line?: number): string|nil
256260

0 commit comments

Comments
 (0)