Skip to content

Commit c631255

Browse files
committed
fix(docql): enforce exact match for class and function queries
Update $.code.class("name") and $.code.function("name") to return only exact name matches, preventing partial matches. For partial or fuzzy search, users should use $.code.query() or filter expressions.
1 parent 3ecb9c6 commit c631255

File tree

1 file changed

+52
-20
lines changed

1 file changed

+52
-20
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/document/docql/CodeDocQLExecutor.kt

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,12 @@ class CodeDocQLExecutor(
261261
*
262262
* Supports wildcard: $.code.class("*") returns all classes (equivalent to $.code.classes[*])
263263
*
264-
* Matching priority:
265-
* 1. Exact match: class name exactly equals the query (e.g., "CodingAgent" matches "class CodingAgent")
266-
* 2. Partial match: class name contains the query (e.g., "Agent" matches "CodingAgent", "CodingAgentContext")
264+
* Matching behavior:
265+
* - Returns ONLY exact class name matches (e.g., "CodingAgent" matches "class CodingAgent" only)
266+
* - Does NOT return partial matches (e.g., "CodingAgent" does NOT match "CodingAgentContext")
267+
* - This is semantically correct: $.code.class("X") means "find the class named X", not "find classes containing X"
267268
*
268-
* When exact match is found, only exact matches are returned.
269-
* This prevents returning unrelated classes like "CodingAgentContext" when searching for "CodingAgent".
269+
* For partial/fuzzy matching, use $.code.classes[?(@.name contains "keyword")] instead.
270270
*/
271271
private suspend fun executeCodeClassQuery(className: String): DocQLResult {
272272
if (documentFile == null) {
@@ -298,24 +298,19 @@ class CodeDocQLExecutor(
298298
return DocQLResult.Empty
299299
}
300300

301-
// Prioritize exact matches over partial matches
302-
// Extract class name from title (e.g., "class CodingAgent" -> "CodingAgent", "class Foo<T>" -> "Foo")
301+
// ONLY return exact matches - this is the correct semantic for $.code.class("Name")
302+
// For partial matching, users should use $.code.classes[?(@.name contains "keyword")]
303303
val exactMatches = classChunks.filter { chunk ->
304304
val extractedName = extractClassNameFromTitle(chunk.chapterTitle ?: "")
305305
extractedName.equals(className, ignoreCase = true)
306306
}
307307

308308
return if (exactMatches.isNotEmpty()) {
309-
// Return only exact matches - this prevents returning CodingAgentContext when searching for CodingAgent
310309
DocQLResult.Chunks(mapOf(documentFile.path to exactMatches))
311310
} else {
312-
// No exact match - return partial matches sorted by relevance
313-
// Sort: shorter names (more specific) come first
314-
val sortedChunks = classChunks.sortedBy { chunk ->
315-
val extractedName = extractClassNameFromTitle(chunk.chapterTitle ?: "")
316-
extractedName.length
317-
}
318-
DocQLResult.Chunks(mapOf(documentFile.path to sortedChunks))
311+
// No exact match in this file - return Empty
312+
// This prevents returning CodingAgentContext when searching for CodingAgent
313+
DocQLResult.Empty
319314
}
320315
}
321316

@@ -355,6 +350,8 @@ class CodeDocQLExecutor(
355350
* Execute $.code.function("functionName") - find specific function/method
356351
*
357352
* Supports wildcard: $.code.function("*") returns all functions (equivalent to $.code.functions[*])
353+
*
354+
* Returns ONLY exact function name matches. For partial matching, use $.code.query("keyword").
358355
*/
359356
private suspend fun executeCodeFunctionQuery(functionName: String): DocQLResult {
360357
if (documentFile == null) {
@@ -366,7 +363,40 @@ class CodeDocQLExecutor(
366363
return executeCodeFunctionsQuery(emptyList())
367364
}
368365

369-
return executeCodeCustomQuery(functionName)
366+
if (parserService == null) {
367+
return DocQLResult.Error("No parser service available")
368+
}
369+
370+
// Use heading query to find the function
371+
val chunks = parserService.queryHeading(functionName)
372+
373+
if (chunks.isEmpty()) {
374+
return DocQLResult.Empty
375+
}
376+
377+
// Filter to function-level chunks and require EXACT name match
378+
val exactMatches = chunks.filter { chunk ->
379+
val title = chunk.chapterTitle ?: ""
380+
// Must be a function (not a class)
381+
val isFunction = title.contains("fun ") ||
382+
(!title.startsWith("class ") &&
383+
!title.startsWith("interface ") &&
384+
!title.startsWith("enum ") &&
385+
!title.startsWith("object ") &&
386+
title.contains("("))
387+
388+
if (!isFunction) return@filter false
389+
390+
val funcName = extractFunctionNameFromTitle(title)
391+
funcName.equals(functionName, ignoreCase = true)
392+
}
393+
394+
return if (exactMatches.isNotEmpty()) {
395+
DocQLResult.Chunks(mapOf(documentFile.path to exactMatches))
396+
} else {
397+
// No exact match - return Empty (for partial match, use $.code.query())
398+
DocQLResult.Empty
399+
}
370400
}
371401

372402
/**
@@ -381,7 +411,8 @@ class CodeDocQLExecutor(
381411
*
382412
* Supports wildcard: $.code.query("*") returns all code chunks
383413
*
384-
* For function queries, prioritizes exact name matches over partial matches.
414+
* Unlike $.code.function("name") which requires exact match, $.code.query() is for flexible search
415+
* and returns all matching code elements (functions, classes, etc.) sorted by relevance.
385416
*/
386417
private suspend fun executeCodeCustomQuery(keyword: String): DocQLResult {
387418
if (parserService == null || documentFile == null) {
@@ -419,12 +450,13 @@ class CodeDocQLExecutor(
419450
return DocQLResult.Empty
420451
}
421452

422-
// Prioritize exact function name matches over partial matches
453+
// For $.code.query(), prioritize exact matches but also include partial matches
454+
// This is different from $.code.function("name") which is exact-match only
423455
val exactMatches = chunks.filter { chunk ->
424456
val title = chunk.chapterTitle ?: ""
425-
// Extract function name from title (e.g., "fun execute()" -> "execute")
426457
val funcName = extractFunctionNameFromTitle(title)
427-
funcName.equals(keyword, ignoreCase = true)
458+
val className = extractClassNameFromTitle(title)
459+
funcName.equals(keyword, ignoreCase = true) || className.equals(keyword, ignoreCase = true)
428460
}
429461

430462
return if (exactMatches.isNotEmpty()) {

0 commit comments

Comments
 (0)