Skip to content

Commit 3d9af93

Browse files
authored
Merge pull request #151 from domaframework/fix/optional-dao-param-type-conversion
Fix: optional dao param type conversion
2 parents 2dcabe8 + 053c7a4 commit 3d9af93

File tree

25 files changed

+380
-88
lines changed

25 files changed

+380
-88
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ The plugin also provides quick fixes for Dao methods where the required SQL file
3838
![inspection.png](images/inspection.png)
3939
- Check the class name and package name for static property calls
4040
![inspectionPackageName.png](images/inspectionPackageName.png)
41+
- Optional types are recognized as their element type (e.g. Optional<String> is treated as String).
4142

4243
## Completion
4344
Adds code completion functionality to support indexing of Doma directives and bind variables
@@ -52,6 +53,7 @@ Adds code completion functionality to support indexing of Doma directives and bi
5253
- Suggest Doma directives
5354
- Directives such as Condition, Loop, Population are suggested after “%”
5455
- Suggest built-in functions after “@”
56+
- Optional types are recognized as their element type (e.g. Optional<String> is treated as String).
5557

5658
## Refactoring
5759
Along with the Dao name change, we will refactor the SQL file directory and file name.

src/main/kotlin/org/domaframework/doma/intellij/common/sql/PsiClassTypeUtil.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,50 @@ class PsiClassTypeUtil {
6363
type = type.parameters.firstOrNull()
6464
count++
6565
}
66-
return type as? PsiClassType
66+
val convertOptional = type?.let { convertOptionalType(it, project) }
67+
return convertOptional as? PsiClassType
6768
}
6869
return null
6970
}
71+
72+
/**
73+
* Check if daoParamType is an instance of PsiClassType representing Optional or its primitive variants
74+
*/
75+
fun convertOptionalType(
76+
daoParamType: PsiType,
77+
project: Project,
78+
): PsiType {
79+
if (daoParamType is PsiClassType) {
80+
val resolved = daoParamType.resolve()
81+
val optionalTypeMap =
82+
mapOf(
83+
"java.util.OptionalInt" to "java.lang.Integer",
84+
"java.util.OptionalDouble" to "java.lang.Double",
85+
"java.util.OptionalLong" to "java.lang.Long",
86+
)
87+
if (resolved != null) {
88+
when (resolved.qualifiedName) {
89+
// If the type is java.util.Optional, return its parameter type if available;
90+
// otherwise, return the original daoParamType.
91+
"java.util.Optional" -> return daoParamType.parameters.firstOrNull()
92+
?: daoParamType
93+
94+
// For primitive Optional types (e.g., OptionalInt, OptionalDouble),
95+
// map them to their corresponding wrapper types (e.g., Integer, Double).
96+
else ->
97+
optionalTypeMap[resolved.qualifiedName]?.let { optionalType ->
98+
val newType =
99+
PsiType.getTypeByName(
100+
optionalType,
101+
project,
102+
GlobalSearchScope.allScope(project),
103+
)
104+
return newType
105+
}
106+
}
107+
}
108+
}
109+
return daoParamType
110+
}
70111
}
71112
}

