Repository: SonarSource/sonar-java
Pull request #5141: SONARJAVA-5482 stop raising issues if variable or identifier symbol is unknown
Author: login: erwan-serandour
Created: 2025-05-09T09:43:31Z | Merged: 2025-05-14T07:20:28Z

[SONARJAVA-5482](https://sonarsource.atlassian.net/browse/SONARJAVA-5482)

Part of 
<!-- 
  Only for standalone PRs without Jira issue in the PR title: 
    * Replace this comment with Epic ID to create a new Task in Jira
    * Replace this comment with Issue ID to create a new Sub-Task in Jira
    * Ignore or delete this note to create a new Task in Jira without a parent 
-->


[SONARJAVA-5482]: https://sonarsource.atlassian.net/browse/SONARJAVA-5482?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S1854.json b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S1854.json
index 5648d63ea63..91271fca57f 100644
--- a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S1854.json
+++ b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S1854.json
@@ -9,14 +9,6 @@
 "org.eclipse.jetty:jetty-project:jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/WebSocketUriMappingTest.java": [
 91
 ],
-"org.eclipse.jetty:jetty-project:jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java": [
-92,
-94,
-97,
-105,
-147,
-272
-],
 "org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java": [
 861
 ],
diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S1854.json b/its/ruling/src/test/resources/eclipse-jetty/java-S1854.json
index 239802a68d3..2532262625f 100644
--- a/its/ruling/src/test/resources/eclipse-jetty/java-S1854.json
+++ b/its/ruling/src/test/resources/eclipse-jetty/java-S1854.json
@@ -9,14 +9,6 @@
 "org.eclipse.jetty:jetty-project:jetty-http/src/test/java/org/eclipse/jetty/http/pathmap/WebSocketUriMappingTest.java": [
 91
 ],
-"org.eclipse.jetty:jetty-project:jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java": [
-92,
-94,
-97,
-105,
-147,
-272
-],
 "org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java": [
 861
 ],
diff --git a/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckBrokenSemantic.java b/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckBrokenSemantic.java
new file mode 100644
index 00000000000..daec3b7e7d3
--- /dev/null
+++ b/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckBrokenSemantic.java
@@ -0,0 +1,9 @@
+package checks;
+
+public class DeadStoreCheckBrokenSemantic {
+
+  public void singleAssignment() {
+    int a = 42;
+    System.out.println(a);
+  }
+}
diff --git a/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckSample.java
index 5edb93d2eec..42eb53a8a84 100644
--- a/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckSample.java
@@ -10,6 +10,10 @@ class DeadStoreCheckSample {
 
   int var;
 
+  void singleAssignment(){
+    int z = 10; // Noncompliant
+  }
+
   int foo(int u) {
     int x = 0;// Compliant - default value
     x = 3; // Noncompliant
diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/CheckVerifier.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/CheckVerifier.java
index 404ebad67db..6a953758e36 100644
--- a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/CheckVerifier.java
+++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/CheckVerifier.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.util.Collection;
+import java.util.function.Consumer;
 import javax.annotation.Nullable;
 import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.sensor.cache.ReadCache;
@@ -25,6 +26,7 @@
 import org.sonar.java.checks.verifier.internal.InternalCheckVerifier;
 import org.sonar.java.checks.verifier.internal.JavaCheckVerifier;
 import org.sonar.plugins.java.api.JavaFileScanner;
+import org.sonar.plugins.java.api.tree.CompilationUnitTree;
 
 /**
  * This interface defines how to use checks (rules) verifiers. Its goal is to provide all the required information
@@ -235,7 +237,21 @@ static CheckVerifier newInternalVerifier() {
    * @param rootDirectory The path of the project root working directory
    * @return the verifier configured with the project root working directory.
    */
-  CheckVerifier withProjectLevelWorkDir(String rootDirectory);
+  default CheckVerifier withProjectLevelWorkDir(String rootDirectory) {
+    throw new UnsupportedOperationException("Method not implemented, feel free to implement.");
+  }
+
+  /**
+   * Allows to modify the compilation unit tree after parsing.
+   * This is useful for adding or modifying nodes in the tree for testing purposes.
+   * There is at most one modifier per {@link CheckVerifier}.
+   *
+   * @param compilationUnitModifier the modifier to apply to the compilation unit tree
+   * @return the verifier configured with the compilation unit modifier.
+   */
+  default CheckVerifier withCompilationUnitModifier(Consumer<CompilationUnitTree> compilationUnitModifier) {
+    throw new UnsupportedOperationException("Method not implemented, feel free to implement.");
+  }
 
   /**
    * Verifies that all the expected issues are correctly raised by the rule(s),
diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java
index 6da4e743f78..e87e4d8c3cc 100644
--- a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java
+++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifier.java
@@ -248,11 +248,6 @@ public CheckVerifier withCache(@Nullable ReadCache readCache, @Nullable WriteCac
     return this;
   }
 
-  @Override
-  public CheckVerifier withProjectLevelWorkDir(String rootDirectory) {
-    throw new RuntimeException("Method not implemented, feel free to implement.");
-  }
-
   @Override
   public void verifyIssues() {
     requiresNonNull(checks, CHECK_OR_CHECKS);
diff --git a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java
index 84b9a84f177..9cd2e0f56eb 100644
--- a/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java
+++ b/java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java
@@ -28,6 +28,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Consumer;
 import javax.annotation.Nullable;
 import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.sensor.cache.ReadCache;
@@ -50,6 +51,7 @@
 import org.sonar.plugins.java.api.JavaFileScanner;
 import org.sonar.plugins.java.api.JavaVersion;
 import org.sonar.plugins.java.api.caching.CacheContext;
+import org.sonar.plugins.java.api.tree.CompilationUnitTree;
 import org.sonarsource.analyzer.commons.checks.verifier.MultiFileVerifier;
 import org.sonarsource.analyzer.commons.checks.verifier.quickfix.QuickFix;
 import org.sonarsource.analyzer.commons.checks.verifier.quickfix.TextEdit;
@@ -90,6 +92,7 @@ public static JavaCheckVerifier newInstance() {
   private List<InputFile> files = null;
   private boolean withoutSemantic = false;
   private boolean isCacheEnabled = false;
+  private Consumer<CompilationUnitTree> compilationUnitModifier = unused -> {};
 
   @VisibleForTesting
   CacheContext cacheContext = null;
@@ -124,7 +127,7 @@ private MultiFileVerifier createVerifier() {
       visitorsBridge.setCacheContext(cacheContext);
       filesToParse = astScanner.scanWithoutParsing(files).get(false);
     }
-    astScanner.scan(filesToParse);
+    astScanner.scanForTesting(filesToParse, compilationUnitModifier);
 
     addComments(verifier, commentLinesVisitor);
 
@@ -335,6 +338,12 @@ public CheckVerifier withProjectLevelWorkDir(String rootDirectory) {
     return this;
   }
 
+  @Override
+  public CheckVerifier withCompilationUnitModifier(Consumer<CompilationUnitTree> compilationUnitModifier) {
+    this.compilationUnitModifier = compilationUnitModifier;
+    return this;
+  }
+
   @Override
   public void verifyIssues() {
     requiresNonNull(checks, CHECK_OR_CHECKS);
diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java
index 2ad0395ff8b..0368e202679 100644
--- a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java
+++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java
@@ -524,7 +524,7 @@ void context_root_working_directory_not_supported() {
     });
 
     assertThat(e)
-      .isInstanceOf(RuntimeException.class)
+      .isInstanceOf(UnsupportedOperationException.class)
       .hasMessage("Method not implemented, feel free to implement.");
   }
 
@@ -1053,4 +1053,21 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_not_supported() {
+
+    InternalCheckVerifier checkVerifier = InternalCheckVerifier.newInstance()
+      .onFile(TEST_FILE);
+
+    Throwable e = catchThrowable(() -> {
+      checkVerifier
+        .withCompilationUnitModifier(tree -> {
+        });
+    });
+
+    assertThat(e)
+      .isInstanceOf(UnsupportedOperationException.class)
+      .hasMessage("Method not implemented, feel free to implement.");
+  }
+
 }
diff --git a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
index e9612af0386..0e9ee95b8a1 100644
--- a/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
+++ b/java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
@@ -20,6 +20,8 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 import org.junit.jupiter.api.Test;
 import org.sonar.api.batch.fs.InputFile;
@@ -33,7 +35,11 @@
 import org.sonar.java.checks.verifier.CheckVerifier;
 import org.sonar.java.checks.verifier.internal.CheckVerifierTestUtils.IssueWithQuickFix;
 import org.sonar.java.checks.verifier.internal.CheckVerifierTestUtils.NoEffectEndOfAnalysisCheck;
+import org.sonar.java.model.InternalSyntaxToken;
 import org.sonar.java.model.JavaVersionImpl;
+import org.sonar.java.model.declaration.ClassTreeImpl;
+import org.sonar.java.model.declaration.ModifiersTreeImpl;
+import org.sonar.java.model.expression.IdentifierTreeImpl;
 import org.sonar.java.reporting.AnalyzerMessage;
 import org.sonar.java.reporting.JavaQuickFix;
 import org.sonar.java.reporting.JavaTextEdit;
@@ -42,6 +48,8 @@
 import org.sonar.plugins.java.api.caching.CacheContext;
 import org.sonar.plugins.java.api.caching.JavaReadCache;
 import org.sonar.plugins.java.api.caching.JavaWriteCache;
+import org.sonar.plugins.java.api.tree.CompilationUnitTree;
+import org.sonar.plugins.java.api.tree.IdentifierTree;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
@@ -307,4 +315,28 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().name()).isEqualTo("Modified");
+      }
+    };
+    
+    JavaCheckVerifier.newInstance()
+      .onFile(TEST_FILE)
+      .withCheck(check)
+      .withCompilationUnitModifier(modifier)
+      .verifyNoIssues();
+  }
+
 }
diff --git a/java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java b/java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
index c77695acdc4..15f8262741f 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
@@ -181,6 +181,7 @@ private void handleVariable(Set<Symbol> out, VariableTree localVar) {
     Symbol symbol = localVar.symbol();
     ExpressionTree initializer = localVar.initializer();
     if (initializer != null
+      && !localVar.symbol().isUnknown()
       && !isUsualDefaultValue(initializer)
       && !out.contains(symbol)
       && !UNRESOLVED_IDENTIFIERS_VISITOR.isUnresolved(symbol.name())) {
diff --git a/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
index 94c533371b1..2aa6fa4fe73 100644
--- a/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
+++ b/java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
@@ -19,6 +19,9 @@
 import org.junit.jupiter.api.Test;
 import org.sonar.java.checks.verifier.CheckVerifier;
 import org.sonar.java.checks.verifier.TestUtils;
+import org.sonar.java.model.declaration.VariableTreeImpl;
+import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
+import org.sonar.plugins.java.api.tree.VariableTree;
 
 class DeadStoreCheckTest {
 
@@ -30,6 +33,16 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckBrokenSemantic.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
   @Test
   void test_non_compiling() {
     CheckVerifier.newVerifier()
@@ -38,4 +51,14 @@ void test_non_compiling() {
       .verifyIssues();
   }
 
+  private static class EraseSymbols extends BaseTreeVisitor {
+
+    @Override
+    public void visitVariable(VariableTree tree) {
+      if (tree instanceof VariableTreeImpl varImpl) {
+        varImpl.variableBinding = null;
+      }
+      super.visitVariable(tree);
+    }
+  }
 }
diff --git a/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java b/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
index f9b5a641318..c5de322bc92 100644
--- a/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
+++ b/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
@@ -42,6 +42,7 @@
 import org.sonar.java.model.JavaTree;
 import org.sonar.java.model.VisitorsBridge;
 import org.sonar.plugins.java.api.JavaVersion;
+import org.sonar.plugins.java.api.tree.CompilationUnitTree;
 
 public class JavaAstScanner {
   private static final Logger LOG = LoggerFactory.getLogger(JavaAstScanner.class);
@@ -78,6 +79,22 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {});
+  }
+
+  /**
+   * Scan the given files and modify
+   *
+   * @param inputFiles The list of files to analyze
+   * @param modifyCompilationUnit allow you to modify the ast before running the analysis on it, for example to remove semantic information
+   */
+  @VisibleForTesting
+  public void scanForTesting(Iterable<? extends InputFile> inputFiles, Consumer<CompilationUnitTree> modifyCompilationUnit) {
+    scan(inputFiles, modifyCompilationUnit);
+  }
+
+
+  private void scan(Iterable<? extends InputFile> inputFiles, Consumer<CompilationUnitTree> modifyCompilationUnit) {
     List<? extends InputFile> filesNames = filterModuleInfo(inputFiles).toList();
     AnalysisProgress analysisProgress = new AnalysisProgress(filesNames.size());
     try {
@@ -90,7 +107,8 @@ public void scan(Iterable<? extends InputFile> inputFiles) {
           analysisProgress,
           (i, r) -> simpleScan(i, r,
             // Due to a bug in ECJ, JAR files remain locked after the analysis on Windows, we unlock them manually. See SONARJAVA-3609.
-            JavaAstScanner::cleanUpAst));
+            JavaAstScanner::cleanUpAst,
+            modifyCompilationUnit));
     } finally {
       endOfAnalysis();
     }
@@ -125,9 +143,16 @@ private boolean analysisCancelled() {
   }
 
   public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consumer<JavaTree.CompilationUnitTreeImpl> cleanUp) {
+    simpleScan(inputFile, result, cleanUp, compilationUnitTree -> {});
+  }
+
+  // modifyCompilationUnit should be used for testing.
+  public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consumer<JavaTree.CompilationUnitTreeImpl> cleanUp,
+    Consumer<CompilationUnitTree> modifyCompilationUnit) {
     visitor.setCurrentFile(inputFile);
     try {
       JavaTree.CompilationUnitTreeImpl ast = result.get();
+      modifyCompilationUnit.accept(ast);
       visitor.visitFile(ast, sonarComponents != null && sonarComponents.fileCanBeSkipped(inputFile));
       String path = inputFile.toString();
       collectUndefinedTypes(path, ast.sema.undefinedTypes());
diff --git a/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java b/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
index 21582c21954..f75ae6a2e6f 100644
--- a/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
+++ b/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
@@ -53,6 +53,7 @@
 import org.sonar.java.model.VisitorsBridge;
 import org.sonar.java.model.declaration.ClassTreeImpl;
 import org.sonar.java.model.declaration.MethodTreeImpl;
+import org.sonar.java.model.expression.IdentifierTreeImpl;
 import org.sonar.java.model.statement.BlockTreeImpl;
 import org.sonar.java.notchecks.VisitorNotInChecksPackage;
 import org.sonar.java.testing.ThreadLocalLogTester;
@@ -60,6 +61,7 @@
 import org.sonar.plugins.java.api.JavaFileScannerContext;
 import org.sonar.plugins.java.api.ModuleScannerContext;
 import org.sonar.plugins.java.api.internal.EndOfAnalysis;
+import org.sonar.plugins.java.api.tree.CompilationUnitTree;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.fail;
@@ -447,6 +449,36 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+    InputFile trivialCompilationUnit = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().symbol().isUnknown()).isTrue();
+      }
+    };
+
+    VisitorsBridge visitorsBridge = new VisitorsBridge(
+      List.of(check),
+      Collections.emptyList(),
+      null);
+
+    JavaAstScanner scanner = new JavaAstScanner(null);
+    scanner.setVisitorBridge(visitorsBridge);
+    scanner.scanForTesting(Collections.singletonList(trivialCompilationUnit), compilationUnit -> {
+      var clazz = (ClassTreeImpl) ((JavaTree.CompilationUnitTreeImpl) compilationUnit).types().get(0);
+      assertThat(clazz.simpleName().name()).isEqualTo("C");
+      assertThat(clazz.simpleName().symbol().isUnknown()).isFalse();
+      IdentifierTreeImpl identifierTree = (IdentifierTreeImpl) clazz.simpleName();
+      identifierTree.binding = null;
+    });
+
+  }
+
   private void scanSingleFile(InputFile file, boolean failOnException) {
     scanFilesWithVisitors(Collections.singletonList(file), Collections.emptyList(), -1, failOnException, false);
   }
