diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..94f480de --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/helper/ExpressionFunctionsHelper.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/helper/ExpressionFunctionsHelper.kt index a86d23cf..36d6e31f 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/helper/ExpressionFunctionsHelper.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/helper/ExpressionFunctionsHelper.kt @@ -45,7 +45,6 @@ class ExpressionFunctionsHelper { expressionClazz.superTypes.firstOrNull()?.canonicalText ?: expressionClazz.psiClassType.canonicalText return project?.let { - expressionClazz.psiClassType.canonicalText project .getJavaClazz(parentType) ?.isInheritor(functionInterface, true) == true diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/DummyPsiParentClass.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/DummyPsiParentClass.kt new file mode 100644 index 00000000..f83bfc52 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/DummyPsiParentClass.kt @@ -0,0 +1,20 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.psi.PsiTypes + +class DummyPsiParentClass : PsiParentClass(PsiTypes.voidType()) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/MethodParamContext.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/MethodParamContext.kt new file mode 100644 index 00000000..04219155 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/MethodParamContext.kt @@ -0,0 +1,48 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.psi.SqlElFunctionCallExpr +import org.domaframework.doma.intellij.psi.SqlElParameters + +class MethodParamContext( + val methodIdExp: PsiElement, + val methodParams: SqlElParameters?, +) { + companion object { + fun of(method: PsiElement): MethodParamContext = + MethodParamContext( + setMethodIdExp(method), + setParameter(method), + ) + + private fun setParameter(method: PsiElement): SqlElParameters? = + if (method is SqlElFunctionCallExpr) { + method.elParameters + } else { + PsiTreeUtil.nextLeaf(method)?.parent as? SqlElParameters + } + + private fun setMethodIdExp(method: PsiElement): PsiElement = + if (method is SqlElFunctionCallExpr) { + method.elIdExpr + } else { + method + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt index 34e7280d..912b6187 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt @@ -16,17 +16,21 @@ package org.domaframework.doma.intellij.common.psi import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement import com.intellij.psi.PsiField +import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifier import com.intellij.psi.PsiType import com.intellij.psi.util.PsiTypesUtil +import org.domaframework.doma.intellij.common.util.MethodMatcher +import org.domaframework.doma.intellij.extension.expr.extractParameterTypes /** * When parsing a field access element with SQL, * manage the reference class information of the previous element */ -class PsiParentClass( +open class PsiParentClass( val type: PsiType, ) { var clazz: PsiClass? = psiClass() @@ -61,6 +65,34 @@ class PsiParentClass( m.name.substringBefore("(") == methodName.substringBefore("(") } + fun findMethod( + methodExpr: PsiElement, + shortName: String = "", + ): MethodMatcher.MatchResult { + val context = MethodParamContext.of(methodExpr) + val methods = findMethods(context.methodIdExp.text) + if (context.methodParams == null) return MethodMatcher.MatchResult(validation = null) + + val actualCount = context.methodParams.elExprList.size + val paramTypes = context.methodParams.extractParameterTypes(PsiManager.getInstance(methodExpr.project)) + val matchResult = + MethodMatcher.findMatchingMethod( + context.methodIdExp, + methods, + paramTypes, + actualCount, + shortName, + ) + + return matchResult + } + + fun findMethods(methodName: String): List = + getMethods() + ?.filter { m -> + m.hasModifierProperty(PsiModifier.PUBLIC) && m.name.substringBefore("(") == methodName.substringBefore("(") + } ?: emptyList() + fun searchMethod(methodName: String): List? = getMethods()?.filter { m -> m.name.substringBefore("(").startsWith(methodName.substringBefore("(")) && @@ -81,4 +113,11 @@ class PsiParentClass( m.hasModifierProperty(PsiModifier.PUBLIC) && m.name.substringBefore("(").startsWith(methodName.substringBefore("(")) } + + fun findStaticMethods(methodName: String): List = + getMethods() + ?.filter { m -> + m.hasModifierProperty(PsiModifier.STATIC) && + m.hasModifierProperty(PsiModifier.PUBLIC) && m.name.substringBefore("(") == methodName.substringBefore("(") + } ?: emptyList() } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/collector/FunctionCallCollector.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/collector/FunctionCallCollector.kt index a6427405..c3ca6a2a 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/collector/FunctionCallCollector.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/collector/FunctionCallCollector.kt @@ -34,7 +34,7 @@ class FunctionCallCollector( private val bind: String, ) : StaticDirectiveHandlerCollector() { public override fun collect(): List? { - var functions = mutableSetOf() + val functions = mutableSetOf() val project = file?.project val module = file?.module ?: return null val isTest = CommonPathParameterUtil.isTest(module, file.virtualFile) @@ -50,7 +50,10 @@ class FunctionCallCollector( project?.let { ExpressionFunctionsHelper.setExpressionFunctionsInterface(it) } ?: return null - val expressionClazz = customFunctionClassName?.let { project.getJavaClazz(it) } + val expressionClazz = + customFunctionClassName?.let { + if (it.isNotEmpty()) project.getJavaClazz(it) else null + } if (expressionClazz != null && ExpressionFunctionsHelper.isInheritor(expressionClazz) ) { @@ -71,17 +74,17 @@ class FunctionCallCollector( return functions .filter { it.name.startsWith(bind.substringAfter("@")) - }.map { - val parameters = it.parameterList.parameters.toList() + }.map { m -> + val parameters = m.parameterList.parameters.toList() LookupElementBuilder - .create(createMethodLookupElement(caretNextText, it)) - .withPresentableText(it.name) + .create(createMethodLookupElement(caretNextText, m)) + .withPresentableText(m.name) .withTailText( "(${ - parameters.joinToString(",") { "${it.type.presentableText} ${it.name}" } + parameters.joinToString(",") { p -> "${p.type.presentableText} ${p.name}" } })", true, - ).withTypeText(it.returnType?.presentableText ?: "void") + ).withTypeText(m.returnType?.presentableText ?: "void") .withAutoCompletionPolicy(AutoCompletionPolicy.ALWAYS_AUTOCOMPLETE) } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/FieldMethodResolver.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/FieldMethodResolver.kt new file mode 100644 index 00000000..cbe6c214 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/FieldMethodResolver.kt @@ -0,0 +1,204 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.util + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import org.domaframework.doma.intellij.common.psi.MethodParamContext +import org.domaframework.doma.intellij.common.psi.PsiParentClass +import org.domaframework.doma.intellij.common.sql.PsiClassTypeUtil +import org.domaframework.doma.intellij.common.validation.result.ValidationCompleteResult +import org.domaframework.doma.intellij.common.validation.result.ValidationNotFoundStaticPropertyResult +import org.domaframework.doma.intellij.common.validation.result.ValidationPropertyResult +import org.domaframework.doma.intellij.common.validation.result.ValidationResult +import org.domaframework.doma.intellij.extension.expr.extractParameterTypes +import org.domaframework.doma.intellij.psi.SqlElParameters + +class FieldMethodResolver { + data class ResolveContext( + var parent: PsiParentClass, + var parentListBaseType: PsiType?, + var nestIndex: Int, + var completeResult: ValidationCompleteResult? = null, + var validationResult: ValidationResult? = null, + ) + + data class ResolveResult( + val type: PsiParentClass? = null, + val validation: ValidationResult? = null, + ) + + companion object { + fun resolveField( + context: ResolveContext, + fieldName: String, + project: Project, + ): PsiParentClass? { + val field = context.parent.findField(fieldName) ?: return null + val convertedType = PsiClassTypeUtil.convertOptionalType(field.type, project) + + val resolvedType = + context.parentListBaseType?.let { + PsiClassTypeUtil.getParameterType(project, convertedType, it, context.nestIndex) + } ?: convertedType + + updateContextForIterableType(context, resolvedType, project) + return PsiParentClass(resolvedType) + } + + /** + * Retrieve, from the defined methods with that name, those whose parameter count and parameter types match. + */ + fun resolveMethod( + context: ResolveContext, + element: PsiElement, + methodName: String, + project: Project, + shortName: String = "", + ): ResolveResult { + val methodContext: MethodParamContext = MethodParamContext.of(element) + val candidateMethods = context.parent.findMethods(methodName) + if (candidateMethods.isEmpty()) { + return ResolveResult( + validation = ValidationPropertyResult(element, context.parent, shortName), + ) + } + return resolveMethodWithParameters( + context, + element, + candidateMethods, + methodContext.methodParams, + project, + shortName, + ) + } + + fun resolveStaticMethod( + context: ResolveContext, + element: PsiElement, + parent: PsiParentClass, + methodName: String, + parametersExpr: SqlElParameters?, + project: Project, + shortName: String = "", + ): ResolveResult { + val candidateMethods = parent.findStaticMethods(methodName) + + if (parametersExpr == null) { + return ResolveResult() + } + + if (candidateMethods.isEmpty()) { + return ResolveResult( + validation = ValidationNotFoundStaticPropertyResult(element, parent.type.canonicalText, shortName), + ) + } + + val paramTypes = + parametersExpr.extractParameterTypes( + PsiManager.getInstance(project), + ) + + val matchResult = + MethodMatcher.findMatchingMethod( + element, + candidateMethods, + paramTypes, + parametersExpr.elExprList.size, + shortName, + ) + + fun result(): ResolveResult { + if (matchResult.validation != null) { + context.validationResult = matchResult.validation + return ResolveResult(validation = matchResult.validation) + } + + val returnType = matchResult.method?.returnType ?: return ResolveResult() + + val convertedType = PsiClassTypeUtil.convertOptionalType(returnType, project) + val resolvedType = + context.parentListBaseType?.let { + PsiClassTypeUtil.getParameterType(project, convertedType, it, context.nestIndex) + } ?: convertedType + + updateContextForIterableType(context, resolvedType, project) + return ResolveResult(type = PsiParentClass(resolvedType)) + } + + return result() + } + + private fun resolveMethodWithParameters( + context: ResolveContext, + element: PsiElement, + candidateMethods: List, + paramExpr: SqlElParameters?, + project: Project, + shortName: String, + ): ResolveResult { + val paramTypes = + paramExpr?.extractParameterTypes( + PsiManager.getInstance(project), + ) ?: emptyList() + + val matchResult = + MethodMatcher.findMatchingMethod( + element, + candidateMethods, + paramTypes, + paramExpr?.elExprList?.size ?: 0, + shortName, + ) + + fun result(): ResolveResult { + if (matchResult.validation != null) { + context.validationResult = matchResult.validation + return ResolveResult(validation = matchResult.validation) + } + + val returnType = matchResult.method?.returnType ?: return ResolveResult() + + val convertedType = PsiClassTypeUtil.convertOptionalType(returnType, project) + val resolvedType = + context.parentListBaseType?.let { + PsiClassTypeUtil.getParameterType(project, convertedType, it, context.nestIndex) + } ?: convertedType + + updateContextForIterableType(context, resolvedType, project) + return ResolveResult(type = PsiParentClass(resolvedType)) + } + + return result() + } + + private fun updateContextForIterableType( + context: ResolveContext, + type: PsiType, + project: Project, + ) { + val classType = type as? PsiClassType + if (classType != null && PsiClassTypeUtil.isIterableType(classType, project)) { + context.parentListBaseType = PsiClassTypeUtil.convertOptionalType(type, project) + context.nestIndex = 0 + } + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/ForDirectiveUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/ForDirectiveUtil.kt index 49e8117b..b1e27e88 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/util/ForDirectiveUtil.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/ForDirectiveUtil.kt @@ -26,29 +26,32 @@ import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.PsiTreeUtil -import com.intellij.psi.util.elementType import org.domaframework.doma.intellij.common.dao.findDaoMethod import org.domaframework.doma.intellij.common.psi.PsiDaoMethod import org.domaframework.doma.intellij.common.psi.PsiParentClass import org.domaframework.doma.intellij.common.psi.PsiStaticElement import org.domaframework.doma.intellij.common.sql.PsiClassTypeUtil import org.domaframework.doma.intellij.common.sql.cleanString +import org.domaframework.doma.intellij.common.sql.foritem.ForDeclarationItem import org.domaframework.doma.intellij.common.sql.foritem.ForItem import org.domaframework.doma.intellij.common.validation.result.ValidationCompleteResult +import org.domaframework.doma.intellij.common.validation.result.ValidationNotFoundStaticPropertyResult import org.domaframework.doma.intellij.common.validation.result.ValidationNotFoundTopTypeResult -import org.domaframework.doma.intellij.common.validation.result.ValidationPropertyResult import org.domaframework.doma.intellij.common.validation.result.ValidationResult import org.domaframework.doma.intellij.extension.expr.accessElements import org.domaframework.doma.intellij.extension.psi.findParameter import org.domaframework.doma.intellij.extension.psi.findStaticField -import org.domaframework.doma.intellij.extension.psi.findStaticMethod import org.domaframework.doma.intellij.extension.psi.getForItem import org.domaframework.doma.intellij.extension.psi.getForItemDeclaration +import org.domaframework.doma.intellij.extension.psi.psiClassType import org.domaframework.doma.intellij.psi.SqlElForDirective import org.domaframework.doma.intellij.psi.SqlElIdExpr import org.domaframework.doma.intellij.psi.SqlElParameters import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr import org.domaframework.doma.intellij.psi.SqlTypes +import org.toml.lang.psi.ext.elementType + +private typealias FieldAccessContext = FieldMethodResolver.ResolveContext class ForDirectiveUtil { data class BlockToken( @@ -84,59 +87,8 @@ class ForDirectiveUtil { cachedForDirectiveBlocks.getOrPut(targetElement) { CachedValuesManager.getManager(targetElement.project).createCachedValue { val file = targetElement.containingFile ?: return@createCachedValue null - val directiveBlocks = - PsiTreeUtil - .findChildrenOfType(file, PsiElement::class.java) - .filter { elm -> - ( - elm.elementType == SqlTypes.EL_FOR || - elm.elementType == SqlTypes.EL_IF || - elm.elementType == SqlTypes.EL_END - ) - }.sortedBy { it.textOffset } - .map { - when (it.elementType) { - SqlTypes.EL_FOR -> { - val item = - (it.parent as? SqlElForDirective)?.getForItem() - BlockToken( - BlockType.FOR, - item ?: it, - item?.textOffset ?: 0, - ) - } - - SqlTypes.EL_IF -> - BlockToken( - BlockType.IF, - it, - it.textOffset, - ) - - else -> BlockToken(BlockType.END, it, it.textOffset) - } - } - var stack = mutableListOf() - val filterPosition = - if (skipSelf) { - directiveBlocks.filter { - it.position < targetElement.textOffset - } - } else { - directiveBlocks.filter { it.position <= targetElement.textOffset } - } - filterPosition.forEach { block -> - when (block.type) { - BlockType.FOR, BlockType.IF -> stack.add(block) - BlockType.END -> if (stack.isNotEmpty()) stack.removeAt(stack.lastIndex) - } - } - if (skipSelf) { - stack = - stack - .filter { !isSameForDirective(it.item, targetElement) } - .toMutableList() - } + val directiveBlocks = extractDirectiveBlocks(file) + val stack = buildDirectiveStack(directiveBlocks, targetElement, skipSelf) CachedValueProvider.Result.create( stack.filter { it.type == BlockType.FOR }, @@ -147,6 +99,61 @@ class ForDirectiveUtil { return cachedValue.value } + private fun extractDirectiveBlocks(file: PsiElement): List = + PsiTreeUtil + .findChildrenOfType(file, PsiElement::class.java) + .filter { isDirectiveElement(it) } + .sortedBy { it.textOffset } + .map { createBlockToken(it) } + + private fun isDirectiveElement(element: PsiElement): Boolean = + element.elementType == SqlTypes.EL_FOR || + element.elementType == SqlTypes.EL_IF || + element.elementType == SqlTypes.EL_END + + private fun createBlockToken(element: PsiElement): BlockToken = + when (element.elementType) { + SqlTypes.EL_FOR -> { + val item = (element.parent as? SqlElForDirective)?.getForItem() + BlockToken( + BlockType.FOR, + item ?: element, + item?.textOffset ?: 0, + ) + } + SqlTypes.EL_IF -> + BlockToken( + BlockType.IF, + element, + element.textOffset, + ) + else -> BlockToken(BlockType.END, element, element.textOffset) + } + + private fun buildDirectiveStack( + directiveBlocks: List, + targetElement: PsiElement, + skipSelf: Boolean, + ): List { + val stack = mutableListOf() + val positionThreshold = if (skipSelf) targetElement.textOffset else targetElement.textOffset + 1 + + directiveBlocks + .filter { it.position < positionThreshold } + .forEach { block -> + when (block.type) { + BlockType.FOR, BlockType.IF -> stack.add(block) + BlockType.END -> if (stack.isNotEmpty()) stack.removeAt(stack.lastIndex) + } + } + + return if (skipSelf) { + stack.filter { !isSameForDirective(it.item, targetElement) } + } else { + stack + } + } + private fun isSameForDirective( token: PsiElement, targetElement: PsiElement, @@ -180,9 +187,8 @@ class ForDirectiveUtil { forDirectiveBlocks: List, targetForItem: PsiElement? = null, ): PsiParentClass? { - // Get the type of the top for directive definition element - // Defined in DAO parameters or static property calls if (forDirectiveBlocks.isEmpty()) return null + val topDirectiveItem = forDirectiveBlocks.first().item val file = topDirectiveItem.containingFile ?: return null val daoMethod = findDaoMethod(file) @@ -193,75 +199,154 @@ class ForDirectiveUtil { project, daoMethod, ) - forDirectiveBlocks.drop(1).forEach { directive -> - // Get the definition type of the target directive - val formItem = ForItem(directive.item) - if (targetForItem != null && formItem.element.textOffset > targetForItem.textOffset) { - return parentClassType - } - val forDirectiveExpr = formItem.getParentForDirectiveExpr() - val forDirectiveDeclaration = forDirectiveExpr?.getForItemDeclaration() - if (forDirectiveDeclaration != null) { - val declarationTopElement = - forDirectiveDeclaration.getDeclarationChildren().first() - val findDeclarationForItem = - findForItem(declarationTopElement, forDirectives = forDirectiveBlocks) - - if (findDeclarationForItem == null && daoMethod != null) { - val matchParam = daoMethod.findParameter(declarationTopElement.text) - if (matchParam != null) { - val convertOptional = - PsiClassTypeUtil.convertOptionalType(matchParam.type, project) - parentClassType = PsiParentClass(convertOptional) - } - } - if (parentClassType != null) { - val isBatchAnnotation = daoMethod?.let { PsiDaoMethod(project, it).daoType.isBatchAnnotation() } == true - val forItemDeclarationBlocks = - forDirectiveDeclaration.getDeclarationChildren() - getFieldAccessLastPropertyClassType( - forItemDeclarationBlocks, - project, - parentClassType, - isBatchAnnotation = isBatchAnnotation, - complete = { lastType -> - val classType = lastType.type as? PsiClassType - val nestClass = - if (classType != null && - PsiClassTypeUtil.isIterableType( - classType, - project, - ) - ) { - classType.parameters.firstOrNull() - } else { - null - } - parentClassType = - if (nestClass != null) { - PsiParentClass(nestClass) - } else { - null - } - }, - ) - if (targetForItem != null && formItem.element.text == targetForItem.text) { - return parentClassType - } - } + + for (directive in forDirectiveBlocks.drop(1)) { + val result = + processDirective( + directive, + targetForItem, + forDirectiveBlocks, + daoMethod, + project, + parentClassType, + ) + + if (result.shouldReturn) { + return result.parentClassType } + parentClassType = result.parentClassType } return parentClassType } + private data class DirectiveProcessResult( + val parentClassType: PsiParentClass?, + val shouldReturn: Boolean = false, + ) + + private fun processDirective( + directive: BlockToken, + targetForItem: PsiElement?, + forDirectiveBlocks: List, + daoMethod: PsiMethod?, + project: Project, + currentParentClassType: PsiParentClass?, + ): DirectiveProcessResult { + val formItem = ForItem(directive.item) + + if (targetForItem != null && formItem.element.textOffset > targetForItem.textOffset) { + return DirectiveProcessResult(currentParentClassType, shouldReturn = true) + } + + val forDirectiveExpr = formItem.getParentForDirectiveExpr() + val forDirectiveDeclaration = + forDirectiveExpr?.getForItemDeclaration() + ?: return DirectiveProcessResult(currentParentClassType) + + var updatedParentClassType = + resolveParentClassType( + forDirectiveDeclaration, + forDirectiveBlocks, + daoMethod, + project, + currentParentClassType, + ) + + if (updatedParentClassType != null) { + updatedParentClassType = + processDeclarationBlocks( + forDirectiveDeclaration, + daoMethod, + project, + updatedParentClassType, + ) + + if (targetForItem != null && formItem.element.text == targetForItem.text) { + return DirectiveProcessResult(updatedParentClassType, shouldReturn = true) + } + } + + return DirectiveProcessResult(updatedParentClassType) + } + + private fun resolveParentClassType( + forDirectiveDeclaration: Any, + forDirectiveBlocks: List, + daoMethod: PsiMethod?, + project: Project, + currentParentClassType: PsiParentClass?, + ): PsiParentClass? { + val declaration = forDirectiveDeclaration as? ForDeclarationItem + val declarationTopElement = + declaration?.getDeclarationChildren()?.first() + ?: return currentParentClassType + + val findDeclarationForItem = findForItem(declarationTopElement, forDirectives = forDirectiveBlocks) + + if (findDeclarationForItem == null && daoMethod != null) { + val matchParam = daoMethod.findParameter(declarationTopElement.text) + if (matchParam != null) { + val convertOptional = PsiClassTypeUtil.convertOptionalType(matchParam.type, project) + return PsiParentClass(convertOptional) + } + } + + return currentParentClassType + } + + private fun processDeclarationBlocks( + forDirectiveDeclaration: Any, + daoMethod: PsiMethod?, + project: Project, + parentClassType: PsiParentClass, + ): PsiParentClass? { + val isBatchAnnotation = + daoMethod?.let { + PsiDaoMethod(project, it).daoType.isBatchAnnotation() + } == true + + val declaration = forDirectiveDeclaration as? ForDeclarationItem + val forItemDeclarationBlocks = + declaration?.getDeclarationChildren() + ?: return parentClassType + + var resultParentClassType: PsiParentClass? = parentClassType + + getFieldAccessLastPropertyClassType( + forItemDeclarationBlocks, + project, + parentClassType, + isBatchAnnotation = isBatchAnnotation, + complete = { lastType -> + resultParentClassType = extractNestedClassType(lastType, project) + }, + ) + + return resultParentClassType + } + + private fun extractNestedClassType( + lastType: PsiParentClass, + project: Project, + ): PsiParentClass? { + val classType = lastType.type as? PsiClassType ?: return null + + if (PsiClassTypeUtil.isIterableType(classType, project)) { + val nestClass = classType.parameters.firstOrNull() + return nestClass?.let { PsiParentClass(it) } + } + + return null + } + fun getTopForDirectiveDeclarationClassType( topForDirectiveItem: PsiElement, project: Project, daoMethod: PsiMethod?, ): PsiParentClass? { var result: PsiParentClass? = null - var fieldAccessTopParentClass: PsiParentClass? = null + var fieldAccessTopParentClass: PsiParentClass? val forDirectiveExpr = PsiTreeUtil.getParentOfType(topForDirectiveItem, SqlElForDirective::class.java) @@ -287,11 +372,15 @@ class ForDirectiveUtil { val referenceClazz = staticElement.getRefClazz() ?: return null // In the case of staticFieldAccess, the property that is called first is retrieved. - fieldAccessTopParentClass = + val staticContext = getStaticFieldAccessTopElementClassType( staticFieldAccessExpr, referenceClazz, ) + if (staticContext?.validationResult != null) { + return null + } + fieldAccessTopParentClass = staticContext?.parent } else { // Defined by DAO parameter if (daoMethod == null) return null @@ -305,17 +394,17 @@ class ForDirectiveUtil { val daoParamType = matchParam?.type ?: return null fieldAccessTopParentClass = PsiParentClass(PsiClassTypeUtil.convertOptionalType(daoParamType, project)) } - fieldAccessTopParentClass?.let { + fieldAccessTopParentClass?.let { parentClass -> getFieldAccessLastPropertyClassType( forItemDeclarationBlocks, topForDirectiveItem.project, - it, + parentClass, isBatchAnnotation = isBatchAnnotation, complete = { lastType -> val classType = lastType.type as? PsiClassType val nestClass = if (classType != null && - PsiClassTypeUtil.Companion.isIterableType(classType, project) + PsiClassTypeUtil.isIterableType(classType, project) ) { classType.parameters.firstOrNull() } else { @@ -332,21 +421,60 @@ class ForDirectiveUtil { fun getStaticFieldAccessTopElementClassType( staticFieldAccessExpr: SqlElStaticFieldAccessExpr, referenceClazz: PsiClass, - ): PsiParentClass? { + shortName: String = "", + ): FieldAccessContext? { val topElement = staticFieldAccessExpr.elIdExprList.firstOrNull() ?: return null val searchText = cleanString(topElement.text) - if (topElement.nextSibling?.elementType != SqlTypes.EL_PARAMETERS) { + val prentClazz = PsiParentClass(referenceClazz.psiClassType) + val context = + FieldAccessContext( + parent = prentClazz, + parentListBaseType = null, + nestIndex = 0, + completeResult = null, + validationResult = null, + ) + val parametersExpr = topElement.nextSibling as? SqlElParameters + if (parametersExpr == null) { val topPropertyField = referenceClazz.findStaticField(searchText) + topPropertyField?.type?.let { - return PsiParentClass(it) + context.parent = PsiParentClass(it) + return context } } else { - val topPropertyMethod = referenceClazz.findStaticMethod(searchText) - topPropertyMethod?.let { - val returnType = it.returnType ?: return null - return PsiParentClass(returnType) + val resolveResult = + FieldMethodResolver.resolveStaticMethod( + context, + topElement, + prentClazz, + topElement.text, + parametersExpr, + topElement.project, + shortName, + ) + if (resolveResult.type != null) { + context.parent = resolveResult.type + return context + } else if (resolveResult.validation != null) { + context.validationResult = resolveResult.validation + return context + } else { + context.validationResult = + ValidationNotFoundStaticPropertyResult( + topElement, + staticFieldAccessExpr.elClass.text, + shortName, + ) + return context } } + context.validationResult = + ValidationNotFoundStaticPropertyResult( + topElement, + staticFieldAccessExpr.elClass.text, + shortName, + ) return null } @@ -369,137 +497,107 @@ class ForDirectiveUtil { shortName: String = "", dropLastIndex: Int = 0, findFieldMethod: ((PsiType) -> PsiParentClass)? = { type -> PsiParentClass(type) }, - complete: ((PsiParentClass) -> Unit) = { parent: PsiParentClass? -> }, + complete: ((PsiParentClass) -> Unit) = { _: PsiParentClass? -> }, ): ValidationResult? { - var parent = - if (isBatchAnnotation) { - val parentType = PsiClassTypeUtil.convertOptionalType(topParent.type, project) - val nextClassType = parentType as? PsiClassType ?: return null - val nestType = nextClassType.parameters.firstOrNull() ?: return null - PsiParentClass(PsiClassTypeUtil.convertOptionalType(nestType, project)) - } else { - val convertOptional = PsiClassTypeUtil.convertOptionalType(topParent.type, project) - PsiParentClass(convertOptional) - } - val parentType = PsiClassTypeUtil.convertOptionalType(parent.type, project) + val initialParent = resolveInitialParent(topParent, project, isBatchAnnotation) ?: return null + val parentType = PsiClassTypeUtil.convertOptionalType(initialParent.type, project) val classType = parentType as? PsiClassType ?: return ValidationNotFoundTopTypeResult(blocks.first(), shortName) - var competeResult: ValidationCompleteResult? = null - val searchBlocks = blocks.drop(1).dropLast(dropLastIndex) if (dropLastIndex > 0 && searchBlocks.isEmpty()) { - complete.invoke(parent) - return ValidationCompleteResult( - blocks.last(), - parent, - ) + complete.invoke(initialParent) + return ValidationCompleteResult(blocks.last(), initialParent) } - // When a List type element is used as the parent, - // the original declared type is retained and the referenced type is obtained by nesting. - var parentListBaseType: PsiType? = - if (PsiClassTypeUtil.isIterableType(classType, project)) { - PsiClassTypeUtil.convertOptionalType(parentType, project) - } else { - null - } - var nestIndex = 0 + val context = + FieldAccessContext( + parent = initialParent, + parentListBaseType = + if (PsiClassTypeUtil.isIterableType(classType, project)) { + PsiClassTypeUtil.convertOptionalType(parentType, project) + } else { + null + }, + nestIndex = 0, + completeResult = null, + validationResult = null, + ) + + val finalContext = + processFieldAccessBlocks( + searchBlocks, + context, + project, + shortName, + findFieldMethod, + ) + + complete.invoke(finalContext.parent) + return finalContext.completeResult ?: finalContext.validationResult + } + + private fun resolveInitialParent( + topParent: PsiParentClass, + project: Project, + isBatchAnnotation: Boolean, + ): PsiParentClass? { + return if (isBatchAnnotation) { + val parentType = PsiClassTypeUtil.convertOptionalType(topParent.type, project) + val nextClassType = parentType as? PsiClassType ?: return null + val nestType = nextClassType.parameters.firstOrNull() ?: return null + PsiParentClass(PsiClassTypeUtil.convertOptionalType(nestType, project)) + } else { + val convertOptional = PsiClassTypeUtil.convertOptionalType(topParent.type, project) + PsiParentClass(convertOptional) + } + } - // Analyze a block element and get the type of the last called property + private fun processFieldAccessBlocks( + searchBlocks: List, + context: FieldAccessContext, + project: Project, + shortName: String, + findFieldMethod: ((PsiType) -> PsiParentClass)?, + ): FieldAccessContext { for (element in searchBlocks) { val searchElm = cleanString(getSearchElementText(element)) if (searchElm.isEmpty()) { - complete.invoke(parent) - return ValidationCompleteResult( - element, - parent, - ) + context.completeResult = ValidationCompleteResult(element, context.parent) + return context } - val field = - parent - .findField(searchElm) - ?.let { match -> - val convertOptional = PsiClassTypeUtil.convertOptionalType(match.type, project) - val type = - parentListBaseType?.let { - PsiClassTypeUtil.getParameterType( - project, - convertOptional, - it, - nestIndex, - ) - } - ?: convertOptional - val classType = type as? PsiClassType - if (classType != null && - PsiClassTypeUtil.isIterableType( - classType, - element.project, - ) - ) { - parentListBaseType = PsiClassTypeUtil.convertOptionalType(type, project) - nestIndex = 0 - } - findFieldMethod?.invoke(type) - } - val method = - parent - .findMethod(searchElm) - ?.let { match -> - val returnType = match.returnType ?: return null - val convertOptionalType = PsiClassTypeUtil.convertOptionalType(returnType, project) - val methodReturnType = - parentListBaseType?.let { - PsiClassTypeUtil.getParameterType( - project, - convertOptionalType, - it, - nestIndex, - ) - } - ?: convertOptionalType - val classType = methodReturnType as? PsiClassType - if (classType != null && - PsiClassTypeUtil.isIterableType( - classType, - element.project, - ) - ) { - parentListBaseType = methodReturnType - nestIndex = 0 - } - findFieldMethod?.invoke(methodReturnType) - } - nestIndex++ - if (field == null && method == null) { - return ValidationPropertyResult( - element, - parent, - shortName, - ) - } + val field = FieldMethodResolver.resolveField(context, searchElm, project) + val methodResult = FieldMethodResolver.resolveMethod(context, element, searchElm, project, shortName) - if (field != null && element.nextSibling !is SqlElParameters) { - parent = field - competeResult = - ValidationCompleteResult( - element, - parent, - ) - } else if (method != null) { - parent = method - competeResult = - ValidationCompleteResult( - element, - parent, - ) + context.nestIndex++ + if (field == null && methodResult.type == null) { + context.completeResult = null + context.validationResult = methodResult.validation + return context } + + val method = methodResult.type + findFieldMethod?.invoke(field?.type ?: method?.type ?: context.parent.type) + updateParentContext(element, field, method, context) + } + return context + } + + private fun updateParentContext( + element: PsiElement, + field: PsiParentClass?, + method: PsiParentClass?, + context: FieldAccessContext, + ) { + if (field != null && element.nextSibling !is SqlElParameters) { + context.parent = field + context.completeResult = ValidationCompleteResult(element, context.parent) + } else if (method != null) { + context.parent = method + context.completeResult = ValidationCompleteResult(element, context.parent) } - complete.invoke(parent) - return competeResult } private fun getSearchElementText(elm: PsiElement): String = diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/MethodMatcher.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/MethodMatcher.kt new file mode 100644 index 00000000..3583a3d6 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/MethodMatcher.kt @@ -0,0 +1,97 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.util + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import org.domaframework.doma.intellij.common.validation.result.ValidationResult +import org.domaframework.doma.intellij.common.validation.result.ValidationResultFunctionCallParameterCount +import org.domaframework.doma.intellij.common.validation.result.ValidationResultFunctionCallParameterTypeMismatch + +class MethodMatcher { + data class MatchResult( + val method: PsiMethod? = null, + val validation: ValidationResult? = null, + ) + + companion object { + fun findMatchingMethod( + element: PsiElement, + candidateMethods: List, + actualParameterTypes: List, + actualParameterCount: Int, + shortName: String = "", + ): MatchResult { + if (candidateMethods.isEmpty()) { + return MatchResult(validation = null) + } + + val methodsWithCorrectParameterCount = filterByParameterCount(candidateMethods, actualParameterCount) + + if (methodsWithCorrectParameterCount.isEmpty()) { + return MatchResult( + validation = + ValidationResultFunctionCallParameterCount( + element, + shortName, + actualParameterCount, + ), + ) + } + + val matchedMethod = findMethodByParameterTypes(methodsWithCorrectParameterCount, actualParameterTypes) + + return if (matchedMethod != null) { + MatchResult(method = matchedMethod) + } else { + MatchResult( + validation = + ValidationResultFunctionCallParameterTypeMismatch( + element, + shortName, + candidateMethods, + actualParameterTypes, + ), + ) + } + } + + private fun filterByParameterCount( + methods: List, + expectedCount: Int, + ): List = methods.filter { it.parameterList.parameters.size == expectedCount } + + private fun findMethodByParameterTypes( + methods: List, + actualParameterTypes: List, + ): PsiMethod? = + methods.firstOrNull { method -> + val methodParams = method.parameterList.parameters + methodParams.size == actualParameterTypes.size && + methodParams.zip(actualParameterTypes).all { (definedParam, actualType) -> + areTypesCompatible(definedParam.type, actualType) + } + } + + private fun areTypesCompatible( + expectedType: PsiType, + actualType: PsiType?, + ): Boolean = + expectedType == actualType || TypeUtil.sameTypeIgnoringBoxing(expectedType, actualType) || + actualType?.superTypes?.any { it == expectedType } == true + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt index 23c92f21..296389a8 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/util/TypeUtil.kt @@ -42,6 +42,19 @@ object TypeUtil { return type } + /** + * Checks if the actual type is the same as the expected type, ignoring boxing. + */ + fun sameTypeIgnoringBoxing( + actual: PsiType, + expect: PsiType?, + ): Boolean { + val au = expect?.let { actual.isAssignableFrom(it) } + val eu = expect?.isAssignableFrom(actual) + + return au == true && eu == true + } + /** * Checks if the given type is an entity. */ diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationInvalidExpressionFunctionsResult.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationInvalidExpressionFunctionsResult.kt index fb2e78d4..07a7d6e7 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationInvalidExpressionFunctionsResult.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationInvalidExpressionFunctionsResult.kt @@ -28,6 +28,7 @@ import org.domaframework.doma.intellij.common.psi.PsiParentClass open class ValidationInvalidExpressionFunctionsResult( override val identify: PsiElement, override val shortName: String, + private val implementClassName: String, ) : ValidationResult(identify, null, shortName) { override fun setHighlight( highlightRange: TextRange, @@ -40,6 +41,7 @@ open class ValidationInvalidExpressionFunctionsResult( identify, MessageBundle.message( "inspection.invalid.sql.notFound.expressionClass", + implementClassName, ), problemHighlightType(project, shortName), highlightRange, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationNotFoundStaticPropertyResult.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationNotFoundStaticPropertyResult.kt index 890cffd6..8679d872 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationNotFoundStaticPropertyResult.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationNotFoundStaticPropertyResult.kt @@ -20,11 +20,10 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import org.domaframework.doma.intellij.bundle.MessageBundle import org.domaframework.doma.intellij.common.psi.PsiParentClass -import org.domaframework.doma.intellij.psi.SqlElClass class ValidationNotFoundStaticPropertyResult( override val identify: PsiElement?, - val clazz: SqlElClass, + val parentClassName: String, override val shortName: String = "", ) : ValidationResult(identify, null, shortName) { override fun setHighlight( @@ -39,7 +38,7 @@ class ValidationNotFoundStaticPropertyResult( MessageBundle.message( "inspection.invalid.sql.staticProperty", identify.text, - clazz.text, + parentClassName, ), problemHighlightType(project, shortName), highlightRange, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationResultFunctionCallParameterCount.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationResultFunctionCallParameterCount.kt new file mode 100644 index 00000000..a8665bfe --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationResultFunctionCallParameterCount.kt @@ -0,0 +1,43 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.validation.result + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.psi.PsiParentClass + +class ValidationResultFunctionCallParameterCount( + override val identify: PsiElement?, + override val shortName: String = "", + private val actual: Int, +) : ValidationResult(identify, null, shortName) { + override fun setHighlight( + highlightRange: TextRange, + identify: PsiElement, + holder: ProblemsHolder, + parent: PsiParentClass?, + ) { + val project = identify.project + holder.registerProblem( + identify, + MessageBundle.message("inspection.invalid.sql.function.call.parameter.count", identify.text, actual), + problemHighlightType(project, shortName), + highlightRange, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationResultFunctionCallParameterTypeMismatch.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationResultFunctionCallParameterTypeMismatch.kt new file mode 100644 index 00000000..e5f0a53f --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/validation/result/ValidationResultFunctionCallParameterTypeMismatch.kt @@ -0,0 +1,60 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.common.validation.result + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.psi.PsiParentClass + +class ValidationResultFunctionCallParameterTypeMismatch( + override val identify: PsiElement?, + override val shortName: String = "", + private val expectedTypes: List, + private val actualTypes: List, +) : ValidationResult(identify, null, shortName) { + override fun setHighlight( + highlightRange: TextRange, + identify: PsiElement, + holder: ProblemsHolder, + parent: PsiParentClass?, + ) { + val project = identify.project + val subMessages = + expectedTypes.map { m -> + "${m.name}(${m.parameterList.parameters.joinToString { it.type.presentableText }})" + } + val subMessageHtml = subMessages.joinToString("
") { "  $it" } + + val description = + MessageBundle.message( + "inspection.invalid.sql.function.call.parameter.type.mismatch", + identify.text, + actualTypes.joinToString(", ") { it?.presentableText ?: "unknown" }, + subMessageHtml, + ) + + holder.registerProblem( + identify, + description, + problemHighlightType(project, shortName), + highlightRange, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/processor/SqlCompletionBlockProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/processor/SqlCompletionBlockProcessor.kt index 7da586e4..fe0e4136 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/processor/SqlCompletionBlockProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/processor/SqlCompletionBlockProcessor.kt @@ -50,13 +50,20 @@ abstract class SqlCompletionBlockProcessor { }.toList() var inParameter = false + val rightLists: MutableList = mutableListOf() val formatElements = mutableListOf() prevElements.forEach { prev -> - if (prev.elementType == SqlTypes.RIGHT_PAREN) inParameter = true - if (!inParameter) { + if (prev.elementType == SqlTypes.RIGHT_PAREN) { + inParameter = true + rightLists.add(prev) + } + if (!inParameter && rightLists.isEmpty()) { formatElements.add(prev) } - if (prev.elementType == SqlTypes.LEFT_PAREN) inParameter = false + if (prev.elementType == SqlTypes.LEFT_PAREN) { + inParameter = false + rightLists.removeLastOrNull() + } } val filterElements = @@ -72,7 +79,7 @@ abstract class SqlCompletionBlockProcessor { }.plus(targetElement) .sortedBy { it.textOffset } - return if (filterElements.isNotEmpty()) filterElements else emptyList() + return filterElements.ifEmpty { emptyList() } } private fun isSqlElSymbol(element: PsiElement): Boolean = diff --git a/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt index b2c2cd97..55b3ee2e 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt @@ -149,7 +149,7 @@ class SqlParameterCompletionProvider : CompletionProvider( // If the parent has field access, get its child element // Completion for the first argument in SqlElParameters. - var parameterParent: PsiElement? = + val parameterParent: PsiElement? = PsiTreeUtil.getParentOfType(targetElement, SqlElParameters::class.java) val processor = SqlCompletionParameterArgsBlockProcessor(targetElement) if (parameterParent != null && parameterParent.children.size <= 1) { @@ -420,7 +420,7 @@ class SqlParameterCompletionProvider : CompletionProvider( caretNextText: String, result: CompletionResultSet, ) { - var psiParentClass = PsiParentClass(topElementType) + val psiParentClass = PsiParentClass(topElementType) // FieldAccess Completion ForDirectiveUtil.getFieldAccessLastPropertyClassType( diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt index 6bc137dd..eb5441bf 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt @@ -15,34 +15,25 @@ */ package org.domaframework.doma.intellij.extension.expr +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes +import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType +import org.domaframework.doma.intellij.common.dao.findDaoMethod import org.domaframework.doma.intellij.psi.SqlCustomElCommentExpr import org.domaframework.doma.intellij.psi.SqlElElseifDirective import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr import org.domaframework.doma.intellij.psi.SqlElForDirective +import org.domaframework.doma.intellij.psi.SqlElFunctionCallExpr import org.domaframework.doma.intellij.psi.SqlElIdExpr import org.domaframework.doma.intellij.psi.SqlElIfDirective +import org.domaframework.doma.intellij.psi.SqlElParameters +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr import org.domaframework.doma.intellij.psi.SqlTypes -val SqlElStaticFieldAccessExpr.accessElements: List - get() = - this.elIdExprList - .sortedBy { it.textOffset } - .toList() - -val SqlElFieldAccessExpr.accessElements: List - get() = - this - .getElPrimaryExprList() - .mapNotNull { it as SqlElIdExpr } - .sortedBy { it.textOffset } - .toList() - -fun SqlElFieldAccessExpr.accessElementsPrevOriginalElement(targetTextOffset: Int): List = - this.accessElements.filter { it != null && it.textOffset <= targetTextOffset }.mapNotNull { it } - fun SqlCustomElCommentExpr.isConditionOrLoopDirective(): Boolean = PsiTreeUtil.getChildOfType(this, SqlElIfDirective::class.java) != null || PsiTreeUtil.getChildOfType(this, SqlElForDirective::class.java) != null || @@ -52,3 +43,34 @@ fun SqlCustomElCommentExpr.isConditionOrLoopDirective(): Boolean = ) != null || this.findElementAt(2)?.elementType == SqlTypes.EL_END || this.findElementAt(2)?.elementType == SqlTypes.EL_ELSE + +fun SqlElParameters.extractParameterTypes(psiManager: PsiManager): List = + this.elExprList.mapNotNull { param -> + when (param) { + is SqlElStaticFieldAccessExpr -> param.extractStaticFieldType() + is SqlElFieldAccessExpr -> param.extractFieldType() + is SqlElIdExpr -> { + val daoMethod = findDaoMethod(this.containingFile) + daoMethod + ?.parameterList + ?.parameters + ?.find { it.name == param.text } + ?.type + } + is SqlElPrimaryExpr -> { + when (param.node.firstChildNode.elementType) { + SqlTypes.EL_NUMBER -> PsiTypes.intType() + SqlTypes.EL_STRING -> { + PsiType.getJavaLangString(psiManager, GlobalSearchScope.allScope(param.project)) + } + SqlTypes.BOOLEAN -> PsiTypes.booleanType() + else -> null + } + } + is SqlElFunctionCallExpr -> { + param.extractFunctionReturnType() + } + + else -> null + } + } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElFieldAccessExprExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElFieldAccessExprExtensions.kt new file mode 100644 index 00000000..8c2d604f --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElFieldAccessExprExtensions.kt @@ -0,0 +1,44 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.extension.expr + +import com.intellij.psi.PsiType +import org.domaframework.doma.intellij.inspection.sql.processor.InspectionFieldAccessVisitorProcessor +import org.domaframework.doma.intellij.inspection.sql.processor.InspectionFunctionCallVisitorProcessor +import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlElFunctionCallExpr +import org.domaframework.doma.intellij.psi.SqlElIdExpr + +val SqlElFieldAccessExpr.accessElements: List + get() = + this + .getElPrimaryExprList() + .mapNotNull { it as SqlElIdExpr } + .sortedBy { it.textOffset } + .toList() + +fun SqlElFieldAccessExpr.accessElementsPrevOriginalElement(targetTextOffset: Int): List = + this.accessElements.filter { it != null && it.textOffset <= targetTextOffset }.mapNotNull { it } + +fun SqlElFieldAccessExpr.extractFieldType(): PsiType? { + val processor = InspectionFieldAccessVisitorProcessor(shortName = "", this) + return processor.getFieldAccessLastPropertyClassType() +} + +fun SqlElFunctionCallExpr.extractFunctionReturnType(): PsiType? { + val processor = InspectionFunctionCallVisitorProcessor(shortName = "", this) + return processor.getFunctionCallType()?.returnType +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElStaticFieldAccessExprExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElStaticFieldAccessExprExtensions.kt new file mode 100644 index 00000000..1de7c578 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElStaticFieldAccessExprExtensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.extension.expr + +import com.intellij.psi.PsiType +import org.domaframework.doma.intellij.inspection.sql.processor.InspectionStaticFieldAccessVisitorProcessor +import org.domaframework.doma.intellij.psi.SqlElIdExpr +import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr + +val SqlElStaticFieldAccessExpr.accessElements: List + get() = + this.elIdExprList + .sortedBy { it.textOffset } + .toList() + +fun SqlElStaticFieldAccessExpr.extractStaticFieldType(): PsiType? { + val processor = InspectionStaticFieldAccessVisitorProcessor(shortName = "") + return processor.getStaticFieldAccessLastPropertyClassType(this) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt index d35a23dd..bdd8cb92 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt @@ -20,7 +20,7 @@ import com.intellij.psi.PsiParameter import org.domaframework.doma.intellij.common.util.DomaClassName private val ignoreUsageCheckType = - listOf( + listOf( DomaClassName.JAVA_FUNCTION, DomaClassName.BI_FUNCTION, DomaClassName.SELECT_OPTIONS, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFieldAccessVisitorProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFieldAccessVisitorProcessor.kt index 439a564d..1c436b67 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFieldAccessVisitorProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFieldAccessVisitorProcessor.kt @@ -16,12 +16,17 @@ package org.domaframework.doma.intellij.inspection.sql.processor import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.psi.DummyPsiParentClass import org.domaframework.doma.intellij.common.psi.PsiDaoMethod import org.domaframework.doma.intellij.common.psi.PsiParentClass import org.domaframework.doma.intellij.common.sql.cleanString import org.domaframework.doma.intellij.common.util.ForDirectiveUtil +import org.domaframework.doma.intellij.common.validation.result.ValidationDaoParamResult import org.domaframework.doma.intellij.extension.expr.accessElements import org.domaframework.doma.intellij.extension.psi.findParameter import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr @@ -31,67 +36,162 @@ import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr class InspectionFieldAccessVisitorProcessor( val shortName: String, private val element: SqlElFieldAccessExpr, -) : InspectionVisitorProcessor(shortName) { - fun check( - holder: ProblemsHolder, - file: PsiFile, - ) { - // Get element inside block comment - val blockElement = getFieldAccessBlocks(element) - val topElm = blockElement.firstOrNull() as SqlElPrimaryExpr - - // Exclude fixed Literal - if (isLiteralOrStatic(topElm)) return - - val topElement = blockElement.firstOrNull() ?: return - val daoMethod = findDaoMethod(file) ?: return - val project = topElement.project - val forDirectiveBlocks = ForDirectiveUtil.getForDirectiveBlocks(topElement) - val forItem = ForDirectiveUtil.findForItem(topElement, forDirectives = forDirectiveBlocks) - var isBatchAnnotation = false - val topElementParentClass = - if (forItem != null) { - val result = ForDirectiveUtil.getForDirectiveItemClassType(project, forDirectiveBlocks, forItem) - if (result == null) { - // If the for directive definition element of the top element is invalid, - // an error is displayed by InspectionPrimaryVisitorProcessor. - return - } - val specifiedClassType = - ForDirectiveUtil.resolveForDirectiveItemClassTypeBySuffixElement(topElement.text) - if (specifiedClassType != null) { - PsiParentClass(specifiedClassType) - } else { - result - } - } else { - val paramType = daoMethod.findParameter(cleanString(topElement.text))?.type - if (paramType == null) { - errorHighlight(topElement, daoMethod, holder) - return - } - isBatchAnnotation = PsiDaoMethod(project, daoMethod).daoType.isBatchAnnotation() - PsiParentClass(paramType) +) : InspectionVisitorProcessor() { + private val project = element.project + private var targetFile: PsiFile = element.containingFile + private var blockElements: List = emptyList() + private var topElement: SqlElIdExpr? = null + private var daoMethod: PsiMethod? = findDaoMethod(targetFile) + private var isBatchAnnotation = false + + /** + * Check that the source of the bind variable in the SQL exists + */ + fun checkBindVariableDefine(holder: ProblemsHolder) { + when (val topElementClass = resolveTopElementType(targetFile)) { + is DummyPsiParentClass -> return + null -> { + handleNullTopElementClass(holder) + return } - val result = - ForDirectiveUtil.getFieldAccessLastPropertyClassType( - blockElement, - project, - topElementParentClass, - shortName = this.shortName, - isBatchAnnotation = isBatchAnnotation, - ) + else -> checkFieldAccess(topElementClass, holder) + } + } + + /** + * Get the final class type of the field access element + */ + fun getFieldAccessLastPropertyClassType(): PsiType? = + when (val topElementClass = resolveTopElementType(targetFile)) { + is DummyPsiParentClass, null -> null + + else -> getLastFieldAccess(topElementClass).type + } + + private fun resolveTopElementType(file: PsiFile): PsiParentClass? { + targetFile = file + blockElements = extractFieldAccessBlocks(element) + + if (!validateBlockElements()) { + return DummyPsiParentClass() + } + + val topElm = topElement ?: return DummyPsiParentClass() + val method = daoMethod ?: return DummyPsiParentClass() + + return resolveTypeFromContext(topElm, method) + } + + private fun validateBlockElements(): Boolean { + val primaryElement = blockElements.firstOrNull() as? SqlElPrimaryExpr + if (primaryElement != null && isLiteralOrStatic(primaryElement)) { + return false + } + + topElement = blockElements.firstOrNull() + return daoMethod != null && topElement != null + } + + private fun resolveTypeFromContext( + topElm: SqlElIdExpr, + method: PsiMethod, + ): PsiParentClass? { + val forDirectiveBlocks = ForDirectiveUtil.getForDirectiveBlocks(topElm) + val forItem = ForDirectiveUtil.findForItem(topElm, forDirectives = forDirectiveBlocks) + + return if (forItem != null) { + resolveForDirectiveType(topElm, forDirectiveBlocks, forItem) + } else { + resolveParameterType(topElm, method) + } + } + + private fun resolveForDirectiveType( + topElm: SqlElIdExpr, + forDirectiveBlocks: List, + forItem: PsiElement, + ): PsiParentClass? { + val baseType = + ForDirectiveUtil.getForDirectiveItemClassType(project, forDirectiveBlocks, forItem) + ?: return DummyPsiParentClass() + + val specifiedType = ForDirectiveUtil.resolveForDirectiveItemClassTypeBySuffixElement(topElm.text) + return if (specifiedType != null) { + PsiParentClass(specifiedType) + } else { + baseType + } + } + + private fun resolveParameterType( + topElm: SqlElIdExpr, + method: PsiMethod, + ): PsiParentClass? { + val paramType = method.findParameter(cleanString(topElm.text))?.type ?: return null + isBatchAnnotation = PsiDaoMethod(project, method).daoType.isBatchAnnotation() + return PsiParentClass(paramType) + } + private fun handleNullTopElementClass(holder: ProblemsHolder) { + val topElm = topElement ?: return + val method = daoMethod ?: return + errorHighlight(topElm, method, holder) + } + + private fun checkFieldAccess( + topElementClass: PsiParentClass, + holder: ProblemsHolder, + ) { + val result = getFieldAccess(topElementClass) result?.highlightElement(holder) } - fun getFieldAccessBlocks(element: SqlElFieldAccessExpr): List { - val blockElements = element.accessElements - (blockElements.firstOrNull() as? SqlElPrimaryExpr) - ?.let { if (isLiteralOrStatic(it)) return emptyList() } - ?: return emptyList() + private fun getFieldAccess(topElementClass: PsiParentClass) = + ForDirectiveUtil.getFieldAccessLastPropertyClassType( + blockElements, + project, + topElementClass, + shortName = this.shortName, + isBatchAnnotation = isBatchAnnotation, + ) - return blockElements.mapNotNull { it as SqlElIdExpr } + private fun getLastFieldAccess(topElementClass: PsiParentClass): PsiParentClass { + var findLastTypeParent = topElementClass + ForDirectiveUtil.getFieldAccessLastPropertyClassType( + blockElements, + project, + topElementClass, + shortName = this.shortName, + isBatchAnnotation = isBatchAnnotation, + findFieldMethod = { type -> + findLastTypeParent = PsiParentClass(type) + findLastTypeParent + }, + ) + return findLastTypeParent + } + + private fun extractFieldAccessBlocks(element: SqlElFieldAccessExpr): List { + val accessElements = element.accessElements + val primaryElement = accessElements.firstOrNull() as? SqlElPrimaryExpr ?: return emptyList() + + if (isLiteralOrStatic(primaryElement)) { + return emptyList() + } + + return accessElements.filterIsInstance() + } + + private fun errorHighlight( + topElement: SqlElIdExpr, + daoMethod: PsiMethod, + holder: ProblemsHolder, + ) { + ValidationDaoParamResult( + topElement, + daoMethod.name, + this.shortName, + ).highlightElement(holder) } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionForDirectiveVisitorProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionForDirectiveVisitorProcessor.kt index 3c85633e..99e6a4b7 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionForDirectiveVisitorProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionForDirectiveVisitorProcessor.kt @@ -24,7 +24,7 @@ import org.domaframework.doma.intellij.psi.SqlElForDirective class InspectionForDirectiveVisitorProcessor( val shortName: String, private val element: SqlElForDirective, -) : InspectionVisitorProcessor(shortName) { +) : InspectionVisitorProcessor() { fun check(holder: ProblemsHolder) { val forItem = element.getForItem() ?: return val directiveBlocks = ForDirectiveUtil.getForDirectiveBlocks(forItem, false) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFunctionCallVisitorProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFunctionCallVisitorProcessor.kt index d053c050..8a1dddc7 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFunctionCallVisitorProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionFunctionCallVisitorProcessor.kt @@ -16,58 +16,153 @@ package org.domaframework.doma.intellij.inspection.sql.processor import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod import org.domaframework.doma.intellij.common.CommonPathParameterUtil import org.domaframework.doma.intellij.common.config.DomaCompileConfigUtil -import org.domaframework.doma.intellij.common.config.DomaCompileConfigUtil.EXPRESSION_FUNCTIONS_NAME import org.domaframework.doma.intellij.common.helper.ExpressionFunctionsHelper +import org.domaframework.doma.intellij.common.psi.PsiParentClass +import org.domaframework.doma.intellij.common.util.MethodMatcher import org.domaframework.doma.intellij.common.validation.result.ValidationInvalidExpressionFunctionsResult import org.domaframework.doma.intellij.common.validation.result.ValidationInvalidFunctionCallResult import org.domaframework.doma.intellij.common.validation.result.ValidationResult import org.domaframework.doma.intellij.extension.getJavaClazz +import org.domaframework.doma.intellij.extension.psi.psiClassType import org.domaframework.doma.intellij.psi.SqlElFunctionCallExpr import org.jetbrains.kotlin.idea.util.projectStructure.module class InspectionFunctionCallVisitorProcessor( val shortName: String, private val element: SqlElFunctionCallExpr, -) : InspectionVisitorProcessor(shortName) { +) : InspectionVisitorProcessor() { fun check(holder: ProblemsHolder) { + val result: ValidationResult? = getFunctionCallValidationResult() + result?.highlightElement(holder) + } + + fun getFunctionCallType(): PsiMethod? { + val result = getFunctionCall() + return result + } + + private fun getFunctionCallValidationResult(): ValidationResult? { val project = element.project - val module = element.module ?: return + val module = element.module ?: return null val expressionHelper = ExpressionFunctionsHelper val expressionFunctionalInterface = expressionHelper.setExpressionFunctionsInterface(project) val functionName = element.elIdExpr - val isTest = CommonPathParameterUtil.isTest(module, element.containingFile.virtualFile) val customFunctionClassName = DomaCompileConfigUtil.getConfigValue(module, isTest, "doma.expr.functions") - var result: ValidationResult = - ValidationInvalidFunctionCallResult( + var methods: List + val expressionClazz = customFunctionClassName?.let { project.getJavaClazz(it) } + + if (customFunctionClassName?.isEmpty() == true) { + return ValidationInvalidExpressionFunctionsResult( functionName, - customFunctionClassName ?: EXPRESSION_FUNCTIONS_NAME, shortName, + customFunctionClassName, ) - var methods: Array = emptyArray() - val expressionClazz = customFunctionClassName?.let { project.getJavaClazz(it) } - if (expressionClazz != null) { - if (expressionHelper.isInheritor(expressionClazz)) { - methods = expressionClazz.findMethodsByName(functionName.text, true) + } + + val parentClass = expressionClazz ?: expressionFunctionalInterface + var result: ValidationResult? = null + if (parentClass != null) { + if (expressionHelper.isInheritor(parentClass) || parentClass == expressionFunctionalInterface) { + methods = parentClass.findMethodsByName(functionName.text, true).mapNotNull { it } + if (methods.isEmpty()) { + return ValidationInvalidFunctionCallResult( + element.elIdExpr, + parentClass.psiClassType.canonicalText, + shortName, + ) + } + result = null + val matchResult = checkParamTypeMatch(parentClass) + if (matchResult.method == null) { + result = matchResult.validation + } } else { result = ValidationInvalidExpressionFunctionsResult( functionName, shortName, + customFunctionClassName ?: "", ) } } + if (result != null && + isImplementExpressionFunction( + parentClass, + expressionFunctionalInterface, + expressionHelper, + ) + ) { + result = + ValidationInvalidExpressionFunctionsResult( + functionName, + shortName, + customFunctionClassName ?: "", + ) + } + return result + } + + private fun getFunctionCall(): PsiMethod? { + val project = element.project + val module = element.module ?: return null + val expressionHelper = ExpressionFunctionsHelper + val expressionFunctionalInterface = expressionHelper.setExpressionFunctionsInterface(project) + val functionName = element.elIdExpr + val isTest = CommonPathParameterUtil.isTest(module, element.containingFile.virtualFile) + val customFunctionClassName = DomaCompileConfigUtil.getConfigValue(module, isTest, "doma.expr.functions") - if (methods.isEmpty()) { - methods = expressionFunctionalInterface?.findMethodsByName(functionName.text, true) ?: emptyArray() + if (customFunctionClassName?.isEmpty() == true) { + return null } - if (methods.isEmpty()) { - result.highlightElement(holder) + var methods: List + val expressionClazz = customFunctionClassName?.let { project.getJavaClazz(it) } + val parentClass = expressionClazz ?: expressionFunctionalInterface + var result: PsiMethod? = null + if (parentClass != null) { + if (expressionHelper.isInheritor(parentClass) || parentClass == expressionFunctionalInterface) { + methods = parentClass.findMethodsByName(functionName.text, true).mapNotNull { it } + if (methods.isEmpty()) { + return null + } + result = getParamTypeMatch(parentClass) + } } + if (isImplementExpressionFunction( + parentClass, + expressionFunctionalInterface, + expressionHelper, + ) + ) { + return null + } + + return result + } + + /** + * Not inheriting from ExpressionFunctions. + */ + private fun isImplementExpressionFunction( + parentClass: PsiClass?, + expressionFunctionalInterface: PsiClass?, + expressionHelper: ExpressionFunctionsHelper.Companion, + ): Boolean = parentClass != expressionFunctionalInterface && !expressionHelper.isInheritor(parentClass) + + private fun checkParamTypeMatch(parentPsiClass: PsiClass): MethodMatcher.MatchResult { + val psiParentClassExpressionClazz = PsiParentClass(parentPsiClass.psiClassType) + return psiParentClassExpressionClazz.findMethod(element, shortName) + } + + private fun getParamTypeMatch(parentPsiClass: PsiClass): PsiMethod? { + val psiParentClassExpressionClazz = PsiParentClass(parentPsiClass.psiClassType) + val methodResult = psiParentClassExpressionClazz.findMethod(element, shortName) + return methodResult.method } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionPrimaryVisitorProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionPrimaryVisitorProcessor.kt index 501ed2a4..b9e343c5 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionPrimaryVisitorProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionPrimaryVisitorProcessor.kt @@ -32,7 +32,7 @@ import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr class InspectionPrimaryVisitorProcessor( val shortName: String, private val element: SqlElPrimaryExpr, -) : InspectionVisitorProcessor(shortName) { +) : InspectionVisitorProcessor() { fun check( holder: ProblemsHolder, file: PsiFile, @@ -43,7 +43,7 @@ class InspectionPrimaryVisitorProcessor( val forDirectiveExp = PsiTreeUtil.getParentOfType(element, SqlElForDirective::class.java) val isSkip = forDirectiveExp != null && forDirectiveExp.getForItem() != element - var forDirectiveBlocks = ForDirectiveUtil.getForDirectiveBlocks(element, skipSelf = isSkip) + val forDirectiveBlocks = ForDirectiveUtil.getForDirectiveBlocks(element, skipSelf = isSkip) val forItem = ForDirectiveUtil.findForItem( element, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionStaticFieldAccessVisitorProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionStaticFieldAccessVisitorProcessor.kt index 7c9d3204..3b542f1d 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionStaticFieldAccessVisitorProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionStaticFieldAccessVisitorProcessor.kt @@ -16,54 +16,166 @@ package org.domaframework.doma.intellij.inspection.sql.processor import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiType +import org.domaframework.doma.intellij.common.psi.DummyPsiParentClass +import org.domaframework.doma.intellij.common.psi.PsiParentClass import org.domaframework.doma.intellij.common.psi.PsiStaticElement import org.domaframework.doma.intellij.common.util.ForDirectiveUtil import org.domaframework.doma.intellij.common.validation.result.ValidationClassPathResult import org.domaframework.doma.intellij.common.validation.result.ValidationNotFoundStaticPropertyResult +import org.domaframework.doma.intellij.common.validation.result.ValidationPropertyResult +import org.domaframework.doma.intellij.common.validation.result.ValidationResult import org.domaframework.doma.intellij.extension.expr.accessElements import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr class InspectionStaticFieldAccessVisitorProcessor( val shortName: String, -) : InspectionVisitorProcessor(shortName) { +) : InspectionVisitorProcessor() { /** * Check for existence of static field */ - fun check( - staticAccuser: SqlElStaticFieldAccessExpr, + fun checkBindVariableDefine( + staticAccessor: SqlElStaticFieldAccessExpr, holder: ProblemsHolder, ) { - val blockElements = staticAccuser.accessElements - val psiStaticClass = PsiStaticElement(staticAccuser.elClass.elIdExprList, staticAccuser.containingFile) - val referenceClass = psiStaticClass.getRefClazz() + val referenceClass = + resolveReferenceClass(staticAccessor) if (referenceClass == null) { - ValidationClassPathResult( - staticAccuser.elClass, - shortName, - ).highlightElement(holder) + highlightClassPathError(staticAccessor, holder) return } - val topParentClass = ForDirectiveUtil.getStaticFieldAccessTopElementClassType(staticAccuser, referenceClass) - if (topParentClass == null) { - blockElements.firstOrNull()?.let { - ValidationNotFoundStaticPropertyResult( - it, - staticAccuser.elClass, - shortName, - ).highlightElement(holder) + val topParentClass = + resolveTopParentClass(staticAccessor, referenceClass, shortName) + when (topParentClass.first) { + is DummyPsiParentClass -> { + if (topParentClass.second is ValidationPropertyResult) { + highlightPropertyNotFoundError(staticAccessor, holder) + } else { + topParentClass.second?.highlightElement(holder) + } + return } - return + null -> { + highlightPropertyNotFoundError(staticAccessor, holder) + return + } + + else -> { + val parent: PsiParentClass = topParentClass.first ?: return + val result = checkFieldAccessValidity(staticAccessor, parent) + result?.highlightElement(holder) + } + } + } + + private fun resolveReferenceClass(staticAccessor: SqlElStaticFieldAccessExpr): PsiClass? { + val psiStaticClass = + PsiStaticElement( + staticAccessor.elClass.elIdExprList, + staticAccessor.containingFile, + ) + + val referenceClass = psiStaticClass.getRefClazz() + + return referenceClass + } + + private fun highlightClassPathError( + staticAccessor: SqlElStaticFieldAccessExpr, + holder: ProblemsHolder, + ) { + ValidationClassPathResult( + staticAccessor.elClass, + shortName, + ).highlightElement(holder) + } + + private fun resolveTopParentClass( + staticAccessor: SqlElStaticFieldAccessExpr, + referenceClass: PsiClass, + shortName: String = "", + ): Pair { + val topParentClass = + ForDirectiveUtil.getStaticFieldAccessTopElementClassType( + staticAccessor, + referenceClass, + shortName, + ) + + val result = topParentClass?.validationResult + if (result != null) { + topParentClass.parent = DummyPsiParentClass() } + + return Pair(topParentClass?.parent, result) + } + + private fun highlightPropertyNotFoundError( + staticAccessor: SqlElStaticFieldAccessExpr, + holder: ProblemsHolder, + ) { + val blockElements = staticAccessor.accessElements + blockElements.firstOrNull()?.let { firstElement -> + ValidationNotFoundStaticPropertyResult( + firstElement, + staticAccessor.elClass.text, + shortName, + ).highlightElement(holder) + } + } + + /** + * Get the final class type of the static field access element + */ + fun getStaticFieldAccessLastPropertyClassType(staticAccessor: SqlElStaticFieldAccessExpr): PsiType? { + val referenceClass = + resolveReferenceClass(staticAccessor) + ?: return null + + val topParentClass = + resolveTopParentClass(staticAccessor, referenceClass) + if (topParentClass.first == null || topParentClass.first is DummyPsiParentClass) { + return null + } + val parent: PsiParentClass = topParentClass.first ?: return null + + val result = getStaticFieldAccessLastProperty(staticAccessor, parent) + return result.type + } + + private fun checkFieldAccessValidity( + staticAccessor: SqlElStaticFieldAccessExpr, + topParentClass: PsiParentClass, + ): ValidationResult? { + val blockElements = staticAccessor.accessElements val result = - topParentClass.let { - ForDirectiveUtil.getFieldAccessLastPropertyClassType( - blockElements, - staticAccuser.project, - it, - shortName = shortName, - ) - } - result?.highlightElement(holder) + ForDirectiveUtil.getFieldAccessLastPropertyClassType( + blockElements, + staticAccessor.project, + topParentClass, + shortName = shortName, + ) + return result + } + + private fun getStaticFieldAccessLastProperty( + staticAccessor: SqlElStaticFieldAccessExpr, + topParentClass: PsiParentClass, + ): PsiParentClass { + val blockElements = staticAccessor.accessElements + var findLastTypeParent = topParentClass + ForDirectiveUtil.getFieldAccessLastPropertyClassType( + blockElements, + staticAccessor.project, + topParentClass, + shortName = shortName, + findFieldMethod = { type -> + findLastTypeParent = PsiParentClass(type) + findLastTypeParent + }, + ) + return findLastTypeParent } } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionVisitorProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionVisitorProcessor.kt index cfc55641..67d07721 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionVisitorProcessor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/processor/InspectionVisitorProcessor.kt @@ -15,30 +15,12 @@ */ package org.domaframework.doma.intellij.inspection.sql.processor -import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElement -import com.intellij.psi.PsiMethod import com.intellij.psi.util.elementType -import org.domaframework.doma.intellij.common.validation.result.ValidationDaoParamResult -import org.domaframework.doma.intellij.psi.SqlElIdExpr import org.domaframework.doma.intellij.psi.SqlElNewExpr import org.domaframework.doma.intellij.psi.SqlTypes -abstract class InspectionVisitorProcessor( - private val shortName: String, -) { - protected fun errorHighlight( - topElement: SqlElIdExpr, - daoMethod: PsiMethod, - holder: ProblemsHolder, - ) { - ValidationDaoParamResult( - topElement, - daoMethod.name, - this.shortName, - ).highlightElement(holder) - } - +abstract class InspectionVisitorProcessor { protected fun isLiteralOrStatic(targetElement: PsiElement): Boolean = ( targetElement.firstChild?.elementType == SqlTypes.EL_STRING || diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlBindVariableInspectionVisitor.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlBindVariableInspectionVisitor.kt index c772b859..347483ea 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlBindVariableInspectionVisitor.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/visitor/SqlBindVariableInspectionVisitor.kt @@ -49,14 +49,13 @@ class SqlBindVariableInspectionVisitor( override fun visitElStaticFieldAccessExpr(element: SqlElStaticFieldAccessExpr) { super.visitElStaticFieldAccessExpr(element) val processor = InspectionStaticFieldAccessVisitorProcessor(this.shortName) - processor.check(element, holder) + processor.checkBindVariableDefine(element, holder) } override fun visitElFieldAccessExpr(element: SqlElFieldAccessExpr) { super.visitElFieldAccessExpr(element) - val file = element.containingFile ?: return val processor = InspectionFieldAccessVisitorProcessor(shortName, element) - processor.check(holder, file) + processor.checkBindVariableDefine(holder) } override fun visitElPrimaryExpr(element: SqlElPrimaryExpr) { diff --git a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElFunctionCallExprReference.kt b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElFunctionCallExprReference.kt index 0648e7fa..8cb0f94b 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElFunctionCallExprReference.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElFunctionCallExprReference.kt @@ -18,13 +18,10 @@ package org.domaframework.doma.intellij.reference import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiMethod -import org.domaframework.doma.intellij.common.CommonPathParameterUtil -import org.domaframework.doma.intellij.common.config.DomaCompileConfigUtil -import org.domaframework.doma.intellij.common.helper.ExpressionFunctionsHelper +import com.intellij.psi.util.PsiTreeUtil import org.domaframework.doma.intellij.common.util.PluginLoggerUtil -import org.domaframework.doma.intellij.extension.getJavaClazz +import org.domaframework.doma.intellij.inspection.sql.processor.InspectionFunctionCallVisitorProcessor import org.domaframework.doma.intellij.psi.SqlElFunctionCallExpr -import org.jetbrains.kotlin.idea.base.util.module class SqlElFunctionCallExprReference( element: PsiElement, @@ -33,38 +30,11 @@ class SqlElFunctionCallExprReference( startTime: Long, file: PsiFile, ): PsiElement? { - val functionCallExpr = element.parent as? SqlElFunctionCallExpr ?: return null - val variableName = functionCallExpr.elIdExpr.text ?: "" - - val project = element.project - val module = file.module ?: return null - val expressionFunctionsInterface = - ExpressionFunctionsHelper.setExpressionFunctionsInterface(project) + val functionCallExpr = + element as? SqlElFunctionCallExpr ?: PsiTreeUtil.getParentOfType(element, SqlElFunctionCallExpr::class.java) ?: return null - - val isTest = - CommonPathParameterUtil - .isTest(module, file.virtualFile) - val customFunctionClassName = DomaCompileConfigUtil.getConfigValue(module, isTest, "doma.expr.functions") - - val implementsClass = - if (customFunctionClassName != null) { - val customFunctionClass = customFunctionClassName - val expressionFunction = project.getJavaClazz(customFunctionClass) - if (ExpressionFunctionsHelper.isInheritor(expressionFunction)) { - expressionFunction - } else { - expressionFunctionsInterface - } - } else { - expressionFunctionsInterface - } - - var reference: PsiMethod? = null - val methods = implementsClass?.findMethodsByName(variableName, true)?.firstOrNull() - if (methods != null) { - reference = methods - } + val processor = InspectionFunctionCallVisitorProcessor("", functionCallExpr) + val reference: PsiMethod? = processor.getFunctionCallType() if (reference == null) { PluginLoggerUtil.countLogging( diff --git a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElIdExprReference.kt b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElIdExprReference.kt index 6af92612..7c253f40 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElIdExprReference.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElIdExprReference.kt @@ -42,7 +42,7 @@ class SqlElIdExprReference( ): PsiElement? { val targetElements = getBlockCommentElements(element, SqlElFieldAccessExpr::class.java) if (targetElements.isEmpty()) return null - val topElm = targetElements.firstOrNull() as? PsiElement ?: return null + val topElm = targetElements.firstOrNull() ?: return null if (topElm.prevSibling.elementType == SqlTypes.AT_SIGN) return null // Refers to an element defined in the for directive @@ -70,7 +70,7 @@ class SqlElIdExprReference( } // Reference to field access elements - var parentClass: PsiParentClass? = null + var parentClass: PsiParentClass? var isBatchAnnotation = false if (forItem != null) { val project = topElm.project @@ -140,7 +140,8 @@ class SqlElIdExprReference( ): PsiElement? { val searchText = cleanString(targetElement.text) val reference = - topParentClass.findField(searchText) ?: topParentClass.findMethod(searchText) + topParentClass.findField(searchText) ?: topParentClass.findMethod(targetElement).method + if (reference != null) { PluginLoggerUtil.countLogging( this::class.java.simpleName, diff --git a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElStaticFieldReference.kt b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElStaticFieldReference.kt index 19a8fc6d..5aeff183 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElStaticFieldReference.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlElStaticFieldReference.kt @@ -34,8 +34,7 @@ class SqlElStaticFieldReference( file: PsiFile, ): PsiElement? { val staticAccessParent = - PsiTreeUtil.getParentOfType(element, SqlElStaticFieldAccessExpr::class.java) - if (staticAccessParent == null) return null + PsiTreeUtil.getParentOfType(element, SqlElStaticFieldAccessExpr::class.java) ?: return null val targetElements = getBlockCommentElements(element, SqlElStaticFieldAccessExpr::class.java) @@ -45,7 +44,7 @@ class SqlElStaticFieldReference( val referenceParentClass = PsiParentClass(referenceClass.psiClassType) if (targetElements.size == 1) { val searchText = element.text ?: "" - val reference = referenceParentClass.findField(searchText) ?: referenceParentClass.findMethod(searchText) + val reference = referenceParentClass.findField(searchText) ?: referenceParentClass.findMethod(element).method if (reference != null) { PluginLoggerUtil.countLogging( this::class.java.simpleName, @@ -62,7 +61,7 @@ class SqlElStaticFieldReference( ForDirectiveUtil.getFieldAccessLastPropertyClassType( targetElements, staticAccessParent.project, - it, + it.parent, dropLastIndex = 1, ) } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlPsiReferenceProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlPsiReferenceProvider.kt index cd878e1e..2658a0ca 100644 --- a/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlPsiReferenceProvider.kt +++ b/src/main/kotlin/org/domaframework/doma/intellij/reference/SqlPsiReferenceProvider.kt @@ -24,6 +24,7 @@ import com.intellij.psi.util.prevLeaf import com.intellij.util.ProcessingContext import org.domaframework.doma.intellij.psi.SqlCustomElExpr import org.domaframework.doma.intellij.psi.SqlElClass +import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr import org.domaframework.doma.intellij.psi.SqlElForDirective import org.domaframework.doma.intellij.psi.SqlElFunctionCallExpr import org.domaframework.doma.intellij.psi.SqlElIdExpr @@ -38,36 +39,28 @@ class SqlPsiReferenceProvider : PsiReferenceProvider() { ): Array { if (element !is SqlCustomElExpr) return PsiReference.EMPTY_ARRAY - return if (element is SqlElClass) { - arrayOf(SqlElClassExprReference(element)) - } else if (element is SqlElIdExpr) { - when { - getParentClassPsiType(element, SqlElParameters::class.java) != null -> - arrayOf(SqlElIdExprReference(element)) - - getParentClassPsiType(element, SqlElFunctionCallExpr::class.java) != null -> - arrayOf(SqlElFunctionCallExprReference(element)) - - getParentClassPsiType(element, SqlElClass::class.java) != null -> - arrayOf( - SqlElClassExprReference(element), - ) - - getParentClassPsiType( - element, - SqlElStaticFieldAccessExpr::class.java, - ) != null -> arrayOf(SqlElStaticFieldReference(element)) - - getParentClassPsiType(element, SqlElForDirective::class.java) != null && - element.prevLeaf()?.prevLeaf()?.elementType == SqlTypes.EL_FOR -> - arrayOf( - SqlElForDirectiveIdExprReference(element), - ) - - else -> arrayOf(SqlElIdExprReference(element)) + return when (element) { + is SqlElClass -> { + arrayOf(SqlElClassExprReference(element)) + } + + is SqlElIdExpr -> { + when { + getParentClassPsiType(element, SqlElParameters::class.java) != null -> { + val childArray = getReferenceByElementOriginal(element) + if (!childArray.contentEquals(PsiReference.EMPTY_ARRAY)) { + return childArray + } + return arrayOf(SqlElIdExprReference(element)) + } + + else -> getReferenceByElementOriginal(element) + } + } + + else -> { + PsiReference.EMPTY_ARRAY } - } else { - PsiReference.EMPTY_ARRAY } } @@ -75,4 +68,46 @@ class SqlPsiReferenceProvider : PsiReferenceProvider() { element: PsiElement, parentClass: Class, ): R? = PsiTreeUtil.getParentOfType(element, parentClass) + + private fun getReferenceByElementOriginal(element: PsiElement): Array { + if (element !is SqlCustomElExpr) return PsiReference.EMPTY_ARRAY + + return when (element) { + is SqlElClass -> { + arrayOf(SqlElClassExprReference(element)) + } + + is SqlElIdExpr -> { + when { + element.parent is SqlElFieldAccessExpr || + element.parent is SqlElParameters -> arrayOf(SqlElIdExprReference(element)) + + getParentClassPsiType(element, SqlElFunctionCallExpr::class.java) != null -> + arrayOf(SqlElFunctionCallExprReference(element)) + + getParentClassPsiType(element, SqlElClass::class.java) != null -> + arrayOf( + SqlElClassExprReference(element), + ) + + getParentClassPsiType( + element, + SqlElStaticFieldAccessExpr::class.java, + ) != null && element.parent !is SqlElFieldAccessExpr -> arrayOf(SqlElStaticFieldReference(element)) + + getParentClassPsiType(element, SqlElForDirective::class.java) != null && + element.prevLeaf()?.prevLeaf()?.elementType == SqlTypes.EL_FOR -> + arrayOf( + SqlElForDirectiveIdExprReference(element), + ) + + else -> arrayOf(SqlElIdExprReference(element)) + } + } + + else -> { + PsiReference.EMPTY_ARRAY + } + } + } } diff --git a/src/main/resources/messages/DomaToolsBundle.properties b/src/main/resources/messages/DomaToolsBundle.properties index 24f479f0..52d39b7b 100644 --- a/src/main/resources/messages/DomaToolsBundle.properties +++ b/src/main/resources/messages/DomaToolsBundle.properties @@ -14,7 +14,7 @@ inspection.invalid.sql.classpath=A non-existent package or class name was specif inspection.invalid.sql.iterable=The type that can be used in the loop directive is an Iterable type inspection.invalid.dao.duplicate=An element name that is a duplicate of an element name defined in SQL is used inspection.invalid.sql.notFound.customFunction=Custom function [{0}] not found in class [{1}] -inspection.invalid.sql.notFound.expressionClass=An invalid ExpressionFunctions implementation class is configured in doma.compile.config +inspection.invalid.sql.notFound.expressionClass=An invalid ExpressionFunctions implementation class is configured in doma.compile.config:[{0}] inspection.invalid.dao.returnType.psiTypes=The return type must be "{0}" inspection.invalid.dao.returnType.invalid=The return type {0} is invalid inspection.invalid.dao.select.returnType.strategy=The return type must match the "{0}" type parameter "{1}" @@ -42,4 +42,6 @@ inspection.invalid.dao.annotation.option.primitive=Field path [{0}] specified in convert.sql.annotation.to.file.family=Convert @Sql annotation to SQL file convert.sql.annotation.to.file.text=Convert to SQL file (set sqlFile=true) convert.sql.file.to.annotation.family=Convert SQL file to @Sql annotation -convert.sql.file.to.annotation.text=Convert to @Sql annotation (set sqlFile=false) \ No newline at end of file +convert.sql.file.to.annotation.text=Convert to @Sql annotation (set sqlFile=false) +inspection.invalid.sql.function.call.parameter.count=Function {0} definition with {1} parameters not found +inspection.invalid.sql.function.call.parameter.type.mismatch=Function {0} parameter type mismatch. Actual types: ({1})
Definition candidates:
{2} \ No newline at end of file diff --git a/src/main/resources/messages/DomaToolsBundle_ja.properties b/src/main/resources/messages/DomaToolsBundle_ja.properties index c35ab2c4..46393dd8 100644 --- a/src/main/resources/messages/DomaToolsBundle_ja.properties +++ b/src/main/resources/messages/DomaToolsBundle_ja.properties @@ -7,7 +7,7 @@ inspection.invalid.sql.property=\u30AF\u30E9\u30B9[{0}]\u306B\u5B58\u5728\u3057\ inspection.invalid.dao.paramUse=SQL\u3067\u4F7F\u7528\u3055\u308C\u3066\u3044\u306A\u3044\u5F15\u6570\u304C\u3042\u308A\u307E\u3059:[{0}] inspection.invalid.dao.parameter=DAO\u30E1\u30BD\u30C3\u30C9[{0}]\u306B\u5B58\u5728\u3057\u306A\u3044\u30D0\u30A4\u30F3\u30C9\u5909\u6570\u304C\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059:[{1}] inspection.invalid.sql.topType=\u6700\u521D\u306E\u8981\u7D20\u306E\u578B\u304C\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093 -inspection.invalid.sql.staticProperty=[{0}]\u0020\u306F\u30AF\u30E9\u30B9\u0020[{1}]\u0020\u306E\u0020\u0070\u0075\u0062\u006C\u0069\u0063\u0020\u307E\u305F\u0020\u0073\u0074\u0061\u0074\u0069\u0063\u0020\u30D7\u30ED\u30D1\u30C6\u30A3\u3067\u306F\u3042\u308A\u307E\u305B\u3093 +inspection.invalid.sql.staticProperty=[{0}]\u0020\u306F\u30AF\u30E9\u30B9\u0020[{1}]\u0020\u306E\u0020\u0070\u0075\u0062\u006C\u0069\u0063\u0020\u307E\u305F\u0020\u0073\u0074\u0061\u0074\u0069\u0063\u0020\u30D7\u30ED\u30D1\u30C6\u30A3\u3067\u306F\u3042\u308A\u307E\u305B\u3093:[{0}] inspection.invalid.sql.testdata=\u30D0\u30A4\u30F3\u30C9\u5909\u6570\u30C7\u30A3\u30EC\u30AF\u30C6\u30A3\u30D6\u3001\u30EA\u30C6\u30E9\u30EB\u5909\u6570\u30C7\u30A3\u30EC\u30AF\u30C6\u30A3\u30D6\u306E\u5F8C\u308D\u306B\u306F\u30C6\u30B9\u30C8\u30C7\u30FC\u30BF\u304C\u5FC5\u8981\u3067\u3059 inspection.invalid.sql.expand=\u0045\u0078\u0070\u0061\u006E\u0064\u30C7\u30A3\u30EC\u30AF\u30C6\u30A3\u30D6\u306E\u5F8C\u308D\u306B\u306F\u30A2\u30B9\u30BF\u30EA\u30B9\u30AF\u304C\u5FC5\u8981\u3067\u3059 inspection.invalid.sql.classpath=\u5B58\u5728\u3057\u306A\u3044\u30D1\u30C3\u30B1\u30FC\u30B8\u307E\u305F\u306F\u30AF\u30E9\u30B9\u540D\u304C\u6307\u5B9A\u3055\u308C\u307E\u3057\u305F\u3002:[{0}] @@ -42,4 +42,6 @@ inspection.invalid.dao.annotation.option.primitive=[{2}]\u30AA\u30D7\u30B7\u30E7 convert.sql.annotation.to.file.family=@Sql\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u3092SQL\u30D5\u30A1\u30A4\u30EB\u306B\u5909\u63DB convert.sql.annotation.to.file.text=SQL\u30D5\u30A1\u30A4\u30EB\u306B\u5909\u63DB (sqlFile=true\u306B\u8A2D\u5B9A) convert.sql.file.to.annotation.family=SQL\u30D5\u30A1\u30A4\u30EB\u3092@Sql\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u306B\u5909\u63DB -convert.sql.file.to.annotation.text=@Sql\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u306B\u5909\u63DB (sqlFile=false\u306B\u8A2D\u5B9A) \ No newline at end of file +convert.sql.file.to.annotation.text=@Sql\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u306B\u5909\u63DB (sqlFile=false\u306B\u8A2D\u5B9A) +inspection.invalid.sql.function.call.parameter.count=\u95a2\u6570{0}\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u6570\u304c{1}\u306e\u5b9a\u7fa9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093 +inspection.invalid.sql.function.call.parameter.type.mismatch=\u95a2\u6570{0}\u306e\u5f15\u6570\u306e\u578b\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002\u5b9f\u969b\u306e\u578b: ({1})
\u5b9a\u7fa9\u5019\u88dc:
{2} \ No newline at end of file diff --git a/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt index dc9bb93b..685aed32 100644 --- a/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt +++ b/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt @@ -57,7 +57,11 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { addEntityJavaFile("EmployeeSummary.java") addEntityJavaFile("Project.java") addEntityJavaFile("ProjectDetail.java") + addEntityJavaFile("DummyProject.java") addEntityJavaFile("Principal.java") + addEntityJavaFile("ColumnEntity.java") + addOtherJavaFile("domain", "ProjectNumber.java") + addOtherJavaFile("domain", "DummyProjectNumber.java") addExpressionJavaFile("TestExpressionFunctions.java") addExpressionJavaFile("TestNotExpressionFunctions.java") @@ -116,8 +120,7 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { } private fun deleteSdk() { - val oldJdk = ProjectJdkTable.getInstance().findJdk("Doma Test JDK") - if (oldJdk == null) return + val oldJdk = ProjectJdkTable.getInstance().findJdk("Doma Test JDK") ?: return WriteAction.runAndWait { ProjectJdkTable.getInstance().removeJdk(oldJdk) if (oldJdk is Disposable) { @@ -151,7 +154,11 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { } } - fun addDaoJavaFile(vararg fileNames: String) { + protected fun addDomaCompileConfig() { + addResourceCompileFile("doma.compile.config") + } + + protected fun addDaoJavaFile(vararg fileNames: String) { for (fileName in fileNames) { val file = File("$testDataPath/$sourceRoot/$packagePath/dao/$fileName") myFixture.addFileToProject( @@ -188,7 +195,7 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { ) } - fun addResourceEmptySqlFile(vararg sqlFileNames: String) { + protected fun addResourceEmptySqlFile(vararg sqlFileNames: String) { for (sqlFileName in sqlFileNames) { myFixture.addFileToProject( "main/$resourceRoot/$RESOURCES_META_INF_PATH/$packagePath/dao/$sqlFileName", @@ -197,7 +204,7 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { } } - fun addResourceCompileFile(readFileName: String) { + protected fun addResourceCompileFile(readFileName: String) { val file = File("$testDataPath/$resourceRoot/$readFileName") myFixture.addFileToProject( "main/$resourceRoot/doma.compile.config", @@ -205,13 +212,13 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { ) } - fun addSqlFile(vararg sqlNames: String) { + protected fun addSqlFile(vararg sqlNames: String) { for (sqlName in sqlNames) { addOtherPackageSqlFile("$packagePath/dao", sqlName) } } - fun addOtherPackageSqlFile( + protected fun addOtherPackageSqlFile( packageName: String, sqlName: String, ) { @@ -223,9 +230,9 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { ) } - fun findSqlFile(sqlName: String): VirtualFile? = findSqlFile(packagePath, sqlName) + protected fun findSqlFile(sqlName: String): VirtualFile? = findSqlFile(packagePath, sqlName) - fun findSqlFile( + protected fun findSqlFile( packageName: String, sqlName: String, ): VirtualFile? { @@ -237,9 +244,9 @@ open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { ) } - fun findDaoClass(testDaoName: String): PsiClass = findDaoClass("doma.example.dao", testDaoName) + protected fun findDaoClass(testDaoName: String): PsiClass = findDaoClass("doma.example.dao", testDaoName) - fun findDaoClass( + protected fun findDaoClass( packageName: String, testDaoName: String, ): PsiClass { diff --git a/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt index 6f37dbf9..1f3bdd3d 100644 --- a/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt +++ b/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt @@ -68,7 +68,7 @@ class SqlCompleteTest : DomaSqlTest() { "department", "rank", "projects", - "getFirstProject()", + "getProject()", ), listOf("getEmployeeRank()"), ) @@ -85,7 +85,7 @@ class SqlCompleteTest : DomaSqlTest() { "employeeName", "department", "rank", - "getFirstProject()", + "getProject()", "toLowerCase()", "charAt()", "contains()", @@ -119,7 +119,7 @@ class SqlCompleteTest : DomaSqlTest() { "department", "rank", "projects", - "getFirstProject()", + "getProject()", ), listOf("projectNumber", "projectDetailId", "members", "getEmployeeRank()"), ) @@ -142,7 +142,7 @@ class SqlCompleteTest : DomaSqlTest() { "department", "rank", "projects", - "getFirstProject()", + "getProject()", ), ) } @@ -230,7 +230,7 @@ class SqlCompleteTest : DomaSqlTest() { innerDirectiveCompleteTest( "$testDaoName/completeBatchInsert.sql", listOf( - "getFirstProject()", + "getProject()", ), listOf( "employeeName", @@ -387,7 +387,7 @@ class SqlCompleteTest : DomaSqlTest() { "userName()", "userAge()", "langCode()", - "isGest()", + "isGuest()", "getId()", "getName()", "getAge()", @@ -509,7 +509,7 @@ class SqlCompleteTest : DomaSqlTest() { innerDirectiveCompleteTest( "$testDaoName/completeParameterFirstPropertyWithMethodParameter.sql", listOf("projectNumber", "projectCategory", "getFirstEmployee()"), - listOf("employeeId", "employeeName", "getFirstProject()"), + listOf("employeeId", "employeeName", "getProject()"), ) innerDirectiveCompleteTest( @@ -711,7 +711,7 @@ class SqlCompleteTest : DomaSqlTest() { } fun testCompleteImplementCustomFunction() { - addResourceCompileFile("doma.compile.config") + addDomaCompileConfig() addSqlFile("$testDaoName/completeImplementCustomFunction.sql") innerDirectiveCompleteTest( "$testDaoName/completeImplementCustomFunction.sql", @@ -723,7 +723,7 @@ class SqlCompleteTest : DomaSqlTest() { "getLangCode()", "isManager()", "langCode()", - "isGest()", + "isGuest()", "isBlank()", "isNotBlank()", ), @@ -746,7 +746,7 @@ class SqlCompleteTest : DomaSqlTest() { "userName()", "userAge()", "langCode()", - "isGest()", + "isGuest()", "getId()", "getName()", "getAge()", diff --git a/src/test/kotlin/org/domaframework/doma/intellij/document/SqlSymbolDocumentTestCase.kt b/src/test/kotlin/org/domaframework/doma/intellij/document/SqlSymbolDocumentTestCase.kt index ffe43550..67fc5f2a 100644 --- a/src/test/kotlin/org/domaframework/doma/intellij/document/SqlSymbolDocumentTestCase.kt +++ b/src/test/kotlin/org/domaframework/doma/intellij/document/SqlSymbolDocumentTestCase.kt @@ -32,22 +32,10 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { override fun setUp() { super.setUp() addDaoJavaFile("$testPackage/$testDaoName.java") - addSqlFile("$testPackage/$testDaoName/documentForItemDaoParam.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemDeclaration.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemElement.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemElementInBindVariable.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemElementInIfDirective.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemElementByFieldAccess.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemFirstElement.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemStaticProperty.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemHasNext.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemIndex.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemOptionalForItem.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemOptionalProperty.sql") - addSqlFile("$testPackage/$testDaoName/documentForItemInvalidPrimary.sql") } fun testDocumentForItemDaoParam() { + addSqlFile("$testPackage/$testDaoName/documentForItemDaoParam.sql") val sqlName = "documentForItemDaoParam" val result: String? = null @@ -55,6 +43,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemDeclaration() { + addSqlFile("$testPackage/$testDaoName/documentForItemDeclaration.sql") val sqlName = "documentForItemDeclaration" val result = "List<" + @@ -64,6 +53,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemOptionalForItem() { + addSqlFile("$testPackage/$testDaoName/documentForItemOptionalForItem.sql") val sqlName = "documentForItemOptionalForItem" val result = "List<Project> optionalProjects" @@ -72,6 +62,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemOptionalForItemProperty() { + addSqlFile("$testPackage/$testDaoName/documentForItemOptionalProperty.sql") val sqlName = "documentForItemOptionalProperty" val result = "List<Integer> optionalIds" @@ -80,6 +71,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemElement() { + addSqlFile("$testPackage/$testDaoName/documentForItemElement.sql") val sqlName = "documentForItemElement" val result = "List<" + @@ -89,6 +81,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemElementInBindVariable() { + addSqlFile("$testPackage/$testDaoName/documentForItemElementInBindVariable.sql") val sqlName = "documentForItemElementInBindVariable" val result = "List<" + @@ -98,6 +91,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemElementInIfDirective() { + addSqlFile("$testPackage/$testDaoName/documentForItemElementInIfDirective.sql") val sqlName = "documentForItemElementInIfDirective" val result = "List<" + @@ -107,6 +101,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemElementByFieldAccess() { + addSqlFile("$testPackage/$testDaoName/documentForItemElementByFieldAccess.sql") val sqlName = "documentForItemElementByFieldAccess" val result = "Project project" @@ -115,6 +110,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemFirstElement() { + addSqlFile("$testPackage/$testDaoName/documentForItemFirstElement.sql") val sqlName = "documentForItemFirstElement" val result = "Permission item" @@ -123,6 +119,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemHasNext() { + addSqlFile("$testPackage/$testDaoName/documentForItemHasNext.sql") val sqlName = "documentForItemHasNext" val result = "boolean item_has_next" @@ -131,6 +128,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemIndex() { + addSqlFile("$testPackage/$testDaoName/documentForItemIndex.sql") val sqlName = "documentForItemIndex" val result = "int item_index" @@ -139,6 +137,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemStaticProperty() { + addSqlFile("$testPackage/$testDaoName/documentForItemStaticProperty.sql") val sqlName = "documentForItemStaticProperty" val result = "Project project" @@ -147,8 +146,9 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { } fun testDocumentForItemInvalidPrimary() { + addSqlFile("$testPackage/$testDaoName/documentForItemInvalidPrimary.sql") val sqlName = "documentForItemInvalidPrimary" - val result = " item" + val result = "Principal item" documentationFindTextTest(sqlName, "item", result) } @@ -162,7 +162,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { if (sqlFile == null) return myFixture.configureFromExistingVirtualFile(sqlFile) - var originalElement: PsiElement = myFixture.elementAtCaret + val originalElement: PsiElement = myFixture.elementAtCaret val resultDocument = myDocumentationProvider.generateDoc(originalElement, originalElement) assertEquals("Documentation should contain expected text", result, resultDocument) } @@ -177,7 +177,7 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { if (sqlFile == null) return myFixture.configureFromExistingVirtualFile(sqlFile) - var originalElement: PsiElement? = + val originalElement: PsiElement? = myFixture.findElementByText(originalElementName, SqlElIdExpr::class.java) ?: fundForDirectiveDeclarationElement(sqlFile, originalElementName) assertNotNull("Not Found Element [$originalElementName]", originalElement) @@ -200,10 +200,9 @@ class SqlSymbolDocumentTestCase : DomaSqlTest() { val forDirective = forDirectiveBlock?.children?.find { it is SqlElForDirective } as? SqlElForDirective ?: return null - val fieldAccessExpr = forDirective.elExprList[1] as? SqlElFieldAccessExpr - if (fieldAccessExpr == null) { - return forDirective.elExprList.firstOrNull { it.text == searchElementName } - } + val fieldAccessExpr = + forDirective.elExprList[1] as? SqlElFieldAccessExpr + ?: return forDirective.elExprList.firstOrNull { it.text == searchElementName } return fieldAccessExpr.accessElements.firstOrNull { it?.text == searchElementName } } diff --git a/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/MethodParameterDefinedTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/MethodParameterDefinedTest.kt new file mode 100644 index 00000000..f99d817e --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/MethodParameterDefinedTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright Doma Tools Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.domaframework.doma.intellij.inspection.sql + +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.inspection.sql.inspector.SqlBindVariableInspection +import org.domaframework.doma.intellij.inspection.sql.inspector.SqlFunctionCallInspection +import org.domaframework.doma.intellij.inspection.sql.inspector.SqlLoopDirectiveTypeInspection + +/** + * A test that inspects whether a bind variable's parameters are defined. + */ +class MethodParameterDefinedTest : DomaSqlTest() { + private val testDaoName = "inspection/FunctionCallValidationTestDao" + + override fun setUp() { + super.setUp() + addDaoJavaFile( + "$testDaoName.java", + ) + + myFixture.enableInspections( + SqlBindVariableInspection(), + SqlLoopDirectiveTypeInspection(), + SqlFunctionCallInspection(), + ) + } + + fun testValidParameterValidation() { + addSqlFile("$testDaoName/testValidParameter.sql") + addDomaCompileConfig() + val sqlFile = findSqlFile("$testDaoName/testValidParameter.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + + fun testInValidParameterCountValidation() { + addSqlFile("$testDaoName/testInvalidParameterCount.sql") + addDomaCompileConfig() + val sqlFile = findSqlFile("$testDaoName/testInvalidParameterCount.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + + fun testInValidParameterTypeMatchValidation() { + addSqlFile("$testDaoName/testInvalidParameterTypes.sql") + addDomaCompileConfig() + val sqlFile = findSqlFile("$testDaoName/testInvalidParameterTypes.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt index 748a654f..fb36cdbd 100644 --- a/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt +++ b/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt @@ -129,7 +129,7 @@ class ParameterDefinedTest : DomaSqlTest() { fun testImplementCustomFunctions() { addSqlFile("$testDaoName/implementCustomFunctions.sql") - addResourceCompileFile("doma.compile.config") + addDomaCompileConfig() val sqlFile = findSqlFile("$testDaoName/implementCustomFunctions.sql") assertNotNull("Not Found SQL File", sqlFile) @@ -149,6 +149,17 @@ class ParameterDefinedTest : DomaSqlTest() { myFixture.testHighlighting(false, false, false, sqlFile) } + fun testEmptyPropertyCustomFunctions() { + addSqlFile("$testDaoName/emptyPropertyCustomFunctions.sql") + addResourceCompileFile("empty.property.doma.compile.config") + val sqlFile = + findSqlFile("$testDaoName/emptyPropertyCustomFunctions.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + fun testEmptyImplementCustomFunctions() { addSqlFile("$testDaoName/emptyImplementCustomFunctions.sql") addResourceCompileFile("empty.doma.compile.config") diff --git a/src/test/kotlin/org/domaframework/doma/intellij/reference/SqlReferenceTestCase.kt b/src/test/kotlin/org/domaframework/doma/intellij/reference/SqlReferenceTestCase.kt index 0279e44b..91608a10 100644 --- a/src/test/kotlin/org/domaframework/doma/intellij/reference/SqlReferenceTestCase.kt +++ b/src/test/kotlin/org/domaframework/doma/intellij/reference/SqlReferenceTestCase.kt @@ -17,6 +17,7 @@ package org.domaframework.doma.intellij.reference import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType import org.domaframework.doma.intellij.DomaSqlTest @@ -36,21 +37,18 @@ class SqlReferenceTestCase : DomaSqlTest() { private val fieldResolve = "PsiField" private val methodResolve = "PsiMethod" private val classResolve = "PsiClass" + private val psiTypeResolve = "PsiType" override fun setUp() { super.setUp() addDaoJavaFile("$testPackage/$testDaoName.java") - addSqlFile("$testPackage/$testDaoName/referenceDaoParameter.sql") - addSqlFile("$testPackage/$testDaoName/referenceEntityProperty.sql") - addSqlFile("$testPackage/$testDaoName/referenceStaticField.sql") - addSqlFile("$testPackage/$testDaoName/referenceListFieldMethod.sql") - addSqlFile("$testPackage/$testDaoName/referenceForItem.sql") - addSqlFile("$testPackage/$testDaoName/referenceCustomFunction.sql") } fun testReferenceDaoMethodParameter() { + val sqlFileName = "referenceDaoParameter" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") referenceTest( - "referenceDaoParameter", + sqlFileName, mapOf( "reportId" to listOf("$daoParameterResolve:reportId"), "tableName" to listOf("$daoParameterResolve:tableName"), @@ -62,8 +60,10 @@ class SqlReferenceTestCase : DomaSqlTest() { } fun testReferenceEntityProperty() { + val sqlFileName = "referenceEntityProperty" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") referenceTest( - "referenceEntityProperty", + sqlFileName, mapOf( "detail" to listOf("$daoParameterResolve:detail"), "getFirstEmployee" to listOf("$methodResolve:getFirstEmployee"), @@ -81,8 +81,10 @@ class SqlReferenceTestCase : DomaSqlTest() { } fun testReferenceStaticField() { + val sqlFileName = "referenceStaticField" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") referenceTest( - "referenceStaticField", + sqlFileName, mapOf( "doma.example.entity.ProjectDetail" to listOf("$classResolve:ProjectDetail"), "doma.example.entity.Project" to listOf("$classResolve:Project"), @@ -98,8 +100,10 @@ class SqlReferenceTestCase : DomaSqlTest() { } fun testReferenceListFieldMethod() { + val sqlFileName = "referenceListFieldMethod" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") referenceTest( - "referenceListFieldMethod", + sqlFileName, mapOf( "doma.example.entity.Employee" to listOf("$classResolve:Employee"), "projects" to listOf("$fieldResolve:projects"), @@ -113,8 +117,10 @@ class SqlReferenceTestCase : DomaSqlTest() { } fun testReferenceForItem() { + val sqlFileName = "referenceForItem" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") referenceTest( - "referenceForItem", + sqlFileName, mapOf( "employeesList" to listOf("$daoParameterResolve:employeesList"), "projects" to listOf("$fieldResolve:projects"), @@ -135,9 +141,11 @@ class SqlReferenceTestCase : DomaSqlTest() { } fun testReferenceCustomFunction() { - addResourceCompileFile("doma.compile.config") + addDomaCompileConfig() + val sqlFileName = "referenceCustomFunction" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") referenceTest( - "referenceCustomFunction", + sqlFileName, mapOf( "detail" to listOf("$daoParameterResolve:detail"), "projectDetailId" to listOf("$fieldResolve:projectDetailId"), @@ -146,6 +154,163 @@ class SqlReferenceTestCase : DomaSqlTest() { ) } + fun testReferenceMethodParameter() { + addDomaCompileConfig() + val sqlFileName = "referenceMethodParameter" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTest( + sqlFileName, + mapOf( + "doma.example.entity.Project" to listOf("$classResolve:Project"), + "getEmployee" to listOf("$methodResolve:getEmployee"), + "employee" to listOf("$daoParameterResolve:employee"), + "project" to listOf("$daoParameterResolve:project"), + "managerId" to listOf("$fieldResolve:managerId"), + "employeeParam" to listOf("$methodResolve:employeeParam"), + "employeeName" to listOf("$fieldResolve:employeeName"), + "processText" to listOf("$methodResolve:processText"), + "getSubEmployee" to listOf("$methodResolve:getSubEmployee"), + "managerId" to listOf("$fieldResolve:managerId"), + "cost" to listOf("$fieldResolve:cost"), + "projectId" to listOf("$fieldResolve:projectId"), + "optionalIds" to listOf("$fieldResolve:optionalIds"), + "getProjectNumber" to listOf("$methodResolve:getProjectNumber"), + "get" to listOf("$methodResolve:get"), + "0" to listOf(null), + "formatName" to listOf("$methodResolve:formatName"), + "str" to listOf("$daoParameterResolve:str"), + "intValue" to listOf("$daoParameterResolve:intValue"), + "floatValue" to listOf("$daoParameterResolve:floatValue"), + "\"suffix\"" to listOf(null), + "\"test\"" to listOf(null), + "charSeq" to listOf("$daoParameterResolve:charSeq"), + "toString" to listOf("$methodResolve:toString"), + "subProject" to listOf("$daoParameterResolve:subProject"), + "number" to listOf("$fieldResolve:number"), + "roundUpTimePart" to listOf("$methodResolve:roundUpTimePart"), + "localDate" to listOf("$daoParameterResolve:localDate"), + "localDateTime" to listOf("$daoParameterResolve:localDateTime"), + "suffix" to listOf("$methodResolve:suffix"), + "isGuest" to listOf("$methodResolve:isGuest"), + "isGuestInProject" to listOf("$methodResolve:isGuestInProject"), + "columns" to listOf("$daoParameterResolve:columns"), + "item" to listOf(forItemResolve), + "params" to listOf("$methodResolve:params"), + "currentYear" to listOf("$methodResolve:currentYear"), + "item_has_next" to listOf(forItemResolve), + ), + ) + } + + /** + * Test reference resolution for overloaded instance methods. + */ + fun testDocumentOverloadInstanceMethod1() { + val sqlFileName = "documentOverloadInstanceMethod1" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "employee" to listOf("$daoParameterResolve:employee"), + "employeeParam" to listOf("$psiTypeResolve:String, $psiTypeResolve:Integer"), + "employeeName" to listOf("$fieldResolve:employeeName"), + "managerId" to listOf("$fieldResolve:managerId"), + ), + ) + } + + fun testDocumentOverloadInstanceMethod2() { + val sqlFileName = "documentOverloadInstanceMethod2" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "employee" to listOf("$daoParameterResolve:employee"), + "employeeParam" to listOf("$psiTypeResolve:int, $psiTypeResolve:Float"), + "employeeName" to listOf("$fieldResolve:employeeName"), + "managerId" to listOf("$fieldResolve:managerId"), + "floatVal" to listOf("$daoParameterResolve:floatVal"), + ), + ) + } + + fun testDocumentOverloadStaticMethod1() { + val sqlFileName = "documentOverloadStaticMethod1" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "doma.example.entity.Project" to listOf("$classResolve:Project"), + "getEmployee" to listOf("$psiTypeResolve:int"), + "employee" to listOf("$daoParameterResolve:employee"), + "managerId" to listOf("$fieldResolve:managerId"), + ), + ) + } + + fun testDocumentOverloadStaticMethod2() { + val sqlFileName = "documentOverloadStaticMethod2" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "doma.example.entity.Project" to listOf("$classResolve:Project"), + "getEmployee" to listOf("$psiTypeResolve:Employee"), + "employee" to listOf("$daoParameterResolve:employee"), + ), + ) + } + + fun testDocumentOverloadCustomFunction1() { + addDomaCompileConfig() + val sqlFileName = "documentOverloadCustomFunction1" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "isGuest" to listOf("$psiTypeResolve:Employee"), + "employee" to listOf("$daoParameterResolve:employee"), + ), + ) + } + + fun testDocumentOverloadCustomFunction2() { + addDomaCompileConfig() + val sqlFileName = "documentOverloadCustomFunction2" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "isGuest" to listOf("$psiTypeResolve:Project"), + "project" to listOf("$daoParameterResolve:project"), + ), + ) + } + + fun testDocumentOverloadBuiltInFunction1() { + val sqlFileName = "documentOverloadBuiltInFunction1" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "roundDownTimePart" to listOf("$psiTypeResolve:Date"), + "date" to listOf("$daoParameterResolve:date"), + ), + ) + } + + fun testDocumentOverloadBuiltInFunction2() { + val sqlFileName = "documentOverloadBuiltInFunction2" + addSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + referenceTestDocument( + sqlFileName, + mapOf( + "roundDownTimePart" to listOf("$psiTypeResolve:LocalDateTime"), + "localDateTime" to listOf("$daoParameterResolve:localDateTime"), + ), + ) + } + private fun referenceTest( sqlFileName: String, resolveExpects: Map>, @@ -186,10 +351,67 @@ class SqlReferenceTestCase : DomaSqlTest() { for (reference in references) { val resolveResult = reference.references.firstOrNull()?.resolve() val expectedResults = resolveExpects[reference.text] + println( + "Reference: ${reference.text}, Resolve: ${(resolveResult as? PsiMethod) + ?.parameterList + ?.parameters + ?.map{ it.type } + ?.joinToString() ?: resolveResult}", + ) assertTrue(expectedResults?.contains(resolveResult?.toString()) == true) } } + /** + * Test whether the referenced method matches, using the documentation generated by reference resolution. + */ + private fun referenceTestDocument( + sqlFileName: String, + resolveExpects: Map>, + ) { + val sqlFile = findSqlFile("$testPackage/$testDaoName/$sqlFileName.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.configureFromExistingVirtualFile(sqlFile) + sqlFile.toPsiFile(project)?.let { + resolveDocumentInTestFile( + it, + resolveExpects, + ) + } + } + + private fun resolveDocumentInTestFile( + sqlFile: PsiFile, + resolveExpects: Map>, + ) { + val references = + PsiTreeUtil.collectElementsOfType(sqlFile, SqlCustomElExpr::class.java).filter { + !isLiteral(it) && + !( + it is SqlElIdExpr && + PsiTreeUtil.getParentOfType( + it, + SqlElClass::class.java, + ) != null + ) + } + for (reference in references) { + val resolveResult = reference.references.firstOrNull()?.resolve() + val resolveResultText = + (resolveResult as? PsiMethod) + ?.parameterList + ?.parameters + ?.map { it.type } + ?.joinToString() + ?: resolveResult.toString() + val expectedResults = resolveExpects[reference.text] + println("Reference: ${reference.text}, Resolve: $resolveResultText Expects: $expectedResults") + assertTrue(expectedResults?.contains(resolveResultText) == true) + } + } + private fun isLiteral(element: PsiElement): Boolean = element.elementType == SqlTypes.EL_STRING || element.elementType == SqlTypes.EL_CHAR || diff --git a/src/test/testData/src/main/java/doma/example/dao/document/DocumentTestDao.java b/src/test/testData/src/main/java/doma/example/dao/document/DocumentTestDao.java index 65b51648..bae5fac0 100644 --- a/src/test/testData/src/main/java/doma/example/dao/document/DocumentTestDao.java +++ b/src/test/testData/src/main/java/doma/example/dao/document/DocumentTestDao.java @@ -1,5 +1,7 @@ package doma.example.dao.document; +import java.time.LocalDateTime; +import java.util.Date; import java.util.List; import java.util.HashSet; import doma.example.entity.*; @@ -9,40 +11,39 @@ @Dao public interface DocumentTestDao { - @Select - List documentForItemDaoParam(HashSet>> employeeIdsList); + @Select + List documentForItemDaoParam(HashSet>> employeeIdsList); - @Select - List documentForItemDeclaration(HashSet>> employeeIdsList); + @Select + List documentForItemDeclaration(HashSet>> employeeIdsList); - @Select - List documentForItemElement(HashSet>> employeeIdsList); + @Select + List documentForItemElement(HashSet>> employeeIdsList); - @Select - List documentForItemElementInBindVariable(HashSet>> employeeIdsList); + @Select + List documentForItemElementInBindVariable(HashSet>> employeeIdsList); - @Select - List documentForItemElementInIfDirective(HashSet>> employeeIdsList); + @Select + List documentForItemElementInIfDirective(HashSet>> employeeIdsList); - @Select - List documentForItemElementByFieldAccess(List> employeesList); + @Select + List documentForItemElementByFieldAccess(List> employeesList); - @Select - int documentForItemFirstElement(Principal principal); + @Select + int documentForItemFirstElement(Principal principal); - @Select - int documentForItemStaticProperty(); + @Select + int documentForItemStaticProperty(); - @Select - int documentForItemHasNext(Principal principal); + @Select + int documentForItemHasNext(Principal principal); - @Select - Project documentForItemOptionalForItem(Optional>> optionalProjects); + @Select + Project documentForItemOptionalForItem(Optional>> optionalProjects); - @Select - Project documentForItemOptionalProperty(Optional>> optionalProjects); - - @Select - int documentForItemInvalidPrimary(Principal item, Principal principal); + @Select + Project documentForItemOptionalProperty(Optional>> optionalProjects); + @Select + int documentForItemInvalidPrimary(Principal item, List principal); } \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/inspection/FunctionCallValidationTestDao.java b/src/test/testData/src/main/java/doma/example/dao/inspection/FunctionCallValidationTestDao.java new file mode 100644 index 00000000..0cb1d155 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/inspection/FunctionCallValidationTestDao.java @@ -0,0 +1,27 @@ +package doma.example.dao.inspection; + +import doma.example.entity.*; +import org.seasar.doma.*; + +import java.util.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Dao +public interface FunctionCallValidationTestDao { + + @Select + List testValidParameter(float floatValue, int intValue + , Employee employee + , Project project, DummyProject subProject + , LocalDate localDate, LocalDateTime localDateTime + , CharSequence charSeq, String str + , List columns); + + @Select + List testInvalidParameterCount(Employee employee, Project project, List columns); + + @Select + List testInvalidParameterTypes(Employee employee, Project project, DummyProject subProject, List columns); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/reference/ReferenceTestDao.java b/src/test/testData/src/main/java/doma/example/dao/reference/ReferenceTestDao.java index 055d619c..c0fbc121 100644 --- a/src/test/testData/src/main/java/doma/example/dao/reference/ReferenceTestDao.java +++ b/src/test/testData/src/main/java/doma/example/dao/reference/ReferenceTestDao.java @@ -2,7 +2,14 @@ import java.util.List; import doma.example.entity.*; -import org.seasar.doma.*; +import java.util.Map; +import java.util.Set; +import org.seasar.doma.Dao; +import org.seasar.doma.Select; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; @Dao public interface ReferenceTestDao { @@ -25,4 +32,37 @@ public interface ReferenceTestDao { @Select Project referenceCustomFunction(ProjectDetail detail); + @Select + Employee referenceMethodParameter(float floatValue, int intValue + , Employee employee + , Project project, DummyProject subProject + , LocalDate localDate, LocalDateTime localDateTime + , CharSequence charSeq, String str + , List columns); + + @Select + Employee documentOverloadInstanceMethod1(Employee employee); + + @Select + Employee documentOverloadInstanceMethod2(Employee employee, float floatVal); + + @Select + Employee documentOverloadStaticMethod1(Employee employee); + + @Select + Employee documentOverloadStaticMethod2(Employee employee); + + @Select + Employee documentOverloadCustomFunction1(Employee employee); + + @Select + Employee documentOverloadCustomFunction2(Project project); + + @Select + Employee documentOverloadBuiltInFunction1(Date date); + + @Select + Employee documentOverloadBuiltInFunction2(LocalDateTime localDateTime); + + } \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/domain/DummyProjectNumber.java b/src/test/testData/src/main/java/doma/example/domain/DummyProjectNumber.java new file mode 100644 index 00000000..28c80974 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/domain/DummyProjectNumber.java @@ -0,0 +1,17 @@ +package doma.example.domain; + +import org.seasar.doma.Domain; + +@Domain(valueType = String.class) +public class DummyProjectNumber extends ProjectNumber { + private final String dummyNumber; + + public DummyProjectNumber(String nmb){ + super(nmb); + dummyNumber = nmb; + } + + public String getDummyNumber(){ + return dummyNumber; + } +} diff --git a/src/test/testData/src/main/java/doma/example/domain/ProjectNumber.java b/src/test/testData/src/main/java/doma/example/domain/ProjectNumber.java new file mode 100644 index 00000000..83251d88 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/domain/ProjectNumber.java @@ -0,0 +1,16 @@ +package doma.example.domain; + +import org.seasar.doma.Domain; + +@Domain(valueType = String.class) +public class ProjectNumber { + private final String number; + + public ProjectNumber(String nmb){ + number = nmb; + } + + public String getNumber(){ + return number; + } +} diff --git a/src/test/testData/src/main/java/doma/example/entity/ColumnEntity.java b/src/test/testData/src/main/java/doma/example/entity/ColumnEntity.java new file mode 100644 index 00000000..8bf3c68f --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/ColumnEntity.java @@ -0,0 +1,31 @@ +package doma.example.entity; + +import java.util.ArrayList; +import java.util.List; +import org.seasar.doma.Entity; +import org.seasar.doma.Table; +import org.seasar.doma.Column; +import org.seasar.doma.jdbc.type.LocalDateType; + +@Entity +@Table(name = "column_entity") +public class ColumnEntity { + @Column(name = "name") + public String name; + @Column(name = "alias") + public String alias; + + public Integer currentYear(){ + + return 2000; + }; + + public List params(){ + return new ArrayList<>(); + } + + public String getAliasDiv(int index){ + return alias.indent(index); + } +} + diff --git a/src/test/testData/src/main/java/doma/example/entity/DummyProject.java b/src/test/testData/src/main/java/doma/example/entity/DummyProject.java new file mode 100644 index 00000000..a0dae9c9 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/DummyProject.java @@ -0,0 +1,16 @@ +package doma.example.entity; + +import doma.example.domain.DummyProjectNumber; +import org.seasar.doma.Entity; + +@Entity +public class DummyProject extends Project { + Integer id ; + DummyProjectNumber number; + String dummyName; + + @Override + public String getEmployeeName(int index) { + return employees.get(index).getUserNameFormat(); + } +} diff --git a/src/test/testData/src/main/java/doma/example/entity/Employee.java b/src/test/testData/src/main/java/doma/example/entity/Employee.java index e728c84a..61e6786e 100644 --- a/src/test/testData/src/main/java/doma/example/entity/Employee.java +++ b/src/test/testData/src/main/java/doma/example/entity/Employee.java @@ -9,32 +9,47 @@ @Entity public class Employee extends User { - @Id - public Integer employeeId; - public String employeeName; - private String department; - private String rank; - public static List projects; - - public Integer managerId; - - // accessible instance methods - public Project getFirstProject() { - return projects.get(0); - } - - // Inaccessible instance methods - private String getEmployeeRank() { - return rank; - } - - public Integer employeeParam(Integer p1, Integer p2) { - return p1 + p2; - } - - enum Rank { - MANAGER, - STAFF, - INTERN - } + @Id + public Integer employeeId; + public String employeeName; + private String department; + private String rank; + public static List projects; + + public Integer managerId; + + // accessible instance methods + public Project getProject() { + return projects.get(0); + } + + public Project getProject(int index) { + return projects.get(index); + } + + // Inaccessible instance methods + private String getEmployeeRank() { + return rank; + } + + public Integer employeeParam(String p1, Integer p2) { + if (p1.isBlank()) { + return 0; + } + return p1.length() + p2; + } + + public Float employeeParam(int p1, Float p2) { + return p1 + p2; + } + + public Project getSubEmployee(Project project){ + return project; + } + + enum Rank { + MANAGER, + STAFF, + INTERN + } } \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/Project.java b/src/test/testData/src/main/java/doma/example/entity/Project.java index 1779e20d..2f7072e1 100644 --- a/src/test/testData/src/main/java/doma/example/entity/Project.java +++ b/src/test/testData/src/main/java/doma/example/entity/Project.java @@ -1,8 +1,11 @@ package doma.example.entity; +import doma.example.domain.DummyProjectNumber; +import doma.example.domain.ProjectNumber; import java.time.LocalDate; import org.seasar.doma.*; import org.seasar.doma.Id; + import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -10,33 +13,71 @@ @Entity public class Project { - @Id - private Integer projectId; - private String projectName; - private static String status; - private Integer rank; + @Id + private Integer projectId; + private String projectName; + private static String status; + private Integer rank; + + public static Optional>> optionalIds; + public static Optional manager; + + // Accessible static fields + public static Integer projectNumber; + private static String projectCategory; + + public static List employees = new ArrayList<>(); + + public static Integer cost; + + public static Employee getFirstEmployee() { + return employees.get(0); + } + + // Accessible Static methods + public static String getTermNumber() { + return projectNumber.toString() + "_term"; + } + + public static Integer calculateCost(Integer base, Integer multiplier) { + return base * multiplier; + } - public static Optional>> optionalIds; - public static Optional manager; + public static String formatName(String prefix, String suffix) { + return prefix + "_" + suffix; + } - // Accessible static fields - public static Integr projectNumber; - private static String projectCategory; + // Static methods that are not accessible + private static String getCategoryName() { + return projectCategory + "_term"; + } - public static Integer cost; + public static Optional getEmployee(Float val, Integer index){ + return employees.stream().filter( e -> e.getProject(index).rank >= val.intValue()).findFirst(); + } - public static Employee getFirstEmployee() { - return employees.get(0); - } + public static Employee getEmployee(int index) { + return employees.get(index); + } - // Accessible Static methods - public static String getTermNumber() { - return projectNumber.toString()+"_term"; - } + public static Employee getEmployee(Employee employee) { + return employee; + } - // Static methods that are not accessible - private static String getCategoryName() { - return projectCategory+"_term"; - } + public static Project getEmployeeByProject(int index, Project project){ + if(index >= 0){ + return employees.get(index).getSubEmployee(project); + } + return project; + } + public static String getProjectNumber(int index, ProjectNumber number) { + if (index >= 100) { + return number.getNumber(); + } + if (number instanceof DummyProjectNumber) + return ((DummyProjectNumber) number).getDummyNumber(); + else + return ""; + } } \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/User.java b/src/test/testData/src/main/java/doma/example/entity/User.java index dfff14cf..c1188805 100644 --- a/src/test/testData/src/main/java/doma/example/entity/User.java +++ b/src/test/testData/src/main/java/doma/example/entity/User.java @@ -30,5 +30,13 @@ public String getUserNameFormat() { private String getEmail(){ return email; } + + public String processText(CharSequence text) { + return text.toString(); + } + + public CharSequence getDescription() { + return "User description"; + } } \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/expression/TestExpressionFunctions.java b/src/test/testData/src/main/java/doma/example/expression/TestExpressionFunctions.java index 4542a5de..7eaeeed4 100644 --- a/src/test/testData/src/main/java/doma/example/expression/TestExpressionFunctions.java +++ b/src/test/testData/src/main/java/doma/example/expression/TestExpressionFunctions.java @@ -1,41 +1,67 @@ package doma.example.expression; +import doma.example.entity.Employee; +import doma.example.entity.Project; import org.seasar.doma.jdbc.dialect.StandardDialect; // doma.example.expression.TestExpressionFunctions public class TestExpressionFunctions extends StandardDialect.StandardExpressionFunctions { - private static final char DEFAULT_ESCAPE_CHAR = '\\'; + private static final char DEFAULT_ESCAPE_CHAR = '\\'; - public TestExpressionFunctions() { - super(DEFAULT_ESCAPE_CHAR, null); - } + public TestExpressionFunctions() { + super(DEFAULT_ESCAPE_CHAR, null); + } - public TestExpressionFunctions(char[] wildcards) { - super(wildcards); - } + public TestExpressionFunctions(char[] wildcards) { + super(wildcards); + } - protected TestExpressionFunctions(char escapeChar, char[] wildcards) { - super(escapeChar, wildcards); - } + protected TestExpressionFunctions(char escapeChar, char[] wildcards) { + super(escapeChar, wildcards); + } - public Integer userId() { - return 10000000; - } + public Integer userId() { + return 10000000; + } - public String userName() { - return "userName"; - } + public String userName() { + return "userName"; + } - public Integer userAge() { - return 99; - } + public Integer userAge() { + return 99; + } - public String langCode() { - return "ja"; - } + public String langCode() { + return "ja"; + } - public boolean isGest() { - return true; - } + public boolean isGuest() { + return true; + } + + public boolean isGuest(Employee employee) { + return employee.employeeId != null; + } + + public boolean isGuest(Project project) { + return project.getEmployeeName(0).isBlank(); + } + + public Integer customCalculate(Integer base, Integer multiplier) { + return base * multiplier; + } + + public String customFormat(String prefix, String suffix) { + return prefix + "_" + suffix; + } + + public Double customCompute(Double value, Integer factor) { + return value * factor; + } + + public boolean isGuestInProject(Project project) { + return project.getEmployeeName(0).equals(userName()); + } } \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBatchInsert.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBatchInsert.sql index dcac6940..63abca63 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBatchInsert.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBatchInsert.sql @@ -2,4 +2,4 @@ INSERT INTO employee (id , name) VALUES ( /* employees.userId */0 - , /* employees.getFirstProject() */'name') + , /* employees.getProject() */'name') diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIfWithMethodParameter.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIfWithMethodParameter.sql index eba1e511..955d620f 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIfWithMethodParameter.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIfWithMethodParameter.sql @@ -3,7 +3,7 @@ update employee e set , e.rank = /* employee.rank */1 where e.employee_id = /* employee.employeeId */1 - /*%if employee.getFirstProject(employee.employeeId).pro */ + /*%if employee.getProject(employee.employeeId).pro */ and e.department = /* employee.department */'department' /*%elseif employee.department.startWith("100") */ and e.sub_department = /* employee.department */'department' diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeFieldAccessAfterOtherElement.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeFieldAccessAfterOtherElement.sql index 52a9dbb4..715a25d3 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeFieldAccessAfterOtherElement.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeFieldAccessAfterOtherElement.sql @@ -8,7 +8,7 @@ from project p on p.project_id = pd.project_id where /*%for userId : userIds */ - pd.manager_id = /* detail.projectCategory employee.getFirstProject( employee.rank ).pro */'TODO' + pd.manager_id = /* detail.projectCategory + employee.getProject(employee.managerId).pro */'TODO' /*%if userId_has_next */ /*# "OR" */ /*%end */ diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyWithMethodParameter.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyWithMethodParameter.sql index c67c2584..c6d26af4 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyWithMethodParameter.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyWithMethodParameter.sql @@ -1 +1 @@ -select * from employee where id = /* employee.employeeParam(name). */1 \ No newline at end of file +select * from employee where id = /* employee.employeeParam(name, 0). */1 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeParameterFirstPropertyWithMethodParameter.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeParameterFirstPropertyWithMethodParameter.sql index 704e9f3e..6d668a65 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeParameterFirstPropertyWithMethodParameter.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeParameterFirstPropertyWithMethodParameter.sql @@ -1 +1 @@ -select * from employee where id = /* employee.getFirstProject(employee.getFirstProject(index). ) */1 \ No newline at end of file +select * from employee where id = /* employee.getProject(employee.getProject(index). ) */1 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql index bb60de77..c6a6d767 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql @@ -1,6 +1,6 @@ insert into project_manager ( project_manager_id , manager_name) -values (/* employee.getFirstProject().projectId */1 +values (/* employee.getProject().projectId */1 , /* employee.employeeName */'name' ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testInvalidParameterCount.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testInvalidParameterCount.sql new file mode 100644 index 00000000..139d859d --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testInvalidParameterCount.sql @@ -0,0 +1,52 @@ +/** +* Method parameter count checking: Error cases +* Error: Function [functionName] definition with [X] parameters not found +*/ +SELECT * + FROM employee + WHERE + -- Static method parameter count check + id = /* @doma.example.entity.Project@getEmployee(employee.employeeName, 0,employee.managerId) */0 + -- Instance method parameter count check + OR id = /* employee.employeeParam(employee.employeeName, project.projectId, 3) */0 + -- Custom function parameter count check + AND cost = /* @userName(project.cost) */false + -- Built-in function parameter check + OR cost = /* @isNotEmpty(employee.employeeName, project.projectId) */false + /** Parameter count check for methods called in the middle */ + -- Static method + AND id = /* @doma.example.entity.Project@manager.getUserNameFormat(employee).indent("indent") */0 + -- Instance method + OR id = /* employee.projects.get(0).getFirstEmployee(employee.employeeName, project.projectId, 3).cost */0 + /** Parameter count check for methods called within parameters */ + -- Static method * インスタンスメソッド + AND static = /* @doma.example.entity.Project@getEmployee(employee.employeeParam(employee.managerId, project.cost, "instance"),project.projectCategory, "value") */0 + OR static = /* @doma.example.entity.Project@getEmployee(@doma.example.entity.DummyProject@getTermNumber(0), project.projectCategory, "static") */0 + OR static = /* @doma.example.entity.Project@getEmployee(@isGuest(0, employee.employeeName), project.projectCategory, "custom") */0 + OR static = /* @doma.example.entity.Project@getEmployee(@isNotBlank(0, employee.employeeName), project.projectCategory, "built") */0 + -- Instance method + AND fields = /* employee.employeeParam(project.cost, employee.processText(0, project.projectCategory), "instance") */0 + OR fields = /* employee.employeeParam(project.cost, @doma.example.entity.DummyProject@getTermNumber(0), "static", "value") */0 + OR fields = /* employee.employeeParam(project.cost, @isGuest(0, employee.employeeName), "custom", "value") */0 + OR fields = /* employee.employeeParam(project.cost, @isNotBlank(0, employee.employeeName), "built", "value") */0 + -- Custom function * Instance method + AND custom = /* @userName(project.calculateCost(0, employee.managerId, "instance")) */false + OR custom = /* @userName(@doma.example.entity.DummyProject@getTermNumber(0)) */false + OR custom = /* @userName(@isGuest(0, employee.employeeName), "custom") */false + OR custom = /* @userName(@isNotBlank(0, employee.employeeName), "built") */false + -- Built-in function * Instance method + AND built = /* @isNotEmpty(@doma.example.entity.DummyProject@getTermNumber(0), "static") */false + OR built = /* @isNotEmpty(employee.getUserNameFormat(0), "instance") */false + OR built = /* @isNotEmpty(@isGuest(0, employee.employeeName), "custom") */false + OR built = /* @isNotEmpty(@isNotBlank(0, employee.employeeName), "built") */false + /** Parameter count check for elements defined in loop directives */ + ORDER BY + /*%for item : columns */ + /* item.params(project.cost, employee.processText(0, project.projectCategory), "instance") */0 + , /* item.params(project.cost, @doma.example.entity.DummyProject@getTermNumber(0), "static") */0 + , /* item.params(project.cost, @isGuest(0, employee.employeeName), "custom") */0 + , /* item.params(project.cost, @isNotBlank(0, employee.employeeName), "built") */0 + /*%if item_has_next*/ + , + /*%end*/ + /*%end*/ diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testInvalidParameterTypes.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testInvalidParameterTypes.sql new file mode 100644 index 00000000..92a04b20 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testInvalidParameterTypes.sql @@ -0,0 +1,61 @@ +/** +* Method parameter type checking: Error cases +* Error: Function [functionName] parameter type mismatch. Actual types: ([actual Types]]) Definition candidates: [functionName]([definition Types]) +*/ +SELECT * + FROM employee + WHERE + -- Static method parameter type check + id = /* @doma.example.entity.Project@getEmployee(employee.employeeName) */0 + -- Instance method parameter type check + OR id = /* employee.employeeParam(employee.managerId, project.projectId) */0 + -- Custom function parameter type check + AND cost = /* @isGuestInProject(project.cost) */false + -- Built-in function parameter check + OR cost = /* @isNotEmpty(project.projectId) */false + /** Parameter type check for methods called in the middle */ + -- Static method + AND id = /* @doma.example.entity.Project@manager.processText(employee).indent("indent") */0 + -- Instance method + OR id = /* employee.projects.get(0).getEmployeeName(employee.employeeName).cost */0 + /** + * Parameter type check for methods called within parameters + */ + -- Static method * Instance method + AND static = /* @doma.example.entity.Project@getEmployee(employee.employeeParam(employee.managerId, project.projectCategory)) */0 + OR static = /* @doma.example.entity.Project@getEmployee(@doma.example.entity.DummyProject@getEmployee(subProject.projectCategory)) */0 + OR static = /* @doma.example.entity.Project@getEmployee(@isGuest(employee.employeeName)) */0 + OR static = /* @doma.example.entity.Project@getEmployee(@isNotBlank(employee.managerId)) */0 + -- Instance method + AND fields = /* employee.employeeParam(employee.employeeParam(employee.managerId, project.projectId), "instance") */0 + OR fields = /* employee.employeeParam(@doma.example.entity.DummyProject@getEmployee(subProject.projectCategory), "static") */0 + OR fields = /* employee.employeeParam("custom", @isGuest(employee.employeeName)) */0 + OR fields = /* employee.employeeParam("built", @isNotBlank(employee.managerId)) */0 + -- Custom function * Instance method + AND custom = /* @isGuestInProject(employee.employeeParam(employee.managerId, project.projectId)) */false + OR custom = /* @isGuestInProject(@doma.example.entity.DummyProject@getEmployee(subProject.projectCategory)) */false + OR custom = /* @isGuestInProject(@isGuest(employee.employeeName)) */false + OR custom = /* @isGuestInProject(@isNotBlank(employee.managerId)) */false + -- Built-in function * Instance method + AND built = /* @isNotEmpty(@doma.example.entity.DummyProject@getEmployee(subProject.projectCategory)) */false + OR built = /* @isNotEmpty(employee.employeeParam(employee.managerId, project.projectId)) */false + OR built = /* @isNotEmpty(@isGuest(employee.employeeName)) */false + OR built = /* @isNotEmpty(@isNotBlank(employee.managerId)) */false + /** When there are error parameters in the middle, recognize the types up to the entered field access */ + -- Static method + AND input = /* @doma.example.entity.Project@getEmployee(employee.processText(employee.managerId), employee.managerId) */0 + OR input = /* employee.employeeParam(employee.processText(employee.managerId), project.projectId) */0 + OR input = /* @isGuestInProject(employee.processText(employee.managerId)) */0 + OR input = /* @isNotEmpty(employee.processText(employee.managerId)) */0 + /** Parameter type check for elements defined in loop directives */ + ORDER BY + /*%for item : columns */ + /*# item.getAliasDiv(employee.employeeParam(employee.managerId, project.projectId)) */ + , /*# item.getAliasDiv(@doma.example.entity.DummyProject@getEmployee(subProject.projectCategory)) */ + , /*# item.getAliasDiv(@isGuest(employee.employeeName)) */ + , /*# item.getAliasDiv(@isNotBlank(employee.managerId)) */ + , /*# item.getAliasDiv(employee.processText(employee.managerId)) */ + /*%if item_has_next*/ + , + /*%end*/ + /*%end*/ diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testValidParameter.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testValidParameter.sql new file mode 100644 index 00000000..9f30809c --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/FunctionCallValidationTestDao/testValidParameter.sql @@ -0,0 +1,49 @@ +/** +* Method parameter checking: Valid cases +* - Cases called directly as bind variables +* - Cases called within method parameters +* - Cases called in the middle of field access (instance methods and static methods only) +* - Combinations with instance methods, static methods, custom functions, and built-in functions +*/ +SELECT * + FROM employee + WHERE + /** Static method parameter count check */ id = /* @doma.example.entity.Project@calculateCost(0, employee.managerId) */0 + AND user_name = /* @doma.example.entity.Project@manager.processText(employee.employeeName).isBlank() */false + /** Method return values within parameters are also recognized and used */ + AND (name = /* employee.employeeParam(employee.employeeName, project.optionalIds.get(0)) */0 + -- Overload + OR name = /* employee.employeeParam(project.projectId, floatValue) */0) + /** Subtypes are allowed */ + AND cost = /* employee.getSubEmployee(subProject).cost */0 + -- Definition: int Parameter type: Integer + OR (id = /* @doma.example.entity.Project@getEmployee(employee.managerId).managerId */0 + -- Overload: Definition: Integer Parameter type: int + OR id = /* @doma.example.entity.Project@getEmployee(floatValue, intValue).managerId */0 + -- Definition: CharSequence Parameter: String + AND description = /* @doma.example.entity.Project@formatName(str, str) */'' +) + AND cost = /* @doma.example.entity.Project@getProjectNumber(employee.managerId, subProject.number) */0 + /** Custom functions */ + AND (empty = /* @isGuest(employee) */false + -- Overload + OR empty = /* @isGuest(project) */false) + -- Subtype + AND id = /*@isGuestInProject(subProject)*/false + /** Built-in functions */ + OR (empty = /* @roundUpTimePart(localDate) */'2099-12-31' + -- Overload + OR empty = /* @roundUpTimePart(localDateTime) */'2099-12-31') + -- Valid: Literal characters are recognized as String + AND name = /* @doma.example.entity.Project@formatName(charSeq.toString(), "suffix") */'' + OR name = /* @doma.example.entity.Project@getEmployee(0).employeeName */'' + -- Valid: Method return value types within parameters are recognized + AND user_desc = /* @doma.example.entity.Project@formatName(@suffix(employee.employeeName), employee.processText("test")) */'' + /** Parameter checking for elements defined in loop directives */ + ORDER BY + /*%for item : columns */ + /* item.params().get(0).currentYear() */'2025-12-31' + /*%if item_has_next*/ + , + /*%end*/ + /*%end*/ diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/batchAnnotationResolvesClassInList.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/batchAnnotationResolvesClassInList.sql index f961ace9..e5e48012 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/batchAnnotationResolvesClassInList.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/batchAnnotationResolvesClassInList.sql @@ -19,7 +19,7 @@ JOIN project p1 ON e1.employee_id = p1.employee_id WHERE -- Public entity method - p1.project_id = /* employees.getFirstProject().projectId */0 + p1.project_id = /* employees.getProject().projectId */0 -- Private entity method AND p1.base_rank >= /* employees.getEmployeeRank() */0 WHERE diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableForEntityAndNonEntityParentClass.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableForEntityAndNonEntityParentClass.sql index 4978b1e3..7671cdd9 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableForEntityAndNonEntityParentClass.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableForEntityAndNonEntityParentClass.sql @@ -22,7 +22,7 @@ INSERT INTO employee_project (employee_name, department, project) JOIN project p1 ON e1.employee_id = p1.employee_id WHERE -- Public entity method - p1.project_id = /* employee.getFirstProject().projectId */0 + p1.project_id = /* employee.getProject().projectId */0 -- Private entity method AND p1.base_rank >= /* employee.getEmployeeRank() */0 WHERE diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableInFunctionParameters.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableInFunctionParameters.sql index 6a8efa92..c6af87ee 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableInFunctionParameters.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/bindVariableInFunctionParameters.sql @@ -1,12 +1,12 @@ -- Check BindVariable Definition In Function Parameters SELECT - e.employee_id +e.employee_id , u.user_id , u.user_name FROM user u -WHERE p.employee_id = /* employee.employeeParam(employee.dist, employee.employeeId) */0 -AND p.base_rank = /* employee.employeeParam(user.userId, count) */0 +WHERE p.employee_id = /* employee.employeeParam(employee.dist, employee.employeeId) */0 +AND p.base_rank = /* employee.employeeParam(user.userId, count) */0 /*%if employee != null && (employee.employeeId > 100 || employee.employeeId < 50) */ - AND p.employee_id = /* employee.employeeParam(employee.dist, rank) */0 -OR flag = /* @isNotBlank() */false + AND p.employee_id = /* employee.employeeParam(employee.dist, rank) */0 +OR flag = /* @isNotBlank("string") */false /*%end */ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyImplementCustomFunctions.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyImplementCustomFunctions.sql index d88a810d..b899f963 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyImplementCustomFunctions.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyImplementCustomFunctions.sql @@ -5,6 +5,6 @@ SELECT FROM user u WHERE p.employee_id = /* employee.employeeId */0 AND p.user_id = /* employee.userId */0 - OR is_gest = /* @isGest() */false + OR is_gest = /* @isGuest() */false OR flag = /* @authUser() */false AND lang = /* @getLangCode() */'en' \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyPropertyCustomFunctions.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyPropertyCustomFunctions.sql new file mode 100644 index 00000000..9f79858a --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/emptyPropertyCustomFunctions.sql @@ -0,0 +1,10 @@ +SELECT + e.employee_id + , u.user_id + , u.user_name + FROM user u + WHERE p.employee_id = /* employee.employeeId */0 + AND p.user_id = /* employee.userId */0 + OR is_gest = /* @isManager() */false + OR flag = /* @authUser() */false + AND lang = /* @isBlank() */'en' diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/implementCustomFunctions.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/implementCustomFunctions.sql index ae736083..27292b0b 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/implementCustomFunctions.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/implementCustomFunctions.sql @@ -5,6 +5,6 @@ SELECT FROM user u WHERE p.employee_id = /* employee.employeeId */0 AND p.user_id = /* employee.userId */0 - OR is_gest = /* @isGest() */false + OR is_gest = /* @isGuest() */false OR flag = /* @authUser() */false AND lang = /* @getLangCode() */'en' \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/invalidImplementCustomFunctions.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/invalidImplementCustomFunctions.sql index 1404a4e4..9231ca2d 100644 --- a/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/invalidImplementCustomFunctions.sql +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/inspection/ParamDefinedTestDao/invalidImplementCustomFunctions.sql @@ -1,10 +1,10 @@ SELECT - e.employee_id - , u.user_id - , u.user_name + e.employee_id + , u.user_id + , u.user_name FROM user u WHERE p.employee_id = /* employee.employeeId */0 AND p.user_id = /* employee.userId */0 - OR is_gest = /* @isManager() */false - OR flag = /* @authUser() */false - AND lang = /* @isBlank() */'en' \ No newline at end of file + OR is_gest = /* @isManager() */false + OR flag = /* @authUser() */false + AND lang = /* @isBlank() */'en' diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadBuiltInFunction1.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadBuiltInFunction1.sql new file mode 100644 index 00000000..3333812f --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadBuiltInFunction1.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* @roundDownTimePart(date) */0 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadBuiltInFunction2.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadBuiltInFunction2.sql new file mode 100644 index 00000000..5e1b1300 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadBuiltInFunction2.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* @roundDownTimePart(localDateTime) */0 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadCustomFunction1.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadCustomFunction1.sql new file mode 100644 index 00000000..612edb4c --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadCustomFunction1.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* @isGuest(employee) */0 diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadCustomFunction2.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadCustomFunction2.sql new file mode 100644 index 00000000..57f2e69e --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadCustomFunction2.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* @isGuest(project) */0 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadInstanceMethod1.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadInstanceMethod1.sql new file mode 100644 index 00000000..b1490abe --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadInstanceMethod1.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* employee.employeeParam(employee.employeeName, employee.managerId) */0 diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadInstanceMethod2.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadInstanceMethod2.sql new file mode 100644 index 00000000..99fb3636 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadInstanceMethod2.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* employee.employeeParam(employee.managerId, floatVal) */0 diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadStaticMethod1.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadStaticMethod1.sql new file mode 100644 index 00000000..66d6d7a7 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadStaticMethod1.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* @doma.example.entity.Project@getEmployee(employee.managerId) */0 diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadStaticMethod2.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadStaticMethod2.sql new file mode 100644 index 00000000..80d4629c --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/documentOverloadStaticMethod2.sql @@ -0,0 +1,3 @@ +SELECT * + FROM employee + WHERE id = /* @doma.example.entity.Project@getEmployee(employee) */0 diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/referenceMethodParameter.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/referenceMethodParameter.sql new file mode 100644 index 00000000..2a07bc29 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/reference/ReferenceTestDao/referenceMethodParameter.sql @@ -0,0 +1,40 @@ +SELECT * + FROM employee + WHERE id = /* @doma.example.entity.Project@getEmployee(employee.managerId) */0 + -- Overload + or user_name = /* @doma.example.entity.Project@getEmployee(employee) */false + AND (name = /* employee.employeeParam(employee.employeeName, project.optionalIds.get(0)) */0 + -- Overload + OR name = /* employee.employeeParam(project.projectId, floatValue) */0) + /** Subtypes are allowed */ + AND cost = /* employee.getSubEmployee(subProject).cost */0 + -- Definition: int Parameter type: Integer + OR (id = /* @doma.example.entity.Project@getEmployee(employee.managerId).managerId */0 + -- Overload: Definition: Integer Parameter type: int + OR id = /* @doma.example.entity.Project@getEmployee(floatValue, intValue).managerId */0 + -- Definition: CharSequence Parameter: String + AND description = /* @doma.example.entity.Project@formatName(str, str) */'') + AND cost = /* @doma.example.entity.Project@getProjectNumber(employee.managerId, subProject.number) */0 + /** Custom functions */ + AND (empty = /* @isGuest(employee) */false + -- Overload + OR empty = /* @isGuest(project) */false) + -- Subtype + AND id = /*@isGuestInProject(subProject)*/false + /** Built-in functions */ + OR (empty = /* @roundUpTimePart(localDate) */'2099-12-31' + -- Overload + OR empty = /* @roundUpTimePart(localDateTime) */'2099-12-31') + -- Valid: Literal characters are recognized as String + AND name = /* @doma.example.entity.Project@formatName(charSeq.toString(), "suffix") */'' + OR name = /* @doma.example.entity.Project@getEmployee(0).employeeName */'' + -- Valid: Method return value types within parameters are recognized + AND user_desc = /* @doma.example.entity.Project@formatName(@suffix(employee.employeeName), employee.processText("test")) */'' + /** Parameter checking for elements defined in loop directives */ + ORDER BY + /*%for item : columns */ + /* item.params().get(0).currentYear() */'2025-12-31' + /*%if item_has_next*/ + , + /*%end*/ + /*%end*/ diff --git a/src/test/testData/src/main/resources/empty.property.doma.compile.config b/src/test/testData/src/main/resources/empty.property.doma.compile.config new file mode 100644 index 00000000..966fab2e --- /dev/null +++ b/src/test/testData/src/main/resources/empty.property.doma.compile.config @@ -0,0 +1 @@ +doma.expr.functions= \ No newline at end of file