Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
intellijPlatform {
create("IC", "2025.1.4.1")
testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Platform)
testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Plugin.Java)

bundledPlugin("com.intellij.java")

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ org.gradle.caching = true

org.gradle.jvmargs=-XX\:MaxHeapSize\=4256m -Xmx4256m -Xms2000m

version = 0.0.11
version = 0.0.12
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package oap.application.plugin.completion

import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.icons.AllIcons
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.ProcessingContext
import oap.application.plugin.ref.WsValidateUtil

class WsValidateCompletionContributor : CompletionContributor() {
init {
extend(
CompletionType.BASIC,
PlatformPatterns.psiElement(),
object : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
// Whether the completion position IS the literal, or a child of it (this
// differs between a freshly-parsed in-memory file and a stub-backed real
// project file), is an implementation detail we shouldn't depend on - look
// for a PsiLiteralExpression starting at the position itself (strict=false).
val literal = PsiTreeUtil.getParentOfType(parameters.position, PsiLiteralExpression::class.java, false) ?: return
val annotation = WsValidateUtil.enclosingAnnotation(literal) ?: return
val owner = WsValidateUtil.annotatedOwner(annotation) ?: return

for (method in WsValidateUtil.candidateValidatorMethods(owner)) {
result.addElement(
LookupElementBuilder.create(method.name)
.withIcon(AllIcons.Nodes.Method)
.withTypeText(WsValidateUtil.VALIDATION_ERRORS_FQN)
)
}
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package oap.application.plugin.ref

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiParameter

class ValidWsValidateInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PsiElementVisitor() {
override fun visitElement(element: PsiElement) {
if (element !is PsiLiteralExpression || !WsValidateUtil.isWsValidateLiteral(element)) {
super.visitElement(element)
return
}

val resolved = WsValidateUtil.resolve(element) ?: return
val validator = resolved.validator

when (val owner = resolved.owner) {
is PsiMethod -> checkMethodLevel(owner, validator, element, holder)
is PsiParameter -> checkParameterLevel(validator, element, holder)
}

checkReturnType(validator, element, holder)
}
}
}

private fun checkMethodLevel(annotatedMethod: PsiMethod, validator: PsiMethod, element: PsiLiteralExpression, holder: ProblemsHolder) {
val missing = WsValidateUtil.missingParameterNames(annotatedMethod, validator)
if (missing.isNotEmpty()) {
holder.registerProblem(
element,
"Validator '${validator.name}' parameter(s) ${missing.joinToString(", ")} not supplied by '${annotatedMethod.name}'",
ProblemHighlightType.ERROR
)
}
}

private fun checkParameterLevel(validator: PsiMethod, element: PsiLiteralExpression, holder: ProblemsHolder) {
if (validator.parameterList.parametersCount != 1) {
holder.registerProblem(
element,
"Validator '${validator.name}' must take exactly one parameter for a parameter-level @WsValidate",
ProblemHighlightType.ERROR
)
}
}

private fun checkReturnType(validator: PsiMethod, element: PsiLiteralExpression, holder: ProblemsHolder) {
if (!WsValidateUtil.returnsValidationErrors(validator)) {
holder.registerProblem(
element,
"Validator '${validator.name}' must return ${WsValidateUtil.VALIDATION_ERRORS_FQN}",
ProblemHighlightType.ERROR
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package oap.application.plugin.ref

import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.ElementManipulators
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceBase
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceProvider
import com.intellij.psi.PsiReferenceRegistrar
import com.intellij.util.ProcessingContext

class WsValidateReferenceContributor : PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(PsiLiteralExpression::class.java),
object : PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
if (element !is PsiLiteralExpression || !WsValidateUtil.isWsValidateLiteral(element)) {
return PsiReference.EMPTY_ARRAY
}
return arrayOf(WsValidateMethodReference(element))
}
}
)
}
}

class WsValidateMethodReference(literal: PsiLiteralExpression) : PsiReferenceBase<PsiLiteralExpression>(literal, ElementManipulators.getValueTextRange(literal)) {
override fun resolve(): PsiElement? {
return WsValidateUtil.findValidatorMethod(element)
}
}
89 changes: 89 additions & 0 deletions src/main/kotlin/oap/application/plugin/ref/WsValidateUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package oap.application.plugin.ref

import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.PsiParameter
import com.intellij.psi.util.PsiTreeUtil

