Skip to content

Commit 7efd750

Browse files
authored
Merge pull request #368 from domaframework/feature/annotation-option-parameter-validation
Enhance annotation option parameter validation for embedded properties
2 parents e5d7054 + 00899bd commit 7efd750

24 files changed

+634
-13
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ The plugin also provides quick fixes for DAO methods where the required SQL file
4242
![returnTypeInspection.png](images/returnTypeInspection.png)
4343
- Checks that parameters of DAO method matches the expected type based on its annotation.
4444
![paramTypeInspection.png](images/paramTypeInspection.png)
45+
- Checks the validity of `include` and `exclude` options in `@Update`, `@BatchUpdate`, `@Insert`, `@BatchInsert`,`@MultiInsert` and `@Returning`annotations:
46+
![inspectionAnnotationOptions.png](images/inspectionAnnotationOptions.png)
4547
- Checks for undefined bind variable names.
4648
- Checks the class name and package name for static property calls.
4749
![inspectionPackageName.png](images/inspectionPackageName.png)
17.8 KB
Loading

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ enum class DomaClassName(
6767
SELECT_TYPE("org.seasar.doma.SelectType"),
6868

6969
ENTITY("org.seasar.doma.Entity"),
70+
EMBEDDABLE("org.seasar.doma.Embeddable"),
7071
DATATYPE("org.seasar.doma.DataType"),
7172
;
7273

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ object TypeUtil {
7777
return clazz?.isDomain() == true
7878
}
7979

80+
fun isEmbeddable(
81+
type: PsiType?,
82+
project: Project,
83+
): Boolean {
84+
val clazz = type?.canonicalText?.let { project.getJavaClazz(it) }
85+
return clazz?.getClassAnnotation(DomaClassName.EMBEDDABLE.className) != null
86+
}
87+
8088
/**
8189
* Checks if the given type is a data type.
8290
*/
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Doma Tools Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.domaframework.doma.intellij.common.validation.result
17+
18+
import com.intellij.codeInspection.ProblemsHolder
19+
import com.intellij.openapi.util.TextRange
20+
import com.intellij.psi.PsiElement
21+
import org.domaframework.doma.intellij.bundle.MessageBundle
22+
import org.domaframework.doma.intellij.common.psi.PsiParentClass
23+
24+
/**
25+
* Validation result for annotation option parameter errors with Embeddable types.
26+
* This is used when an include/exclude option references an Embeddable field without specifying its properties.
27+
*/
28+
class ValidationAnnotationOptionEmbeddableResult(
29+
override val identify: PsiElement?,
30+
override val shortName: String = "",
31+
private val embeddableFieldName: String,
32+
private val optionName: String,
33+
private val embeddableClassName: String,
34+
private val availableProperties: String,
35+
) : ValidationResult(identify, null, shortName) {
36+
override fun setHighlight(
37+
highlightRange: TextRange,
38+
identify: PsiElement,
39+
holder: ProblemsHolder,
40+
parent: PsiParentClass?,
41+
) {
42+
val project = identify.project
43+
holder.registerProblem(
44+
identify,
45+
MessageBundle.message(
46+
"inspection.invalid.dao.annotation.option.embeddable",
47+
embeddableFieldName,
48+
optionName,
49+
embeddableClassName,
50+
availableProperties,
51+
),
52+
problemHighlightType(project, shortName),
53+
highlightRange,
54+
)
55+
}
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Doma Tools Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.domaframework.doma.intellij.common.validation.result
17+
18+
import com.intellij.codeInspection.ProblemsHolder
19+
import com.intellij.openapi.util.TextRange
20+
import com.intellij.psi.PsiElement
21+
import org.domaframework.doma.intellij.bundle.MessageBundle
22+
import org.domaframework.doma.intellij.common.psi.PsiParentClass
23+
24+
/**
25+
* Validation result for annotation option parameter errors.
26+
* This is used when an include/exclude option references a non-existent field.
27+
*/
28+
class ValidationAnnotationOptionParameterResult(
29+
override val identify: PsiElement?,
30+
override val shortName: String = "",
31+
private val invalidFieldName: String,
32+
private val optionName: String,
33+
private val entityName: String,
34+
private val availableFields: String,
35+
) : ValidationResult(identify, null, shortName) {
36+
override fun setHighlight(
37+
highlightRange: TextRange,
38+
identify: PsiElement,
39+
holder: ProblemsHolder,
40+
parent: PsiParentClass?,
41+
) {
42+
val project = identify.project
43+
holder.registerProblem(
44+
identify,
45+
MessageBundle.message(
46+
"inspection.invalid.dao.annotation.option.field",
47+
invalidFieldName,
48+
optionName,
49+
entityName,
50+
availableFields,
51+
),
52+
problemHighlightType(project, shortName),
53+
highlightRange,
54+
)
55+
}
56+
}

src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiClassExtension.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ fun PsiClass.isEntity(): Boolean = this.getClassAnnotation(DomaClassName.ENTITY.
5050

5151
fun PsiClass.isDomain(): Boolean = this.getClassAnnotation(DomaClassName.DOMAIN.className) != null
5252

53+
fun PsiClass.isEmbeddable(): Boolean = this.getClassAnnotation(DomaClassName.EMBEDDABLE.className) != null
54+
5355
fun PsiClass.isDataType(): Boolean = this.getClassAnnotation(DomaClassName.DATATYPE.className) != null && this.isRecord
5456

5557
fun PsiClassType.getSuperType(superClassName: String): PsiClassType? {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Doma Tools Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.domaframework.doma.intellij.inspection.dao.inspector
17+
18+
import com.intellij.codeHighlighting.HighlightDisplayLevel
19+
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool
20+
import com.intellij.codeInspection.ProblemsHolder
21+
import com.intellij.psi.PsiElementVisitor
22+
import org.domaframework.doma.intellij.inspection.dao.visitor.DaoAnnotationOptionParameterInspectionVisitor
23+
24+
class DaoAnnotationOptionParameterInspection : AbstractBaseJavaLocalInspectionTool() {
25+
override fun getDisplayName(): String = "Check annotation option parameters of DAO method"
26+
27+
override fun getShortName(): String = "org.domaframework.doma.intellij.annotation.option"
28+
29+
override fun getGroupDisplayName(): String = "DomaTools"
30+
31+
override fun isEnabledByDefault(): Boolean = true
32+
33+
override fun getDefaultLevel(): HighlightDisplayLevel = HighlightDisplayLevel.ERROR
34+
35+
override fun buildVisitor(
36+
holder: ProblemsHolder,
37+
isOnTheFly: Boolean,
38+
): PsiElementVisitor = DaoAnnotationOptionParameterInspectionVisitor(holder, this.shortName)
39+
}

src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/processor/TypeCheckerProcessor.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ import org.domaframework.doma.intellij.extension.psi.isDomain
4040
* @property project The IntelliJ project instance.
4141
*/
4242
abstract class TypeCheckerProcessor(
43-
psiDaoMethod: PsiDaoMethod,
43+
private val psiDaoMethod: PsiDaoMethod,
4444
) {
45+
protected val returningFqn = DomaClassName.RETURNING.className
4546
protected val method = psiDaoMethod.psiMethod
4647
protected val project = method.project
4748

@@ -52,6 +53,13 @@ abstract class TypeCheckerProcessor(
5253
(param.type as? PsiClassType)?.getSuperType(typeName) != null
5354
}
5455

56+
protected fun hasReturingOption(): Boolean {
57+
val methodAnnotation: PsiAnnotation =
58+
getAnnotation(psiDaoMethod.daoType.fqdn) ?: return false
59+
val returningOption: PsiAnnotation? = getDaoAnnotationOption(methodAnnotation, "returning")
60+
return returningOption?.nameReferenceElement?.qualifiedName == returningFqn
61+
}
62+
5563
protected fun getDaoAnnotationOption(
5664
psiAnnotation: PsiAnnotation,
5765
findOptionName: String,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright Doma Tools Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.domaframework.doma.intellij.inspection.dao.processor.option
17+
18+
import com.intellij.codeInspection.ProblemsHolder
19+
import com.intellij.openapi.project.Project
20+
import com.intellij.psi.PsiAnnotation
21+
import com.intellij.psi.PsiClass
22+
import com.intellij.psi.PsiClassType
23+
import com.intellij.psi.PsiField
24+
import com.intellij.psi.PsiLiteralExpression
25+
import org.domaframework.doma.intellij.common.psi.PsiDaoMethod
26+
import org.domaframework.doma.intellij.common.util.TypeUtil
27+
import org.domaframework.doma.intellij.common.validation.result.ValidationAnnotationOptionEmbeddableResult
28+
import org.domaframework.doma.intellij.common.validation.result.ValidationAnnotationOptionParameterResult
29+
import org.domaframework.doma.intellij.extension.getJavaClazz
30+
import org.domaframework.doma.intellij.extension.psi.DomaAnnotationType
31+
import org.domaframework.doma.intellij.extension.psi.isEmbeddable
32+
import org.domaframework.doma.intellij.extension.psi.isEntity
33+
import org.domaframework.doma.intellij.inspection.dao.processor.TypeCheckerProcessor
34+
35+
/**
36+
* Processor for checking annotation option parameters in DAO methods.
37+
* This class validates the include and exclude options in @Update and @BatchUpdate annotations.
38+
*/
39+
class DaoAnnotationOptionParameterCheckProcessor(
40+
private val psiDaoMethod: PsiDaoMethod,
41+
private val shortName: String,
42+
) : TypeCheckerProcessor(psiDaoMethod) {
43+
fun checkAnnotationOptions(holder: ProblemsHolder) {
44+
val annotation = getAnnotation(psiDaoMethod.daoType.fqdn) ?: return
45+
46+
// Check if sqlFile option is true - if so, include/exclude are not referenced
47+
if (psiDaoMethod.isUseSqlFileMethod() || psiDaoMethod.useSqlAnnotation()) {
48+
return
49+
}
50+
51+
// Get the entity parameter
52+
val methodParams = method.parameterList.parameters
53+
val entityParam = methodParams.firstOrNull() ?: return
54+
var entityType = entityParam.type
55+
56+
// For batch operations, extract the element type from Iterable
57+
if (psiDaoMethod.daoType.isBatchAnnotation() || psiDaoMethod.daoType == DomaAnnotationType.MultiInsert) {
58+
val paramClassType = entityType as? PsiClassType
59+
val iterableParam = paramClassType?.parameters?.firstOrNull()
60+
if (iterableParam != null) {
61+
entityType = iterableParam
62+
}
63+
}
64+
65+
val entityClass = project.getJavaClazz(entityType.canonicalText) ?: return
66+
if (!entityClass.isEntity()) {
67+
return
68+
}
69+
70+
checkAnnotationOptions(annotation, entityClass, holder)
71+
72+
val returningOption = getDaoAnnotationOption(annotation, "returning") ?: return
73+
if (hasReturingOption()) {
74+
checkAnnotationOptions(returningOption, entityClass, holder)
75+
}
76+
}
77+
78+
private fun checkAnnotationOptions(
79+
annotation: PsiAnnotation,
80+
entityClass: PsiClass,
81+
holder: ProblemsHolder,
82+
) {
83+
checkArrayOption(annotation, "include", entityClass, holder)
84+
checkArrayOption(annotation, "exclude", entityClass, holder)
85+
}
86+
87+
private fun checkArrayOption(
88+
annotation: PsiAnnotation,
89+
optionName: String,
90+
entityClass: PsiClass,
91+
holder: ProblemsHolder,
92+
) {
93+
val arrayValues =
94+
annotation.parameterList.attributes
95+
.find { it.name == optionName }
96+
?.value
97+
?.children
98+
?.filter { it is PsiLiteralExpression } ?: return
99+
if (arrayValues.isEmpty()) return
100+
101+
val project = method.project
102+
arrayValues.map { fields ->
103+
val valueFields = fields.text.replace("\"", "").split(".")
104+
var searchParamClass: PsiClass? = entityClass
105+
var preSearchParamClass: PsiClass? = entityClass
106+
var hasError = false
107+
valueFields.map { field ->
108+
searchParamClass
109+
?.fields
110+
?.find { property -> isOptionTargetProperty(property, field, project) }
111+
?.let { f ->
112+
preSearchParamClass = searchParamClass
113+
searchParamClass = project.getJavaClazz(f.type.canonicalText) ?: return@map
114+
}
115+
?: run {
116+
ValidationAnnotationOptionParameterResult(
117+
fields,
118+
shortName,
119+
field,
120+
optionName,
121+
searchParamClass?.name ?: "Unknown",
122+
getTargetOptionProperties(preSearchParamClass),
123+
).highlightElement(holder)
124+
hasError = true
125+
return@map
126+
}
127+
}
128+
// Error if the last field is Embeddable
129+
if (!hasError && searchParamClass?.isEmbeddable() == true) {
130+
ValidationAnnotationOptionEmbeddableResult(
131+
fields,
132+
shortName,
133+
valueFields.lastOrNull() ?: "Unknown",
134+
optionName,
135+
searchParamClass.name ?: "Unknown",
136+
getEmbeddableProperties(searchParamClass),
137+
).highlightElement(holder)
138+
}
139+
}
140+
}
141+
142+
private fun getTargetOptionProperties(paramClass: PsiClass?) =
143+
paramClass?.fields?.filter { isOptionTargetProperty(it, it.name, project) }?.joinToString(", ") { it.name.substringAfter(":") }
144+
?: "No fields found"
145+
146+
private fun getEmbeddableProperties(embeddableClass: PsiClass?) =
147+
embeddableClass
148+
?.fields
149+
?.filter { !TypeUtil.isEntity(it.type, project) && !TypeUtil.isEmbeddable(it.type, project) }
150+
?.joinToString(", ") { it.name }
151+
?: "No properties found"
152+
153+
private fun isOptionTargetProperty(
154+
field: PsiField,
155+
optionPropertyName: String,
156+
project: Project,
157+
): Boolean =
158+
(
159+
field.name == optionPropertyName && (
160+
!TypeUtil.isEntity(field.type, project) ||
161+
TypeUtil.isEmbeddable(field.type, project)
162+
)
163+
)
164+
}

0 commit comments

Comments
 (0)