diff --git a/java-symbolic-execution/java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/SECheckVerifier.java b/java-symbolic-execution/java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/SECheckVerifier.java
index 26f0991b5de..00e45a514d5 100644
--- a/java-symbolic-execution/java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/SECheckVerifier.java
+++ b/java-symbolic-execution/java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/SECheckVerifier.java
@@ -133,11 +133,6 @@ public CheckVerifier withCache(@Nullable ReadCache readCache, @Nullable WriteCac
     return this.withCache(readCache, writeCache);
   }
 
-  @Override
-  public CheckVerifier withProjectLevelWorkDir(String rootDirectory) {
-    throw new RuntimeException("Method not implemented, feel free to implement.");
-  }
-
   @Override
   public void verifyIssues() {
     checkVerifier.verifyIssues();


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR addresses SONARJAVA-5482 by preventing issues from being raised when a variable or identifier symbol is unknown. It also introduces a new API method to modify the compilation unit tree for testing purposes.
- Added conditions in DeadStoreCheck to skip processing unknown symbols.
- Introduced the withCompilationUnitModifier method to allow test-time modifications of the compilation unit.
- Updated exception types in unimplemented methods to use UnsupportedOperationException.

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/SECheckVerifier.java | Updated exception type and added a placeholder for compilation unit modification. |
| java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java | Overloaded scan and simpleScan methods with an additional Consumer parameter for compilation unit modifications. |
| java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java | Added a new test for handling unknown variable symbols and included additional imports. |
| java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java | Modified conditions to ignore unknown symbols when processing assignments and variable declarations. |
| java-checks-testkit/* | Updated tests and verifier implementations to support the new compilation unit modifier API and standardized exception types. |
| java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/CheckVerifier.java | Extended the CheckVerifier interface with a new method for compilation unit modification. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java:154**
* The added condition '!symbol.isUnknown()' prevents further processing of unknown symbols; please verify that it does not inadvertently filter out valid local variables. Consider adding explicit parentheses to clarify the boolean logic.
```
if ( !symbol.isUnknown() && symbol.isLocalVariable()
```
**java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java:52**
* [nitpick] Consider renaming the inner class 'EraseSymbols' to 'SymbolEraser' for improved clarity on its purpose in modifying the AST.
```
if (tree instanceof VariableTreeImpl varImpl) {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T09:50:30Z
user: login: tomasz-tylenda-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:29:05Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:18:50Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:19:31Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:31:00Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:32:09Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:34:31Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:35:12Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:04:40Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:15:07Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:17:25Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:17:47Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:17:57Z
user: login: tomasz-tylenda-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:15:38Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:21:45Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:22:00Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:25:03Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:25:12Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:25:53Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:50:22Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:59:07Z
user: login: tomasz-tylenda-sonarsource
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:36:02Z

Review comments:
diff_hunk: @@ -30,10 +35,40 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
+
+    @Override
+    public void visitVariable(VariableTree tree) {
+      if (tree instanceof VariableTreeImpl varImpl) {
+        varImpl.variableBinding = null;
+      }
+      super.visitVariable(tree);
+    }
+
+    @Override
+    public void visitIdentifier(IdentifierTree tree) {
+      if (tree instanceof IdentifierTreeImpl id) {
+        id.binding = null;
+      }
+      super.visitIdentifier(tree);
+    }
+  }
+
   @Test
   void test_non_compiling() {
     CheckVerifier.newVerifier()
       .onFile(TestUtils.nonCompilingTestSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withoutSemantic()
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: tomasz-tylenda-sonarsource
body: Why is this needed? Should we test both with and without semantics?
created_at: 2025-05-12T12:15:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,10 +35,40 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
+
+    @Override
+    public void visitVariable(VariableTree tree) {
+      if (tree instanceof VariableTreeImpl varImpl) {
+        varImpl.variableBinding = null;
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: tomasz-tylenda-sonarsource
body: I tried commenting out this line and no test fails. Do we need additional tests?
created_at: 2025-05-12T12:17:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -524,7 +524,7 @@ void context_root_working_directory_not_supported() {
     });
 
     assertThat(e)
-      .isInstanceOf(RuntimeException.class)
+      .isInstanceOf(UnsupportedOperationException.class)
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java
user: login: tomasz-tylenda-sonarsource
body: This change seems a bit random. Why is it needed?
created_at: 2025-05-12T12:44:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,26 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var checkInstance = JavaCheckVerifier.newInstance()
+      .onFile(TEST_FILE)
+      .withCheck(new JavaFileScanner() {
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: tomasz-tylenda-sonarsource
body: Could you please extract it to a local variable `check` just before `JavaCheckVerifier.newInstance()` for better readability?
created_at: 2025-05-12T12:58:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,26 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var checkInstance = JavaCheckVerifier.newInstance()
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: tomasz-tylenda-sonarsource
body: It should be called `verifierInstance`.
created_at: 2025-05-12T12:59:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,26 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var checkInstance = JavaCheckVerifier.newInstance()
+      .onFile(TEST_FILE)
+      .withCheck(new JavaFileScanner() {
+        @Override
+        public void scanFile(JavaFileScannerContext context) {
+          CompilationUnitTree tree = context.getTree();
+          ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+          assertThat(classTree.simpleName().name()).isEqualTo("Modified");
+        }
+      })
+      .withCompilationUnitModifier(modifier);
+    assertThatCode(checkInstance::verifyNoIssues).doesNotThrowAnyException();
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: tomasz-tylenda-sonarsource
body: Calling `verifyNoIssues()` directly should be enough. If it throws an exception, the test will fail.
```
JavaCheckVerifier.newInstance()
      .onFile(TEST_FILE)
      .withCheck(check)
      .withCompilationUnitModifier(modifier)
      .verifyNoIssues();
```
created_at: 2025-05-12T13:01:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,7 +151,8 @@ private void handleAssignment(Set<Symbol> out, Set<Tree> assignmentLHS, Assignme
     ExpressionTree lhs = ExpressionUtils.skipParentheses(element.variable());
     if (lhs.is(Tree.Kind.IDENTIFIER)) {
       Symbol symbol = ((IdentifierTree) lhs).symbol();
-      if (symbol.isLocalVariable()
+      if ( !symbol.isUnknown() &&
path: java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
user: login: tomasz-tylenda-sonarsource
body: Is this needed? If yes, is there a test for this line?
created_at: 2025-05-12T13:09:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +79,12 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {
+    });
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: tomasz-tylenda-sonarsource
body: You can make it one line.
created_at: 2025-05-12T13:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +79,12 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {
+    });
+  }
+
+  // modifyCompilationUnit should be used for testing.
+  public void scan(Iterable<? extends InputFile> inputFiles, Consumer<CompilationUnitTree> modifyCompilationUnit) {
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: tomasz-tylenda-sonarsource
body: It should be more difficult to call this from production code. How about we make this method private, and then introduce public `scanForTesting` which call it? That way you either call `scan` above, or `scanForTesting` with modifyCompilationUnit.
created_at: 2025-05-12T13:19:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +79,12 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {
+    });
+  }
+
+  // modifyCompilationUnit should be used for testing.
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: tomasz-tylenda-sonarsource
body: This should be a JavaDoc, because it is meant for the users and it documents how to call this method.

// coments describe how something works internally and are meant for developers changing the implementation.


created_at: 2025-05-12T13:22:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -524,7 +524,7 @@ void context_root_working_directory_not_supported() {
     });
 
     assertThat(e)
-      .isInstanceOf(RuntimeException.class)
+      .isInstanceOf(UnsupportedOperationException.class)
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java
user: login: erwan-serandour
body: When I implemented the working_directory method. I was not sure about what was the most conventional RuntimeException or UnsupportedOperationException. My opinion now is that UnsupportedOperationException is a bit better.
created_at: 2025-05-12T14:18:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -524,7 +524,7 @@ void context_root_working_directory_not_supported() {
     });
 
     assertThat(e)
-      .isInstanceOf(RuntimeException.class)
+      .isInstanceOf(UnsupportedOperationException.class)
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/InternalCheckVerifierTest.java
user: login: erwan-serandour
body: I think we should always use either RuntimeException or UnsupportedOperationException.
created_at: 2025-05-12T14:19:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +79,12 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {
+    });
+  }
+
+  // modifyCompilationUnit should be used for testing.
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T14:31:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +79,12 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {
+    });
+  }
+
+  // modifyCompilationUnit should be used for testing.
+  public void scan(Iterable<? extends InputFile> inputFiles, Consumer<CompilationUnitTree> modifyCompilationUnit) {
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: erwan-serandour
body: it is a good idea, I wanted to apply @VisibleForTesting to the argument, but you can only apply it to methods
created_at: 2025-05-12T14:32:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,6 +79,12 @@ public Map<Boolean, List<InputFile>> scanWithoutParsing(Iterable<? extends Input
   }
 
   public void scan(Iterable<? extends InputFile> inputFiles) {
+    scan(inputFiles, compilationUnitTree -> {
+    });
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T14:34:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,10 +35,40 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
+
+    @Override
+    public void visitVariable(VariableTree tree) {
+      if (tree instanceof VariableTreeImpl varImpl) {
+        varImpl.variableBinding = null;
+      }
+      super.visitVariable(tree);
+    }
+
+    @Override
+    public void visitIdentifier(IdentifierTree tree) {
+      if (tree instanceof IdentifierTreeImpl id) {
+        id.binding = null;
+      }
+      super.visitIdentifier(tree);
+    }
+  }
+
   @Test
   void test_non_compiling() {
     CheckVerifier.newVerifier()
       .onFile(TestUtils.nonCompilingTestSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withoutSemantic()
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: erwan-serandour
body: good catch, it was unintentional
created_at: 2025-05-12T14:35:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,7 +151,8 @@ private void handleAssignment(Set<Symbol> out, Set<Tree> assignmentLHS, Assignme
     ExpressionTree lhs = ExpressionUtils.skipParentheses(element.variable());
     if (lhs.is(Tree.Kind.IDENTIFIER)) {
       Symbol symbol = ((IdentifierTree) lhs).symbol();
-      if (symbol.isLocalVariable()
+      if ( !symbol.isUnknown() &&
path: java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T15:04:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,10 +35,40 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
+
+    @Override
+    public void visitVariable(VariableTree tree) {
+      if (tree instanceof VariableTreeImpl varImpl) {
+        varImpl.variableBinding = null;
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T15:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,26 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var checkInstance = JavaCheckVerifier.newInstance()
+      .onFile(TEST_FILE)
+      .withCheck(new JavaFileScanner() {
+        @Override
+        public void scanFile(JavaFileScannerContext context) {
+          CompilationUnitTree tree = context.getTree();
+          ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+          assertThat(classTree.simpleName().name()).isEqualTo("Modified");
+        }
+      })
+      .withCompilationUnitModifier(modifier);
+    assertThatCode(checkInstance::verifyNoIssues).doesNotThrowAnyException();
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T15:17:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,26 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var checkInstance = JavaCheckVerifier.newInstance()
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T15:17:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,26 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var checkInstance = JavaCheckVerifier.newInstance()
+      .onFile(TEST_FILE)
+      .withCheck(new JavaFileScanner() {
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-12T15:17:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,29 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().name()).isEqualTo("Modified");
+      }
+    };
+    assertThatCode(() -> {
+      JavaCheckVerifier.newInstance()
+        .onFile(TEST_FILE)
+        .withCheck(check)
+        .withCompilationUnitModifier(modifier)
+        .verifyNoIssues();
+    }).doesNotThrowAnyException();
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: tomasz-tylenda-sonarsource
body: Does this assertion do anything useful? If a test throws an exception, it will fail automatically.
created_at: 2025-05-13T11:33:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,7 +151,7 @@ private void handleAssignment(Set<Symbol> out, Set<Tree> assignmentLHS, Assignme
     ExpressionTree lhs = ExpressionUtils.skipParentheses(element.variable());
     if (lhs.is(Tree.Kind.IDENTIFIER)) {
       Symbol symbol = ((IdentifierTree) lhs).symbol();
-      if (symbol.isLocalVariable()
+      if ( symbol.isLocalVariable()
path: java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
user: login: tomasz-tylenda-sonarsource
body: Please remove the weird space after `(`
created_at: 2025-05-13T11:34:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,10 +35,40 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckSample.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
+
+    @Override
+    public void visitVariable(VariableTree tree) {
+      if (tree instanceof VariableTreeImpl varImpl) {
+        varImpl.variableBinding = null;
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: tomasz-tylenda-sonarsource
body: I tried again and it still passes.
created_at: 2025-05-13T11:55:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,9 +143,17 @@ private boolean analysisCancelled() {
   }
 
   public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consumer<JavaTree.CompilationUnitTreeImpl> cleanUp) {
+    simpleScan(inputFile, result, cleanUp, compilationUnitTree -> {
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: tomasz-tylenda-sonarsource
body: Let's merge this line an one after that. There is not need to put `{}` on two lines.
created_at: 2025-05-13T11:59:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +33,27 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckBrokenSemantic.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: tomasz-tylenda-sonarsource
body: Let's move it to the end of this class, after all tests.
created_at: 2025-05-13T12:04:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,6 +449,37 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+
+    InputFile trivialCompilationUnit = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().symbol().isUnknown()).isTrue();
+      }
+    };
+    VisitorsBridge visitorsBridge = new VisitorsBridge(
+      List.of(check),
+      Collections.emptyList(),
+      null);
+
+    assertDoesNotThrow(() -> {
path: java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
user: login: tomasz-tylenda-sonarsource
body: Does `assertDoesNotThrow` do anything? I think you can just execute the body of the lambda and if it throws anything, the test will fail.
created_at: 2025-05-13T12:14:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,6 +449,37 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+
path: java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
user: login: tomasz-tylenda-sonarsource
body: No need for an empty line here.
created_at: 2025-05-13T12:14:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,6 +449,37 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+
+    InputFile trivialCompilationUnit = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().symbol().isUnknown()).isTrue();
+      }
+    };
+    VisitorsBridge visitorsBridge = new VisitorsBridge(
path: java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
user: login: tomasz-tylenda-sonarsource
body: Please add an empty line before `visitorsBridge`
created_at: 2025-05-13T12:15:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,6 +449,37 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+
+    InputFile trivialCompilationUnit = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().symbol().isUnknown()).isTrue();
+      }
+    };
+    VisitorsBridge visitorsBridge = new VisitorsBridge(
path: java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-13T12:21:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,6 +449,37 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+
path: java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-13T12:22:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +33,27 @@ void test() {
       .verifyIssues();
   }
 
+  @Test
+  void test_variable_identifier_with_unknown_symbol() {
+    EraseSymbols eraser = new EraseSymbols();
+    CheckVerifier.newVerifier()
+      .onFile(TestUtils.mainCodeSourcesPath("checks/DeadStoreCheckBrokenSemantic.java"))
+      .withCheck(new DeadStoreCheck())
+      .withCompilationUnitModifier(eraser::visitCompilationUnit)
+      .verifyNoIssues();
+  }
+
+  private static class EraseSymbols extends BaseTreeVisitor {
path: java-checks/src/test/java/org/sonar/java/checks/DeadStoreCheckTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-13T12:25:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,7 +151,7 @@ private void handleAssignment(Set<Symbol> out, Set<Tree> assignmentLHS, Assignme
     ExpressionTree lhs = ExpressionUtils.skipParentheses(element.variable());
     if (lhs.is(Tree.Kind.IDENTIFIER)) {
       Symbol symbol = ((IdentifierTree) lhs).symbol();
-      if (symbol.isLocalVariable()
+      if ( symbol.isLocalVariable()
path: java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-13T12:25:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,6 +449,37 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned
     assertThat(actual.get(true)).containsExactly(successful, successful);
   }
 
+  @Test
+  void test_modifyCompilationUnit_modify_ast() {
+
+    InputFile trivialCompilationUnit = TestUtils.inputFile("src/test/resources/AstScannerNoParseError.txt");
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().symbol().isUnknown()).isTrue();
+      }
+    };
+    VisitorsBridge visitorsBridge = new VisitorsBridge(
+      List.of(check),
+      Collections.emptyList(),
+      null);
+
+    assertDoesNotThrow(() -> {
path: java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java
user: login: erwan-serandour
body: :+1: 

created_at: 2025-05-13T12:25:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,4 +315,29 @@ void withCache_can_handle_a_mix_of_caches_combination() {
     assertThat(dummyReadInternalWrite.cacheContext.getWriteCache()).isInstanceOf(JavaWriteCache.class);
   }
 
+  @Test
+  void compilationUnitModifier_modify_tree() {
+    Consumer<CompilationUnitTree> modifier = tree -> {
+      ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+      IdentifierTree ident = new IdentifierTreeImpl(new InternalSyntaxToken(1, 1, "Modified", List.of(), false));
+      classTree.complete((ModifiersTreeImpl)classTree.modifiers(), classTree.declarationKeyword(), ident);
+    };
+
+    var check = new JavaFileScanner() {
+      @Override
+      public void scanFile(JavaFileScannerContext context) {
+        CompilationUnitTree tree = context.getTree();
+        ClassTreeImpl classTree = (ClassTreeImpl) tree.types().get(0);
+        assertThat(classTree.simpleName().name()).isEqualTo("Modified");
+      }
+    };
+    assertThatCode(() -> {
+      JavaCheckVerifier.newInstance()
+        .onFile(TEST_FILE)
+        .withCheck(check)
+        .withCompilationUnitModifier(modifier)
+        .verifyNoIssues();
+    }).doesNotThrowAnyException();
path: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifierTest.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-13T12:50:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,9 +143,17 @@ private boolean analysisCancelled() {
   }
 
   public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consumer<JavaTree.CompilationUnitTreeImpl> cleanUp) {
+    simpleScan(inputFile, result, cleanUp, compilationUnitTree -> {
path: java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-13T12:59:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqube-next[bot]
created_at: 2025-05-13T13:08:01Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.java%3Ajava&pullRequest=5141) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5141&resolved=false) [0 New issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5141&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&fixedInPullRequest=5141) [1 Fixed issue](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&fixedInPullRequest=5141)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5141&issueStatuses=ACCEPTED) [0 Accepted issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5141&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.java%3Ajava&pullRequest=5141) [0 Security Hotspots](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.java%3Ajava&pullRequest=5141)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/dependency-risks?id=org.sonarsource.java%3Ajava&pullRequest=5141) [0 Dependency risks](https://next.sonarqube.com/sonarqube/dependency-risks?id=org.sonarsource.java%3Ajava&pullRequest=5141)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5141&metric=new_coverage&view=list) [100.0% Coverage on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5141&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5141&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5141&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.java%3Ajava&pullRequest=5141)


author_association: NONE