This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts-intellij-plugin.git
The following commit(s) were added to refs/heads/main by this push:
new 6311474 Fix namespace-relative JSP result path resolution and
validation (#102)
6311474 is described below
commit 6311474547b91675a38419f14789c053f9825a82
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Jun 26 08:53:33 2026 +0200
Fix namespace-relative JSP result path resolution and validation (#102)
* Add spec and plan for namespace-relative JSP path resolution fix.
Co-authored-by: Cursor <[email protected]>
* Add StrutsResultPathUtil for namespace-relative result paths.
Co-authored-by: Cursor <[email protected]>
* Normalize namespace-relative paths in dispatch result references.
Co-authored-by: Cursor <[email protected]>
* Suppress symbol errors for namespace-relative dispatch result paths.
Co-authored-by: Cursor <[email protected]>
* Add tests for namespace-relative JSP result path resolution.
Co-authored-by: Cursor <[email protected]>
* fix: limit JSP-only symbol error suppression in result inspection.
Co-authored-by: Cursor <[email protected]>
* Document fix for namespace-relative JSP result paths.
Co-authored-by: Cursor <[email protected]>
* fix: restore result path completion and missing JSP validation
Use a proper NamespaceRelativeFileReferenceSet for IntelliJ 2026.1, default
to dispatcher when no result type is declared, and highlight unresolved JSP
paths via StrutsResultPathAnnotator with a Create file intention.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
---
CHANGELOG.md | 5 +
.../2026-06-26-jsp-path-without-leading-slash.md | 371 +++++++++++++++++++++
...-06-26-jsp-path-without-leading-slash-design.md | 129 +++++++
.../annotators/StrutsResultPathAnnotator.java | 193 +++++++++++
.../dom/inspection/Struts2ModelInspection.java | 7 +-
.../impl/path/DispatchPathResultContributor.java | 5 +-
.../dom/struts/impl/path/JspResultFileCreator.java | 123 +++++++
.../path/NamespaceRelativeFileReferenceSet.java | 68 ++++
.../dom/struts/impl/path/ResultTypeUtil.java | 58 ++++
.../path/StrutsDispatchResultPathInspection.java | 166 +++++++++
.../struts/impl/path/StrutsResultContributor.java | 8 +-
.../dom/struts/impl/path/StrutsResultPathUtil.java | 40 +++
src/main/resources/META-INF/plugin.xml | 1 +
.../resources/messages/Struts2Bundle.properties | 2 +
.../dom/struts/StrutsResultResolvingTest.java | 10 +
.../struts/impl/path/StrutsResultPathUtilTest.java | 52 +++
.../testData/strutsXml/result/WEB-INF/upload.jsp | 2 +
.../strutsXml/result/struts-path-dispatcher.xml | 2 +-
.../result/struts-path-extends-default.xml | 13 +
.../result/struts-path-webinf-relative.xml | 18 +
20 files changed, 1259 insertions(+), 14 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1882c56..416376d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,11 @@
### Fixed
+- Fix false "Cannot resolve symbol" errors for namespace-relative JSP result
paths (e.g. `WEB-INF/upload.jsp` without leading slash)
+- Fix path completion inside `<result>` tags by restoring correct
`FileReferenceSet` construction for IntelliJ 2026.1
+- Assume default `dispatcher` result type when a package does not declare
result types (typical `extends="struts-default"` setups)
+- Report missing JSP result targets via `StrutsResultPathAnnotator` using
unresolved `FileReference`s
+- Offer "Create file" intention for missing JSP result paths (Option+Enter)
- Pin Marketplace ZIP Signer to `0.1.43` so `signPlugin` is deterministically
resolvable and no longer fails with "No Marketplace ZIP Signer executable
found" on a stale Gradle cache in the nightly/release workflows
## [261.19017.1] - 2026-04-28
diff --git
a/docs/superpowers/plans/2026-06-26-jsp-path-without-leading-slash.md
b/docs/superpowers/plans/2026-06-26-jsp-path-without-leading-slash.md
new file mode 100644
index 0000000..bcf33e8
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-26-jsp-path-without-leading-slash.md
@@ -0,0 +1,371 @@
+# JSP Result Path Without Leading Slash — Implementation Plan
+
+> **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:** Fix false "Cannot resolve symbol" errors for namespace-relative JSP
result paths (e.g. `WEB-INF/upload.jsp`) by normalizing them to
servlet-context-absolute paths before reference resolution.
+
+**Architecture:** Add `StrutsResultPathUtil.toAbsoluteWebPath()` mirroring
Struts `ServletDispatcherResult` semantics. Use a custom `FileReferenceSet` in
`DispatchPathResultContributor` that resolves against the normalized path.
Broaden `Struts2ModelInspection` suppression so generic DOM checks defer to
file references for dispatch-type results.
+
+**Tech Stack:** IntelliJ Platform (`FileReferenceSet`, Web facet), Apache
Struts 2 DOM model, JUnit 4 light tests.
+
+**Spec:**
`docs/superpowers/specs/2026-06-26-jsp-path-without-leading-slash-design.md`
+
+---
+
+## File Structure
+
+| File | Action | Responsibility |
+|---|---|---|
+|
`src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java`
| Create | Namespace-relative → absolute web path normalization |
+|
`src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java`
| Modify | Custom `FileReferenceSet` using normalized path |
+|
`src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java`
| Modify | Suppress generic symbol check for dispatch web-resource paths |
+|
`src/test/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtilTest.java`
| Create | Unit tests for normalization |
+| `src/test/testData/strutsXml/result/struts-path-webinf-relative.xml` |
Create | Highlighting fixture for WEB-INF paths without `/` |
+| `src/test/testData/strutsXml/result/WEB-INF/upload.jsp` | Create | Existing
JSP fixture |
+|
`src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java`
| Modify | Add `testPathWebInfRelative()` |
+| `src/test/testData/strutsXml/result/struts-path-dispatcher.xml` | Modify |
Update `valid1` — `index.jsp` should no longer error |
+| `CHANGELOG.md` | Modify | Unreleased bugfix entry |
+
+---
+
+### Task 1: Path normalization utility
+
+**Files:**
+- Create:
`src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java`
+- Create:
`src/test/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtilTest.java`
+
+- [ ] **Step 1: Write the failing test class**
+
+Create `StrutsResultPathUtilTest.java`:
+
+```java
+package com.intellij.struts2.dom.struts.impl.path;
+
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import junit.framework.TestCase;
+
+public class StrutsResultPathUtilTest extends TestCase {
+
+ public void testRootNamespacePrependsSlash() {
+ assertEquals("/WEB-INF/upload.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("WEB-INF/upload.jsp",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+
+ public void testAbsolutePathUnchanged() {
+ assertEquals("/WEB-INF/upload.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("/WEB-INF/upload.jsp",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+
+ public void testNonRootNamespacePrependsNamespace() {
+ assertEquals("/admin/list.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("list.jsp", "/admin"));
+ }
+
+ public void testNonRootNamespaceWithTrailingSlash() {
+ assertEquals("/admin/list.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("list.jsp",
"/admin/"));
+ }
+
+ public void testOgnlExpressionUnchanged() {
+ assertEquals("${someProperty}",
+ StrutsResultPathUtil.toAbsoluteWebPath("${someProperty}",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+
+ public void testUrlUnchanged() {
+ assertEquals("http://example.com/page",
+
StrutsResultPathUtil.toAbsoluteWebPath("http://example.com/page",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `./gradlew test -x rat --tests "StrutsResultPathUtilTest"`
+Expected: FAIL — class `StrutsResultPathUtil` not found
+
+- [ ] **Step 3: Write minimal implementation**
+
+Create `StrutsResultPathUtil.java`:
+
+```java
+package com.intellij.struts2.dom.struts.impl.path;
+
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public final class StrutsResultPathUtil {
+
+ private StrutsResultPathUtil() {
+ }
+
+ @NotNull
+ public static String toAbsoluteWebPath(@NotNull String path, @NotNull String
namespace) {
+ if (path.startsWith("/") || path.startsWith("${") ||
URLUtil.containsScheme(path)) {
+ return path;
+ }
+ if (Objects.equals(namespace, StrutsPackage.DEFAULT_NAMESPACE)) {
+ return "/" + path;
+ }
+ final String prefix = namespace.endsWith("/") ? namespace : namespace +
"/";
+ return prefix + path;
+ }
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `./gradlew test -x rat --tests "StrutsResultPathUtilTest"`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add
src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java
\
+
src/test/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtilTest.java
+git commit -m "$(cat <<'EOF'
+Add StrutsResultPathUtil for namespace-relative result paths.
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Apply normalization in DispatchPathResultContributor
+
+**Files:**
+- Modify:
`src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java`
+
+- [ ] **Step 1: Replace FileReferenceSet.createSet with normalized subclass**
+
+In `createReferences()`, replace:
+
+```java
+final FileReferenceSet fileReferenceSet =
FileReferenceSet.createSet(psiElement, soft, false, true);
+```
+
+with:
+
+```java
+final String normalizedNamespace = packageNamespace;
+final FileReferenceSet fileReferenceSet = new FileReferenceSet(psiElement,
soft, false, true) {
+ @Override
+ public @NotNull String getPathString() {
+ return StrutsResultPathUtil.toAbsoluteWebPath(super.getPathString(),
normalizedNamespace);
+ }
+};
+```
+
+Add import for `StrutsResultPathUtil`.
+
+- [ ] **Step 2: Compile**
+
+Run: `./gradlew compileJava compileTestJava`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add
src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java
+git commit -m "$(cat <<'EOF'
+Normalize namespace-relative paths in dispatch result references.
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Fix inspection suppression
+
+**Files:**
+- Modify:
`src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java`
+
+- [ ] **Step 1: Replace narrow JSP regex with normalized-path check**
+
+Replace the block:
+
+```java
+ // WEB-INF/**/*.jsp
+ if (stringValue.matches("/*/.*\\.jsp")) {
+ LOG.info("Inspecting jsp file: " + stringValue);
+ return false;
+ }
+```
+
+with:
+
+```java
+ // Let FileReferenceSet report file-level errors for web-resource paths
+ final StrutsPackage strutsPackage = DomUtil.getParentOfType(value,
StrutsPackage.class, true);
+ if (strutsPackage != null) {
+ final String namespace = strutsPackage.searchNamespace();
+ if (namespace != null) {
+ final String absolutePath =
StrutsResultPathUtil.toAbsoluteWebPath(stringValue, namespace);
+ if (absolutePath.startsWith("/") && absolutePath.contains(".")) {
+ return false;
+ }
+ }
+ }
+```
+
+Add imports for `StrutsPackage`, `StrutsResultPathUtil`, and `DomUtil` (if not
already present).
+
+- [ ] **Step 2: Compile**
+
+Run: `./gradlew compileJava`
+Expected: BUILD SUCCESSFUL
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add
src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java
+git commit -m "$(cat <<'EOF'
+Suppress symbol errors for namespace-relative dispatch result paths.
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Highlighting tests
+
+**Files:**
+- Create: `src/test/testData/strutsXml/result/WEB-INF/upload.jsp`
+- Create: `src/test/testData/strutsXml/result/struts-path-webinf-relative.xml`
+- Modify: `src/test/testData/strutsXml/result/struts-path-dispatcher.xml`
+- Modify:
`src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java`
+
+- [ ] **Step 1: Create JSP fixture**
+
+Create `src/test/testData/strutsXml/result/WEB-INF/upload.jsp`:
+
+```jsp
+<%@ page contentType="text/html;charset=UTF-8" %>
+<html><body>upload</body></html>
+```
+
+- [ ] **Step 2: Create highlighting fixture**
+
+Create `src/test/testData/strutsXml/result/struts-path-webinf-relative.xml`:
+
+```xml
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
+ "http://struts.apache.org/dtds/struts-2.0.dtd">
+<struts>
+ <package name="default" namespace="/" extends="struts-default">
+ <action name="upload">
+ <result name="input">WEB-INF/upload.jsp</result>
+ <result name="missing"><error descr="Cannot resolve file
'missing.jsp'">WEB-INF/missing.jsp</error></result>
+ <result name="absolute">/WEB-INF/upload.jsp</result>
+ </action>
+ </package>
+</struts>
+```
+
+- [ ] **Step 3: Update struts-path-dispatcher.xml valid1**
+
+Change line 38 from:
+
+```xml
+ <result name="valid1"><error descr="Cannot resolve file
'index.jsp'">index.jsp</error></result>
+```
+
+to:
+
+```xml
+ <result name="valid1">index.jsp</result>
+```
+
+(`index.jsp` normalizes to `/index.jsp` which exists in test `jsp/` web root.)
+
+- [ ] **Step 4: Add test method**
+
+In `StrutsResultResolvingTest.java`, add:
+
+```java
+ public void testPathWebInfRelative() {
+ performHighlightingTest("struts-path-webinf-relative.xml");
+ }
+```
+
+- [ ] **Step 5: Run tests**
+
+Run: `./gradlew test -x rat --tests
"StrutsResultResolvingTest.testPathWebInfRelative" --tests
"StrutsResultPathUtilTest"`
+Expected: BUILD SUCCESSFUL
+
+If `testPathWebInfRelative` fails on error message text or offsets, adjust the
`<error descr="...">` annotations in the fixture to match actual IDE output
(same pattern as other tests in this class).
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/test/testData/strutsXml/result/WEB-INF/upload.jsp \
+ src/test/testData/strutsXml/result/struts-path-webinf-relative.xml \
+ src/test/testData/strutsXml/result/struts-path-dispatcher.xml \
+
src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java
+git commit -m "$(cat <<'EOF'
+Add tests for namespace-relative JSP result path resolution.
+
+EOF
+)"
+```
+
+---
+
+### Task 5: Changelog and full test run
+
+**Files:**
+- Modify: `CHANGELOG.md`
+
+- [ ] **Step 1: Add changelog entry**
+
+Under `[Unreleased]` / `### Fixed`:
+
+```markdown
+- Fix false "Cannot resolve symbol" errors for namespace-relative JSP result
paths (e.g. `WEB-INF/upload.jsp` without leading slash)
+```
+
+- [ ] **Step 2: Run full test suite**
+
+Run: `./gradlew test -x rat`
+Expected: BUILD SUCCESSFUL (pre-existing disabled tests remain disabled)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add CHANGELOG.md
+git commit -m "$(cat <<'EOF'
+Document fix for namespace-relative JSP result paths.
+
+EOF
+)"
+```
+
+---
+
+## Spec Coverage Checklist
+
+| Spec requirement | Task |
+|---|---|
+| No false symbol errors for `WEB-INF/upload.jsp` | Task 2 + 3 + 4 |
+| Missing JSPs still report file errors | Task 4 fixture `missing` result |
+| Namespace-relative `/admin/list.jsp` | Task 1 unit tests; optional follow-up
fixture |
+| Absolute paths unchanged | Task 1 + Task 4 `absolute` result |
+| Manual verification on file-upload example | Post-merge manual check |
+
+## Execution Handoff
+
+Plan complete and saved to
`docs/superpowers/plans/2026-06-26-jsp-path-without-leading-slash.md`.
+
+**Two execution options:**
+
+1. **Subagent-Driven (recommended)** — dispatch a fresh subagent per task,
review between tasks
+2. **Inline Execution** — implement all tasks in this session with checkpoints
+
+Which approach do you prefer?
diff --git
a/docs/superpowers/specs/2026-06-26-jsp-path-without-leading-slash-design.md
b/docs/superpowers/specs/2026-06-26-jsp-path-without-leading-slash-design.md
new file mode 100644
index 0000000..5122324
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-26-jsp-path-without-leading-slash-design.md
@@ -0,0 +1,129 @@
+# JSP Result Path Resolution Without Leading Slash
+
+**Date:** 2026-06-26
+**Status:** Approved for implementation planning
+
+## Problem
+
+The Struts plugin reports **"Cannot resolve symbol 'WEB-INF/upload.jsp'"** for
valid `<result>` paths that omit the leading `/`, even when the JSP file exists
under the Web facet root (`src/main/webapp/WEB-INF/upload.jsp`).
+
+Confirmed by user: changing `WEB-INF/upload.jsp` to `/WEB-INF/upload.jsp`
makes the error disappear. Missing classes (e.g. `FailedAction`) and genuinely
missing JSPs (`failed.jsp`) are correctly flagged — only namespace-relative
path syntax is broken.
+
+## Root Cause
+
+Two cooperating mechanisms fail for paths without a leading `/`:
+
+1. **`Struts2ModelInspection.shouldCheckResolveProblems()`** suppresses the
generic DOM **"Cannot resolve symbol"** check only when the path matches
`/*/.*\.jsp` (leading `/` required). Paths like `WEB-INF/upload.jsp` bypass
suppression, so `PathReference.fromString()` returning `null` triggers a
false-positive symbol error.
+
+2. **`DispatchPathResultContributor`** builds a standard `FileReferenceSet`
from the raw XML text. IntelliJ resolves paths with a leading `/` against Web
facet roots; paths without `/` are not treated as servlet-context-absolute, so
`fromString()` fails even when the target file exists.
+
+At **runtime**, Struts 2 resolves result locations without a leading `/`
relative to the action's package namespace (`ServletDispatcherResult`
semantics). For namespace `/`, `WEB-INF/upload.jsp` is equivalent to
`/WEB-INF/upload.jsp`.
+
+## Goals
+
+1. No false **"Cannot resolve symbol"** errors for valid namespace-relative
JSP paths (e.g. `WEB-INF/upload.jsp` with namespace `/`).
+2. File-level validation still works: missing JSPs report **"Cannot resolve
file"** (not symbol errors).
+3. Namespace-relative resolution matches Struts: `list.jsp` in package
`/admin` resolves to `/admin/list.jsp`.
+4. Existing absolute paths (`/WEB-INF/upload.jsp`, `/index.jsp`) continue to
work unchanged.
+
+## Non-Goals
+
+- Fixing missing Java action classes — working as intended.
+- Changing FreeMarker/Velocity result contributors (separate path logic;
follow-up if reported).
+- Re-enabling all disabled `StrutsResultResolvingTest` cases unrelated to this
fix.
+
+## Decision
+
+**Approach A — Normalize paths at resolution time** (selected over
suppression-only or documentation-only).
+
+Introduce a small path-normalization utility and apply it when building
`FileReferenceSet` references. Align inspection suppression with normalized
paths (or suppress generic DOM checks for all dispatch-type result paths
handled by a contributor).
+
+## Architecture
+
+```
+<result>WEB-INF/upload.jsp</result>
+ │
+ ▼
+DispatchPathResultContributor.createReferences()
+ │
+ ├─ namespace = parent <package> namespace (e.g. "/")
+ │
+ ▼
+StrutsResultPathUtil.toAbsoluteWebPath("WEB-INF/upload.jsp", "/")
+ → "/WEB-INF/upload.jsp"
+ │
+ ▼
+FileReferenceSet (custom: uses normalized path for resolution)
+ + web roots from WebFacet
+ │
+ ▼
+Resolves to src/main/webapp/WEB-INF/upload.jsp ✓
+
+Struts2ModelInspection.shouldCheckResolveProblems()
+ │
+ ├─ dispatch result + web-resource path → suppress generic symbol check
+ └─ FileReferenceSet reports missing files only
+```
+
+## Components
+
+### New: `StrutsResultPathUtil`
+
+Location:
`src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java`
+
+```java
+public final class StrutsResultPathUtil {
+ @Nullable
+ public static String toAbsoluteWebPath(@NotNull String path, @NotNull String
namespace) {
+ if (path.startsWith("/") || path.startsWith("${") ||
URLUtil.containsScheme(path)) {
+ return path;
+ }
+ if (Objects.equals(namespace, StrutsPackage.DEFAULT_NAMESPACE)) {
+ return "/" + path;
+ }
+ final String prefix = namespace.endsWith("/") ? namespace : namespace +
"/";
+ return prefix + path;
+ }
+}
+```
+
+Package-private or public; covered by unit tests.
+
+### Modified: `DispatchPathResultContributor`
+
+Wrap `FileReferenceSet.createSet(...)` in a subclass (or anonymous subclass)
that overrides path retrieval to return `toAbsoluteWebPath(rawPath,
packageNamespace)` before reference resolution. Keep existing
`FileReferenceSetHelper.addWebDirectoryAndCurrentNamespaceAsRoots(...)` call
unchanged.
+
+### Modified: `Struts2ModelInspection`
+
+Replace the narrow `/*/.*\.jsp` regex with logic that suppresses generic DOM
resolve checks for dispatch-type `<result>` paths where a
`StrutsResultContributor` is active — **excluding** paths already handled by
existing suppressions (OGNL `${...}`, URLs, wildcards, nested `<param>` bodies).
+
+Alternative (acceptable): normalize the path with `StrutsResultPathUtil`
inside `shouldCheckResolveProblems` and apply the existing JSP suffix check on
the normalized value.
+
+### Tests
+
+| Test | File | Expectation |
+|---|---|---|
+| Unit: path normalization | `StrutsResultPathUtilTest.java` | Covers `/`,
`/admin`, OGNL, URLs |
+| Highlighting: WEB-INF no slash | `struts-path-webinf-relative.xml` |
`WEB-INF/upload.jsp` → no error when JSP exists |
+| Highlighting: missing JSP | same file | `WEB-INF/missing.jsp` → file error
only |
+| Highlighting: namespace-relative | `struts-path-namespace-relative.xml` |
`list.jsp` in `/admin` package |
+| Re-enable dispatcher test | `StrutsResultResolvingTest._testPathDispatcher`
| Rename to `testPathDispatcher` once fixtures pass |
+
+## Error Handling
+
+| Input | Behavior |
+|---|---|
+| `${dynamicPath}` | Pass through unchanged; existing OGNL suppression |
+| `http://example.com/page` | Pass through; URL suppression |
+| Wildcard `{1}/page.jsp` | Existing wildcard suppression |
+| Empty path | No change; existing empty-path handling |
+| No Web facet | Existing silent no-op in contributor |
+
+## Verification
+
+```bash
+./gradlew test -x rat --tests "StrutsResultPathUtilTest"
+./gradlew test -x rat --tests "StrutsResultResolvingTest"
+```
+
+Manual: open `struts-examples/file-upload` with `WEB-INF/upload.jsp` (no
leading slash) — no symbol error on existing JSP.
diff --git
a/src/main/java/com/intellij/struts2/annotators/StrutsResultPathAnnotator.java
b/src/main/java/com/intellij/struts2/annotators/StrutsResultPathAnnotator.java
new file mode 100644
index 0000000..5b78d5a
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/annotators/StrutsResultPathAnnotator.java
@@ -0,0 +1,193 @@
+/*
+ * 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.annotators;
+
+import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider;
+import com.intellij.codeInsight.intention.IntentionAction;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.Annotator;
+import com.intellij.lang.annotation.HighlightSeverity;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+import
com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.psi.xml.XmlText;
+import com.intellij.struts2.StrutsBundle;
+import com.intellij.struts2.dom.struts.impl.path.JspResultFileCreator;
+import
com.intellij.struts2.dom.struts.impl.path.StrutsDispatchResultPathInspection;
+import com.intellij.struts2.dom.struts.impl.path.StrutsResultPathUtil;
+import com.intellij.struts2.dom.struts.model.StrutsManager;
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import com.intellij.struts2.facet.ui.StrutsFileSet;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.GenericDomValue;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+
+/**
+ * Highlights unresolved {@link FileReference}s in dispatch-type {@code
<result>} JSP paths.
+ */
+public final class StrutsResultPathAnnotator implements Annotator {
+
+ @Override
+ public void annotate(@NotNull final PsiElement element, @NotNull final
AnnotationHolder holder) {
+ if (!(element instanceof XmlText xmlText)) {
+ return;
+ }
+
+ final XmlTag tag = xmlText.getParentTag();
+ if (tag == null) {
+ return;
+ }
+
+ final String localName = tag.getLocalName();
+ if (!"result".equals(localName) && !"global-result".equals(localName)) {
+ return;
+ }
+
+ final PsiFile containingFile = tag.getContainingFile();
+ if (!(containingFile instanceof XmlFile xmlFile)) {
+ return;
+ }
+
+ final Module module = ModuleUtilCore.findModuleForPsiElement(tag);
+ if (module == null || !isInConfiguredFileSet(xmlFile, module)) {
+ return;
+ }
+
+ final var domElement = DomUtil.getDomElement(tag);
+ if (!(domElement instanceof GenericDomValue<?> resultValue)) {
+ return;
+ }
+
+ final String stringValue = resultValue.getStringValue();
+ if
(!StrutsDispatchResultPathInspection.isUnresolvedDispatchJspPath(resultValue,
stringValue)) {
+ return;
+ }
+
+ final com.intellij.psi.xml.XmlElement valueElement =
DomUtil.getValueElement(resultValue);
+ if (valueElement == null) {
+ return;
+ }
+
+ final PsiReference unresolvedReference =
findUnresolvedFileReference(valueElement.getReferences());
+ if (unresolvedReference == null) {
+ return;
+ }
+
+ final StrutsPackage strutsPackage = DomUtil.getParentOfType(resultValue,
StrutsPackage.class, true);
+ if (strutsPackage == null) {
+ return;
+ }
+
+ final String namespace = strutsPackage.searchNamespace();
+ if (namespace == null) {
+ return;
+ }
+
+ final String absolutePath =
StrutsResultPathUtil.toAbsoluteWebPath(stringValue, namespace);
+ final String fileName = StringUtil.substringAfterLast(absolutePath, "/");
+
+ final String message = unresolvedReference instanceof
EmptyResolveMessageProvider provider
+ ? provider.getUnresolvedMessagePattern()
+ : "Cannot resolve file '" + fileName + "'";
+
+ holder.newAnnotation(HighlightSeverity.ERROR, message)
+ .withFix(new CreateJspResultFileIntention(tag, absolutePath, fileName))
+ .create();
+ }
+
+ private static PsiReference findUnresolvedFileReference(final PsiReference
@NotNull [] references) {
+ for (int i = references.length - 1; i >= 0; i--) {
+ final PsiReference reference = references[i];
+ if (reference.resolve() != null) {
+ return null;
+ }
+ if (reference instanceof FileReference) {
+ return reference;
+ }
+ }
+ return null;
+ }
+
+ private static boolean isInConfiguredFileSet(@NotNull final XmlFile xmlFile,
@NotNull final Module module) {
+ final VirtualFile virtualFile = xmlFile.getVirtualFile();
+ if (virtualFile == null) {
+ return false;
+ }
+
+ final Set<StrutsFileSet> fileSets =
StrutsManager.getInstance(xmlFile.getProject()).getAllConfigFileSets(module);
+ for (final StrutsFileSet fileSet : fileSets) {
+ if (fileSet.hasFile(virtualFile)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static final class CreateJspResultFileIntention implements
IntentionAction {
+
+ private final @NotNull PsiElement myContext;
+ private final @NotNull String myAbsoluteWebPath;
+ private final @NotNull String myFileName;
+
+ private CreateJspResultFileIntention(@NotNull final PsiElement context,
+ @NotNull final String absoluteWebPath,
+ @NotNull final String fileName) {
+ myContext = context;
+ myAbsoluteWebPath = absoluteWebPath;
+ myFileName = fileName;
+ }
+
+ @Override
+ @NotNull
+ public String getText() {
+ return StrutsBundle.message("dom.result.path.create.file", myFileName);
+ }
+
+ @Override
+ @NotNull
+ public String getFamilyName() {
+ return StrutsBundle.message("dom.result.path.create.file.family");
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull final Project project, final Editor
editor, final PsiFile psiFile) {
+ return true;
+ }
+
+ @Override
+ public void invoke(@NotNull final Project project, final Editor editor,
final PsiFile psiFile)
+ throws IncorrectOperationException {
+ JspResultFileCreator.create(project, myContext, myAbsoluteWebPath,
getText(), getFamilyName());
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return false;
+ }
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java
b/src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java
index b7f8dd0..2bcba0e 100644
---
a/src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java
+++
b/src/main/java/com/intellij/struts2/dom/inspection/Struts2ModelInspection.java
@@ -33,6 +33,7 @@ import com.intellij.struts2.dom.struts.action.Action;
import com.intellij.struts2.dom.struts.action.ActionMethodConverter;
import com.intellij.struts2.dom.struts.action.Result;
import com.intellij.struts2.dom.struts.action.StrutsPathReferenceConverter;
+import
com.intellij.struts2.dom.struts.impl.path.StrutsDispatchResultPathInspection;
import com.intellij.struts2.dom.struts.impl.path.ResultTypeResolver;
import com.intellij.struts2.dom.struts.model.StrutsManager;
import com.intellij.struts2.dom.struts.strutspackage.ResultType;
@@ -193,10 +194,8 @@ public class Struts2ModelInspection extends
BasicDomElementsInspection<StrutsRoo
if (URLUtil.containsScheme(stringValue)) {
return false;
}
-
- // WEB-INF/**/*.jsp
- if (stringValue.matches("/*/.*\\.jsp")) {
- LOG.info("Inspecting jsp file: " + stringValue);
+ // Namespace-relative JSP paths are validated via FileReferenceSet;
suppress generic DOM symbol check
+ if (StrutsDispatchResultPathInspection.skipGenericDomResolveCheck(value,
stringValue)) {
return false;
}
}
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java
index 5cf7afb..9cccd25 100644
---
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/DispatchPathResultContributor.java
@@ -55,7 +55,8 @@ public class DispatchPathResultContributor extends
StrutsResultContributor {
return false; // setup error, web-facet must be present in current or
dependent module
}
- final FileReferenceSet fileReferenceSet =
FileReferenceSet.createSet(psiElement, soft, false, true);
+ final FileReferenceSet fileReferenceSet =
+ new NamespaceRelativeFileReferenceSet(psiElement, soft,
packageNamespace);
FileReferenceSetHelper.addWebDirectoryAndCurrentNamespaceAsRoots(psiElement,
packageNamespace, webFacet, fileReferenceSet);
fileReferenceSet.setEmptyPathAllowed(false);
Collections.addAll(references, fileReferenceSet.getAllReferences());
@@ -68,4 +69,4 @@ public class DispatchPathResultContributor extends
StrutsResultContributor {
return createDefaultPathReference(path, element, null);
}
-}
\ No newline at end of file
+}
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/JspResultFileCreator.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/JspResultFileCreator.java
new file mode 100644
index 0000000..6a231d5
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/JspResultFileCreator.java
@@ -0,0 +1,123 @@
+/*
+ * 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.dom.struts.impl.path;
+
+import com.intellij.javaee.web.WebRoot;
+import com.intellij.javaee.web.WebUtil;
+import com.intellij.javaee.web.facet.WebFacet;
+import com.intellij.jsp.highlighter.NewJspFileType;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.Navigatable;
+import com.intellij.psi.*;
+import com.intellij.util.OpenSourceUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Creates a JSP file under a web facet root for a Struts result path.
+ */
+public final class JspResultFileCreator {
+
+ private static final String JSP_TEMPLATE = "<%@ page
contentType=\"text/html;charset=UTF-8\" %>\n";
+
+ private JspResultFileCreator() {
+ }
+
+ public static void create(@NotNull final Project project,
+ @NotNull final PsiElement context,
+ @NotNull final String absoluteWebPath,
+ @NotNull final String commandName,
+ @NotNull final String commandGroup) {
+ final String fileName = StringUtil.substringAfterLast(absoluteWebPath,
"/");
+ if (StringUtil.isEmpty(fileName)) {
+ return;
+ }
+
+ final WebFacet webFacet = WebUtil.getWebFacet(context);
+ if (webFacet == null) {
+ return;
+ }
+
+ WriteCommandAction.runWriteCommandAction(project, commandName,
commandGroup, () -> {
+ final PsiDirectory parentDirectory =
findOrCreateParentDirectory(project, webFacet, absoluteWebPath);
+ if (parentDirectory == null) {
+ return;
+ }
+
+ final PsiFile jspFile = PsiFileFactory.getInstance(project)
+ .createFileFromText(fileName, NewJspFileType.INSTANCE, JSP_TEMPLATE);
+ final PsiElement created = parentDirectory.add(jspFile);
+ if (created instanceof Navigatable navigatable) {
+ OpenSourceUtil.navigate(navigatable);
+ }
+ });
+ }
+
+ @Nullable
+ private static PsiDirectory findOrCreateParentDirectory(@NotNull final
Project project,
+ @NotNull final
WebFacet webFacet,
+ @NotNull final
String absoluteWebPath) {
+ final String relativePath = absoluteWebPath.startsWith("/")
+ ? absoluteWebPath.substring(1)
+ : absoluteWebPath;
+ final int lastSlash = relativePath.lastIndexOf('/');
+ final String parentPath = lastSlash >= 0 ? relativePath.substring(0,
lastSlash) : "";
+
+ for (final WebRoot webRoot : webFacet.getWebRoots()) {
+ final VirtualFile rootFile = webRoot.getFile();
+ if (rootFile == null) {
+ continue;
+ }
+
+ final PsiDirectory rootDir =
PsiManager.getInstance(project).findDirectory(rootFile);
+ if (rootDir == null) {
+ continue;
+ }
+
+ if (!isPathInWebRoot(absoluteWebPath, webRoot.getRelativePath())) {
+ continue;
+ }
+
+ PsiDirectory current = rootDir;
+ if (StringUtil.isNotEmpty(parentPath)) {
+ for (final String segment : parentPath.split("/")) {
+ if (segment.isEmpty()) {
+ continue;
+ }
+ PsiDirectory subdirectory = current.findSubdirectory(segment);
+ if (subdirectory == null) {
+ subdirectory = current.createSubdirectory(segment);
+ }
+ current = subdirectory;
+ }
+ }
+ return current;
+ }
+ return null;
+ }
+
+ private static boolean isPathInWebRoot(@NotNull final String absoluteWebPath,
+ @NotNull final String
webRootRelativePath) {
+ if ("/".equals(webRootRelativePath)) {
+ return true;
+ }
+ final String prefix = webRootRelativePath.endsWith("/") ?
webRootRelativePath : webRootRelativePath + "/";
+ return absoluteWebPath.startsWith(prefix) ||
absoluteWebPath.equals(webRootRelativePath);
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/NamespaceRelativeFileReferenceSet.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/NamespaceRelativeFileReferenceSet.java
new file mode 100644
index 0000000..d7f5cdf
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/NamespaceRelativeFileReferenceSet.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2011 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.dom.struts.impl.path;
+
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiElement;
+import
com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceHelper;
+import
com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceHelperRegistrar;
+import
com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link FileReferenceSet} that resolves namespace-relative Struts result
paths.
+ */
+final class NamespaceRelativeFileReferenceSet extends FileReferenceSet {
+
+ private final @NotNull String myNamespace;
+ private final boolean mySoft;
+
+ NamespaceRelativeFileReferenceSet(@NotNull final PsiElement element,
+ final boolean soft,
+ @NotNull final String namespace) {
+ super(createPath(element), element, createOffset(element), null, true,
true);
+ mySoft = soft;
+ myNamespace = namespace;
+ }
+
+ @Override
+ protected boolean isSoft() {
+ return mySoft;
+ }
+
+ @Override
+ protected boolean isUrlEncoded() {
+ return true;
+ }
+
+ @Override
+ public @NotNull String getPathString() {
+ return StrutsResultPathUtil.toAbsoluteWebPath(super.getPathString(),
myNamespace);
+ }
+
+ @NotNull
+ private static String createPath(@NotNull final PsiElement element) {
+ String text = ElementManipulators.getValueText(element);
+ for (final FileReferenceHelper helper :
FileReferenceHelperRegistrar.getHelpers()) {
+ text = helper.trimUrl(text);
+ }
+ return text;
+ }
+
+ private static int createOffset(@NotNull final PsiElement element) {
+ return ElementManipulators.getValueTextRange(element).getStartOffset();
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/ResultTypeUtil.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/ResultTypeUtil.java
new file mode 100644
index 0000000..8df86e6
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/ResultTypeUtil.java
@@ -0,0 +1,58 @@
+/*
+ * 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.dom.struts.impl.path;
+
+import com.intellij.struts2.dom.struts.HasResultType;
+import com.intellij.struts2.dom.struts.strutspackage.ResultType;
+import com.intellij.util.xml.DomUtil;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Resolves effective Struts result type names, including the framework
default.
+ */
+public final class ResultTypeUtil {
+
+ /**
+ * Struts built-in default result type when none is declared on the package
hierarchy.
+ *
+ * @see org.apache.struts2.dispatcher.ServletDispatcherResult
+ */
+ @NonNls
+ public static final String DEFAULT_DISPATCHER_TYPE = "dispatcher";
+
+ private ResultTypeUtil() {
+ }
+
+ /**
+ * Returns the effective result type name for path resolution and validation.
+ */
+ @Nullable
+ public static String resolveEffectiveResultTypeName(@NotNull final
HasResultType hasResultType) {
+ if (DomUtil.hasXml(hasResultType.getType())) {
+ final ResultType localType = hasResultType.getType().getValue();
+ return localType != null ? localType.getName().getStringValue() : null;
+ }
+
+ final ResultType effectiveResultType =
hasResultType.getEffectiveResultType();
+ if (effectiveResultType != null) {
+ return effectiveResultType.getName().getStringValue();
+ }
+
+ return DEFAULT_DISPATCHER_TYPE;
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsDispatchResultPathInspection.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsDispatchResultPathInspection.java
new file mode 100644
index 0000000..3ec0284
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsDispatchResultPathInspection.java
@@ -0,0 +1,166 @@
+/*
+ * 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.dom.struts.impl.path;
+
+import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.xml.XmlElement;
+import com.intellij.struts2.dom.ConverterUtil;
+import com.intellij.struts2.dom.params.ParamsElement;
+import com.intellij.struts2.dom.struts.HasResultType;
+import com.intellij.struts2.dom.struts.action.StrutsPathReferenceConverter;
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import com.intellij.util.io.URLUtil;
+import com.intellij.util.xml.DomUtil;
+import com.intellij.util.xml.GenericDomValue;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Validates dispatch-type {@code <result>} JSP paths when generic DOM
checking is suppressed.
+ */
+public final class StrutsDispatchResultPathInspection {
+
+ private StrutsDispatchResultPathInspection() {
+ }
+
+ /**
+ * Whether generic DOM resolve checking should be skipped for this result
path value.
+ */
+ public static boolean skipGenericDomResolveCheck(@NotNull final
GenericDomValue<?> value,
+ @Nullable final String
stringValue) {
+ if (!(value.getConverter() instanceof StrutsPathReferenceConverter)) {
+ return false;
+ }
+ return isDispatchJspPath(value, stringValue);
+ }
+
+ /**
+ * Whether the given dispatch JSP path does not resolve to an existing JSP
file.
+ */
+ public static boolean isUnresolvedDispatchJspPath(@NotNull final
GenericDomValue<?> resultValue,
+ @Nullable final String
stringValue) {
+ if (!isDispatchJspPath(resultValue, stringValue)) {
+ return false;
+ }
+
+ final XmlElement xmlElement = DomUtil.getValueElement(resultValue);
+ if (xmlElement == null) {
+ return false;
+ }
+
+ return !isResolvedToJsp(resolveReferences(xmlElement));
+ }
+
+ @NotNull
+ public static String resolveErrorMessage(@NotNull final XmlElement
xmlElement, @NotNull final String fileName) {
+ return resolveErrorMessage(resolveReferences(xmlElement), fileName);
+ }
+
+ @NotNull
+ private static PsiReference[] resolveReferences(@NotNull final XmlElement
xmlElement) {
+ final PsiReference[] references = xmlElement.getReferences();
+ if (references.length > 0) {
+ return references;
+ }
+ return new StrutsPathReferenceConverterImpl().createReferences(xmlElement,
true);
+ }
+
+ private static boolean isResolvedToJsp(final PsiReference @NotNull []
references) {
+ for (int i = references.length - 1; i >= 0; i--) {
+ final PsiElement resolved = references[i].resolve();
+ if (resolved != null) {
+ return isJspFile(resolved);
+ }
+ }
+ return false;
+ }
+
+ private static boolean isJspFile(@Nullable final PsiElement element) {
+ if (!(element instanceof PsiFile file)) {
+ return false;
+ }
+ if (file.getVirtualFile() == null) {
+ return false;
+ }
+ return "jsp".equalsIgnoreCase(file.getVirtualFile().getExtension());
+ }
+
+ @NotNull
+ private static String resolveErrorMessage(final PsiReference @NotNull []
references, @NotNull final String fileName) {
+ for (int i = references.length - 1; i >= 0; i--) {
+ final PsiReference reference = references[i];
+ if (reference.resolve() != null) {
+ continue;
+ }
+ if (reference instanceof EmptyResolveMessageProvider provider) {
+ final String pattern = provider.getUnresolvedMessagePattern();
+ if (StringUtil.isNotEmpty(pattern)) {
+ return pattern;
+ }
+ }
+ }
+ return "Cannot resolve file '" + fileName + "'";
+ }
+
+ private static boolean isDispatchJspPath(@NotNull final GenericDomValue<?>
value,
+ @Nullable final String stringValue)
{
+ if (stringValue == null) {
+ return false;
+ }
+
+ if (!(value instanceof ParamsElement paramsElement) || !(value instanceof
HasResultType hasResultType)) {
+ return false;
+ }
+
+ if (!paramsElement.getParams().isEmpty()) {
+ return false;
+ }
+
+ final String resultTypeName =
ResultTypeUtil.resolveEffectiveResultTypeName(hasResultType);
+ if (resultTypeName == null ||
+ !ResultTypeResolver.hasResultTypeContributor(resultTypeName)) {
+ return false;
+ }
+
+ if (!ResultTypeResolver.isDispatchType(resultTypeName)) {
+ return false;
+ }
+
+ if (ConverterUtil.hasWildcardReference(stringValue)) {
+ return false;
+ }
+
+ if (StringUtil.startsWith(stringValue, "${") ||
URLUtil.containsScheme(stringValue)) {
+ return false;
+ }
+
+ final StrutsPackage strutsPackage = DomUtil.getParentOfType(value,
StrutsPackage.class, true);
+ if (strutsPackage == null) {
+ return false;
+ }
+
+ final String namespace = strutsPackage.searchNamespace();
+ if (namespace == null) {
+ return false;
+ }
+
+ return StrutsResultPathUtil.toAbsoluteWebPath(stringValue,
namespace).endsWith(".jsp");
+ }
+}
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultContributor.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultContributor.java
index d8b0251..353d16f 100644
---
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultContributor.java
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultContributor.java
@@ -22,7 +22,6 @@ import com.intellij.openapi.util.Iconable;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.struts2.dom.struts.HasResultType;
-import com.intellij.struts2.dom.struts.strutspackage.ResultType;
import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
import com.intellij.util.ConstantFunction;
import com.intellij.util.Function;
@@ -81,12 +80,7 @@ public abstract class StrutsResultContributor implements
PathReferenceProvider {
assert resultElement instanceof HasResultType : "not instance of
HasResultType: " + resultElement +
", text: " +
psiElement.getText();
- final ResultType effectiveResultType = ((HasResultType)
resultElement).getEffectiveResultType();
- if (effectiveResultType == null) {
- return null;
- }
-
- final String resultType = effectiveResultType.getName().getStringValue();
+ final String resultType =
ResultTypeUtil.resolveEffectiveResultTypeName((HasResultType) resultElement);
if (resultType == null ||
!matchesResultType(resultType)) {
return null;
diff --git
a/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java
new file mode 100644
index 0000000..faf74c5
--- /dev/null
+++
b/src/main/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtil.java
@@ -0,0 +1,40 @@
+/*
+ * 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.dom.struts.impl.path;
+
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public final class StrutsResultPathUtil {
+
+ private StrutsResultPathUtil() {
+ }
+
+ @NotNull
+ public static String toAbsoluteWebPath(@NotNull String path, @NotNull String
namespace) {
+ if (path.startsWith("/") || path.startsWith("${") ||
URLUtil.containsScheme(path)) {
+ return path;
+ }
+ if (Objects.equals(namespace, StrutsPackage.DEFAULT_NAMESPACE)) {
+ return "/" + path;
+ }
+ final String prefix = namespace.endsWith("/") ? namespace : namespace +
"/";
+ return prefix + path;
+ }
+}
diff --git a/src/main/resources/META-INF/plugin.xml
b/src/main/resources/META-INF/plugin.xml
index d20a9a9..b516e26 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -156,6 +156,7 @@
<annotator language="XML"
implementationClass="com.intellij.struts2.annotators.StrutsFileSetCheckingAnnotator"/>
<annotator language="XML"
implementationClass="com.intellij.struts2.annotators.StrutsWebFacetCheckingAnnotator"/>
+ <annotator language="XML"
implementationClass="com.intellij.struts2.annotators.StrutsResultPathAnnotator"/>
<compiler.inspectionValidator
implementation="com.intellij.struts2.dom.inspection.Struts2ModelValidator"/>
<projectService
serviceInterface="com.intellij.struts2.dom.struts.model.StrutsManager"
diff --git a/src/main/resources/messages/Struts2Bundle.properties
b/src/main/resources/messages/Struts2Bundle.properties
index 69a849d..d2800f6 100644
--- a/src/main/resources/messages/Struts2Bundle.properties
+++ b/src/main/resources/messages/Struts2Bundle.properties
@@ -43,6 +43,8 @@ annotators.webfacet.notification.content=Module ''{0}'' has a
Struts 2 facet but
dom.extendable.class.converter.type.class=class
dom.extendable.class.converter.type.spring=Spring bean
dom.extendable.class.converter.cannot.resolve=Cannot resolve {0} ''{1}''
+dom.result.path.create.file=Create file ''{0}''
+dom.result.path.create.file.family=Create JSP result file
dom.highlighting.struts.package.must.start.with.slash=Namespace must start
with '/'
diff --git
a/src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java
b/src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java
index be028ea..a09935d 100644
---
a/src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java
+++
b/src/test/java/com/intellij/struts2/dom/struts/StrutsResultResolvingTest.java
@@ -62,6 +62,7 @@ public class StrutsResultResolvingTest extends
StrutsLightHighlightingTestCase {
myFixture.copyDirectoryToProject("jsp", "jsp");
myFixture.copyDirectoryToProject("jsp2", "jsp2");
myFixture.copyDirectoryToProject("WEB-INF", "WEB-INF");
+ myFixture.copyFileToProject("WEB-INF/upload.jsp",
"jsp/WEB-INF/upload.jsp");
try {
final String jspUrl =
myFixture.getTempDirFixture().findOrCreateDir("/jsp/").getUrl();
WebRoot jsp = webFacet.addWebRoot(jspUrl, "/");
@@ -89,6 +90,15 @@ public class StrutsResultResolvingTest extends
StrutsLightHighlightingTestCase {
performHighlightingTest("struts-path-dispatcher.xml");
}
+ public void testPathWebInfRelative() {
+ performHighlightingTest("struts-path-webinf-relative.xml");
+ }
+
+ public void testPathExtendsDefaultPackage() {
+ myFixture.copyFileToProject("../highlighting/struts-default.xml",
"struts-default.xml");
+ performHighlightingTest("struts-default.xml",
"struts-path-extends-default.xml");
+ }
+
/**
* @see com.intellij.struts2.dom.struts.impl.path.ActionPathResultContributor
*/
diff --git
a/src/test/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtilTest.java
b/src/test/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtilTest.java
new file mode 100644
index 0000000..92a819b
--- /dev/null
+++
b/src/test/java/com/intellij/struts2/dom/struts/impl/path/StrutsResultPathUtilTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.dom.struts.impl.path;
+
+import com.intellij.struts2.dom.struts.strutspackage.StrutsPackage;
+import junit.framework.TestCase;
+
+public class StrutsResultPathUtilTest extends TestCase {
+
+ public void testRootNamespacePrependsSlash() {
+ assertEquals("/WEB-INF/upload.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("WEB-INF/upload.jsp",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+
+ public void testAbsolutePathUnchanged() {
+ assertEquals("/WEB-INF/upload.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("/WEB-INF/upload.jsp",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+
+ public void testNonRootNamespacePrependsNamespace() {
+ assertEquals("/admin/list.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("list.jsp", "/admin"));
+ }
+
+ public void testNonRootNamespaceWithTrailingSlash() {
+ assertEquals("/admin/list.jsp",
+ StrutsResultPathUtil.toAbsoluteWebPath("list.jsp",
"/admin/"));
+ }
+
+ public void testOgnlExpressionUnchanged() {
+ assertEquals("${someProperty}",
+ StrutsResultPathUtil.toAbsoluteWebPath("${someProperty}",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+
+ public void testUrlUnchanged() {
+ assertEquals("http://example.com/page",
+
StrutsResultPathUtil.toAbsoluteWebPath("http://example.com/page",
StrutsPackage.DEFAULT_NAMESPACE));
+ }
+}
diff --git a/src/test/testData/strutsXml/result/WEB-INF/upload.jsp
b/src/test/testData/strutsXml/result/WEB-INF/upload.jsp
new file mode 100644
index 0000000..a4dcbea
--- /dev/null
+++ b/src/test/testData/strutsXml/result/WEB-INF/upload.jsp
@@ -0,0 +1,2 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<html><body>upload</body></html>
diff --git a/src/test/testData/strutsXml/result/struts-path-dispatcher.xml
b/src/test/testData/strutsXml/result/struts-path-dispatcher.xml
index 153e46d..735c3c8 100644
--- a/src/test/testData/strutsXml/result/struts-path-dispatcher.xml
+++ b/src/test/testData/strutsXml/result/struts-path-dispatcher.xml
@@ -35,7 +35,7 @@
</global-results>
<action name="testValidPaths">
- <result name="valid1"><error descr="Cannot resolve file
'index.jsp'">index.jsp</error></result>
+ <result name="valid1">index.jsp</result>
<result name="valid2">/index.jsp</result>
<result name="valid3">
/index.jsp
diff --git a/src/test/testData/strutsXml/result/struts-path-extends-default.xml
b/src/test/testData/strutsXml/result/struts-path-extends-default.xml
new file mode 100644
index 0000000..6f0b9b2
--- /dev/null
+++ b/src/test/testData/strutsXml/result/struts-path-extends-default.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
+ "http://struts.apache.org/dtds/struts-2.0.dtd">
+<struts>
+ <package name="default" namespace="/" extends="struts-default">
+
+ <action name="upload">
+ <result name="input">WEB-INF/upload.jsp</result>
+ <result name="missing"><error descr="Cannot resolve file
'missing.jsp'">WEB-INF/missing.jsp</error></result>
+ </action>
+ </package>
+</struts>
diff --git a/src/test/testData/strutsXml/result/struts-path-webinf-relative.xml
b/src/test/testData/strutsXml/result/struts-path-webinf-relative.xml
new file mode 100644
index 0000000..fcf3c23
--- /dev/null
+++ b/src/test/testData/strutsXml/result/struts-path-webinf-relative.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
+ "http://struts.apache.org/dtds/struts-2.0.dtd">
+<struts>
+ <package name="default" namespace="/">
+
+ <result-types>
+ <result-type name="dispatcher"
class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/>
+ </result-types>
+
+ <action name="upload">
+ <result name="input">WEB-INF/upload.jsp</result>
+ <result name="missing"><error descr="Cannot resolve file
'missing.jsp'">WEB-INF/missing.jsp</error></result>
+ <result name="absolute">/WEB-INF/upload.jsp</result>
+ </action>
+ </package>
+</struts>