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);
+ }
+}