For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a Java inspection that warns when Struts action setters or public fields will not bind because struts.parameters.requireAnnotations=true and @StrutsParameter is missing.
Architecture: Implement a focused LocalInspectionTool for Java files. The inspection delegates action-class recognition, annotation/member checks, and configuration checks to small utilities so later quick-fixes or getter/depth validation can build on the same foundation.
Tech Stack: IntelliJ Platform PSI/inspection APIs, Struts facet/model APIs, JUnit 3 light fixture tests, Gradle.
src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspection.javasrc/main/java/com/intellij/struts2/inspection/StrutsActionClassUtil.javasrc/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationUtil.java@StrutsParameter availability, annotation, setter, and field checks.src/main/java/com/intellij/struts2/inspection/StrutsParameterConfigUtil.javasrc/main/java/com/intellij/struts2/StrutsConstants.java@StrutsParameter FQN constant.src/main/java/com/intellij/struts2/model/constant/contributor/StrutsCoreConstantContributor.javastruts.parameters.requireAnnotations.src/main/resources/META-INF/plugin.xmlsrc/main/resources/messages/Struts2Bundle.propertiessrc/test/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspectionTest.javasrc/test/testData/inspection/strutsParameter/struts-require-annotations.xmlrequireAnnotations=true and one mapped action.src/test/testData/inspection/strutsParameter/struts-disable-annotations.xmlrequireAnnotations=false and one mapped action.CHANGELOG.mdFiles:
Create: src/test/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspectionTest.java
Create: src/test/testData/inspection/strutsParameter/struts-require-annotations.xml
Create: src/test/testData/inspection/strutsParameter/struts-disable-annotations.xml
[ ] Step 1: Add Struts config fixture with required annotations
Create src/test/testData/inspection/strutsParameter/struts-require-annotations.xml:
<?xml version="1.0" encoding="UTF-8"?> <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you 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 http://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. --> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" "https://struts.apache.org/dtds/struts-6.0.dtd"> <struts> <constant name="struts.parameters.requireAnnotations" value="true"/> <package name="default" extends="struts-default"> <action name="sample" class="test.SampleAction"/> </package> </struts>
Create src/test/testData/inspection/strutsParameter/struts-disable-annotations.xml:
<?xml version="1.0" encoding="UTF-8"?> <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you 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 http://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. --> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" "https://struts.apache.org/dtds/struts-6.0.dtd"> <struts> <constant name="struts.parameters.requireAnnotations" value="false"/> <package name="default" extends="struts-default"> <action name="sample" class="test.SampleAction"/> </package> </struts>
Create src/test/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspectionTest.java:
/* * Copyright 2026 The 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 * * http://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 com.intellij.struts2.inspection; import com.intellij.codeInspection.InspectionProfileEntry; import com.intellij.struts2.BasicLightHighlightingTestCase; import org.jetbrains.annotations.NotNull; public class StrutsParameterAnnotationInspectionTest extends BasicLightHighlightingTestCase { private static final String WARNING = "Parameter injection requires @StrutsParameter when struts.parameters.requireAnnotations is enabled"; @Override protected InspectionProfileEntry[] getHighlightingInspections() { return new InspectionProfileEntry[]{new StrutsParameterAnnotationInspection()}; } @NotNull @Override protected String getTestDataLocation() { return "inspection/strutsParameter/"; } public void testWarnsAboutUnannotatedSetterAndPublicField() { configureStrutsParameterAnnotation(); createStrutsFileSet("struts-require-annotations.xml"); myFixture.configureByText("test/SampleAction.java", """ package test; public class SampleAction { public String <warning descr="%s">username</warning>; public void <warning descr="%s">setPassword</warning>(String password) { } } """.formatted(WARNING, WARNING)); myFixture.checkHighlighting(); } public void testDoesNotWarnAboutAnnotatedMembers() { configureStrutsParameterAnnotation(); createStrutsFileSet("struts-require-annotations.xml"); myFixture.configureByText("test/SampleAction.java", """ package test; import org.apache.struts2.interceptor.parameter.StrutsParameter; public class SampleAction { @StrutsParameter public String username; @StrutsParameter public void setPassword(String password) { } } """); myFixture.checkHighlighting(); } public void testDoesNotWarnWhenRequireAnnotationsIsFalse() { configureStrutsParameterAnnotation(); createStrutsFileSet("struts-disable-annotations.xml"); myFixture.configureByText("test/SampleAction.java", """ package test; public class SampleAction { public String username; public void setPassword(String password) { } } """); myFixture.checkHighlighting(); } public void testDoesNotWarnInNonActionClass() { configureStrutsParameterAnnotation(); createStrutsFileSet("struts-require-annotations.xml"); myFixture.configureByText("test/NotAction.java", """ package test; public class NotAction { public String username; public void setPassword(String password) { } } """); myFixture.checkHighlighting(); } public void testDoesNotWarnAboutNonPublicMembersOrGetters() { configureStrutsParameterAnnotation(); createStrutsFileSet("struts-require-annotations.xml"); myFixture.configureByText("test/SampleAction.java", """ package test; public class SampleAction { private String privateField; protected String protectedField; String packagePrivateField; public static String staticField; private void setPrivateValue(String value) { } protected void setProtectedValue(String value) { } void setPackagePrivateValue(String value) { } public static void setStaticValue(String value) { } public String getUser() { return ""; } } """); myFixture.checkHighlighting(); } private void configureStrutsParameterAnnotation() { myFixture.configureByText("org/apache/struts2/interceptor/parameter/StrutsParameter.java", """ package org.apache.struts2.interceptor.parameter; public @interface StrutsParameter { int depth() default 0; } """); } }
Run:
./gradlew test -x rat --tests "StrutsParameterAnnotationInspectionTest" --no-configuration-cache
Expected: compilation fails because StrutsParameterAnnotationInspection does not exist.
git add src/test/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspectionTest.java \ src/test/testData/inspection/strutsParameter/struts-require-annotations.xml \ src/test/testData/inspection/strutsParameter/struts-disable-annotations.xml git commit -m "$(cat <<'EOF' test: cover StrutsParameter annotation inspection EOF )"
Files:
Modify: src/main/java/com/intellij/struts2/StrutsConstants.java
Modify: src/main/java/com/intellij/struts2/model/constant/contributor/StrutsCoreConstantContributor.java
Create: src/main/java/com/intellij/struts2/inspection/StrutsActionClassUtil.java
Create: src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationUtil.java
Create: src/main/java/com/intellij/struts2/inspection/StrutsParameterConfigUtil.java
[ ] Step 1: Add the annotation FQN constant
In src/main/java/com/intellij/struts2/StrutsConstants.java, add this constant near the other plugin-wide class-name constants:
@NonNls public static final String STRUTS_PARAMETER_ANNOTATION = "org.apache.struts2.interceptor.parameter.StrutsParameter";
struts.parameters.requireAnnotationsIn src/main/java/com/intellij/struts2/model/constant/contributor/StrutsCoreConstantContributor.java, add this public key after ACTION_EXTENSION:
/** * {@code struts.parameters.requireAnnotations}. */ public static final StrutsConstantKey<Boolean> REQUIRE_ANNOTATIONS = StrutsConstantKey.create( "struts.parameters.requireAnnotations");
Then replace the string literal in the constant list:
addBooleanProperty(REQUIRE_ANNOTATIONS.getKey()),
Create src/main/java/com/intellij/struts2/inspection/StrutsActionClassUtil.java:
/* * Copyright 2026 The 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 * * http://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 com.intellij.struts2.inspection; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiModifier; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.InheritanceUtil; import com.intellij.struts2.StrutsConstants; import com.intellij.struts2.dom.struts.model.StrutsManager; import com.intellij.struts2.dom.struts.model.StrutsModel; import com.intellij.struts2.model.jam.convention.StrutsConventionConstants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; final class StrutsActionClassUtil { private StrutsActionClassUtil() { } static boolean isActionClass(@NotNull PsiClass psiClass) { if (!isConcretePublicClass(psiClass)) { return false; } final Module module = ModuleUtilCore.findModuleForPsiElement(psiClass); if (module == null) { return false; } final StrutsModel strutsModel = StrutsManager.getInstance(psiClass.getProject()).getCombinedModel(module); if (strutsModel != null && strutsModel.isActionClass(psiClass)) { return true; } if (AnnotationUtil.isAnnotated(psiClass, StrutsConventionConstants.ACTION, 0) || AnnotationUtil.isAnnotated(psiClass, StrutsConventionConstants.ACTIONS, 0)) { return true; } if (!isConventionPluginPresent(psiClass)) { return false; } final String className = psiClass.getName(); if (className != null && StringUtil.endsWith(className, "Action")) { return true; } return InheritanceUtil.isInheritor(psiClass, StrutsConstants.XWORK_ACTION_CLASS); } private static boolean isConcretePublicClass(@Nullable PsiClass psiClass) { return psiClass != null && !psiClass.isInterface() && !psiClass.isEnum() && !psiClass.isAnnotationType() && psiClass.hasModifierProperty(PsiModifier.PUBLIC) && !psiClass.hasModifierProperty(PsiModifier.ABSTRACT); } private static boolean isConventionPluginPresent(@NotNull PsiElement element) { final Module module = ModuleUtilCore.findModuleForPsiElement(element); if (module == null) { return false; } final GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false); return JavaPsiFacade.getInstance(element.getProject()) .findClass(StrutsConventionConstants.CONVENTIONS_SERVICE, scope) != null; } }
Create src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationUtil.java:
/* * Copyright 2026 The 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 * * http://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 com.intellij.struts2.inspection; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiField; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiModifierListOwner; import com.intellij.psi.util.PropertyUtilBase; import com.intellij.struts2.StrutsConstants; import org.jetbrains.annotations.NotNull; final class StrutsParameterAnnotationUtil { private StrutsParameterAnnotationUtil() { } static boolean isStrutsParameterAvailable(@NotNull PsiElement context) { return JavaPsiFacade.getInstance(context.getProject()) .findClass(StrutsConstants.STRUTS_PARAMETER_ANNOTATION, context.getResolveScope()) != null; } static boolean isAnnotatedWithStrutsParameter(@NotNull PsiModifierListOwner owner) { return AnnotationUtil.isAnnotated(owner, StrutsConstants.STRUTS_PARAMETER_ANNOTATION, 0); } static boolean isInjectableSetter(@NotNull PsiMethod method) { return method.hasModifierProperty(PsiModifier.PUBLIC) && !method.hasModifierProperty(PsiModifier.STATIC) && !method.hasModifierProperty(PsiModifier.ABSTRACT) && PropertyUtilBase.isSimplePropertySetter(method); } static boolean isInjectableField(@NotNull PsiField field) { return field.hasModifierProperty(PsiModifier.PUBLIC) && !field.hasModifierProperty(PsiModifier.STATIC); } }
Create src/main/java/com/intellij/struts2/inspection/StrutsParameterConfigUtil.java:
/* * Copyright 2026 The 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 * * http://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 com.intellij.struts2.inspection; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.struts2.facet.ui.StrutsVersionDetector; import com.intellij.struts2.model.constant.StrutsConstantManager; import com.intellij.struts2.model.constant.contributor.StrutsCoreConstantContributor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; final class StrutsParameterConfigUtil { private StrutsParameterConfigUtil() { } static boolean isRequireAnnotationsEnabled(@NotNull PsiElement context) { final PsiFile containingFile = context.getContainingFile(); if (containingFile == null) { return false; } final Boolean configuredValue = StrutsConstantManager.getInstance(context.getProject()) .getConvertedValue(containingFile, StrutsCoreConstantContributor.REQUIRE_ANNOTATIONS); if (configuredValue != null) { return configuredValue; } final Module module = ModuleUtilCore.findModuleForPsiElement(context); if (module == null) { return false; } return isStruts7OrNewer(StrutsVersionDetector.detectStrutsVersion(module)); } static boolean isStruts7OrNewer(@Nullable String version) { if (version == null || version.isBlank()) { return false; } final int firstDot = version.indexOf('.'); final String majorVersion = firstDot == -1 ? version : version.substring(0, firstDot); try { return Integer.parseInt(majorVersion) >= 7; } catch (NumberFormatException e) { return false; } } }
Run:
./gradlew test -x rat --tests "StrutsParameterAnnotationInspectionTest" --no-configuration-cache
Expected: compilation still fails because StrutsParameterAnnotationInspection does not exist, but the new utilities compile.
git add src/main/java/com/intellij/struts2/StrutsConstants.java \ src/main/java/com/intellij/struts2/model/constant/contributor/StrutsCoreConstantContributor.java \ src/main/java/com/intellij/struts2/inspection/StrutsActionClassUtil.java \ src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationUtil.java \ src/main/java/com/intellij/struts2/inspection/StrutsParameterConfigUtil.java git commit -m "$(cat <<'EOF' feat: add StrutsParameter inspection utilities EOF )"
Files:
Create: src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspection.java
Modify: src/main/resources/META-INF/plugin.xml
Modify: src/main/resources/messages/Struts2Bundle.properties
[ ] Step 1: Add bundle messages
In src/main/resources/messages/Struts2Bundle.properties, add these keys near the other inspection keys:
inspections.struts.parameter.annotation.display.name=Missing StrutsParameter annotation inspections.struts.parameter.annotation.message=Parameter injection requires @StrutsParameter when struts.parameters.requireAnnotations is enabled
Create src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspection.java:
/* * Copyright 2026 The 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 * * http://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 com.intellij.struts2.inspection; import com.intellij.codeInspection.LocalInspectionTool; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.psi.JavaElementVisitor; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiField; import com.intellij.psi.PsiIdentifier; import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiMethod; import com.intellij.struts2.StrutsBundle; import com.intellij.struts2.facet.StrutsFacet; import org.jetbrains.annotations.NotNull; public final class StrutsParameterAnnotationInspection extends LocalInspectionTool { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { if (!(holder.getFile() instanceof PsiJavaFile) || StrutsFacet.getInstance(holder.getFile()) == null || !StrutsParameterAnnotationUtil.isStrutsParameterAvailable(holder.getFile()) || !StrutsParameterConfigUtil.isRequireAnnotationsEnabled(holder.getFile())) { return new PsiElementVisitor() { }; } return new JavaElementVisitor() { @Override public void visitClass(@NotNull PsiClass psiClass) { if (!StrutsActionClassUtil.isActionClass(psiClass)) { return; } for (PsiMethod method : psiClass.getMethods()) { if (StrutsParameterAnnotationUtil.isInjectableSetter(method) && !StrutsParameterAnnotationUtil.isAnnotatedWithStrutsParameter(method)) { registerProblem(method.getNameIdentifier()); } } for (PsiField field : psiClass.getFields()) { if (StrutsParameterAnnotationUtil.isInjectableField(field) && !StrutsParameterAnnotationUtil.isAnnotatedWithStrutsParameter(field)) { registerProblem(field.getNameIdentifier()); } } } private void registerProblem(PsiIdentifier identifier) { if (identifier == null) { return; } holder.registerProblem(identifier, StrutsBundle.message("inspections.struts.parameter.annotation.message")); } }; } }
In src/main/resources/META-INF/plugin.xml, add this entry after HardcodedActionUrlInspection:
<localInspection language="JAVA" groupPath="Struts" shortName="StrutsParameterAnnotation" applyToDialects="false" bundle="messages.Struts2Bundle" key="inspections.struts.parameter.annotation.display.name" groupKey="inspections.group.display.name" enabledByDefault="true" level="WARNING" implementationClass="com.intellij.struts2.inspection.StrutsParameterAnnotationInspection"/>
Run:
./gradlew test -x rat --tests "StrutsParameterAnnotationInspectionTest" --no-configuration-cache
Expected: all tests in StrutsParameterAnnotationInspectionTest pass.
git add src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspection.java \ src/main/resources/META-INF/plugin.xml \ src/main/resources/messages/Struts2Bundle.properties git commit -m "$(cat <<'EOF' feat: inspect missing StrutsParameter annotations EOF )"
Files:
Modify: CHANGELOG.md
[ ] Step 1: Add changelog entry
In CHANGELOG.md, add this under [Unreleased] → ### Added:
- Add Java inspection for Struts action setters and public fields missing `@StrutsParameter` when annotation-based parameter binding is required
Run:
./gradlew test -x rat --tests "StrutsParameterAnnotationInspectionTest" --no-configuration-cache
Expected: build succeeds and the inspection tests pass.
Run:
./gradlew test -x rat --no-configuration-cache
Expected: build succeeds.
Use the IDE diagnostics for these files:
src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationInspection.java src/main/java/com/intellij/struts2/inspection/StrutsActionClassUtil.java src/main/java/com/intellij/struts2/inspection/StrutsParameterAnnotationUtil.java src/main/java/com/intellij/struts2/inspection/StrutsParameterConfigUtil.java src/main/java/com/intellij/struts2/StrutsConstants.java src/main/java/com/intellij/struts2/model/constant/contributor/StrutsCoreConstantContributor.java
Expected: no newly introduced errors.
git add CHANGELOG.md git commit -m "$(cat <<'EOF' docs: add changelog entry for StrutsParameter inspection EOF )"
Run:
git status --short git log --oneline -8
Expected: working tree is clean and the latest commits are the spec, tests, utilities, inspection, and changelog.