This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 30f2bb574b Issue 8774: Clear Lint results after switching projects  
(#8896)
30f2bb574b is described below

commit 30f2bb574b72fbb8118ae46c78313c6f771358ba
Author: Bart Maertens <[email protected]>
AuthorDate: Wed Oct 14 13:40:58 2026 +0200

    Issue 8774: Clear Lint results after switching projects  (#8896)
    
    * Fixes #8774 : Clear lint results when switching projects in Hop GUI
    
    Enabling another project drops the findings and the checked-file tracker of 
the previous one, and a background project lint still running for it stops 
publishing.
    
    * Fixes #8774 : Lint the whole project again after re-enabling the linter
    
    * Issue #8774 : Drop lint passes started before a project switch or before 
the linter was switched off
    
    * Issue #8774 : Keep the lint publish tests independent of a display
---
 .../org/apache/hop/lint/BackgroundLintService.java | 168 ++++++++++++++++++---
 .../org/apache/hop/lint/ExplorerLintGuiPlugin.java |  29 +++-
 .../java/org/apache/hop/lint/LintCheckTracker.java |   6 +
 .../org/apache/hop/lint/LinterConfigPlugin.java    |   2 +-
 .../java/org/apache/hop/lint/LinterGuiPlugin.java  |  10 +-
 .../hop/lint/PipelineVerifyLintExtension.java      |   7 +-
 .../hop/lint/ProjectLoadedLintExtension.java       |   8 +-
 .../apache/hop/lint/WorkflowVerifyLintService.java |   7 +-
 .../org/apache/hop/lint/DisabledLinterTest.java    |  44 ++++++
 .../org/apache/hop/lint/LintCheckTrackerTest.java  |  13 ++
 .../org/apache/hop/lint/ProjectSwitchLintTest.java | 155 +++++++++++++++++++
 11 files changed, 412 insertions(+), 37 deletions(-)

diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
index 9b101a71a9..e865f6838c 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
@@ -66,6 +66,15 @@ public class BackgroundLintService {
   private final LintCheckTracker tracker;
   private final Map<String, AtomicInteger> deferredGenerations = new 
ConcurrentHashMap<>();
 
+  /**
+   * Bumped whenever the findings already published stop counting: another 
project was enabled, or
+   * the linter was switched off. A pass remembers the value it started with 
and does not publish
+   * once it has changed, nor mark its file as checked.
+   */
+  private final AtomicInteger generation = new AtomicInteger(0);
+
+  private String currentProjectPath;
+
   private BackgroundLintService() {
     this.tracker = new LintCheckTracker();
   }
@@ -114,7 +123,8 @@ public class BackgroundLintService {
       LintProblemsBarManager.getInstance().updateProblemsBar(filePath);
       return;
     }
-    submit(() -> lintFileInternal(filePath, graphId));
+    int passGeneration = currentGeneration();
+    submit(() -> lintFileInternal(filePath, graphId, passGeneration));
   }
 
   public void scheduleGraphLint(HopGuiAbstractGraph graph, boolean force) {
@@ -141,11 +151,112 @@ public class BackgroundLintService {
         });
   }
 
+  /**
+   * Called when a project is enabled. When it is another project than before, 
the findings and the
+   * record of what was checked belong to the project that was left: drop 
both, and stop the passes
+   * still running for it from publishing.
+   *
+   * <p>The tracker has to go as well. It skips files that have not changed 
since they were last
+   * checked, so switching back to a project would otherwise leave it without 
findings.
+   */
+  public void switchProject(String projectPath) {
+    String normalized = Utils.isEmpty(projectPath) ? "" : 
LintPathUtils.normalizePath(projectPath);
+    LintResultsManager manager = LintResultsManager.getInstance();
+    synchronized (manager) {
+      if (normalized.equals(currentProjectPath)) {
+        return;
+      }
+      // The first project of the session: nothing to drop, and the editors it 
just restored may
+      // already have published their findings.
+      boolean firstProject = currentProjectPath == null;
+      currentProjectPath = normalized;
+      generation.incrementAndGet();
+      if (!firstProject) {
+        tracker.clear();
+        manager.clearResults();
+      }
+    }
+  }
+
+  /**
+   * Called after the linter was switched off. Drops the findings and the 
record of what was
+   * checked, and stops the passes still running from publishing or marking a 
file as checked.
+   *
+   * <p>The tracker has to go: switching the linter back on would otherwise 
skip every file that has
+   * not changed since it was last checked, and the project would come back 
without findings.
+   */
+  public void discardResults() {
+    LintResultsManager manager = LintResultsManager.getInstance();
+    synchronized (manager) {
+      generation.incrementAndGet();
+      tracker.clear();
+      manager.clearResults();
+    }
+  }
+
+  /**
+   * The value a pass takes when it starts, to hand to {@link #publish} when 
it is done. Read it
+   * where the pass is started, on the UI thread: that is where a project 
switch happens.
+   */
+  int currentGeneration() {
+    return generation.get();
+  }
+
+  /**
+   * Publish the findings of a pass, unless the project was switched or the 
linter switched off
+   * since it started.
+   *
+   * <p>Checked and published under the lock {@link #switchProject} and {@link 
#discardResults}
+   * clear the results with, so a pass cannot slip its findings in right after 
either.
+   *
+   * @return false when the findings were dropped
+   */
+  boolean publish(String filePath, List<LintResult> results, int 
passGeneration) {
+    return publish(filePath, results, passGeneration, false);
+  }
+
+  /** {@link #publish}, and record the file as checked so an unchanged file is 
not linted again. */
+  private boolean publishChecked(String filePath, List<LintResult> results, 
int passGeneration) {
+    return publish(filePath, results, passGeneration, true);
+  }
+
+  private boolean publish(
+      String filePath, List<LintResult> results, int passGeneration, boolean 
markChecked) {
+    LintResultsManager manager = LintResultsManager.getInstance();
+    synchronized (manager) {
+      if (!isCurrent(passGeneration)) {
+        return false;
+      }
+      if (markChecked) {
+        tracker.markChecked(filePath);
+      }
+      manager.updateResultsForFile(filePath, results);
+      return true;
+    }
+  }
+
+  /** {@link #publish} for a pass over the whole project, which replaces every 
finding. */
+  boolean publishAll(List<LintResult> results, int passGeneration) {
+    LintResultsManager manager = LintResultsManager.getInstance();
+    synchronized (manager) {
+      if (!isCurrent(passGeneration)) {
+        return false;
+      }
+      manager.updateResults(results);
+      return true;
+    }
+  }
+
+  private boolean isCurrent(int passGeneration) {
+    return passGeneration == generation.get() && isEnabled();
+  }
+
   public void lintProjectAsync(
       String projectPath, IHopMetadataProvider metadataProvider, IVariables 
variables) {
     if (!isEnabled() || Utils.isEmpty(projectPath)) {
       return;
     }
+    int passGeneration = currentGeneration();
     submit(
         () -> {
           try {
@@ -161,12 +272,17 @@ public class BackgroundLintService {
 
             List<LintResult> accumulated = new ArrayList<>();
             for (String filePath : filesToCheck) {
+              if (passGeneration != generation.get()) {
+                log.logBasic(
+                    "Background linting of " + projectPath + " stopped: 
results discarded");
+                return;
+              }
               try {
                 List<LintResult> results =
                     lintFileForGui(filePath, linter, metadataProvider, 
variables);
-                accumulated.addAll(results);
-                tracker.markChecked(filePath);
-                
LintResultsManager.getInstance().updateResultsForFile(filePath, results);
+                if (publishChecked(filePath, results, passGeneration)) {
+                  accumulated.addAll(results);
+                }
               } catch (Exception e) {
                 log.logError("Error linting " + filePath, e);
               }
@@ -209,6 +325,7 @@ public class BackgroundLintService {
       return;
     }
 
+    int passGeneration = currentGeneration();
     HopGui hopGuiForSnapshot = HopGui.peekInstance();
     EditorSnapshot snapshot =
         snapshotOf(graph, hopGuiForSnapshot != null ? 
hopGuiForSnapshot.getVariables() : null);
@@ -228,9 +345,9 @@ public class BackgroundLintService {
 
             List<LintResult> results =
                 lintSnapshot(snapshot, normalizedPath, metadataProvider, 
variables);
-            tracker.markChecked(filename);
-
-            
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
+            if (!publishChecked(filename, results, passGeneration)) {
+              return;
+            }
             Display display = graph.getDisplay();
             if (display != null && !display.isDisposed()) {
               display.asyncExec(
@@ -323,19 +440,12 @@ public class BackgroundLintService {
     }
   }
 
-  private void lintFileInternal(String filePath, String graphId) {
+  /** Runs on a worker thread. */
+  private void lintFileInternal(String filePath, String graphId, int 
passGeneration) {
     try {
-      HopGui hopGui = HopGui.peekInstance();
-      IHopMetadataProvider metadataProvider = hopGui != null ? 
hopGui.getMetadataProvider() : null;
-      IVariables variables = hopGui != null ? hopGui.getVariables() : null;
-
-      HopLinter linter = new HopLinter();
-      List<LintResult> results = lintFileForGui(filePath, linter, 
metadataProvider, variables);
-      tracker.markChecked(filePath);
-
-      LintResultsManager.getInstance().updateResultsForFile(filePath, results);
-      LintProblemsBarManager.getInstance().updateProblemsBar(filePath);
-      log.logDetailed("Linted " + filePath + ": " + results.size() + " 
issues");
+      if (lintAndPublishFile(filePath, passGeneration)) {
+        LintProblemsBarManager.getInstance().updateProblemsBar(filePath);
+      }
     } catch (Exception e) {
       log.logError("Error linting file " + filePath + ": " + e.getMessage(), 
e);
     } finally {
@@ -345,6 +455,26 @@ public class BackgroundLintService {
     }
   }
 
+  /**
+   * Lint a saved file and publish what it finds, unless the results were 
discarded since the pass
+   * was scheduled. Touches no widget, so it can run without a display.
+   *
+   * @return false when the findings were dropped
+   */
+  boolean lintAndPublishFile(String filePath, int passGeneration) throws 
HopException {
+    HopGui hopGui = HopGui.peekInstance();
+    IHopMetadataProvider metadataProvider = hopGui != null ? 
hopGui.getMetadataProvider() : null;
+    IVariables variables = hopGui != null ? hopGui.getVariables() : null;
+
+    HopLinter linter = new HopLinter();
+    List<LintResult> results = lintFileForGui(filePath, linter, 
metadataProvider, variables);
+    if (!publishChecked(filePath, results, passGeneration)) {
+      return false;
+    }
+    log.logDetailed("Linted " + filePath + ": " + results.size() + " issues");
+    return true;
+  }
+
   private List<LintResult> lintFileForGui(
       String filePath,
       HopLinter linter,
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
index a7c311ee31..22e778f4af 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
@@ -321,6 +321,8 @@ public class ExplorerLintGuiPlugin {
 
     final HopGuiAbstractGraph graph = (HopGuiAbstractGraph) handler;
     final IHopFileTypeHandler openHandler = handler;
+    final BackgroundLintService service = BackgroundLintService.getInstance();
+    final int passGeneration = service.currentGeneration();
 
     BackgroundThreadFacade.start(
         () -> {
@@ -328,7 +330,10 @@ public class ExplorerLintGuiPlugin {
             List<LintResult> results =
                 lintFileResults(normalizedPath, openHandler, metadataProvider, 
variables);
 
-            
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
+            // Another project was enabled, or the linter switched off, while 
this ran.
+            if (!service.publish(normalizedPath, results, passGeneration)) {
+              return;
+            }
 
             Display.getDefault()
                 .asyncExec(
@@ -398,6 +403,8 @@ public class ExplorerLintGuiPlugin {
         LintEditorGraphHelper.findOpenHandlerForPath(normalizedPath);
     final IVariables variables = hopGui.getVariables();
     final IHopMetadataProvider metadataProvider = hopGui.getMetadataProvider();
+    final BackgroundLintService service = BackgroundLintService.getInstance();
+    final int passGeneration = service.currentGeneration();
 
     BackgroundThreadFacade.start(
         () -> {
@@ -405,7 +412,10 @@ public class ExplorerLintGuiPlugin {
             List<LintResult> results =
                 lintFileResults(normalizedPath, openHandler, metadataProvider, 
variables);
 
-            
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
+            // Another project was enabled, or the linter switched off, while 
this ran.
+            if (!service.publish(normalizedPath, results, passGeneration)) {
+              return;
+            }
 
             Display.getDefault()
                 .asyncExec(
@@ -510,6 +520,10 @@ public class ExplorerLintGuiPlugin {
     final IHopMetadataProvider metadataProvider = hopGui.getMetadataProvider();
 
     final LinterProgressDialog progressDialog = new 
LinterProgressDialog(hopGui.getShell());
+    // The dialog does not block the rest of Hop GUI, so another project can 
be enabled while
+    // this runs. The report still lists what was found; the stored findings 
do not take it.
+    final BackgroundLintService service = BackgroundLintService.getInstance();
+    final int passGeneration = service.currentGeneration();
 
     BackgroundThreadFacade.start(
         () -> {
@@ -570,8 +584,7 @@ public class ExplorerLintGuiPlugin {
                     fileResults = linter.processFile(file, metadataProvider, 
variables);
                   }
                   results.addAll(fileResults);
-                  LintResultsManager.getInstance()
-                      .updateResultsForFile(normalizedPath, fileResults);
+                  service.publish(normalizedPath, fileResults, passGeneration);
                   processedFilesCount++;
                 } catch (Exception e) {
                   log.logError("Error processing file: " + 
file.getAbsolutePath(), e);
@@ -583,10 +596,10 @@ public class ExplorerLintGuiPlugin {
                           "Failed to process file: " + e.getMessage(),
                           LintPathUtils.normalizePath(file.getAbsolutePath()));
                   results.add(errorResult);
-                  LintResultsManager.getInstance()
-                      .updateResultsForFile(
-                          LintPathUtils.normalizePath(file.getAbsolutePath()),
-                          List.of(errorResult));
+                  service.publish(
+                      LintPathUtils.normalizePath(file.getAbsolutePath()),
+                      List.of(errorResult),
+                      passGeneration);
                   processedFilesCount++;
                 }
               }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCheckTracker.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCheckTracker.java
index 7bd170ad01..9086303395 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCheckTracker.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCheckTracker.java
@@ -63,6 +63,12 @@ public class LintCheckTracker {
     }
   }
 
+  /** Forget every file, so the next pass lints all of them again. */
+  public void clear() {
+    lastCheckTimes.clear();
+    lastModifiedTimes.clear();
+  }
+
   private String normalize(String filePath) {
     try {
       return new File(filePath).getAbsolutePath();
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
index 71cc6aca36..194304ac2a 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
@@ -220,7 +220,7 @@ public class LinterConfigPlugin implements IConfigOptions, 
IGuiPluginCompositeWi
    */
   void applyEnabledState() {
     if (!isLinterEnabled()) {
-      LintResultsManager.getInstance().clearResults();
+      BackgroundLintService.getInstance().discardResults();
       try {
         LintProblemsBarManager.getInstance().refreshAllOpenEditors();
       } catch (Exception | LinkageError e) {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java
index c0d7f7312c..a2f9342cc9 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java
@@ -127,6 +127,11 @@ public class LinterGuiPlugin {
       final String finalProjectName =
           !Utils.isEmpty(projectName) ? projectName : new 
java.io.File(projectPath).getName();
 
+      // The progress dialog does not block the rest of Hop GUI, so another 
project can be enabled
+      // while this runs. The report still shows what was found; the stored 
findings do not take it.
+      final BackgroundLintService service = 
BackgroundLintService.getInstance();
+      final int passGeneration = service.currentGeneration();
+
       // Run linter in background thread
       BackgroundThreadFacade.start(
           () -> {
@@ -152,8 +157,9 @@ public class LinterGuiPlugin {
                       () -> {
                         try {
                           // Update results manager for GUI integration
-                          
LintResultsManager.getInstance().updateResults(results);
-                          
LintProblemsBarManager.getInstance().refreshAllOpenEditors();
+                          if (service.publishAll(results, passGeneration)) {
+                            
LintProblemsBarManager.getInstance().refreshAllOpenEditors();
+                          }
 
                           LinterGuiPlugin plugin = new LinterGuiPlugin();
                           plugin.displayResults(results, totalLintTime, 
finalProjectName);
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
index 0e27764705..3b27bfa530 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/PipelineVerifyLintExtension.java
@@ -54,6 +54,8 @@ public class PipelineVerifyLintExtension implements 
IExtensionPoint<CheckTransfo
       if (pipelineMeta == null) {
         return;
       }
+      BackgroundLintService service = BackgroundLintService.getInstance();
+      int passGeneration = service.currentGeneration();
 
       String fileName = 
LintPathUtils.normalizePath(pipelineMeta.getFilename());
       HopLinter linter = new HopLinter();
@@ -85,8 +87,9 @@ public class PipelineVerifyLintExtension implements 
IExtensionPoint<CheckTransfo
       // and a message prefixed with the rule id, which is not how the command 
line reports them.
       results.addAll(policyResults);
       List<LintResult> verifyViewResults = 
LintResultDeduplicator.deduplicate(results);
-      LintResultsManager.getInstance().updateResultsForFile(fileName, 
verifyViewResults);
-      LintProblemsBarManager.getInstance().updateProblemsBar(fileName);
+      if (service.publish(fileName, verifyViewResults, passGeneration)) {
+        LintProblemsBarManager.getInstance().updateProblemsBar(fileName);
+      }
 
       log.logDetailed(
           "Added "
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
index bdb1b3b9ab..de204ac946 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
@@ -34,12 +34,14 @@ public class ProjectLoadedLintExtension implements 
IExtensionPoint<Object> {
       ILogChannel log, org.apache.hop.core.variables.IVariables variables, 
Object object)
       throws HopException {
     String projectPath = variables != null ? 
variables.getVariable("PROJECT_HOME") : null;
+    BackgroundLintService service = BackgroundLintService.getInstance();
+    // Before the empty check: leaving a project for none must still drop its 
findings.
+    service.switchProject(projectPath);
     if (Utils.isEmpty(projectPath)) {
       return;
     }
     HopGui hopGui = HopGui.peekInstance();
-    BackgroundLintService.getInstance()
-        .lintProjectAsync(
-            projectPath, hopGui != null ? hopGui.getMetadataProvider() : null, 
variables);
+    service.lintProjectAsync(
+        projectPath, hopGui != null ? hopGui.getMetadataProvider() : null, 
variables);
   }
 }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
index efde4a1453..139325b4f3 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
@@ -46,6 +46,8 @@ public final class WorkflowVerifyLintService {
       delegate.addWorkflowCheck();
       graph.extraViewTabFolder.setSelection(delegate.getWorkflowCheckTab());
 
+      BackgroundLintService service = BackgroundLintService.getInstance();
+      int passGeneration = service.currentGeneration();
       HopGui hopGui = HopGui.peekInstance();
       WorkflowMeta workflowMeta = graph.getWorkflowMeta();
       String fileName = 
LintPathUtils.normalizePath(workflowMeta.getFilename());
@@ -57,8 +59,9 @@ public final class WorkflowVerifyLintService {
               hopGui != null ? hopGui.getMetadataProvider() : null,
               graph.getVariables());
 
-      LintResultsManager.getInstance().updateResultsForFile(fileName, results);
-      LintProblemsBarManager.getInstance().updateProblemsBar(fileName);
+      if (service.publish(fileName, results, passGeneration)) {
+        LintProblemsBarManager.getInstance().updateProblemsBar(fileName);
+      }
       WorkflowProblemsTabSync.refreshGraph(graph, results);
 
       log.logDetailed(
diff --git 
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/DisabledLinterTest.java 
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/DisabledLinterTest.java
index 16d5acaf59..ed78c57711 100644
--- 
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/DisabledLinterTest.java
+++ 
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/DisabledLinterTest.java
@@ -22,10 +22,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verifyNoInteractions;
 
+import java.io.File;
+import java.nio.file.Files;
 import java.util.List;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 /**
  * Switching the linter off has to stop marking files.
@@ -60,6 +63,47 @@ class DisabledLinterTest {
     assertTrue(manager.getAllResults().isEmpty(), "a disabled linter must not 
leave a file marked");
   }
 
+  @Test
+  void switchingTheLinterOffForgetsWhichFilesWereChecked(@TempDir File folder) 
throws Exception {
+    File file = new File(folder, "example.hpl");
+    Files.writeString(file.toPath(), "<pipeline/>");
+    LintCheckTracker tracker = 
BackgroundLintService.getInstance().getTracker();
+    tracker.markChecked(file.getAbsolutePath());
+
+    LinterConfigPlugin config = LinterConfigPlugin.getInstance();
+    config.setLinterEnabled(false);
+    config.applyEnabledState();
+
+    assertTrue(
+        tracker.needsLinting(file.getAbsolutePath()),
+        "switching the linter back on would skip the file and leave it without 
findings");
+  }
+
+  /**
+   * A pass still running when the linter is switched off finishes after the 
tracker was cleared. If
+   * it marked its file as checked, switching the linter back on would skip 
that file.
+   */
+  @Test
+  void aPassFinishingAfterTheLinterIsOffDoesNotMarkItsFile(@TempDir File 
folder) throws Exception {
+    String file = ProjectSwitchLintTest.writeWorkflow(folder);
+    BackgroundLintService service = BackgroundLintService.getInstance();
+    LintCheckTracker tracker = service.getTracker();
+
+    // Control: the same pass, still current, does mark the file.
+    assertTrue(service.lintAndPublishFile(file, service.currentGeneration()));
+    assertFalse(tracker.needsLinting(file), "a current pass marks its file");
+
+    tracker.invalidate(file);
+    int startedBefore = service.currentGeneration();
+    LinterConfigPlugin config = LinterConfigPlugin.getInstance();
+    config.setLinterEnabled(false);
+    config.applyEnabledState();
+    assertFalse(service.lintAndPublishFile(file, startedBefore));
+
+    assertTrue(tracker.needsLinting(file), "switching back on would skip the 
file");
+    assertTrue(LintResultsManager.getInstance().getAllResults().isEmpty());
+  }
+
   @Test
   void anInFlightUpdateDoesNotPublishAfterTheLinterIsOff() {
     LinterConfigPlugin config = LinterConfigPlugin.getInstance();
diff --git 
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintCheckTrackerTest.java 
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintCheckTrackerTest.java
index 670061940d..1b39dc2a20 100644
--- 
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintCheckTrackerTest.java
+++ 
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintCheckTrackerTest.java
@@ -74,4 +74,17 @@ public class LintCheckTrackerTest {
 
     assertTrue(tracker.needsLinting(temp.getAbsolutePath()));
   }
+
+  @Test
+  public void clearForcesRecheckOfEveryFile() throws IOException {
+    File temp = File.createTempFile("lint-tracker", ".hpl");
+    temp.deleteOnExit();
+    Files.writeString(temp.toPath(), "test");
+
+    LintCheckTracker tracker = new LintCheckTracker();
+    tracker.markChecked(temp.getAbsolutePath());
+    tracker.clear();
+
+    assertTrue(tracker.needsLinting(temp.getAbsolutePath()));
+  }
 }
diff --git 
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/ProjectSwitchLintTest.java
 
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/ProjectSwitchLintTest.java
new file mode 100644
index 0000000000..7e446cc789
--- /dev/null
+++ 
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/ProjectSwitchLintTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.lint;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Enabling another project has to drop the findings of the one that was left 
(issue #8774).
+ *
+ * <p>The background project lint merges file by file, so the files of the 
previous project kept
+ * their findings and the Lint Results view listed both projects.
+ */
+class ProjectSwitchLintTest {
+
+  @TempDir File projectA;
+  @TempDir File projectB;
+
+  private final BackgroundLintService service = 
BackgroundLintService.getInstance();
+  private final LintResultsManager manager = LintResultsManager.getInstance();
+
+  @BeforeEach
+  void setUp() {
+    LinterConfigPlugin config = LinterConfigPlugin.getInstance();
+    config.setLinterEnabled(true);
+    config.saveToHopConfig();
+    manager.clearResults();
+  }
+
+  @AfterEach
+  void tearDown() {
+    manager.clearResults();
+  }
+
+  @Test
+  void switchingToAnotherProjectDropsTheFindingsOfThePreviousOne() {
+    service.switchProject(projectA.getAbsolutePath());
+    String file = new File(projectA, "a.hpl").getAbsolutePath();
+    manager.updateResultsForFile(file, List.of(finding(file)));
+
+    service.switchProject(projectB.getAbsolutePath());
+
+    assertTrue(manager.getAllResults().isEmpty(), "project A's findings must 
not stay listed");
+  }
+
+  @Test
+  void enablingTheSameProjectAgainKeepsItsFindings() {
+    service.switchProject(projectA.getAbsolutePath());
+    String file = new File(projectA, "a.hpl").getAbsolutePath();
+    manager.updateResultsForFile(file, List.of(finding(file)));
+
+    service.switchProject(projectA.getAbsolutePath());
+
+    assertEquals(1, manager.getAllResults().size());
+  }
+
+  @Test
+  void aPassStillRunningForThePreviousProjectDoesNotPublish() throws Exception 
{
+    service.switchProject(projectA.getAbsolutePath());
+    int generation = service.currentGeneration();
+    File file = new File(projectA, "a.hpl");
+    Files.writeString(file.toPath(), "<pipeline/>");
+
+    service.switchProject(projectB.getAbsolutePath());
+    boolean published =
+        service.publish(file.getAbsolutePath(), 
List.of(finding(file.getPath())), generation);
+
+    assertFalse(published);
+    assertTrue(manager.getAllResults().isEmpty(), "a late pass must not put 
project A back");
+    assertTrue(
+        service.getTracker().needsLinting(file.getAbsolutePath()),
+        "a dropped pass must not mark the file as checked");
+  }
+
+  @Test
+  void switchingBackLintsTheUnchangedFilesAgain() throws Exception {
+    service.switchProject(projectA.getAbsolutePath());
+    File file = new File(projectA, "a.hpl");
+    Files.writeString(file.toPath(), "<pipeline/>");
+    service.getTracker().markChecked(file.getAbsolutePath());
+    assertFalse(service.getTracker().needsLinting(file.getAbsolutePath()));
+
+    service.switchProject(projectB.getAbsolutePath());
+    service.switchProject(projectA.getAbsolutePath());
+
+    assertTrue(
+        service.getTracker().needsLinting(file.getAbsolutePath()),
+        "project A would come back without findings");
+  }
+
+  /**
+   * A file lint already on the pool when the project is switched, from an 
editor that was closed or
+   * a file opened just before. It finishes afterwards and must not bring the 
previous project back.
+   */
+  @Test
+  void aFileLintScheduledBeforeTheSwitchDoesNotReappear() throws Exception {
+    service.switchProject(projectA.getAbsolutePath());
+    String file = writeWorkflow(projectA);
+
+    // Control: the same pass, still current, does publish. Without it, a file 
that cannot be
+    // linted would pass the test below for the wrong reason.
+    assertTrue(service.lintAndPublishFile(file, service.currentGeneration()));
+    assertFalse(service.getTracker().needsLinting(file), "a current pass 
publishes");
+
+    service.getTracker().invalidate(file);
+    int scheduledBefore = service.currentGeneration();
+    service.switchProject(projectB.getAbsolutePath());
+    assertFalse(service.lintAndPublishFile(file, scheduledBefore));
+
+    assertTrue(manager.getAllResults().isEmpty(), "project A's file must not 
reappear");
+    assertTrue(
+        service.getTracker().needsLinting(file), "a dropped pass must not mark 
the file checked");
+  }
+
+  /**
+   * A workflow, not a pipeline: without Hop GUI there is no metadata 
provider, and a pipeline
+   * refuses to load without one.
+   */
+  static String writeWorkflow(File folder) throws Exception {
+    WorkflowMeta workflowMeta = new WorkflowMeta();
+    workflowMeta.setName("example");
+    File file = new File(folder, "example.hwf");
+    Files.writeString(file.toPath(), 
workflowMeta.getXml(Variables.getADefaultVariableSpace()));
+    return file.getAbsolutePath();
+  }
+
+  private static LintResult finding(String file) {
+    return new LintResult("HOP-CHECK", "SQL", "ERROR", "SQL cannot be null or 
blank.", file);
+  }
+}

Reply via email to