src/main/kotlin/org/domaframework/doma/intellij/common/util/ForDirectiveUtil.kt

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ class ForDirectiveUtil {
259259

260260
val matchParam = daoMethod.findParameter(cleanString(topElementText))
261261
val daoParamType = matchParam?.type ?: return null
262-
fieldAccessTopParentClass = PsiParentClass(daoParamType)
262+
fieldAccessTopParentClass = PsiParentClass(PsiClassTypeUtil.convertOptionalType(daoParamType, project))
263263
}
264264
fieldAccessTopParentClass?.let {
265265
getFieldAccessLastPropertyClassType(
@@ -329,14 +329,15 @@ class ForDirectiveUtil {
329329
): ValidationResult? {
330330
var parent =
331331
if (isBatchAnnotation) {
332-
val parentType = topParent.type
332+
val parentType = PsiClassTypeUtil.convertOptionalType(topParent.type, project)
333333
val nextClassType = parentType as? PsiClassType ?: return null
334334
val nestType = nextClassType.parameters.firstOrNull() ?: return null
335-
PsiParentClass(nestType)
335+
PsiParentClass(PsiClassTypeUtil.convertOptionalType(nestType, project))
336336
} else {
337-
topParent
337+
val convertOptional = PsiClassTypeUtil.convertOptionalType(topParent.type, project)
338+
PsiParentClass(convertOptional)
338339
}
339-
val parentType = parent.type
340+
val parentType = PsiClassTypeUtil.convertOptionalType(parent.type, project)
340341
val classType = parentType as? PsiClassType ?: return null
341342

342343
var competeResult: ValidationCompleteResult? = null
@@ -353,8 +354,8 @@ class ForDirectiveUtil {
353354
// When a List type element is used as the parent,
354355
// the original declared type is retained and the referenced type is obtained by nesting.
355356
var parentListBaseType: PsiType? =
356-
if (PsiClassTypeUtil.Companion.isIterableType(classType, project)) {
357-
parentType
357+
if (PsiClassTypeUtil.isIterableType(classType, project)) {
358+
PsiClassTypeUtil.convertOptionalType(parentType, project)
358359
} else {
359360
null
360361
}
@@ -375,24 +376,25 @@ class ForDirectiveUtil {
375376
parent
376377
.findField(searchElm)
377378
?.let { match ->
379+
val convertOptional = PsiClassTypeUtil.convertOptionalType(match.type, project)
378380
val type =
379381
parentListBaseType?.let {
380-
PsiClassTypeUtil.Companion.getParameterType(
382+
PsiClassTypeUtil.getParameterType(
381383
project,
382-
match.type,
384+
convertOptional,
383385
it,
384386
nestIndex,
385387
)
386388
}
387-
?: match.type
389+
?: convertOptional
388390
val classType = type as? PsiClassType
389391
if (classType != null &&
390-
PsiClassTypeUtil.Companion.isIterableType(
392+
PsiClassTypeUtil.isIterableType(
391393
classType,
392394
element.project,
393395
)
394396
) {
395-
parentListBaseType = type
397+
parentListBaseType = PsiClassTypeUtil.convertOptionalType(type, project)
396398
nestIndex = 0
397399
}
398400
findFieldMethod?.invoke(type)
@@ -402,19 +404,20 @@ class ForDirectiveUtil {
402404
.findMethod(searchElm)
403405
?.let { match ->
404406
val returnType = match.returnType ?: return null
407+
val convertOptionalType = PsiClassTypeUtil.convertOptionalType(returnType, project)
405408
val methodReturnType =
406409
parentListBaseType?.let {
407-
PsiClassTypeUtil.Companion.getParameterType(
410+
PsiClassTypeUtil.getParameterType(
408411
project,
409-
returnType,
412+
convertOptionalType,
410413
it,
411414
nestIndex,
412415
)
413416
}
414-
?: returnType
417+
?: convertOptionalType
415418
val classType = methodReturnType as? PsiClassType
416419
if (classType != null &&
417-
PsiClassTypeUtil.Companion.isIterableType(
420+
PsiClassTypeUtil.isIterableType(
418421
classType,
419422
element.project,
420423
)

src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.intellij.codeInsight.completion.CompletionProvider
2020
import com.intellij.codeInsight.completion.CompletionResultSet
2121
import com.intellij.codeInsight.lookup.LookupElementBuilder
2222
import com.intellij.codeInsight.lookup.VariableLookupItem
23+
import com.intellij.openapi.project.Project
2324
import com.intellij.psi.PsiClass
2425
import com.intellij.psi.PsiDirectory
2526
import com.intellij.psi.PsiElement
@@ -33,8 +34,10 @@ import com.intellij.psi.util.elementType
3334
import com.intellij.psi.util.prevLeafs
3435
import com.intellij.util.ProcessingContext
3536
import org.domaframework.doma.intellij.common.dao.findDaoMethod
37+
import org.domaframework.doma.intellij.common.psi.PsiDaoMethod
3638
import org.domaframework.doma.intellij.common.psi.PsiParentClass
3739
import org.domaframework.doma.intellij.common.psi.PsiPatternUtil
40+
import org.domaframework.doma.intellij.common.sql.PsiClassTypeUtil
3841
import org.domaframework.doma.intellij.common.sql.cleanString
3942
import org.domaframework.doma.intellij.common.sql.directive.DirectiveCompletion
4043
import org.domaframework.doma.intellij.common.sql.validator.result.ValidationCompleteResult
@@ -45,8 +48,6 @@ import org.domaframework.doma.intellij.extension.psi.findNodeParent
4548
import org.domaframework.doma.intellij.extension.psi.findSelfBlocks
4649
import org.domaframework.doma.intellij.extension.psi.findStaticField
4750
import org.domaframework.doma.intellij.extension.psi.findStaticMethod
48-
import org.domaframework.doma.intellij.extension.psi.getDomaAnnotationType
49-
import org.domaframework.doma.intellij.extension.psi.getIterableClazz
5051
import org.domaframework.doma.intellij.extension.psi.isNotWhiteSpace
5152
import org.domaframework.doma.intellij.extension.psi.searchParameter
5253
import org.domaframework.doma.intellij.extension.psi.searchStaticField
@@ -270,66 +271,49 @@ class SqlParameterCompletionProvider : CompletionProvider<CompletionParameters>(
270271
originalFile: PsiFile,
271272
result: CompletionResultSet,
272273
) {
274+
val daoMethod = findDaoMethod(originalFile)
273275
val searchText = cleanString(getSearchElementText(position))
274276
var topElementType: PsiType? = null
275-
if (elements.isEmpty()) {
276-
getElementTypeByFieldAccess(originalFile, elements, result)
277+
if (elements.isEmpty() && daoMethod != null) {
278+
getElementTypeByFieldAccess(originalFile, elements, daoMethod, result)
277279
return
278280
}
279281
val top = elements.first()
280-
281282
val topText = cleanString(getSearchElementText(top))
282283
val prevWord = PsiPatternUtil.getBindSearchWord(originalFile, elements.last(), " ")
283284
if (prevWord.startsWith("@") && prevWord.endsWith("@")) {
284-
setStaticFieldAccess(top, prevWord, topText, result)
285+
setCompletionStaticFieldAccess(top, prevWord, topText, result)
285286
return
286287
}
288+
289+
var isBatchAnnotation = false
287290
if (top.parent !is PsiFile && top.parent?.parent !is PsiDirectory) {
288291
val staticDirective = top.findNodeParent(SqlTypes.EL_STATIC_FIELD_ACCESS_EXPR)
289292
staticDirective?.let {
290293
topElementType = getElementTypeByStaticFieldAccess(top, it, topText) ?: return
291294
}
292295
}
293296

297+
if (daoMethod == null) return
298+
val project = originalFile.project
299+
val psiDaoMethod = PsiDaoMethod(project, daoMethod)
294300
if (topElementType == null) {
295-
if (isFieldAccessByForItem(top, elements, searchText, result)) return
301+
isBatchAnnotation = psiDaoMethod.daoType.isBatchAnnotation()
302+
if (isFieldAccessByForItem(top, elements, searchText, isBatchAnnotation, result)) return
296303
topElementType =
297-
getElementTypeByFieldAccess(originalFile, elements, result) ?: return
304+
getElementTypeByFieldAccess(originalFile, elements, daoMethod, result) ?: return
298305
}
299306

300-
var psiParentClass = PsiParentClass(topElementType)
301-
// FieldAccess Completion
302-
ForDirectiveUtil.getFieldAccessLastPropertyClassType(
307+
setCompletionFieldAccess(
308+
topElementType,
309+
originalFile.project,
310+
isBatchAnnotation,
303311
elements,
304-
top.project,
305-
psiParentClass,
306-
shortName = "",
307-
dropLastIndex = 1,
308-
complete = { lastType ->
309-
val searchWord = cleanString(getSearchElementText(position))
310-
setFieldsAndMethodsCompletionResultSet(
311-
lastType.searchField(searchWord)?.toTypedArray() ?: emptyArray(),
312-
lastType.searchMethod(searchWord)?.toTypedArray() ?: emptyArray(),
313-
result,
314-
)
315-
},
312+
searchText,
313+
result,
316314
)
317315
}
318316

319-
private fun setStaticFieldAccess(
320-
top: PsiElement,
321-
prevWord: String,
322-
topText: String,
323-
result: CompletionResultSet,
324-
) {
325-
val clazz = getRefClazz(top) { prevWord.replace("@", "") } ?: return
326-
val matchFields = clazz.searchStaticField(topText)
327-
val matchMethod = clazz.searchStaticMethod(topText)
328-
329-
// When you enter here, it is the top element, so return static fields and methods.
330-
setFieldsAndMethodsCompletionResultSet(matchFields, matchMethod, result)
331-
}
332-
333317
private fun getSearchElementText(elm: PsiElement?): String =
334318
if (elm is SqlElIdExpr || elm.elementType == SqlTypes.EL_IDENTIFIER) {
335319
elm?.text ?: ""
@@ -368,9 +352,9 @@ class SqlParameterCompletionProvider : CompletionProvider<CompletionParameters>(
368352
private fun getElementTypeByFieldAccess(
369353
originalFile: PsiFile,
370354
elements: List<PsiElement>,
355+
daoMethod: PsiMethod,
371356
result: CompletionResultSet,
372357
): PsiType? {
373-
val daoMethod = findDaoMethod(originalFile) ?: return null
374358
val topText = cleanString(getSearchElementText(elements.firstOrNull()))
375359
val matchParams = daoMethod.searchParameter(topText)
376360
val findParam = matchParams.find { it.name == topText }
@@ -384,8 +368,8 @@ class SqlParameterCompletionProvider : CompletionProvider<CompletionParameters>(
384368
if (findParam == null) {
385369
return null
386370
}
387-
val immediate = findParam.getIterableClazz(daoMethod.getDomaAnnotationType())
388-
return immediate.type
371+
val immediate = findParam.type
372+
return PsiClassTypeUtil.convertOptionalType(immediate, originalFile.project)
389373
}
390374

391375
private fun getRefClazz(
@@ -416,10 +400,10 @@ class SqlParameterCompletionProvider : CompletionProvider<CompletionParameters>(
416400
private fun isFieldAccessByForItem(
417401
top: PsiElement,
418402
elements: List<PsiElement>,
419-
positionText: String,
403+
searchWord: String,
404+
isBatchAnnotation: Boolean = false,
420405
result: CompletionResultSet,
421406
): Boolean {
422-
val searchWord = cleanString(positionText)
423407
val project = top.project
424408
val forDirectiveBlocks = ForDirectiveUtil.getForDirectiveBlocks(top)
425409
ForDirectiveUtil.findForItem(top, forDirectives = forDirectiveBlocks) ?: return false
@@ -438,6 +422,7 @@ class SqlParameterCompletionProvider : CompletionProvider<CompletionParameters>(
438422
elements,
439423
project,
440424
topClassType,
425+
isBatchAnnotation = isBatchAnnotation,
441426
shortName = "",
442427
dropLastIndex = 1,
443428
complete = { lastType ->
@@ -450,4 +435,46 @@ class SqlParameterCompletionProvider : CompletionProvider<CompletionParameters>(
450435
)
451436
return result is ValidationCompleteResult
452437
}
438+
439+
private fun setCompletionFieldAccess(
440+
topElementType: PsiType,
441+
project: Project,
442+
isBatchAnnotation: Boolean,
443+
elements: List<PsiElement>,
444+
searchWord: String,
445+
result: CompletionResultSet,
446+
) {
447+
var psiParentClass = PsiParentClass(topElementType)
448+
449+
// FieldAccess Completion
450+
ForDirectiveUtil.getFieldAccessLastPropertyClassType(
451+
elements,
452+
project,
453+
psiParentClass,
454+
isBatchAnnotation = isBatchAnnotation,
455+
shortName = "",
456+
dropLastIndex = 1,
457+
complete = { lastType ->
458+
setFieldsAndMethodsCompletionResultSet(
459+
lastType.searchField(searchWord)?.toTypedArray() ?: emptyArray(),
460+
lastType.searchMethod(searchWord)?.toTypedArray() ?: emptyArray(),
461+
result,
462+
)
463+
},
464+
)
465+
}
466+
467+
private fun setCompletionStaticFieldAccess(
468+
top: PsiElement,
469+
prevWord: String,
470+
topText: String,
471+
result: CompletionResultSet,
472+
) {
473+
val clazz = getRefClazz(top) { prevWord.replace("@", "") } ?: return
474+
val matchFields = clazz.searchStaticField(topText)
475+
val matchMethod = clazz.searchStaticMethod(topText)
476+
477+
// When you enter here, it is the top element, so return static fields and methods.
478+
setFieldsAndMethodsCompletionResultSet(matchFields, matchMethod, result)
479+
}
453480
}

src/main/kotlin/org/domaframework/doma/intellij/document/generator/DocumentDaoParameterGenerator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr
2828

2929
class DocumentDaoParameterGenerator(
3030
val originalElement: PsiElement,
31-
val project: Project,
31+
override val project: Project,
3232
val result: MutableList<String?>,
33-
) : DocumentGenerator() {
33+
) : DocumentGenerator(project) {
3434
override fun generateDocument() {
3535
var topParentType: PsiParentClass? = null
3636
val selfSkip = isSelfSkip(originalElement)

0 commit comments

Comments
 (0)