diff --git a/.github/fork-specific-files.txt b/.github/fork-specific-files.txt
new file mode 100644
index 00000000000..ae527d989ce
--- /dev/null
+++ b/.github/fork-specific-files.txt
@@ -0,0 +1,10 @@
+.github/dependabot.yml
+.github/workflows/codacy.yml
+.github/workflows/codeql.yml
+.github/workflows/maven.yml
+.github/workflows/rebase.yml
+.github/workflows/rebase-upstream.yml
+.github/workflows/sync-upstream.yml
+.github/workflows/create-upstream-pr.yml
+.github/fork-specific-files.txt
+README.md
diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml
new file mode 100644
index 00000000000..649e970f935
--- /dev/null
+++ b/.github/workflows/codacy.yml
@@ -0,0 +1,67 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+# This workflow checks out code, performs a Codacy security scan
+# and integrates the results with the
+# GitHub Advanced Security code scanning feature. For more information on
+# the Codacy security scan action usage and parameters, see
+# https://github.com/codacy/codacy-analysis-cli-action.
+# For more information on Codacy Analysis CLI in general, see
+# https://github.com/codacy/codacy-analysis-cli.
+
+name: Codacy Security Scan
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ master ]
+ schedule:
+ - cron: '24 8 * * 2'
+
+permissions:
+ contents: read
+
+jobs:
+ codacy-security-scan:
+ permissions:
+ contents: read # for actions/checkout to fetch code
+ security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+ name: Codacy Security Scan
+ runs-on: ubuntu-latest
+ steps:
+ # Checkout the repository to the GitHub Actions runner
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
+ - name: Run Codacy Analysis CLI
+ uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b
+# uses: codacy/codacy-analysis-cli-action@33d455949345bddfdb845fba76b57b70cc83754b
+ env:
+ CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"interpret-results":["--max-paths", 1]}}'
+
+ with:
+ # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
+ # You can also omit the token and run the tools that support default configurations
+ project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
+ verbose: true
+ output: results.sarif
+ format: sarif
+ # Adjust severity of non-security issues
+ gh-code-scanning-compat: true
+ # Force 0 exit code to allow SARIF file generation
+ # This will handover control about PR rejection to the GitHub side
+ max-allowed-issues: 2147483647
+
+
+
+
+ # Upload the SARIF file generated in the previous step
+ - name: Upload SARIF results file
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: results.sarif
diff --git a/.github/workflows/create-upstream-pr.yml b/.github/workflows/create-upstream-pr.yml
new file mode 100644
index 00000000000..3540542cff2
--- /dev/null
+++ b/.github/workflows/create-upstream-pr.yml
@@ -0,0 +1,126 @@
+name: Create Upstream PR Branch
+
+on:
+ workflow_dispatch:
+ inputs:
+ issue_number:
+ description: 'Issue number (used for branch name: upstream-pr/issue-{number})'
+ required: true
+ type: string
+ source_branch:
+ description: 'Source branch containing the commits to cherry-pick'
+ required: true
+ type: string
+ commit_shas:
+ description: 'Comma-separated list of commit SHAs to cherry-pick onto upstream/master'
+ required: true
+ type: string
+ pr_title:
+ description: 'Title for the upstream PR (informational only)'
+ required: false
+ type: string
+
+jobs:
+ create-upstream-pr-branch:
+ name: Create Clean Upstream PR Branch
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Configure Git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Fetch upstream master
+ run: |
+ git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git
+ git fetch upstream master
+
+ - name: Create branch based on upstream/master
+ run: |
+ BRANCH_NAME="upstream-pr/issue-${{ inputs.issue_number }}"
+ git checkout -b "$BRANCH_NAME" upstream/master
+ echo "Created branch: $BRANCH_NAME"
+
+ - name: Cherry-pick specified commits
+ id: cherry_pick
+ run: |
+ BRANCH_NAME="upstream-pr/issue-${{ inputs.issue_number }}"
+ COMMIT_SHAS="${{ inputs.commit_shas }}"
+
+ # Convert comma-separated list to space-separated
+ SHAS=$(echo "$COMMIT_SHAS" | tr ',' ' ' | tr -s ' ')
+
+ echo "Cherry-picking commits: $SHAS"
+
+ SUCCESS=true
+ for SHA in $SHAS; do
+ SHA=$(echo "$SHA" | xargs) # trim whitespace
+ if [ -z "$SHA" ]; then
+ continue
+ fi
+ echo "Cherry-picking $SHA..."
+ if ! git cherry-pick "$SHA"; then
+ echo "Cherry-pick failed for $SHA"
+ git cherry-pick --abort 2>/dev/null || true
+ SUCCESS=false
+ break
+ fi
+ done
+
+ if [ "$SUCCESS" = "true" ]; then
+ echo "success=true" >> $GITHUB_OUTPUT
+ else
+ echo "success=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Verify branch contents
+ if: steps.cherry_pick.outputs.success == 'true'
+ run: |
+ echo "=== Commits on this branch (not in upstream/master) ==="
+ git log upstream/master..HEAD --oneline
+ echo ""
+ echo "=== Files changed vs upstream/master ==="
+ git diff upstream/master..HEAD --stat
+
+ - name: Force-push branch to fork
+ if: steps.cherry_pick.outputs.success == 'true'
+ run: |
+ BRANCH_NAME="upstream-pr/issue-${{ inputs.issue_number }}"
+ git push --force origin "$BRANCH_NAME"
+ echo "â
Branch '$BRANCH_NAME' pushed to fork"
+
+ - name: Output PR creation link
+ if: steps.cherry_pick.outputs.success == 'true'
+ run: |
+ BRANCH_NAME="upstream-pr/issue-${{ inputs.issue_number }}"
+ REPO_OWNER="${{ github.repository_owner }}"
+ REPO_NAME="${{ github.event.repository.name }}"
+ PR_LINK="https://github.com/eclipse-jdt/eclipse.jdt.ui/compare/master...${REPO_OWNER}:${REPO_NAME}:${BRANCH_NAME}"
+
+ echo ""
+ echo "=========================================="
+ echo "â
Branch ready for upstream PR!"
+ echo "=========================================="
+ echo ""
+ echo "Branch: $BRANCH_NAME"
+ if [ -n "${{ inputs.pr_title }}" ]; then
+ echo "PR Title: ${{ inputs.pr_title }}"
+ fi
+ echo ""
+ echo "Create PR at:"
+ echo "$PR_LINK"
+ echo ""
+
+ - name: Fail if cherry-pick failed
+ if: steps.cherry_pick.outputs.success == 'false'
+ run: |
+ echo "â Cherry-pick failed. Check the logs above for details."
+ exit 1
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 00000000000..fc0dbaf44dc
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,36 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: Java CI with Maven
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ workflow_run:
+ workflows: ["Sync Fork with Upstream"]
+ types: [completed]
+ branches: [master]
+
+jobs:
+ build:
+ if: >-
+ github.event_name != 'workflow_run' ||
+ github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # v5
+ with:
+ maven-version: 3.9.9
+ - name: Build with Maven
+ run: mvn -B package -Pbuild-individual-bundles --file pom.xml
diff --git a/.github/workflows/rebase-upstream.yml b/.github/workflows/rebase-upstream.yml
new file mode 100644
index 00000000000..d4aa9219044
--- /dev/null
+++ b/.github/workflows/rebase-upstream.yml
@@ -0,0 +1,230 @@
+name: Rebase Upstream
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request:
+ types: [opened]
+
+jobs:
+ add-rebase-button:
+ name: Add Rebase Instructions
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: Add comment with rebase instructions
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const repoUrl = context.payload.repository.html_url;
+ const workflowUrl = `${repoUrl}/actions/workflows/rebase-upstream.yml`;
+
+ const comment = `## đ Rebase onto Upstream
+
+ To rebase this PR onto the latest \`eclipse-jdt/eclipse.jdt.ui:master\`, comment:
+
+ \`\`\`
+ /rebase-upstream
+ \`\`\`
+
+ This will rebase your PR branch onto the upstream repository's master branch.
+
+ đ [View Workflow](${workflowUrl})`;
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: comment
+ });
+
+ rebase-upstream:
+ name: Rebase onto Upstream Master
+ if: |
+ github.event_name == 'issue_comment' &&
+ github.event.issue.pull_request &&
+ contains(github.event.comment.body, '/rebase-upstream')
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Check user permission
+ id: check
+ uses: actions/github-script@v8
+ with:
+ result-encoding: string
+ script: |
+ const authorAssociation = context.payload.comment.author_association;
+ const allowedRoles = ['OWNER', 'MEMBER', 'COLLABORATOR'];
+
+ if (allowedRoles.includes(authorAssociation)) {
+ return 'true';
+ } else {
+ return 'false';
+ }
+
+ - name: Add unauthorized reaction
+ if: steps.check.outputs.result != 'true'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '-1'
+ });
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: 'â Only repository collaborators can trigger the rebase.'
+ });
+
+ - name: Exit if unauthorized
+ if: steps.check.outputs.result != 'true'
+ run: exit 1
+
+ - name: Add rocket reaction
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'rocket'
+ });
+
+ - name: Get PR branch info
+ id: pr
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const { data: pr } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.issue.number
+ });
+
+ core.setOutput('head_ref', pr.head.ref);
+ core.setOutput('head_repo', pr.head.repo.full_name);
+ core.setOutput('head_clone_url', pr.head.repo.clone_url);
+
+ - name: Checkout PR branch
+ uses: actions/checkout@v4
+ with:
+ repository: ${{ steps.pr.outputs.head_repo }}
+ ref: ${{ steps.pr.outputs.head_ref }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ fetch-depth: 0
+
+ - name: Configure Git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Add upstream remote and rebase
+ id: rebase
+ run: |
+ git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git
+ git fetch upstream master
+
+ # Find the actual fork point with upstream
+ MERGE_BASE=$(git merge-base HEAD upstream/master)
+ echo "Merge base with upstream: $MERGE_BASE"
+
+ # Count actual commits to rebase (between merge-base and HEAD)
+ ACTUAL_COMMITS=$(git rev-list --count $MERGE_BASE..HEAD)
+ echo "Actual commits to rebase: $ACTUAL_COMMITS"
+
+ if [ "$ACTUAL_COMMITS" -eq 0 ]; then
+ echo "Error: No commits to rebase"
+ echo "success=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+
+ # Rebase using merge-base (correct: only replays commits after the fork point)
+ if git rebase --onto upstream/master $MERGE_BASE; then
+ echo "success=true" >> $GITHUB_OUTPUT
+ else
+ echo "success=false" >> $GITHUB_OUTPUT
+ git rebase --abort
+ fi
+
+ - name: Push rebased branch
+ if: steps.rebase.outputs.success == 'true'
+ run: |
+ git push --force-with-lease origin ${{ steps.pr.outputs.head_ref }}
+
+ - name: Add success comment
+ if: steps.rebase.outputs.success == 'true'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: 'â
Successfully rebased onto `eclipse-jdt/eclipse.jdt.ui:master`'
+ });
+
+ - name: Add failure comment
+ if: steps.rebase.outputs.success == 'false'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '-1'
+ });
+
+ const comment = `â Rebase failed due to conflicts.
+
+ To resolve manually:
+
+ \`\`\`bash
+ # Add upstream remote
+ git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git
+
+ # Fetch upstream
+ git fetch upstream master
+
+ # Count the number of commits in your PR (check GitHub PR page)
+ # For example, if your PR has 3 commits:
+ PR_COMMIT_COUNT=3
+
+ # Rebase only your PR commits onto upstream/master
+ git rebase --onto upstream/master HEAD~\${PR_COMMIT_COUNT}
+
+ # Resolve conflicts, then:
+ git add .
+ git rebase --continue
+
+ # Force push when done
+ git push --force-with-lease
+ \`\`\``;
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: comment
+ });
+
+ - name: Fail workflow if rebase failed
+ if: steps.rebase.outputs.success == 'false'
+ run: exit 1
diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml
new file mode 100644
index 00000000000..072ad1bf8f8
--- /dev/null
+++ b/.github/workflows/sync-upstream.yml
@@ -0,0 +1,347 @@
+name: Sync Fork with Upstream
+
+on:
+ schedule:
+ - cron: '0 3 * * *' # Daily at 3:00 AM UTC
+ workflow_dispatch:
+ issue_comment:
+ types: [created]
+
+jobs:
+ sync-scheduled:
+ name: Sync Fork (Scheduled)
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout fork master
+ uses: actions/checkout@v4
+ with:
+ ref: master
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Configure Git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Add upstream remote
+ run: |
+ git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git
+ git fetch upstream master
+
+ - name: Check if sync needed
+ id: check_sync
+ run: |
+ UPSTREAM_SHA=$(git rev-parse upstream/master)
+ LAST_COMMIT_MSG=$(git log -1 --pretty=%s)
+ if [ "$LAST_COMMIT_MSG" = "Fork-specific CI and workflow configurations" ]; then
+ CURRENT_PARENT=$(git rev-parse HEAD~1 2>/dev/null || echo "none")
+ if [ "$UPSTREAM_SHA" = "$CURRENT_PARENT" ]; then
+ echo "Upstream has not changed since last sync. Skipping."
+ echo "needs_sync=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+ fi
+ echo "Upstream has new commits. Syncing..."
+ echo "needs_sync=true" >> $GITHUB_OUTPUT
+
+ - name: Identify and backup fork-specific files
+ id: backup
+ if: steps.check_sync.outputs.needs_sync == 'true'
+ run: |
+ # Create temporary directory for fork-specific files
+ mkdir -p /tmp/fork-specific
+
+ # Read the list of fork-specific files from the manifest
+ MANIFEST=".github/fork-specific-files.txt"
+ if [ ! -f "$MANIFEST" ]; then
+ echo "Warning: $MANIFEST not found, no files to backup"
+ echo "has_fork_files=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+
+ HAS_FILES=false
+ while IFS= read -r file || [ -n "$file" ]; do
+ # Skip empty lines and comments
+ [ -z "$file" ] && continue
+ case "$file" in \#*) continue ;; esac
+ if [ -f "$file" ]; then
+ echo "Backing up fork-specific file: $file"
+ mkdir -p "/tmp/fork-specific/$(dirname "$file")"
+ cp "$file" "/tmp/fork-specific/$file"
+ HAS_FILES=true
+ fi
+ done < "$MANIFEST"
+
+ if [ "$HAS_FILES" = "true" ]; then
+ echo "has_fork_files=true" >> $GITHUB_OUTPUT
+ echo "Found fork-specific files:"
+ find /tmp/fork-specific -type f
+ else
+ echo "has_fork_files=false" >> $GITHUB_OUTPUT
+ echo "No fork-specific files found"
+ fi
+
+ - name: Reset to upstream master
+ if: steps.check_sync.outputs.needs_sync == 'true'
+ run: |
+ # Reset fork master to upstream master
+ git reset --hard upstream/master
+
+ - name: Restore fork-specific files
+ if: steps.check_sync.outputs.needs_sync == 'true' && steps.backup.outputs.has_fork_files == 'true'
+ run: |
+ # Restore all backed-up fork-specific files
+ if [ -d "/tmp/fork-specific" ]; then
+ cp -r /tmp/fork-specific/. ./
+ fi
+
+ # Stage all restored files
+ git add -A
+
+ - name: Commit fork-specific changes
+ if: steps.check_sync.outputs.needs_sync == 'true' && steps.backup.outputs.has_fork_files == 'true'
+ run: |
+ # Check if there are changes to commit
+ if ! git diff --cached --quiet; then
+ git commit -m "Fork-specific CI and workflow configurations"
+ echo "â
Fork-specific changes committed"
+ else
+ echo "âšī¸ No fork-specific changes to commit"
+ fi
+
+ - name: Push to fork master
+ if: steps.check_sync.outputs.needs_sync == 'true'
+ run: |
+ git push --force origin master
+ echo "â
Successfully synced with upstream"
+
+ sync-manual:
+ name: Sync Fork (Manual)
+ if: |
+ github.event_name == 'issue_comment' &&
+ contains(github.event.comment.body, '/sync-upstream')
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ issues: write
+ steps:
+ - name: Check user permission
+ id: check
+ uses: actions/github-script@v8
+ with:
+ result-encoding: string
+ script: |
+ const authorAssociation = context.payload.comment.author_association;
+ const allowedRoles = ['OWNER', 'MEMBER', 'COLLABORATOR'];
+
+ if (allowedRoles.includes(authorAssociation)) {
+ return 'true';
+ } else {
+ return 'false';
+ }
+
+ - name: Add unauthorized reaction
+ if: steps.check.outputs.result != 'true'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '-1'
+ });
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: 'â Only repository collaborators can trigger the sync.'
+ });
+
+ - name: Exit if unauthorized
+ if: steps.check.outputs.result != 'true'
+ run: exit 1
+
+ - name: Add rocket reaction
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'rocket'
+ });
+
+ - name: Checkout fork master
+ uses: actions/checkout@v4
+ with:
+ ref: master
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Configure Git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Add upstream remote
+ run: |
+ git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git
+ git fetch upstream master
+
+ - name: Check if sync needed
+ id: check_sync
+ run: |
+ UPSTREAM_SHA=$(git rev-parse upstream/master)
+ LAST_COMMIT_MSG=$(git log -1 --pretty=%s)
+ if [ "$LAST_COMMIT_MSG" = "Fork-specific CI and workflow configurations" ]; then
+ CURRENT_PARENT=$(git rev-parse HEAD~1 2>/dev/null || echo "none")
+ if [ "$UPSTREAM_SHA" = "$CURRENT_PARENT" ]; then
+ echo "Upstream has not changed since last sync. Skipping."
+ echo "needs_sync=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+ fi
+ echo "Upstream has new commits. Syncing..."
+ echo "needs_sync=true" >> $GITHUB_OUTPUT
+
+ - name: Identify and backup fork-specific files
+ id: backup
+ if: steps.check_sync.outputs.needs_sync == 'true'
+ run: |
+ # Create temporary directory for fork-specific files
+ mkdir -p /tmp/fork-specific
+
+ # Read the list of fork-specific files from the manifest
+ MANIFEST=".github/fork-specific-files.txt"
+ if [ ! -f "$MANIFEST" ]; then
+ echo "Warning: $MANIFEST not found, no files to backup"
+ echo "has_fork_files=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+
+ HAS_FILES=false
+ while IFS= read -r file || [ -n "$file" ]; do
+ # Skip empty lines and comments
+ [ -z "$file" ] && continue
+ case "$file" in \#*) continue ;; esac
+ if [ -f "$file" ]; then
+ echo "Backing up fork-specific file: $file"
+ mkdir -p "/tmp/fork-specific/$(dirname "$file")"
+ cp "$file" "/tmp/fork-specific/$file"
+ HAS_FILES=true
+ fi
+ done < "$MANIFEST"
+
+ if [ "$HAS_FILES" = "true" ]; then
+ echo "has_fork_files=true" >> $GITHUB_OUTPUT
+ echo "Found fork-specific files:"
+ find /tmp/fork-specific -type f
+ else
+ echo "has_fork_files=false" >> $GITHUB_OUTPUT
+ echo "No fork-specific files found"
+ fi
+
+ - name: Reset to upstream master
+ id: reset
+ if: steps.check_sync.outputs.needs_sync == 'true'
+ run: |
+ # Reset fork master to upstream master
+ git reset --hard upstream/master
+ echo "success=true" >> $GITHUB_OUTPUT
+
+ - name: Restore fork-specific files
+ if: steps.check_sync.outputs.needs_sync == 'true' && steps.backup.outputs.has_fork_files == 'true'
+ run: |
+ # Restore all backed-up fork-specific files
+ if [ -d "/tmp/fork-specific" ]; then
+ cp -r /tmp/fork-specific/. ./
+ fi
+
+ # Stage all restored files
+ git add -A
+
+ - name: Commit fork-specific changes
+ if: steps.check_sync.outputs.needs_sync == 'true' && steps.backup.outputs.has_fork_files == 'true'
+ run: |
+ # Check if there are changes to commit
+ if ! git diff --cached --quiet; then
+ git commit -m "Fork-specific CI and workflow configurations"
+ echo "â
Fork-specific changes committed"
+ else
+ echo "âšī¸ No fork-specific changes to commit"
+ fi
+
+ - name: Push to fork master
+ id: push
+ if: steps.check_sync.outputs.needs_sync == 'true'
+ run: |
+ if git push --force origin master; then
+ echo "success=true" >> $GITHUB_OUTPUT
+ else
+ echo "success=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Add already-up-to-date comment
+ if: steps.check_sync.outputs.needs_sync == 'false'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: 'âšī¸ Fork master is already up to date with `eclipse-jdt/eclipse.jdt.ui:master`. No sync needed.'
+ });
+
+ - name: Add success comment
+ if: steps.push.outputs.success == 'true'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '+1'
+ });
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: 'â
Successfully synced fork master with `eclipse-jdt/eclipse.jdt.ui:master`'
+ });
+
+ - name: Add failure comment
+ if: steps.push.outputs.success == 'false'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: '-1'
+ });
+
+ const comment = `â Sync failed. Please check the workflow logs for details.
+
+ [View workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId})`;
+
+ github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: comment
+ });
+
+ - name: Fail workflow if push failed
+ if: steps.push.outputs.success == 'false'
+ run: exit 1
diff --git a/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestCaseElement.java b/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestCaseElement.java
index 2f60518c2a9..65810a454b7 100644
--- a/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestCaseElement.java
+++ b/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestCaseElement.java
@@ -25,6 +25,21 @@ public class TestCaseElement extends TestElement implements ITestCaseElement {
private boolean fIgnored;
private boolean fIsDynamicTest;
+ /**
+ * @since 3.15
+ */
+ private boolean fIsParameterizedTest= false;
+
+ /**
+ * @since 3.15
+ */
+ private String fParameterSourceType= null;
+
+ /**
+ * @since 3.15
+ */
+ private String fParameterEnumType= null;
+
public TestCaseElement(TestSuiteElement parent, String id, String testName, String displayName, boolean isDynamicTest, String[] parameterTypes, String uniqueId) {
super(parent, id, testName, displayName, parameterTypes, uniqueId);
Assert.isNotNull(parent);
@@ -86,4 +101,67 @@ public String toString() {
public boolean isDynamicTest() {
return fIsDynamicTest;
}
+
+ /**
+ * Returns whether this test case is a parameterized test invocation.
+ *
+ * @return true if this is a parameterized test
+ * @since 3.15
+ */
+ public boolean isParameterizedTest() {
+ return fIsParameterizedTest;
+ }
+
+ /**
+ * Sets whether this test case is a parameterized test invocation.
+ *
+ * @param parameterizedTest true if this is a parameterized test
+ * @since 3.15
+ */
+ public void setParameterizedTest(boolean parameterizedTest) {
+ fIsParameterizedTest= parameterizedTest;
+ }
+
+ /**
+ * Returns the parameter source annotation type (e.g. "EnumSource", "ValueSource").
+ * Returns null if metadata has not been populated yet, or an empty string
+ * if metadata was populated but this is not a recognized parameterized source.
+ *
+ * @return the parameter source type, or null if not yet populated
+ * @since 3.15
+ */
+ public String getParameterSourceType() {
+ return fParameterSourceType;
+ }
+
+ /**
+ * Sets the parameter source annotation type.
+ *
+ * @param parameterSourceType the source annotation simple name (e.g. "EnumSource")
+ * @since 3.15
+ */
+ public void setParameterSourceType(String parameterSourceType) {
+ fParameterSourceType= parameterSourceType;
+ }
+
+ /**
+ * Returns the fully qualified name of the enum type used in {@code @EnumSource}, or
+ * null if not applicable.
+ *
+ * @return the enum type FQN, or null
+ * @since 3.15
+ */
+ public String getParameterEnumType() {
+ return fParameterEnumType;
+ }
+
+ /**
+ * Sets the fully qualified name of the enum type used in {@code @EnumSource}.
+ *
+ * @param parameterEnumType the enum type FQN
+ * @since 3.15
+ */
+ public void setParameterEnumType(String parameterEnumType) {
+ fParameterEnumType= parameterEnumType;
+ }
}
diff --git a/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestSuiteElement.java b/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestSuiteElement.java
index a939e1422ec..604798ef4e0 100644
--- a/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestSuiteElement.java
+++ b/org.eclipse.jdt.junit.core/src/org/eclipse/jdt/internal/junit/model/TestSuiteElement.java
@@ -50,6 +50,21 @@ public ITestElement[] getChildren() {
return fChildren.toArray(new ITestElement[fChildren.size()]);
}
+ /**
+ * Returns the single dynamic child of this suite if it has exactly one dynamic
+ * (parameterized / generated) child, or null if there are zero, two,
+ * or more children.
+ *
+ * @return the single dynamic child, or null
+ * @since 3.15
+ */
+ public TestElement getSingleDynamicChild() {
+ if (fChildren.size() == 1) {
+ return fChildren.get(0);
+ }
+ return null;
+ }
+
public void addChild(TestElement child) {
fChildren.add(child);
}
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/EnumSourceValidator.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/EnumSourceValidator.java
new file mode 100644
index 00000000000..6670c4d0127
--- /dev/null
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/EnumSourceValidator.java
@@ -0,0 +1,455 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Carsten Hammer and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Carsten Hammer using github copilot - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.junit.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.TextEdit;
+
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.dom.AST;
+import org.eclipse.jdt.core.dom.ASTParser;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.ArrayInitializer;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.IAnnotationBinding;
+import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
+import org.eclipse.jdt.core.dom.IMethodBinding;
+import org.eclipse.jdt.core.dom.ITypeBinding;
+import org.eclipse.jdt.core.dom.IVariableBinding;
+import org.eclipse.jdt.core.dom.MemberValuePair;
+import org.eclipse.jdt.core.dom.MethodDeclaration;
+import org.eclipse.jdt.core.dom.NormalAnnotation;
+import org.eclipse.jdt.core.dom.StringLiteral;
+import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
+import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
+import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
+
+import org.eclipse.jdt.ui.CodeStyleConfiguration;
+
+/**
+ * Utility class for validating and modifying {@code @EnumSource} annotations.
+ *
+ *
Provides methods to: + *
true if EXCLUDE mode is active
+ * @throws JavaModelException if there is an error accessing the Java model
+ */
+ public static boolean isExcludeMode(IMethod method) throws JavaModelException {
+ IAnnotationBinding binding= findEnumSourceBinding(method);
+ if (binding == null) {
+ return false;
+ }
+ return ENUM_SOURCE_MODE_EXCLUDE.equals(getMode(binding));
+ }
+
+ /**
+ * Returns the list of currently excluded enum constant names from {@code @EnumSource}.
+ * Returns an empty list if there are no exclusions or the method does not have
+ * an {@code @EnumSource} annotation in EXCLUDE mode.
+ *
+ * @param method the method to inspect
+ * @return list of excluded names (never null)
+ * @throws JavaModelException if there is an error accessing the Java model
+ */
+ public static Listnull if none is declared explicitly.
+ *
+ * @param method the method to inspect
+ * @return the enum type FQN, or null
+ * @throws JavaModelException if there is an error accessing the Java model
+ */
+ public static String getEnumTypeName(IMethod method) throws JavaModelException {
+ IAnnotationBinding binding= findEnumSourceBinding(method);
+ if (binding == null) {
+ return null;
+ }
+ for (IMemberValuePairBinding pair : binding.getDeclaredMemberValuePairs()) {
+ if (MEMBER_VALUE.equals(pair.getName())) {
+ Object val= pair.getValue();
+ if (val instanceof ITypeBinding) {
+ return ((ITypeBinding) val).getQualifiedName();
+ }
+ }
+ }
+ // try to infer from method parameter
+ try {
+ ICompilationUnit cu= method.getCompilationUnit();
+ if (cu != null) {
+ ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
+ parser.setSource(cu);
+ parser.setResolveBindings(true);
+ CompilationUnit astRoot= (CompilationUnit) parser.createAST(null);
+ final String[] result= { null };
+ astRoot.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(MethodDeclaration node) {
+ if (node.getName().getIdentifier().equals(method.getElementName())) {
+ IMethodBinding mb= node.resolveBinding();
+ if (mb != null && mb.getParameterTypes().length > 0) {
+ ITypeBinding paramType= mb.getParameterTypes()[0];
+ if (paramType != null && paramType.isEnum()) {
+ result[0]= paramType.getQualifiedName();
+ }
+ }
+ }
+ return false;
+ }
+ });
+ return result[0];
+ }
+ } catch (Exception e) {
+ // fall through
+ }
+ return null;
+ }
+
+ /**
+ * Removes all exclusions from {@code @EnumSource} by removing the {@code mode} and
+ * {@code names} attributes. Also removes the {@code Mode} import if it is no longer
+ * needed.
+ *
+ * @param method the method to modify
+ * @throws JavaModelException if there is an error accessing the Java model
+ */
+ public static void removeExcludeMode(IMethod method) throws JavaModelException {
+ ICompilationUnit cu= method.getCompilationUnit();
+ if (cu == null) {
+ return;
+ }
+
+ ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
+ parser.setSource(cu);
+ parser.setResolveBindings(true);
+ CompilationUnit astRoot= (CompilationUnit) parser.createAST(null);
+
+ AST ast= astRoot.getAST();
+ ASTRewrite rewrite= ASTRewrite.create(ast);
+ final boolean[] modified= { false };
+
+ astRoot.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(MethodDeclaration node) {
+ if (!node.getName().getIdentifier().equals(method.getElementName())) {
+ return false;
+ }
+ for (Object modifier : node.modifiers()) {
+ if (modifier instanceof NormalAnnotation) {
+ NormalAnnotation annotation= (NormalAnnotation) modifier;
+ IAnnotationBinding annBinding= annotation.resolveAnnotationBinding();
+ if (annBinding == null) continue;
+ ITypeBinding annType= annBinding.getAnnotationType();
+ if (annType == null || !ENUM_SOURCE_ANNOTATION.equals(annType.getQualifiedName())) continue;
+
+ // Collect the value pair (if any), remove mode and names
+ MemberValuePair valuePair= null;
+ List> values= annotation.values();
+ for (Object v : values) {
+ MemberValuePair mvp= (MemberValuePair) v;
+ if (MEMBER_VALUE.equals(mvp.getName().getIdentifier())) {
+ valuePair= mvp;
+ }
+ }
+
+ // Build replacement: NormalAnnotation with only value (if present)
+ NormalAnnotation newAnnotation= ast.newNormalAnnotation();
+ newAnnotation.setTypeName(ast.newName("EnumSource")); //$NON-NLS-1$
+ if (valuePair != null) {
+ MemberValuePair newValuePair= ast.newMemberValuePair();
+ newValuePair.setName(ast.newSimpleName(MEMBER_VALUE));
+ newValuePair.setValue((org.eclipse.jdt.core.dom.Expression) rewrite.createCopyTarget(valuePair.getValue()));
+ newAnnotation.values().add(newValuePair);
+ }
+
+ rewrite.replace(annotation, newAnnotation, null);
+ modified[0]= true;
+ break;
+ }
+ }
+ return false;
+ }
+ });
+
+ if (modified[0]) {
+ applyChangesWithModeImportCleanup(cu, astRoot, rewrite);
+ }
+ }
+
+ /**
+ * Removes a single enum constant name from the {@code @EnumSource} exclusion list.
+ * If the exclusion list becomes empty after the removal, removes both {@code mode} and
+ * {@code names} attributes entirely.
+ *
+ * @param method the method to modify
+ * @param enumValueName the enum constant name to re-include
+ * @throws JavaModelException if there is an error accessing the Java model
+ */
+ public static void removeValueFromExclusion(IMethod method, String enumValueName) throws JavaModelException {
+ ICompilationUnit cu= method.getCompilationUnit();
+ if (cu == null) {
+ return;
+ }
+
+ List
+ * JUnit 5 default format is {@code "[N] CONSTANT_NAME"}. This method strips
+ * the leading {@code "[N] "} prefix. If the display name already looks like a plain
+ * constant name (no prefix), it is returned as-is.
+ *
+ * @param displayName the display name of the test case element
+ * @return the extracted enum constant name
+ */
+ public static String extractEnumConstantFromDisplayName(String displayName) {
+ if (displayName == null) {
+ return null;
+ }
+ // Strip "[N] " prefix (JUnit default parameterized name format)
+ String stripped= displayName.replaceFirst(DISPLAY_NAME_INDEX_PREFIX_REGEX, "").trim(); //$NON-NLS-1$
+ return stripped.isEmpty() ? displayName : stripped;
+ }
+
+ /**
+ * Returns whether the given method has an {@code @EnumSource} annotation.
+ *
+ * @param method the method to check
+ * @return This action is enabled when:
+ * A warning dialog is shown if excluding the value would leave 0 or 1 values in the test.
+ *
+ * @since 3.15
+ */
+public class ExcludeParameterValueAction extends Action {
+
+ private TestCaseElement fTestCaseElement;
+ private String fEnumValueName;
+
+ public ExcludeParameterValueAction() {
+ super(JUnitMessages.ExcludeParameterValueAction_label);
+ }
+
+ /**
+ * Updates the action based on the currently selected test element.
+ *
+ * @param testCaseElement the selected test case element
+ */
+ public void update(TestCaseElement testCaseElement) {
+ fTestCaseElement= null;
+ fEnumValueName= null;
+ setEnabled(false);
+
+ if (testCaseElement == null) {
+ return;
+ }
+
+ // Populate metadata lazily
+ ParameterizedTestMetadataExtractor.populate(testCaseElement);
+
+ if (!"EnumSource".equals(testCaseElement.getParameterSourceType())) { //$NON-NLS-1$
+ return;
+ }
+
+ // Extract the enum constant name from the display name
+ String displayName= testCaseElement.getDisplayName();
+ if (displayName == null) {
+ displayName= testCaseElement.getTestMethodName();
+ }
+ String enumValue= EnumSourceValidator.extractEnumConstantFromDisplayName(displayName);
+ if (enumValue == null || enumValue.isEmpty()) {
+ return;
+ }
+
+ fTestCaseElement= testCaseElement;
+ fEnumValueName= enumValue;
+ setEnabled(true);
+ }
+
+ @Override
+ public void run() {
+ if (fTestCaseElement == null || fEnumValueName == null) {
+ return;
+ }
+
+ try {
+ TestSuiteElement parent= fTestCaseElement.getParent();
+ if (parent == null) {
+ return;
+ }
+
+ IMethod method= TestMethodFinder.findMethodForParameterizedTest(parent);
+ if (method == null) {
+ return;
+ }
+
+ // Warn if only 0 or 1 test invocations would remain
+ ITestElement[] siblings= parent.getChildren();
+ int remaining= siblings.length - 1;
+ if (remaining <= 1) {
+ String message= remaining == 0
+ ? Messages.format(JUnitMessages.ExcludeParameterValueAction_warning_noValues, fEnumValueName)
+ : Messages.format(JUnitMessages.ExcludeParameterValueAction_warning_oneValue, fEnumValueName);
+ boolean proceed= MessageDialog.openQuestion(
+ null,
+ JUnitMessages.ExcludeParameterValueAction_label,
+ message);
+ if (!proceed) {
+ return;
+ }
+ }
+
+ TestAnnotationModifier.excludeEnumValue(method, fEnumValueName);
+
+ // Open the editor at the method
+ try {
+ JavaUI.openInEditor(method);
+ } catch (Exception e) {
+ JUnitPlugin.log(e);
+ }
+
+ } catch (JavaModelException e) {
+ JUnitPlugin.log(e);
+ }
+ }
+}
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java
index 058451fa791..7b0b42d1555 100644
--- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java
@@ -54,6 +54,12 @@ public final class JUnitMessages extends NLS {
public static String EnableStackFilterAction_action_tooltip;
public static String DisableTestAction_label;
public static String DisableTestAction_enable_label;
+ public static String ExcludeParameterValueAction_label;
+ public static String ExcludeParameterValueAction_warning_noValues;
+ public static String ExcludeParameterValueAction_warning_oneValue;
+ public static String ReincludeAllEnumValuesAction_label;
+ public static String ReincludeEnumValueAction_label;
+ public static String TestViewer_reinclude_submenu_label;
public static String ExpandAllAction_text;
public static String ExpandAllAction_tooltip;
public static String CollapseAllAction_text;
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties
index 479b17218c0..d780c792131 100644
--- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties
@@ -37,6 +37,12 @@ EnableStackFilterAction_action_description=Filter the stack trace
EnableStackFilterAction_action_tooltip=Filter Stack Trace
DisableTestAction_label=Disable This Test
DisableTestAction_enable_label=Enable This Test
+ExcludeParameterValueAction_label=Exclude Enum Value from @EnumSource
+ExcludeParameterValueAction_warning_noValues=Excluding ''{0}'' would leave no values. Proceed anyway?
+ExcludeParameterValueAction_warning_oneValue=Excluding ''{0}'' would leave only one value. Proceed anyway?
+ReincludeAllEnumValuesAction_label=Re-include All Enum Values
+ReincludeEnumValueAction_label=Re-include ''{0}''
+TestViewer_reinclude_submenu_label=Re-include Excluded Enum Values
ScrollLockAction_action_label=Scroll Lock
ScrollLockAction_action_tooltip=Scroll Lock
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ParameterizedTestMetadataExtractor.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ParameterizedTestMetadataExtractor.java
new file mode 100644
index 00000000000..00f86b21189
--- /dev/null
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ParameterizedTestMetadataExtractor.java
@@ -0,0 +1,166 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Carsten Hammer and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Carsten Hammer using github copilot - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.junit.ui;
+
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.dom.AST;
+import org.eclipse.jdt.core.dom.ASTParser;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.IAnnotationBinding;
+import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
+import org.eclipse.jdt.core.dom.IMethodBinding;
+import org.eclipse.jdt.core.dom.ITypeBinding;
+import org.eclipse.jdt.core.dom.MethodDeclaration;
+
+import org.eclipse.jdt.internal.junit.model.TestCaseElement;
+import org.eclipse.jdt.internal.junit.model.TestSuiteElement;
+
+/**
+ * Utility class that populates parameterized-test metadata on {@link TestCaseElement} instances
+ * by parsing AST source.
+ *
+ * Metadata is populated lazily (on demand) and cached on the {@link TestCaseElement}.
+ * After the first call to {@link #populate(TestCaseElement)}, subsequent calls are no-ops.
+ *
+ * @since 3.15
+ */
+public class ParameterizedTestMetadataExtractor {
+
+ private static final String JUNIT5_PARAMETERIZED_TEST= "org.junit.jupiter.params.ParameterizedTest"; //$NON-NLS-1$
+ private static final String JUNIT5_ENUM_SOURCE= "org.junit.jupiter.params.provider.EnumSource"; //$NON-NLS-1$
+ private static final String JUNIT5_VALUE_SOURCE= "org.junit.jupiter.params.provider.ValueSource"; //$NON-NLS-1$
+ private static final String JUNIT5_METHOD_SOURCE= "org.junit.jupiter.params.provider.MethodSource"; //$NON-NLS-1$
+ private static final String JUNIT5_CSV_SOURCE= "org.junit.jupiter.params.provider.CsvSource"; //$NON-NLS-1$
+
+ /**
+ * Populates parameterized-test metadata on the given {@link TestCaseElement}.
+ *
+ * Metadata is only populated if it has not been populated yet
+ * ({@code getParameterSourceType() == null}).
+ *
+ * @param testCaseElement the test case element to populate
+ */
+ public static void populate(TestCaseElement testCaseElement) {
+ if (testCaseElement.getParameterSourceType() != null) {
+ return; // already populated
+ }
+
+ // Mark as "checked but not parameterized" by default
+ testCaseElement.setParameterSourceType(""); //$NON-NLS-1$
+
+ TestSuiteElement parent= testCaseElement.getParent();
+ if (parent == null) {
+ return;
+ }
+
+ IMethod method= TestMethodFinder.findMethodForParameterizedTest(parent);
+ if (method == null) {
+ return;
+ }
+
+ try {
+ ICompilationUnit cu= method.getCompilationUnit();
+ if (cu == null) {
+ return;
+ }
+
+ ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
+ parser.setSource(cu);
+ parser.setResolveBindings(true);
+ CompilationUnit astRoot= (CompilationUnit) parser.createAST(null);
+
+ final String[] sourceType= { null };
+ final String[] enumTypeName= { null };
+
+ astRoot.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(MethodDeclaration node) {
+ if (!node.getName().getIdentifier().equals(method.getElementName())) {
+ return false;
+ }
+
+ IMethodBinding mb= node.resolveBinding();
+ if (mb == null) {
+ return false;
+ }
+
+ boolean isParameterized= false;
+ for (IAnnotationBinding ann : mb.getAnnotations()) {
+ ITypeBinding annType= ann.getAnnotationType();
+ if (annType == null) continue;
+ String fqn= annType.getQualifiedName();
+ if (JUNIT5_PARAMETERIZED_TEST.equals(fqn)) {
+ isParameterized= true;
+ break;
+ }
+ }
+
+ if (!isParameterized) {
+ return false;
+ }
+
+ for (IAnnotationBinding ann : mb.getAnnotations()) {
+ ITypeBinding annType= ann.getAnnotationType();
+ if (annType == null) continue;
+ String fqn= annType.getQualifiedName();
+ if (JUNIT5_ENUM_SOURCE.equals(fqn)) {
+ sourceType[0]= "EnumSource"; //$NON-NLS-1$
+ // Try to get the declared enum type
+ for (IMemberValuePairBinding pair : ann.getDeclaredMemberValuePairs()) {
+ if ("value".equals(pair.getName())) { //$NON-NLS-1$
+ Object val= pair.getValue();
+ if (val instanceof ITypeBinding) {
+ enumTypeName[0]= ((ITypeBinding) val).getQualifiedName();
+ }
+ break;
+ }
+ }
+ // If not explicit, try first method parameter
+ if (enumTypeName[0] == null && mb.getParameterTypes().length > 0) {
+ ITypeBinding paramType= mb.getParameterTypes()[0];
+ if (paramType != null && paramType.isEnum()) {
+ enumTypeName[0]= paramType.getQualifiedName();
+ }
+ }
+ break;
+ } else if (JUNIT5_VALUE_SOURCE.equals(fqn)) {
+ sourceType[0]= "ValueSource"; //$NON-NLS-1$
+ } else if (JUNIT5_METHOD_SOURCE.equals(fqn)) {
+ sourceType[0]= "MethodSource"; //$NON-NLS-1$
+ } else if (JUNIT5_CSV_SOURCE.equals(fqn)) {
+ sourceType[0]= "CsvSource"; //$NON-NLS-1$
+ }
+ }
+
+ return false;
+ }
+ });
+
+ if (sourceType[0] != null) {
+ testCaseElement.setParameterizedTest(true);
+ testCaseElement.setParameterSourceType(sourceType[0]);
+ testCaseElement.setParameterEnumType(enumTypeName[0]);
+ }
+
+ } catch (Exception e) {
+ JUnitPlugin.log(e);
+ }
+ }
+
+ private ParameterizedTestMetadataExtractor() {
+ // Utility class - no instances
+ }
+}
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ReincludeAllEnumValuesAction.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ReincludeAllEnumValuesAction.java
new file mode 100644
index 00000000000..1eb2df280bf
--- /dev/null
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ReincludeAllEnumValuesAction.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Carsten Hammer and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Carsten Hammer using github copilot - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.junit.ui;
+
+import org.eclipse.jface.action.Action;
+
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.JavaModelException;
+
+import org.eclipse.jdt.ui.JavaUI;
+
+
+import org.eclipse.jdt.internal.junit.model.TestSuiteElement;
+
+/**
+ * Action that removes all exclusions from {@code @EnumSource} (removes the {@code mode} and
+ * {@code names} attributes), effectively re-including all enum values.
+ *
+ * @since 3.15
+ */
+public class ReincludeAllEnumValuesAction extends Action {
+
+ private IMethod fMethod;
+
+ public ReincludeAllEnumValuesAction() {
+ super(JUnitMessages.ReincludeAllEnumValuesAction_label);
+ }
+
+ /**
+ * Updates this action with the method to modify.
+ *
+ * @param method the test method carrying the {@code @EnumSource} annotation
+ * @param testSuiteElement the corresponding test suite element (unused, reserved for future use)
+ */
+ public void update(IMethod method, TestSuiteElement testSuiteElement) {
+ fMethod= method;
+ setEnabled(method != null);
+ }
+
+ @Override
+ public void run() {
+ if (fMethod == null) {
+ return;
+ }
+ try {
+ EnumSourceValidator.removeExcludeMode(fMethod);
+ try {
+ JavaUI.openInEditor(fMethod);
+ } catch (Exception e) {
+ JUnitPlugin.log(e);
+ }
+ } catch (JavaModelException e) {
+ JUnitPlugin.log(e);
+ }
+ }
+}
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ReincludeEnumValueAction.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ReincludeEnumValueAction.java
new file mode 100644
index 00000000000..b71113bf8dc
--- /dev/null
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/ReincludeEnumValueAction.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2025 Carsten Hammer and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Carsten Hammer using github copilot - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.junit.ui;
+
+import org.eclipse.jface.action.Action;
+
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.JavaModelException;
+
+import org.eclipse.jdt.ui.JavaUI;
+
+
+import org.eclipse.jdt.internal.junit.Messages;
+
+/**
+ * Action that re-includes a single excluded enum value from {@code @EnumSource}.
+ *
+ * Removes the given value from the {@code names} array. If the array becomes empty,
+ * the {@code mode} and {@code names} attributes are removed entirely.
+ *
+ * @since 3.15
+ */
+public class ReincludeEnumValueAction extends Action {
+
+ private final IMethod fMethod;
+ private final String fEnumValueName;
+
+ /**
+ * Creates a new action for re-including the given enum constant.
+ *
+ * @param method the test method to modify
+ * @param enumValueName the enum constant name to re-include
+ */
+ public ReincludeEnumValueAction(IMethod method, String enumValueName) {
+ super(Messages.format(JUnitMessages.ReincludeEnumValueAction_label, enumValueName));
+ fMethod= method;
+ fEnumValueName= enumValueName;
+ }
+
+ @Override
+ public void run() {
+ if (fMethod == null || fEnumValueName == null) {
+ return;
+ }
+ try {
+ EnumSourceValidator.removeValueFromExclusion(fMethod, fEnumValueName);
+ try {
+ JavaUI.openInEditor(fMethod);
+ } catch (Exception e) {
+ JUnitPlugin.log(e);
+ }
+ } catch (JavaModelException e) {
+ JUnitPlugin.log(e);
+ }
+ }
+}
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestAnnotationModifier.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestAnnotationModifier.java
index e5989240147..d5f19bea85b 100644
--- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestAnnotationModifier.java
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/TestAnnotationModifier.java
@@ -25,11 +25,19 @@
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
+import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
+import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
+import org.eclipse.jdt.core.dom.IVariableBinding;
+import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
+import org.eclipse.jdt.core.dom.NormalAnnotation;
+import org.eclipse.jdt.core.dom.QualifiedName;
+import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
+import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
@@ -63,6 +71,9 @@ public class TestAnnotationModifier {
private static final String JUNIT5_TEST_FACTORY_ANNOTATION= "org.junit.jupiter.api.TestFactory"; //$NON-NLS-1$
private static final String JUNIT5_TEST_TEMPLATE_ANNOTATION= "org.junit.jupiter.api.TestTemplate"; //$NON-NLS-1$
+ private static final String JUNIT5_ENUM_SOURCE_ANNOTATION= "org.junit.jupiter.params.provider.EnumSource"; //$NON-NLS-1$
+ private static final String JUNIT5_ENUM_SOURCE_MODE_FQN= "org.junit.jupiter.params.provider.EnumSource.Mode"; //$NON-NLS-1$
+
/**
* Add @Disabled (JUnit 5) or @Ignore (JUnit 4) annotation to a method.
*
@@ -300,6 +311,164 @@ private static void applyChanges(ICompilationUnit cu, CompilationUnit astRoot, A
}
}
+ /**
+ * Adds an enum constant name to the {@code @EnumSource} exclusion list on the given method.
+ *
+ * If the annotation currently has no mode (defaults to {@code INCLUDE}), the mode is
+ * changed to {@code EXCLUDE} and the value is added to the names list. If the annotation
+ * already uses {@code EXCLUDE} mode, the value is appended to the existing names.
+ *
+ * @param method the test method that carries the {@code @EnumSource} annotation
+ * @param enumValueName the enum constant name to exclude
+ * @throws JavaModelException if there is an error accessing the Java model
+ * @since 3.15
+ */
+ public static void excludeEnumValue(IMethod method, String enumValueName) throws JavaModelException {
+ ICompilationUnit cu= method.getCompilationUnit();
+ if (cu == null) {
+ return;
+ }
+
+ ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
+ parser.setSource(cu);
+ parser.setResolveBindings(true);
+ CompilationUnit astRoot= (CompilationUnit) parser.createAST(null);
+
+ AST ast= astRoot.getAST();
+ ASTRewrite rewrite= ASTRewrite.create(ast);
+ final boolean[] modified= { false };
+
+ astRoot.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(MethodDeclaration node) {
+ if (!node.getName().getIdentifier().equals(method.getElementName())) {
+ return false;
+ }
+ for (Object modifier : node.modifiers()) {
+ if (modifier instanceof Annotation) {
+ Annotation annotation= (Annotation) modifier;
+ IAnnotationBinding annBinding= annotation.resolveAnnotationBinding();
+ if (annBinding == null) continue;
+ ITypeBinding annType= annBinding.getAnnotationType();
+ if (annType == null || !JUNIT5_ENUM_SOURCE_ANNOTATION.equals(annType.getQualifiedName())) continue;
+
+ modifyEnumSourceInMethod(ast, rewrite, annotation, annBinding, enumValueName);
+ modified[0]= true;
+ break;
+ }
+ }
+ return false;
+ }
+ });
+
+ if (modified[0]) {
+ applyChanges(cu, astRoot, rewrite, JUNIT5_ENUM_SOURCE_MODE_FQN);
+ }
+ }
+
+ /**
+ * Replaces the {@code @EnumSource} annotation node with a new one that includes the given
+ * value in the {@code EXCLUDE} names list.
+ */
+ private static void modifyEnumSourceInMethod(AST ast, ASTRewrite rewrite, Annotation annotation,
+ IAnnotationBinding annBinding, String enumValueName) {
+
+ // Read current state from binding
+ String currentMode= null;
+ List Validates {@link TestAnnotationModifier#excludeEnumValue(IMethod, String)},
+ * {@link EnumSourceValidator#removeExcludeMode(IMethod)}, and
+ * {@link EnumSourceValidator#removeValueFromExclusion(IMethod, String)}.
+ */
+public class ExcludeParameterValueDisplayNameTest {
+
+ @RegisterExtension
+ public ProjectTestSetup projectSetup= new Java1d8ProjectTestSetup();
+
+ private IJavaProject fJProject;
+ private IPackageFragmentRoot fSourceFolder;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ fJProject= projectSetup.getProject();
+ fSourceFolder= JavaProjectHelper.addSourceContainer(fJProject, "src");
+
+ JavaProjectHelper.addRTJar(fJProject);
+ IClasspathEntry cpe= JavaCore.newContainerEntry(JUnitCore.JUNIT5_CONTAINER_PATH);
+ JavaProjectHelper.addToClasspath(fJProject, cpe);
+ JavaProjectHelper.set18CompilerOptions(fJProject);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ JavaProjectHelper.clear(fJProject, projectSetup.getDefaultClasspath());
+ }
+
+ @Test
+ public void testExcludeEnumValue_AddsExcludeMode() throws Exception {
+ IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null);
+ String original= """
+ package test1;
+
+ import org.junit.jupiter.params.ParameterizedTest;
+ import org.junit.jupiter.params.provider.EnumSource;
+
+ public class MyTest {
+ enum Color { RED, GREEN, BLUE }
+
+ @ParameterizedTest
+ @EnumSource(Color.class)
+ public void testWithEnum(Color color) {
+ }
+ }
+ """;
+
+ ICompilationUnit cu= pack1.createCompilationUnit("MyTest.java", original, false, null);
+ IType type= cu.getType("MyTest");
+ IMethod method= type.getMethod("testWithEnum", new String[] { "QColor;" });
+
+ assertFalse(EnumSourceValidator.isExcludeMode(method), "Initially should not be in EXCLUDE mode");
+
+ TestAnnotationModifier.excludeEnumValue(method, "RED");
+
+ String source= cu.getSource();
+ assertTrue(source.contains("mode"), "Should add mode attribute");
+ assertTrue(source.contains("EXCLUDE"), "Should set EXCLUDE mode");
+ assertTrue(source.contains("\"RED\""), "Should add RED to names");
+ assertTrue(EnumSourceValidator.isExcludeMode(method), "Should now be in EXCLUDE mode");
+ }
+
+ @Test
+ public void testExcludeEnumValue_AppendsToExistingExclusions() throws Exception {
+ IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null);
+ String original= """
+ package test1;
+
+ import org.junit.jupiter.params.ParameterizedTest;
+ import org.junit.jupiter.params.provider.EnumSource;
+ import org.junit.jupiter.params.provider.EnumSource.Mode;
+
+ public class MyTest {
+ enum Color { RED, GREEN, BLUE }
+
+ @ParameterizedTest
+ @EnumSource(value = Color.class, mode = Mode.EXCLUDE, names = {"RED"})
+ public void testWithEnum(Color color) {
+ }
+ }
+ """;
+
+ ICompilationUnit cu= pack1.createCompilationUnit("MyTest.java", original, false, null);
+ IType type= cu.getType("MyTest");
+ IMethod method= type.getMethod("testWithEnum", new String[] { "QColor;" });
+
+ assertTrue(EnumSourceValidator.isExcludeMode(method), "Initially should be in EXCLUDE mode");
+ Listtrue if the method declares {@code @EnumSource}
+ * @throws JavaModelException if there is an error accessing the Java model
+ */
+ public static boolean hasEnumSource(IMethod method) throws JavaModelException {
+ return findEnumSourceBinding(method) != null;
+ }
+
+ /**
+ * Returns a copy of the excluded names after removing the given value.
+ * Useful for validation before committing the change.
+ *
+ * @param excludedNames current list of excluded names
+ * @param valueToRemove the value that would be re-included
+ * @return new list without the removed value
+ */
+ public static List
+ *
+ *
+ *