This is an automated email from the ASF dual-hosted git repository.
bamaer 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 a7e7a21b43 Fixes #8735 : Sort lint results, default the Ignore dialog
to the rep… (#8745)
a7e7a21b43 is described below
commit a7e7a21b43aad61999674de31c7b96bef2afb378
Author: Bart Maertens <[email protected]>
AuthorDate: Fri Oct 9 19:59:55 2026 +0200
Fixes #8735 : Sort lint results, default the Ignore dialog to the rep…
(#8745)
* Fixes #8735 : Sort lint results, default the Ignore dialog to the
reported rules, stop writing results files and mark ignored elements clearly
* Issue #8735 : Set the linter options' enabled state when the
configuration page opens, resolve each path once when sorting lint results
---
.../modules/ROOT/pages/hop-tools/hop-lint.adoc | 2 +-
.../modules/ROOT/pages/linting/index.adoc | 2 +-
.../modules/ROOT/pages/linting/lint-rules.adoc | 2 +-
.../modules/ROOT/partials/config/all-plugins.adoc | 34 ++--
.../ROOT/partials/config/linter-config.adoc | 34 ++--
.../apache/hop/lint/LintCanvasOverlayHelper.java | 19 ++-
.../org/apache/hop/lint/LintResultGrouping.java | 58 +++++++
.../java/org/apache/hop/lint/LintResultsPanel.java | 122 ++++++-------
.../org/apache/hop/lint/LintSuppressDialog.java | 24 +--
.../org/apache/hop/lint/LinterConfigPlugin.java | 27 ++-
.../java/org/apache/hop/lint/LinterGuiPlugin.java | 188 ++-------------------
.../hop/lint/messages/messages_en_US.properties | 8 +-
.../org/apache/hop/lint/LintResultsViewTest.java | 131 ++++++++++++++
.../hop/lint/LintSuppressDialogDefaultTest.java | 62 +++++++
.../configuration/tabs/ConfigPluginOptionsTab.java | 21 ++-
.../tabs/ConfigPluginOptionsTabTest.java | 58 +++++++
16 files changed, 481 insertions(+), 311 deletions(-)
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-lint.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-lint.adoc
index becac14f5e..16a6100805 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-lint.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-lint.adoc
@@ -164,7 +164,7 @@ This writes a `pre-commit` hook into the current
repository's `.git/hooks`.
The hook lints the staged Hop files and blocks the commit when they fail.
It is a copy, so run `hop lint --install-hook` again after upgrading Hop to
pick up changes to it.
-Hop Gui can do the same for commits made from within Hop Gui. See
*Configuration -> Linter -> Block Git Commits*, which is *off by default*:
installing and enabling the linter does not on its own change how committing
behaves.
+Hop Gui can do the same for commits made from within Hop Gui. See
*Configuration -> Plugins -> Linter -> Block Git Commits on Lint Failures*,
which is *off by default*: installing and enabling the linter does not on its
own change how committing behaves.
As with git's own `pre-commit` hook, only commits you author are checked.
Reverting a file and cherry-picking a file are not, because both replay
content that is already in the history — and reverting is often how you fix a
lint failure in the first place.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/linting/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/linting/index.adoc
index 49814a6f00..f6259fc9fd 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/linting/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/linting/index.adoc
@@ -105,7 +105,7 @@ A rule's severity is part of the rule, and a project can
override it.
== Configuration
-Linter behaviour is configured under *Configuration -> Linter* in Hop Gui, and
stored in `hop-config.json`.
+Linter behaviour is configured under *Configuration -> Plugins -> Linter* in
Hop Gui, and stored in `hop-config.json`.
The linter can be switched off entirely there, and each of its parts — linting
while you edit, the problems bar, the canvas overlays, the checks added to
*Verify*, and blocking git commits — can be switched off individually.
Blocking git commits is *off by default*. Nothing about committing changes
until you switch it on, and see xref:hop-tools/hop-lint.adoc[hop lint] for
which git operations it applies to.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/linting/lint-rules.adoc
b/docs/hop-user-manual/modules/ROOT/pages/linting/lint-rules.adoc
index 94ce4de96d..3c7412f9b9 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/linting/lint-rules.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/linting/lint-rules.adoc
@@ -474,7 +474,7 @@ suppress:
`rule: "*"` accepts whatever is reported on that element, including a rule
that starts reporting on it later, which is what a transform filled in at
runtime needs.
It is only accepted together with a `path` or a `source`: on its own it would
be the linter switched off under another name, and it is refused the same way
an entry without a rule is.
-An element whose findings are accepted is drawn with a muted outline on the
canvas rather than nothing at all, so the absence of a warning reads as
somebody's decision rather than as a check that never ran.
+An element whose findings are accepted is drawn with a dashed outline and a
small greyed-out info badge on the canvas rather than nothing at all, so the
absence of a warning reads as somebody's decision rather than as a check that
never ran.
Turn that off with *Mark Ignored Transforms and Actions* in the linter options.
== How the effective rule set is built
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc
b/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc
index 72eb0ccfcd..69ef876a8b 100644
--- a/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc
@@ -335,14 +335,14 @@ These options are stored under `pass:c[googleDrive]` in
`hop-config.json`.
|===
|Option|Description|Default
-|pass:c[Configuration File]
-|pass:c[Path to hop-lint.yml configuration file (leave empty for
project-specific). Accepts variables.]
-|
-
|pass:c[Enable Linter]
|pass:c[Enable or disable the Hop Linter globally]
|`pass:c[true]`
+|pass:c[Lint Automatically While Editing]
+|pass:c[Re-run lint checks shortly after you change an open pipeline or
workflow. When disabled, files are only linted on open, save, or on demand.]
+|`pass:c[true]`
+
|pass:c[Include Lint Rules in Pipeline Verify]
|pass:c[Add YAML/custom lint findings to the native pipeline verify Problems
tab]
|`pass:c[true]`
@@ -355,30 +355,30 @@ These options are stored under `pass:c[googleDrive]` in
`hop-config.json`.
|pass:c[Also run Hops built-in pipeline/workflow verify checks during lint
project/file scans]
|`pass:c[true]`
-|pass:c[Lint Automatically While Editing]
-|pass:c[Re-run lint checks shortly after you change an open pipeline or
workflow. When disabled, files are only linted on open, save, or on demand.]
+|pass:c[Show Lint Indicators on Canvas]
+|pass:c[Show the lint totals overlay (top-left of the canvas) and the severity
badges on transforms/actions in open pipeline and workflow editors]
|`pass:c[true]`
-|pass:c[Block Commits on Warnings]
-|pass:c[When enabled, warnings also block commits. When disabled, only errors
block commits]
-|`pass:c[false]`
+|pass:c[Mark Ignored Transforms and Actions]
+|pass:c[Draw a dashed outline and a small badge on a transform or action whose
lint findings have been marked as expected, so the absence of a warning reads
as a decision rather than as a check that never ran]
+|`pass:c[true]`
+
+|pass:c[Configuration File]
+|pass:c[Path to hop-lint.yml configuration file (leave empty for
project-specific). Accepts variables.]
+|
|pass:c[Block Git Commits on Lint Failures]
|pass:c[When enabled, lint checks run before Explorer git commits and block
the commit if issues are found]
|`pass:c[false]`
+|pass:c[Block Commits on Warnings]
+|pass:c[When enabled, warnings also block commits. When disabled, only errors
block commits]
+|`pass:c[false]`
+
|pass:c[Include Metadata in Pre-Commit Checks]
|pass:c[Also lint staged metadata files (e.g. metadata/**/*.json) during
pre-commit checks]
|`pass:c[true]`
-|pass:c[Mark Ignored Transforms and Actions]
-|pass:c[Draw a muted outline on a transform or action whose lint findings have
been marked as expected, so the absence of a warning reads as a decision rather
than as a check that never ran]
-|`pass:c[true]`
-
-|pass:c[Show Lint Indicators on Canvas]
-|pass:c[Show the lint totals overlay (top-left of the canvas) and the severity
badges on transforms/actions in open pipeline and workflow editors]
-|`pass:c[true]`
-
|===
== Microsoft Azure
diff --git
a/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc
b/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc
index 52f6ad8d69..1a6bd1ac05 100644
--- a/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc
@@ -21,14 +21,14 @@ under the License.
|===
|Option|Description|Default
-|pass:c[Configuration File]
-|pass:c[Path to hop-lint.yml configuration file (leave empty for
project-specific). Accepts variables.]
-|
-
|pass:c[Enable Linter]
|pass:c[Enable or disable the Hop Linter globally]
|`pass:c[true]`
+|pass:c[Lint Automatically While Editing]
+|pass:c[Re-run lint checks shortly after you change an open pipeline or
workflow. When disabled, files are only linted on open, save, or on demand.]
+|`pass:c[true]`
+
|pass:c[Include Lint Rules in Pipeline Verify]
|pass:c[Add YAML/custom lint findings to the native pipeline verify Problems
tab]
|`pass:c[true]`
@@ -41,28 +41,28 @@ under the License.
|pass:c[Also run Hops built-in pipeline/workflow verify checks during lint
project/file scans]
|`pass:c[true]`
-|pass:c[Lint Automatically While Editing]
-|pass:c[Re-run lint checks shortly after you change an open pipeline or
workflow. When disabled, files are only linted on open, save, or on demand.]
+|pass:c[Show Lint Indicators on Canvas]
+|pass:c[Show the lint totals overlay (top-left of the canvas) and the severity
badges on transforms/actions in open pipeline and workflow editors]
|`pass:c[true]`
-|pass:c[Block Commits on Warnings]
-|pass:c[When enabled, warnings also block commits. When disabled, only errors
block commits]
-|`pass:c[false]`
+|pass:c[Mark Ignored Transforms and Actions]
+|pass:c[Draw a dashed outline and a small badge on a transform or action whose
lint findings have been marked as expected, so the absence of a warning reads
as a decision rather than as a check that never ran]
+|`pass:c[true]`
+
+|pass:c[Configuration File]
+|pass:c[Path to hop-lint.yml configuration file (leave empty for
project-specific). Accepts variables.]
+|
|pass:c[Block Git Commits on Lint Failures]
|pass:c[When enabled, lint checks run before Explorer git commits and block
the commit if issues are found]
|`pass:c[false]`
+|pass:c[Block Commits on Warnings]
+|pass:c[When enabled, warnings also block commits. When disabled, only errors
block commits]
+|`pass:c[false]`
+
|pass:c[Include Metadata in Pre-Commit Checks]
|pass:c[Also lint staged metadata files (e.g. metadata/**/*.json) during
pre-commit checks]
|`pass:c[true]`
-|pass:c[Mark Ignored Transforms and Actions]
-|pass:c[Draw a muted outline on a transform or action whose lint findings have
been marked as expected, so the absence of a warning reads as a decision rather
than as a check that never ran]
-|`pass:c[true]`
-
-|pass:c[Show Lint Indicators on Canvas]
-|pass:c[Show the lint totals overlay (top-left of the canvas) and the severity
badges on transforms/actions in open pipeline and workflow editors]
-|`pass:c[true]`
-
|===
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayHelper.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayHelper.java
index 129957ea52..d00da4c283 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayHelper.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayHelper.java
@@ -251,18 +251,27 @@ public final class LintCanvasOverlayHelper {
}
/**
- * A muted outline on an element whose findings have been accepted.
+ * Mark a transform or action whose findings were ignored on purpose.
*
- * <p>Deliberately quiet: no badge, no colour that competes with a real
finding. It exists so that
- * the absence of a warning reads as a decision rather than as a check that
never ran.
+ * <p>A dashed outline set off from the icon, with a greyed-out info badge
in the corner where an
+ * error badge would sit, in grey so it does not compete with a real
finding. The solid 1px grey
+ * outline it replaces was all but the border every transform has, so an
ignored transform looked
+ * like any other.
*/
public static void drawIgnoredOverlay(IGc gc, int x, int y, int iconSize,
boolean selected) {
if (gc == null) {
return;
}
gc.setLineWidth(selected ? 2 : 1);
- gc.setForeground(IGc.EColor.GRAY);
- gc.drawRoundRectangle(x - 2, y - 2, iconSize + 3, iconSize + 3, 8, 8);
+ gc.setForeground(IGc.EColor.DARKGRAY);
+ gc.setLineStyle(IGc.ELineStyle.DASH);
+ gc.drawRoundRectangle(x - 5, y - 5, iconSize + 9, iconSize + 9, 10, 10);
+ gc.setLineStyle(IGc.ELineStyle.SOLID);
+ try {
+ gc.drawImage(IGc.EImage.INFO_DISABLED, x - 8, y + iconSize - 8,
gc.getMagnification());
+ } catch (Exception ignored) {
+ // Icon drawing is best-effort on the canvas overlay
+ }
}
public static void drawOverlay(
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultGrouping.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultGrouping.java
index d618ef72eb..32f8dd6d56 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultGrouping.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultGrouping.java
@@ -16,7 +16,10 @@
*/
package org.apache.hop.lint;
+import java.io.File;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -101,6 +104,61 @@ public final class LintResultGrouping {
return grouped;
}
+ /** What the results view sorts the findings of a severity on. */
+ public enum SortKey {
+ FILE,
+ RULE,
+ MESSAGE
+ }
+
+ /**
+ * The order of the findings within a severity: by the chosen column, then
by the others, so the
+ * same results always come out in the same order. Findings used to appear
in the order they were
+ * found, which moved a file's findings to the bottom each time it was
linted again.
+ */
+ public static Comparator<LintResult> order(SortKey key, boolean ascending) {
+ // Resolving a path goes through VFS, so each path is resolved once per
comparator and not on
+ // every comparison. It also gives the same path the same key throughout a
sort.
+ Map<String, String> fileNames = new HashMap<>();
+ Comparator<LintResult> byFile =
+ Comparator.comparing(
+ (LintResult r) ->
+ fileNames.computeIfAbsent(text(r.getFileName()), path ->
fileName(path)),
+ String.CASE_INSENSITIVE_ORDER)
+ .thenComparing(r -> text(r.getFileName()),
String.CASE_INSENSITIVE_ORDER);
+ Comparator<LintResult> byRule =
+ Comparator.comparing((LintResult r) -> text(r.getRuleId()),
String.CASE_INSENSITIVE_ORDER);
+ Comparator<LintResult> byMessage =
+ Comparator.comparing((LintResult r) -> text(r.getMessage()),
String.CASE_INSENSITIVE_ORDER);
+
+ Comparator<LintResult> order =
+ switch (key) {
+ case RULE -> byRule.thenComparing(byFile).thenComparing(byMessage);
+ case MESSAGE ->
byMessage.thenComparing(byFile).thenComparing(byRule);
+ default -> byFile.thenComparing(byRule).thenComparing(byMessage);
+ };
+ return ascending ? order : order.reversed();
+ }
+
+ /** ERROR, WARNING and INFO in that order, whatever came first; anything
else after them. */
+ public static Comparator<String> severityOrder() {
+ List<String> known = List.of("ERROR", "WARNING", "INFO");
+ return Comparator.comparing(
+ (String severity) -> {
+ int index = known.indexOf(severity == null ? "" :
severity.toUpperCase());
+ return index < 0 ? known.size() : index;
+ })
+ .thenComparing(severity -> text(severity),
String.CASE_INSENSITIVE_ORDER);
+ }
+
+ private static String fileName(String path) {
+ return path.isEmpty() ? path : new
File(LintPathUtils.normalizePath(path)).getName();
+ }
+
+ private static String text(String value) {
+ return value == null ? "" : value;
+ }
+
public static int countBySeverity(List<LintResult> results, String severity)
{
return (int) results.stream().filter(result ->
severity.equals(result.getSeverity())).count();
}
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
index c5c4fec09c..6b6bc49d7e 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
@@ -47,6 +47,8 @@ public class LintResultsPanel extends Composite implements
LintResultsManager.Li
private LintResultsManager resultsManager;
private Tree resultsTree;
+ private LintResultGrouping.SortKey sortKey = LintResultGrouping.SortKey.FILE;
+ private boolean sortAscending = true;
private Text detailsText;
private Label statusLabel;
private String fileFilter;
@@ -140,6 +142,13 @@ public class LintResultsPanel extends Composite implements
LintResultsManager.Li
fileColumn.setText(BaseMessages.getString(PKG,
"LintResultsPanel.Column.File"));
fileColumn.setWidth(200);
+ ruleColumn.addListener(SWT.Selection, e -> sortOn(ruleColumn,
LintResultGrouping.SortKey.RULE));
+ messageColumn.addListener(
+ SWT.Selection, e -> sortOn(messageColumn,
LintResultGrouping.SortKey.MESSAGE));
+ fileColumn.addListener(SWT.Selection, e -> sortOn(fileColumn,
LintResultGrouping.SortKey.FILE));
+ resultsTree.setSortColumn(fileColumn);
+ resultsTree.setSortDirection(SWT.UP);
+
// Details panel
Group detailsGroup = new Group(sashForm, SWT.NONE);
detailsGroup.setText(BaseMessages.getString(PKG,
"LintResultsPanel.Group.Details"));
@@ -219,81 +228,50 @@ public class LintResultsPanel extends Composite
implements LintResultsManager.Li
displayedResults.size(), scope, errorCount,
warningCount));
Map<String, List<LintResult>> resultsBySeverity =
groupBySeverity(displayedResults);
-
- // Add error group
- if (resultsBySeverity.containsKey("ERROR")) {
- TreeItem errorGroup = new TreeItem(resultsTree, SWT.NONE);
- errorGroup.setText(
- new String[] {
- "ERROR", "", "Errors (" +
resultsBySeverity.get("ERROR").size() + ")", ""
- });
- errorGroup.setExpanded(true);
-
- for (LintResult result : resultsBySeverity.get("ERROR")) {
- TreeItem item = new TreeItem(errorGroup, SWT.NONE);
- item.setText(
- new String[] {
- result.getSeverity(),
- result.getRuleId(),
- result.getMessage(),
- LintEditorGraphHelper.displayName(result.getFileName())
- });
- item.setData(result);
- }
+ List<String> severities = new
java.util.ArrayList<>(resultsBySeverity.keySet());
+ severities.sort(LintResultGrouping.severityOrder());
+ for (String severity : severities) {
+ addSeverityGroup(severity, resultsBySeverity.get(severity));
}
+ });
+ }
- // Add warning group
- if (resultsBySeverity.containsKey("WARNING")) {
- TreeItem warningGroup = new TreeItem(resultsTree, SWT.NONE);
- warningGroup.setText(
- new String[] {
- "WARNING",
- "",
- "Warnings (" + resultsBySeverity.get("WARNING").size() +
")",
- ""
- });
- warningGroup.setExpanded(true);
-
- for (LintResult result : resultsBySeverity.get("WARNING")) {
- TreeItem item = new TreeItem(warningGroup, SWT.NONE);
- item.setText(
- new String[] {
- result.getSeverity(),
- result.getRuleId(),
- result.getMessage(),
- LintEditorGraphHelper.displayName(result.getFileName())
- });
- item.setData(result);
- }
- }
+ private void addSeverityGroup(String severity, List<LintResult> results) {
+ String label =
+ switch (severity) {
+ case "ERROR" -> "Errors";
+ case "WARNING" -> "Warnings";
+ default -> severity;
+ };
+ TreeItem group = new TreeItem(resultsTree, SWT.NONE);
+ group.setText(new String[] {severity, "", label + " (" + results.size() +
")", ""});
+
+ List<LintResult> sorted = new java.util.ArrayList<>(results);
+ sorted.sort(LintResultGrouping.order(sortKey, sortAscending));
+ for (LintResult result : sorted) {
+ TreeItem item = new TreeItem(group, SWT.NONE);
+ item.setText(
+ new String[] {
+ result.getSeverity(),
+ result.getRuleId(),
+ result.getMessage(),
+ LintEditorGraphHelper.displayName(result.getFileName())
+ });
+ item.setData(result);
+ }
+ group.setExpanded(true);
+ }
- // Add other severity groups
- for (Map.Entry<String, List<LintResult>> entry :
resultsBySeverity.entrySet()) {
- if (!"ERROR".equals(entry.getKey()) &&
!"WARNING".equals(entry.getKey())) {
- TreeItem group = new TreeItem(resultsTree, SWT.NONE);
- group.setText(
- new String[] {
- entry.getKey(),
- "",
- entry.getKey() + " (" + entry.getValue().size() + ")",
- ""
- });
- group.setExpanded(true);
-
- for (LintResult result : entry.getValue()) {
- TreeItem item = new TreeItem(group, SWT.NONE);
- item.setText(
- new String[] {
- result.getSeverity(),
- result.getRuleId(),
- result.getMessage(),
- result.getFileName()
- });
- item.setData(result);
- }
- }
- }
- });
+ /**
+ * Sort on a column, or turn the order round when it is already the one
sorted on. The severity
+ * groups keep their order: errors first.
+ */
+ private void sortOn(TreeColumn column, LintResultGrouping.SortKey key) {
+ sortAscending = key != sortKey || !sortAscending;
+ sortKey = key;
+ resultsTree.setSortColumn(column);
+ resultsTree.setSortDirection(sortAscending ? SWT.UP : SWT.DOWN);
+ refreshResults();
}
private List<LintResult> getDisplayedResults() {
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintSuppressDialog.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintSuppressDialog.java
index 1a46f8fca2..0d4fc3757c 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintSuppressDialog.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintSuppressDialog.java
@@ -84,10 +84,10 @@ public class LintSuppressDialog extends Dialog {
createContents();
- shell.setSize(560, 420);
+ shell.setSize(640, 440);
shell.setLocation(
- parent.getLocation().x + (parent.getSize().x - 560) / 2,
- parent.getLocation().y + (parent.getSize().y - 420) / 2);
+ parent.getLocation().x + (parent.getSize().x - 640) / 2,
+ parent.getLocation().y + (parent.getSize().y - 440) / 2);
shell.open();
Display display = parent.getDisplay();
@@ -102,7 +102,8 @@ public class LintSuppressDialog extends Dialog {
private void createContents() {
int margin = 10;
- Label header = new Label(shell, SWT.LEFT);
+ // Wrapped: at the default width the line was cut off before it said where
the decision goes.
+ Label header = new Label(shell, SWT.LEFT | SWT.WRAP);
header.setText(BaseMessages.getString(PKG,
"LintSuppressDialog.Label.Element", elementName));
FormData headerData = new FormData();
headerData.left = new FormAttachment(0, margin);
@@ -138,10 +139,10 @@ public class LintSuppressDialog extends Dialog {
allRulesButton.setLayoutData(allRulesData);
listedRulesButton = new Button(shell, SWT.RADIO);
- listedRulesButton.setText(
- BaseMessages.getString(PKG, "LintSuppressDialog.Scope.ListedRules",
ruleList()));
+ // The rules are listed in the findings above; in the label a long list
was cut off.
+ listedRulesButton.setText(BaseMessages.getString(PKG,
"LintSuppressDialog.Scope.ListedRules"));
listedRulesButton.setToolTipText(
- BaseMessages.getString(PKG,
"LintSuppressDialog.Scope.ListedRules.ToolTip"));
+ BaseMessages.getString(PKG,
"LintSuppressDialog.Scope.ListedRules.ToolTip", ruleList()));
listedRulesButton.setEnabled(!ruleIdsOfFindings().isEmpty());
FormData listedRulesData = new FormData();
listedRulesData.left = new FormAttachment(0, margin);
@@ -149,9 +150,12 @@ public class LintSuppressDialog extends Dialog {
listedRulesData.top = new FormAttachment(allRulesButton, margin / 2);
listedRulesButton.setLayoutData(listedRulesData);
- // Accepting everything on the element is the metadata injection case,
which is the reason
- // this dialog exists; naming the rules is the careful option for anyone
who wants it.
- allRulesButton.setSelection(true);
+ // Naming the rules reported now is the default: ignoring everything on
the element also
+ // ignores rules added later, security ones included. That broad option is
for an element
+ // filled in at runtime, such as by metadata injection, and it is a
deliberate choice.
+ boolean rulesToName = !ruleIdsOfFindings().isEmpty();
+ listedRulesButton.setSelection(rulesToName);
+ allRulesButton.setSelection(!rulesToName);
Label reasonLabel = new Label(shell, SWT.LEFT);
reasonLabel.setText(BaseMessages.getString(PKG,
"LintSuppressDialog.Label.Reason"));
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 01b99b4928..b72ac48c6c 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
@@ -116,15 +116,29 @@ public class LinterConfigPlugin implements
IConfigOptions, IGuiPluginCompositeWi
@Override
public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
- // Nothing to do.
+ enableDependentWidgets(compositeWidgets);
}
@Override
public void widgetModified(
GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
+ enableDependentWidgets(compositeWidgets);
persistContents(compositeWidgets);
}
+ /**
+ * Blocking commits on warnings only means anything when commits are blocked
at all, so it is
+ * greyed out while they are not. "Include Metadata in Pre-Commit Checks"
stays available: the
+ * background lint reads it as well.
+ */
+ static void enableDependentWidgets(GuiCompositeWidgets compositeWidgets) {
+ Map<String, Control> widgets = compositeWidgets.getWidgetsMap();
+ if (widgets.get("linter-pre-commit-enabled") instanceof Button preCommit
+ && widgets.get("linter-pre-commit-block-warnings") instanceof Button
blockWarnings) {
+ blockWarnings.setEnabled(preCommit.getSelection());
+ }
+ }
+
/**
* Write what the user chose back into hop-config.json.
*
@@ -256,6 +270,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
// Global linter settings
@GuiWidgetElement(
id = "linter-enabled",
+ order = "0100",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.Enabled.Label",
@@ -268,6 +283,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-lint-on-edit",
+ order = "0110",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.LintOnEdit.Label",
@@ -280,6 +296,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-show-problems-bar",
+ order = "0300",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.ShowIndicators.Label",
@@ -292,6 +309,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-show-ignored-markers",
+ order = "0310",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.ShowIgnoredMarkers.Label",
@@ -304,6 +322,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-config-file",
+ order = "0400",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.FILENAME,
variables = true,
@@ -316,6 +335,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-pre-commit-enabled",
+ order = "0500",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PreCommit.Label",
@@ -328,6 +348,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-pre-commit-block-warnings",
+ order = "0510",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PreCommitWarnings.Label",
@@ -340,6 +361,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-pre-commit-include-metadata",
+ order = "0520",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PreCommitMetadata.Label",
@@ -352,6 +374,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-include-in-pipeline-verify",
+ order = "0200",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PipelineVerify.Label",
@@ -364,6 +387,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-include-in-workflow-verify",
+ order = "0210",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.WorkflowVerify.Label",
@@ -376,6 +400,7 @@ public class LinterConfigPlugin implements IConfigOptions,
IGuiPluginCompositeWi
@GuiWidgetElement(
id = "linter-include-native-checks",
+ order = "0220",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.NativeChecks.Label",
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 487aa78b91..c0d7f7312c 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
@@ -99,7 +99,9 @@ public class LinterGuiPlugin {
}
String projectPath = variables.getVariable("PROJECT_HOME");
- String projectName = variables.getVariable("PROJECT_NAME");
+ // HOP_PROJECT_NAME is what the projects plugin sets; nothing sets
PROJECT_NAME, so every
+ // run was reported as "Unknown Project".
+ String projectName = variables.getVariable("HOP_PROJECT_NAME");
if (Utils.isEmpty(projectPath)) {
// No project loaded, show error message
@@ -122,7 +124,8 @@ public class LinterGuiPlugin {
final String finalProjectPath = projectPath;
final IVariables finalVariables = variables;
final HopGui finalHopGui = hopGui;
- final String finalProjectName = projectName != null ? projectName :
"Unknown Project";
+ final String finalProjectName =
+ !Utils.isEmpty(projectName) ? projectName : new
java.io.File(projectPath).getName();
// Run linter in background thread
BackgroundThreadFacade.start(
@@ -143,9 +146,6 @@ public class LinterGuiPlugin {
return;
}
- // Generate rule summary
- Map<String, HopLinter.RuleSummary> ruleSummary =
linter.generateRuleSummary(results);
-
// Process and display results on UI thread
Display.getDefault()
.asyncExec(
@@ -156,8 +156,7 @@ public class LinterGuiPlugin {
LintProblemsBarManager.getInstance().refreshAllOpenEditors();
LinterGuiPlugin plugin = new LinterGuiPlugin();
- plugin.displayResults(
- finalHopGui, results, ruleSummary,
totalLintTime, finalProjectName);
+ plugin.displayResults(results, totalLintTime,
finalProjectName);
} catch (Exception e) {
LogChannel.GENERAL.logError(
"Error displaying results: " + e.getMessage(),
e);
@@ -208,33 +207,20 @@ public class LinterGuiPlugin {
}
/**
- * Display the linting results to the user
+ * Report a project lint in the Hop log. The findings themselves are in Show
Lint Results and on
+ * the canvas.
*
- * @param hopGui The HopGui instance
- * @param results List of lint results
- * @param ruleSummary Summary of results by rule
- * @param executionTimeMs Total execution time in milliseconds
- * @param projectName Name of the project that was linted
+ * <p>Each run also wrote a hop-lint-results_<timestamp>.txt into the
project root. They
+ * piled up, were picked up by a plain "git add .", and carried whatever the
findings said.
*/
- private void displayResults(
- HopGui hopGui,
- List<LintResult> results,
- Map<String, HopLinter.RuleSummary> ruleSummary,
- long executionTimeMs,
- String projectName) {
- // Write detailed results to file instead of console
- String projectPath = writeResultsToFile(results, ruleSummary,
executionTimeMs, projectName);
-
- // Log summary to the Hop log
+ private void displayResults(List<LintResult> results, long executionTimeMs,
String projectName) {
LogChannel.GENERAL.logBasic("=== LINT RESULTS FOR PROJECT: " + projectName
+ " ===");
LogChannel.GENERAL.logBasic("Execution time: " +
formatExecutionTime(executionTimeMs));
- LogChannel.GENERAL.logBasic("Detailed results written to: " + projectPath);
if (results.isEmpty()) {
LogChannel.GENERAL.logBasic(
"✓ No issues found! Your project follows all configured best
practices.");
} else {
- // Group results by severity
Map<String, List<LintResult>> resultsBySeverity =
results.stream().collect(Collectors.groupingBy(LintResult::getSeverity));
@@ -248,157 +234,7 @@ public class LinterGuiPlugin {
}
LogChannel.GENERAL.logBasic("=== END LINT RESULTS ===");
-
- if (results.isEmpty()) {
- LintResultsUi.logSummary(results, projectName);
- } else {
- LintResultsUi.logSummary(results, projectName);
- }
- }
-
- /**
- * Write detailed lint results to a file in the project root
- *
- * @param results All lint results
- * @param ruleSummary Summary by rule
- * @param executionTimeMs Execution time
- * @param projectName Project name
- * @return Path to the results file
- */
- private String writeResultsToFile(
- List<LintResult> results,
- Map<String, HopLinter.RuleSummary> ruleSummary,
- long executionTimeMs,
- String projectName) {
- try {
- // Get project path from HopGui variables
- HopGui hopGui = HopGui.peekInstance();
- IVariables variables = hopGui.getVariables();
- String projectPath = variables.getVariable("PROJECT_HOME");
-
- if (Utils.isEmpty(projectPath)) {
- // Fallback to current working directory
- projectPath = System.getProperty("user.dir");
- }
-
- // Create results file with timestamp
- String timestamp =
- java.time.LocalDateTime.now()
-
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
- String fileName = "hop-lint-results_" + timestamp + ".txt";
- java.io.File resultsFile = new java.io.File(projectPath, fileName);
-
- // Write results to file
- try (java.io.PrintWriter writer =
- new java.io.PrintWriter(
- new java.io.FileWriter(resultsFile,
java.nio.charset.StandardCharsets.UTF_8))) {
-
- writer.println("=== HOP LINT CHECKER RESULTS ===");
- writer.println("Project: " + projectName);
- writer.println("Execution time: " +
formatExecutionTime(executionTimeMs));
- writer.println(
- "Generated: "
- + java.time.LocalDateTime.now()
-
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
- writer.println();
-
- // Write rule summary
- writer.println("RULE SUMMARY:");
- ruleSummary.entrySet().stream()
- .sorted(Map.Entry.comparingByKey())
- .forEach(entry -> writer.println(" " +
entry.getValue().toString()));
- writer.println();
-
- if (results.isEmpty()) {
- writer.println("✓ No issues found! Your project follows all
configured best practices.");
- } else {
- // Group results by severity
- Map<String, List<LintResult>> resultsBySeverity =
-
results.stream().collect(Collectors.groupingBy(LintResult::getSeverity));
-
- int errorCount = resultsBySeverity.getOrDefault("ERROR",
List.of()).size();
- int warningCount = resultsBySeverity.getOrDefault("WARNING",
List.of()).size();
-
- writer.println("SUMMARY:");
- writer.println(" Total issues: " + results.size());
- writer.println(" Errors: " + errorCount);
- writer.println(" Warnings: " + warningCount);
- writer.println();
-
- // Write detailed results
- writer.println("DETAILED RESULTS:");
- writer.println();
-
- // Write errors first
- List<LintResult> errors = resultsBySeverity.get("ERROR");
- if (errors != null && !errors.isEmpty()) {
- writer.println("ERRORS:");
- for (LintResult error : errors) {
- writer.println(
- " [ERROR] "
- + error.getRuleId()
- + " - "
- + error.getRuleName()
- + ": "
- + error.getMessage()
- + " (File: "
- + error.getFileName()
- + ")");
- }
- writer.println();
- }
-
- // Write warnings
- List<LintResult> warnings = resultsBySeverity.get("WARNING");
- if (warnings != null && !warnings.isEmpty()) {
- writer.println("WARNINGS:");
- for (LintResult warning : warnings) {
- writer.println(
- " [WARNING] "
- + warning.getRuleId()
- + " - "
- + warning.getRuleName()
- + ": "
- + warning.getMessage()
- + " (File: "
- + warning.getFileName()
- + ")");
- }
- writer.println();
- }
-
- // Write any other severity levels
- for (Map.Entry<String, List<LintResult>> entry :
resultsBySeverity.entrySet()) {
- if (!"ERROR".equals(entry.getKey()) &&
!"WARNING".equals(entry.getKey())) {
- writer.println(entry.getKey() + ":");
- for (LintResult result : entry.getValue()) {
- writer.println(
- " ["
- + result.getSeverity()
- + "] "
- + result.getRuleId()
- + " - "
- + result.getRuleName()
- + ": "
- + result.getMessage()
- + " (File: "
- + result.getFileName()
- + ")");
- }
- writer.println();
- }
- }
- }
-
- writer.println("=== END LINT RESULTS ===");
- }
-
- return resultsFile.getAbsolutePath();
-
- } catch (Exception e) {
- LogChannel.GENERAL.logError("Error writing results to file: " +
e.getMessage(), e);
- return "Error writing to file: " + e.getMessage();
- }
+ LintResultsUi.logSummary(results, projectName);
}
/**
diff --git
a/plugins/misc/lint/src/main/resources/org/apache/hop/lint/messages/messages_en_US.properties
b/plugins/misc/lint/src/main/resources/org/apache/hop/lint/messages/messages_en_US.properties
index 94a2ec2775..cc2dc1aa7b 100644
---
a/plugins/misc/lint/src/main/resources/org/apache/hop/lint/messages/messages_en_US.properties
+++
b/plugins/misc/lint/src/main/resources/org/apache/hop/lint/messages/messages_en_US.properties
@@ -39,7 +39,7 @@ LinterConfigPlugin.Option.LintOnEdit.ToolTip=Re-run lint
checks shortly after yo
LinterConfigPlugin.Option.ShowIndicators.Label=Show Lint Indicators on Canvas
LinterConfigPlugin.Option.ShowIndicators.ToolTip=Show the lint totals overlay
(top-left of the canvas) and the severity badges on transforms/actions in open
pipeline and workflow editors
LinterConfigPlugin.Option.ShowIgnoredMarkers.Label=Mark Ignored Transforms and
Actions
-LinterConfigPlugin.Option.ShowIgnoredMarkers.ToolTip=Draw a muted outline on a
transform or action whose lint findings have been marked as expected, so the
absence of a warning reads as a decision rather than as a check that never ran
+LinterConfigPlugin.Option.ShowIgnoredMarkers.ToolTip=Draw a dashed outline and
a small badge on a transform or action whose lint findings have been marked as
expected, so the absence of a warning reads as a decision rather than as a
check that never ran
LinterConfigPlugin.Option.ConfigFile.Label=Configuration File
LinterConfigPlugin.Option.ConfigFile.ToolTip=Path to hop-lint.yml
configuration file (leave empty for project-specific)
LinterConfigPlugin.Option.PreCommit.Label=Block Git Commits on Lint Failures
@@ -147,11 +147,11 @@ LintSuppressDialog.Label.Findings=Currently reported:
LintSuppressDialog.NoFindings=Nothing is currently reported here. Anything
found later will be ignored as well.
LintSuppressDialog.Scope.AllRules=Ignore every finding on this element, now
and later
LintSuppressDialog.Scope.AllRules.ToolTip=Use this for an element that is
filled in at runtime, for example by metadata injection: whatever a check
reports about it at design time is expected.
-LintSuppressDialog.Scope.ListedRules=Ignore only these rules: {0}
-LintSuppressDialog.Scope.ListedRules.ToolTip=Keep checking this element, but
accept the rules reported right now. A new kind of problem here will still be
reported.
+LintSuppressDialog.Scope.ListedRules=Ignore only the rules reported now
+LintSuppressDialog.Scope.ListedRules.ToolTip=Keep checking this element, but
accept {0}. A new kind of problem here will still be reported.
LintSuppressDialog.Label.Reason=Reason (required):
LintSuppressDialog.ReasonRequired.Title=Reason Required
-LintSuppressDialog.ReasonRequired.Message=Please say why these findings are
expected. It is stored with the pipeline or workflow, so whoever reads it next
knows this was a decision rather than an oversight.
+LintSuppressDialog.ReasonRequired.Message=Please say why these findings are
expected. It is recorded in the project''s hop-lint.yml, so whoever reads it
next knows this was a decision rather than an oversight.
# ---------------------------------------------------------------------------
# Keeping a file or folder out of linting
diff --git
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintResultsViewTest.java
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintResultsViewTest.java
new file mode 100644
index 0000000000..caa9461076
--- /dev/null
+++
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintResultsViewTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.gui.IGc;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+
+/**
+ * Show Lint Results, the ignored marker and the linter settings.
+ *
+ * @see <a href="https://github.com/apache/hop/issues/8735">#8735</a>
+ */
+public class LintResultsViewTest {
+
+ private static LintResult finding(String file, String ruleId) {
+ return new LintResult(ruleId, ruleId, "WARNING", "message " + ruleId,
file);
+ }
+
+ private static List<String> sorted(
+ List<LintResult> results, LintResultGrouping.SortKey key, boolean
ascending) {
+ List<LintResult> copy = new ArrayList<>(results);
+ copy.sort(LintResultGrouping.order(key, ascending));
+ return copy.stream()
+ .map(r -> new java.io.File(r.getFileName()).getName() + " " +
r.getRuleId())
+ .toList();
+ }
+
+ private static final List<LintResult> RESULTS =
+ List.of(
+ finding("/project/load/b.hpl", "STRUCT-003"),
+ finding("/project/main.hwf", "DOC-002"),
+ finding("/project/load/a.hpl", "NAMING-004"),
+ finding("/project/load/b.hpl", "DOC-001"));
+
+ /** Files came in no clear order, main.hwf between two .hpl files, with
rules mixed per file. */
+ @Test
+ public void resultsAreOrderedByFileThenRule() {
+ assertEquals(
+ List.of("a.hpl NAMING-004", "b.hpl DOC-001", "b.hpl STRUCT-003",
"main.hwf DOC-002"),
+ sorted(RESULTS, LintResultGrouping.SortKey.FILE, true));
+ }
+
+ @Test
+ public void resultsCanBeOrderedByRuleEitherWay() {
+ List<String> byRule = sorted(RESULTS, LintResultGrouping.SortKey.RULE,
true);
+ assertEquals(
+ List.of("b.hpl DOC-001", "main.hwf DOC-002", "a.hpl NAMING-004",
"b.hpl STRUCT-003"),
+ byRule);
+ assertEquals(
+ byRule.reversed(), sorted(RESULTS, LintResultGrouping.SortKey.RULE,
false), "reversed");
+ }
+
+ @Test
+ public void errorsComeFirst() {
+ List<String> severities = new ArrayList<>(List.of("INFO", "CUSTOM",
"WARNING", "ERROR"));
+ severities.sort(LintResultGrouping.severityOrder());
+ assertEquals(List.of("ERROR", "WARNING", "INFO", "CUSTOM"), severities);
+ }
+
+ /**
+ * A 1px grey rounded outline was all but the normal transform border, so an
ignored transform
+ * looked like any other.
+ */
+ @Test
+ public void anIgnoredTransformGetsADashedOutlineAndABadge() throws Exception
{
+ IGc gc = mock(IGc.class);
+ when(gc.getMagnification()).thenReturn(1f);
+
+ LintCanvasOverlayHelper.drawIgnoredOverlay(gc, 100, 100, 32, false);
+
+ InOrder order = inOrder(gc);
+ order.verify(gc).setLineStyle(IGc.ELineStyle.DASH);
+ order.verify(gc).drawRoundRectangle(anyInt(), anyInt(), anyInt(),
anyInt(), anyInt(), anyInt());
+ order.verify(gc).setLineStyle(IGc.ELineStyle.SOLID);
+ verify(gc).drawImage(eq(IGc.EImage.INFO_DISABLED), anyInt(), anyInt(),
anyFloat());
+ }
+
+ /**
+ * "Block Commits on Warnings" means nothing while commits are not blocked,
and stayed enabled
+ * when they were not.
+ */
+ @Test
+ public void blockingOnWarningsFollowsBlockingCommits() {
+ Button preCommit = mock(Button.class);
+ Button blockWarnings = mock(Button.class);
+ GuiCompositeWidgets widgets = mock(GuiCompositeWidgets.class);
+ when(widgets.getWidgetsMap())
+ .thenReturn(
+ Map.<String, Control>of(
+ "linter-pre-commit-enabled", preCommit,
+ "linter-pre-commit-block-warnings", blockWarnings));
+
+ when(preCommit.getSelection()).thenReturn(false);
+ LinterConfigPlugin.enableDependentWidgets(widgets);
+ verify(blockWarnings).setEnabled(false);
+
+ when(preCommit.getSelection()).thenReturn(true);
+ LinterConfigPlugin.enableDependentWidgets(widgets);
+ verify(blockWarnings).setEnabled(true);
+ }
+}
diff --git
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressDialogDefaultTest.java
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressDialogDefaultTest.java
new file mode 100644
index 0000000000..9879ce27c5
--- /dev/null
+++
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LintSuppressDialogDefaultTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Ignore Lint Findings dialog.
+ *
+ * <p>It defaulted to ignoring every finding on the element, now and later,
which also silences
+ * rules added afterwards, security ones included.
+ *
+ * @see <a href="https://github.com/apache/hop/issues/8735">#8735</a>
+ */
+@Tag("uitest")
+class LintSuppressDialogDefaultTest extends SwtBotTestBase {
+
+ @Test
+ void ignoringOnlyTheReportedRulesIsTheDefault() {
+ List<LintResult> findings =
+ List.of(
+ new LintResult(
+ "SEC-002", "Hardcoded Password", "ERROR", "hardcoded value",
"/p/load.hpl"));
+ AtomicBoolean listedSelected = new AtomicBoolean();
+ AtomicBoolean allSelected = new AtomicBoolean();
+
+ withDialog(
+ parent -> new LintSuppressDialog(parent, "HTTP client",
findings).open(),
+ bot -> {
+ SWTBot dialog = bot.shell("Ignore Lint Findings").bot();
+ listedSelected.set(dialog.radio("Ignore only the rules reported
now").isSelected());
+ allSelected.set(
+ dialog.radio("Ignore every finding on this element, now and
later").isSelected());
+ dialog.button(buttonLabel("System.Button.Cancel")).click();
+ });
+
+ assertTrue(listedSelected.get(), "only the reported rules");
+ assertFalse(allSelected.get(), "not everything, now and later");
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
index f6f3018886..7a86244409 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
@@ -144,6 +144,20 @@ public class ConfigPluginOptionsTab {
wPluginsTab.setControl(wPluginsTabComp);
}
+ /**
+ * Let a plugin that listens to its widgets save changes, and tell it the
widgets are filled.
+ * Filling the widgets fires no events, so without that call the plugin
cannot set up which
+ * widgets are enabled until the user changes something.
+ */
+ static void attachWidgetsListener(GuiCompositeWidgets compositeWidgets,
Object pluginSourceData) {
+ if (pluginSourceData instanceof IGuiPluginCompositeWidgetsListener
listener) {
+ // This listener saves the changed values immediately, so we don't have
to worry about that.
+ //
+ compositeWidgets.setWidgetsListener(listener);
+ listener.widgetsPopulated(compositeWidgets);
+ }
+ }
+
/**
* Show settings for a specific plugin. This is called from the tree
selection in
* ConfigurationPerspective.
@@ -181,12 +195,7 @@ public class ConfigPluginOptionsTab {
compositeWidgets.createCompositeWidgets(
pluginSourceData, null, wPluginsComp, GUI_WIDGETS_PARENT_ID, null);
compositeWidgets.setWidgetsContents(pluginSourceData, wPluginsComp,
GUI_WIDGETS_PARENT_ID);
- if (pluginSourceData
- instanceof IGuiPluginCompositeWidgetsListener
iGuiPluginCompositeWidgetsListener) {
- // This listener saves the changed values immediately, so we don't have
to worry about that.
- //
- compositeWidgets.setWidgetsListener(iGuiPluginCompositeWidgetsListener);
- }
+ attachWidgetsListener(compositeWidgets, pluginSourceData);
wPluginsComp.layout();
wPluginsComp.pack();
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTabTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTabTest.java
new file mode 100644
index 0000000000..eafe2fcba7
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTabTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ui.hopgui.perspective.configuration.tabs;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+
+class ConfigPluginOptionsTabTest {
+
+ /**
+ * Filling the widgets fires no events, so a plugin only learns they are
filled from
+ * widgetsPopulated. Without it, widgets that depend on others kept the
wrong enabled state until
+ * the first change.
+ */
+ @Test
+ void aListeningPluginIsToldItsWidgetsAreFilled() {
+ GuiCompositeWidgets widgets = mock(GuiCompositeWidgets.class);
+ IGuiPluginCompositeWidgetsListener plugin =
mock(IGuiPluginCompositeWidgetsListener.class);
+
+ ConfigPluginOptionsTab.attachWidgetsListener(widgets, plugin);
+
+ InOrder order = inOrder(widgets, plugin);
+ order.verify(widgets).setWidgetsListener(plugin);
+ order.verify(plugin).widgetsPopulated(widgets);
+ }
+
+ @Test
+ void aPluginThatDoesNotListenIsLeftAlone() {
+ GuiCompositeWidgets widgets = mock(GuiCompositeWidgets.class);
+
+ ConfigPluginOptionsTab.attachWidgetsListener(widgets, new Object());
+
+ verify(widgets, never()).setWidgetsListener(any());
+ }
+}