// Shared lookup logic for the oap.ws.validate.WsValidate annotation contract (see CLAUDE.md,
// "oap-ws Module" section) - a validator method name is always resolved by simple name on the
// SAME class as the annotated element, whether @WsValidate sits on a method or on a parameter.
object WsValidateUtil {
const val ANNOTATION_FQN = "oap.ws.validate.WsValidate"
const val VALIDATION_ERRORS_FQN = "oap.ws.validate.ValidationErrors"

fun enclosingAnnotation(literal: PsiLiteralExpression): PsiAnnotation? {
val annotation = PsiTreeUtil.getParentOfType(literal, PsiAnnotation::class.java) ?: return null
return if (annotation.qualifiedName == ANNOTATION_FQN) annotation else null
}

fun annotatedOwner(annotation: PsiAnnotation): PsiModifierListOwner? {
return PsiTreeUtil.getParentOfType(annotation, PsiModifierListOwner::class.java)
}

fun containingClass(owner: PsiModifierListOwner): PsiClass? {
return when (owner) {
is PsiParameter -> (owner.declarationScope as? PsiMethod)?.containingClass
is PsiMethod -> owner.containingClass
else -> null
}
}

fun findValidatorMethod(literal: PsiLiteralExpression): PsiMethod? {
return resolve(literal)?.validator
}

class Resolved(val owner: PsiModifierListOwner, val validator: PsiMethod)

fun resolve(literal: PsiLiteralExpression): Resolved? {
val name = literal.value as? String ?: return null
val annotation = enclosingAnnotation(literal) ?: return null
val owner = annotatedOwner(annotation) ?: return null
val psiClass = containingClass(owner) ?: return null
val validator = psiClass.findMethodsByName(name, true).firstOrNull() ?: return null
return Resolved(owner, validator)
}

fun isWsValidateLiteral(element: PsiElement): Boolean {
return element is PsiLiteralExpression && element.value is String && enclosingAnnotation(element) != null
}

// Names required by `candidate` that `owner`'s own parameters don't supply - empty means the
// candidate's parameter list is fully satisfiable (see ValidWsValidateInspection.checkMethodLevel
// for the runtime contract this mirrors: unmatched names throw IllegalArgumentException at
// request time in oap-ws's own MethodValidatorPeer).
fun missingParameterNames(owner: PsiMethod, candidate: PsiMethod): List<String> {
val ownerParamNames = owner.parameterList.parameters.map { it.name }.toSet()
return candidate.parameterList.parameters.map { it.name }.filter { it !in ownerParamNames }
}

fun returnsValidationErrors(method: PsiMethod): Boolean {
return method.returnType?.canonicalText == VALIDATION_ERRORS_FQN
}

fun isShapeCompatible(owner: PsiModifierListOwner, candidate: PsiMethod): Boolean {
return when (owner) {
is PsiMethod -> missingParameterNames(owner, candidate).isEmpty()
is PsiParameter -> candidate.parameterList.parametersCount == 1
else -> false
}
}

// Only methods that would NOT be flagged by ValidWsValidateInspection - completion should
// never suggest something the inspection then reports as wrong.
//
// Uses allMethods (declared + inherited), not methods (declared only) - resolve() above finds
// a validator via findMethodsByName(name, checkBases=true), which searches superclasses too
// (WS classes commonly extend a shared base class holding common validators), so candidate
// enumeration must search the same scope or it silently misses every inherited validator.
fun candidateValidatorMethods(owner: PsiModifierListOwner): List<PsiMethod> {
val psiClass = containingClass(owner) ?: return emptyList()
return psiClass.allMethods
.distinctBy { it.name }
.filter { returnsValidationErrors(it) && isShapeCompatible(owner, it) }
}
}
9 changes: 9 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@
<completion.contributor language="OAP" implementationClass="oap.application.plugin.completion.OapReferenceCompletionContributor" order="first"/>
<completion.contributor language="OAP" implementationClass="oap.application.plugin.completion.OapKeywordCompletionContributor" order="last"/>
<completion.contributor language="OAP" implementationClass="oap.application.plugin.completion.OapParameterCompletionContributor" order="last"/>
<completion.contributor language="JAVA" implementationClass="oap.application.plugin.completion.WsValidateCompletionContributor" order="first"/>
<psi.referenceContributor implementation="oap.application.plugin.ref.OapReferenceContributor" language="OAP"/>
<psi.referenceContributor
language="OAP"
implementation="oap.application.plugin.ref.OapJavaReferenceContributor"/>
<psi.referenceContributor
language="JAVA"
implementation="oap.application.plugin.ref.WsValidateReferenceContributor"/>

<lang.elementManipulator forClass="oap.application.plugin.gen.psi.OapClassNamePsi"
implementationClass="oap.application.plugin.manipulators.OapValueManipulator"/>
Expand Down Expand Up @@ -97,6 +101,11 @@
displayName="Include validation" groupName="OAP" enabledByDefault="true"
level="ERROR"/>

<localInspection language="JAVA"
implementationClass="oap.application.plugin.ref.ValidWsValidateInspection"
displayName="Validate @WsValidate method reference" groupName="OAP" enabledByDefault="true"
level="ERROR"/>

<!-- index -->
<stubIndex implementation="oap.application.plugin.stub.OapModuleNameIndex"/>
<stubIndex implementation="oap.application.plugin.stub.OapModuleServicesServiceIndex"/>
Expand Down
28 changes: 28 additions & 0 deletions src/test/java/oap/application/plugin/TestWsValidateService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package oap.application.plugin;

import oap.ws.validate.ValidationErrors;
import oap.ws.validate.WsValidate;

public class TestWsValidateService {
@WsValidate("isValid")
public String methodLevelValid(String skipDeprecated) {
return skipDeprecated;
}

public ValidationErrors isValid(String skipDeprecated) {
return ValidationErrors.empty();
}

public String parameterLevelValid(@WsValidate("oddParamValidator") int oddParam) {
return String.valueOf(oddParam);
}

public ValidationErrors oddParamValidator(int oddParam) {
return ValidationErrors.empty();
}

@WsValidate("noSuchValidator")
public String methodLevelUnresolved(String requiredParameter) {
return requiredParameter;
}
}
7 changes: 7 additions & 0 deletions src/test/java/oap/ws/validate/ValidationErrors.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package oap.ws.validate;

public class ValidationErrors {
public static ValidationErrors empty() {
return new ValidationErrors();
}
}
12 changes: 12 additions & 0 deletions src/test/java/oap/ws/validate/WsValidate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package oap.ws.validate;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface WsValidate {
String[] value();
}
Loading
Loading