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>

Reply via email to