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 965c847b39 Fixes #8733 : Recognise Hop Gui's default names, fix
SQL-002, add IS_… (#8743)
965c847b39 is described below
commit 965c847b397fa0f194bae1398b04a1d9cebedc37
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Oct 8 19:32:57 2026 +0200
Fixes #8733 : Recognise Hop Gui's default names, fix SQL-002, add IS_…
(#8743)
* Fixes #8733 : Recognise Hop Gui's default names, fix SQL-002, add
IS_EMPTY and read composed rules without type: custom
* Issue #8733 : Report a missing or negative row limit in SQL-002, describe
the names NAMING-004 recognises, correct the IS_EMPTY example
---
.../modules/ROOT/pages/linting/lint-rules.adoc | 20 +-
.../org/apache/hop/lint/CustomRuleExecutor.java | 96 +++++++++-
.../main/java/org/apache/hop/lint/LintCommand.java | 12 ++
.../java/org/apache/hop/lint/RuleCondition.java | 1 +
.../org/apache/hop/lint/RuleManagerDialog.java | 4 +-
.../java/org/apache/hop/lint/RuleTargetFields.java | 1 +
.../hop/lint/registry/YamlRulePackParser.java | 15 +-
.../misc/lint/src/main/resources/hop-lint-core.yml | 14 +-
.../hop/lint/messages/messages_en_US.properties | 6 +-
.../org/apache/hop/lint/CoreRulePackFixesTest.java | 207 +++++++++++++++++++++
10 files changed, 355 insertions(+), 21 deletions(-)
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 7486c4fc12..94ce4de96d 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
@@ -94,6 +94,9 @@ Scope such a rule with `appliesTo`, naming the metadata key —
the folder name
|`NOT_EMPTY`, `NOT_NULL`
|has a value
+|`IS_EMPTY`
+|has no value
+
|`NOT_EMPTY_COLLECTION`
|is a collection with at least one entry
@@ -140,7 +143,7 @@ Scope such a rule with `appliesTo`, naming the metadata key
— the folder name
|Whether any other file in the project calls this pipeline or workflow. Only a
project lint can answer this, see <<project-rules>>
|`hasDefaultName`
-|The transform or action still carries its auto-generated name
+|The transform or action still carries the name Hop Gui gave it: the plugin's
name, such as `Table input`, followed by a number when that name was taken, as
in `Dummy (do nothing) 2`. The plugin name is the one of the language Hop runs
in. A workflow's Start action never counts
|`isDummy`
|The transform is a Dummy
@@ -183,15 +186,22 @@ Some problems are only worth reporting when several
things are wrong together, a
- TableInput
allOf:
- targetField: sql
- condition: MATCHES_PATTERN
+ condition: NOT_MATCHES_PATTERN
conditionValue: "(?is).*\\bselect\\s+\\*.*"
- targetField: rowLimit
- condition: NOT_EMPTY
+ condition: NOT_MATCHES_PATTERN
+ conditionValue: "^\\s*([+-]?0*|-\\d+)\\s*$"
name: "Unbounded SELECT *"
description: "A Table Input that selects every column and sets no row
limit"
----
-`allOf` reports only when *every* clause is broken, `anyOf` when *at least
one* is.
+As everywhere else, each condition says what is *required*, and the rule
reports when that is not met.
+`allOf` reports only when *every* condition is not met, `anyOf` when *at least
one* is not.
+The rule above reports a Table Input whose SQL selects every column *and*
whose row limit is unset, `0` or negative, which Hop reads as no limit.
+
+To report something that is set, require it to be empty: a plain `http://` URL
with a login is `url` `MATCHES_PATTERN` `^https://.*` (not met for `http://`)
together with `httpLogin` `IS_EMPTY` (not met when a login is set), under
`allOf`.
+
+A rule with `allOf` or `anyOf` and a `target` is a rule definition even
without `type: custom`.
Each clause names its own field, condition and value, and the finding says
which clauses were broken and what the values actually were.
A rule with neither block behaves exactly as before, so nothing that was
written against the single-condition form needs changing.
@@ -298,7 +308,7 @@ Hop's core rule pack is deliberately small. A rule is
enabled by default only wh
|`NAMING-004`
|WARNING
-|A transform still carrying its auto-generated name
+|A transform still carrying the name Hop Gui gave it, such as `Table input` or
`Table input 2`
|`HOP-CHECK`
|WARNING
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/CustomRuleExecutor.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/CustomRuleExecutor.java
index faeec3e56e..65b133bb7e 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/CustomRuleExecutor.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/CustomRuleExecutor.java
@@ -20,11 +20,19 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.regex.Pattern;
import org.apache.hop.core.database.DatabaseMeta;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.plugins.ActionPluginType;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.IPluginType;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.plugins.TransformPluginType;
import org.apache.hop.core.util.Utils;
import org.apache.hop.metadata.api.HopMetadata;
import org.apache.hop.metadata.api.HopMetadataProperty;
@@ -483,6 +491,41 @@ public class CustomRuleExecutor {
return null;
}
+ /**
+ * The fields every transform has, whatever its plugin, with their types.
The linter works them
+ * out itself rather than reading them from the plugin, so {@code hop lint
--list-fields} lists
+ * them from here. Keep in step with {@link #extractFieldFromTransform}.
+ */
+ public static final Map<String, String> TRANSFORM_FIELDS =
+ orderedFields(
+ "name", "String",
+ "description", "String",
+ "pluginId", "String",
+ "copies", "int",
+ "isDummy", "boolean",
+ "hasDefaultName", "boolean",
+ "isOrphaned", "boolean",
+ "isBlockingTransform", "boolean");
+
+ /**
+ * As {@link #TRANSFORM_FIELDS}, for actions. Keep in step with {@link
#extractFieldFromAction}.
+ */
+ public static final Map<String, String> ACTION_FIELDS =
+ orderedFields(
+ "name", "String",
+ "description", "String",
+ "pluginId", "String",
+ "hasDefaultName", "boolean",
+ "isOrphaned", "boolean");
+
+ private static Map<String, String> orderedFields(String... namesAndTypes) {
+ Map<String, String> fields = new LinkedHashMap<>();
+ for (int i = 0; i < namesAndTypes.length; i += 2) {
+ fields.put(namesAndTypes[i], namesAndTypes[i + 1]);
+ }
+ return Collections.unmodifiableMap(fields);
+ }
+
/** Extract field value from a transform using reflection */
private static Object extractFieldFromTransform(
TransformMeta transformMeta, String fieldName, CustomLintRule rule) {
@@ -501,7 +544,10 @@ public class CustomRuleExecutor {
case "isDummy":
return
"Dummy".equalsIgnoreCase(transformMeta.getTransformPluginId());
case "hasDefaultName":
- return hasDefaultGeneratedName(transformMeta.getName());
+ return hasDefaultGeneratedName(
+ transformMeta.getName(),
+ transformMeta.getTransformPluginId(),
+ TransformPluginType.class);
case "isOrphaned":
return SUBJECT.get() instanceof PipelineMeta pipeline
? isOrphaned(transformMeta, pipeline.getPipelineHops(),
pipeline.getTransforms())
@@ -534,7 +580,12 @@ public class CustomRuleExecutor {
case "pluginId":
return actionMeta.getAction().getPluginId();
case "hasDefaultName":
- return hasDefaultGeneratedName(actionMeta.getName());
+ // Every workflow starts at Start; there is no better name for it.
+ return !actionMeta.isStart()
+ && hasDefaultGeneratedName(
+ actionMeta.getName(),
+ actionMeta.getAction().getPluginId(),
+ ActionPluginType.class);
case "isOrphaned":
return SUBJECT.get() instanceof WorkflowMeta workflow
? isOrphaned(actionMeta, workflow.getWorkflowHops(),
workflow.getActions())
@@ -553,11 +604,35 @@ public class CustomRuleExecutor {
}
}
- private static boolean hasDefaultGeneratedName(String name) {
+ /**
+ * Whether a transform or action still has the name Hop Gui gave it: the
plugin's name, such as
+ * "Table input", followed by a number when that name was taken, as in
"Dummy (do nothing) 2".
+ *
+ * <p>Only "Transform 1" and "Action 1" used to count, and Hop Gui never
generates those. The
+ * plugin's name is the one of the language Hop runs in, so a name Hop Gui
gave in another
+ * language is not recognised.
+ */
+ static boolean hasDefaultGeneratedName(
+ String name, String pluginId, Class<? extends IPluginType> pluginType) {
if (Utils.isEmpty(name)) {
return false;
}
- return name.matches("(?i)(Transform|Action)\\s+\\d+");
+ String trimmed = name.trim();
+ if (trimmed.matches("(?i)(Transform|Action)\\s+\\d+")) {
+ return true;
+ }
+ if (Utils.isEmpty(pluginId)) {
+ return false;
+ }
+ IPlugin plugin = PluginRegistry.getInstance().findPluginWithId(pluginType,
pluginId);
+ if (plugin == null || Utils.isEmpty(plugin.getName())) {
+ return false;
+ }
+ String pluginName = plugin.getName().trim();
+ return trimmed.equalsIgnoreCase(pluginName)
+ || (trimmed.length() > pluginName.length() + 1
+ && trimmed.substring(0,
pluginName.length()).equalsIgnoreCase(pluginName)
+ && trimmed.substring(pluginName.length()).matches("\\s+\\d+"));
}
/**
@@ -1000,7 +1075,12 @@ public class CustomRuleExecutor {
// fire on it. Hop returns null for an unset description, which is
exactly the case
// "description NOT_EMPTY" exists to catch; treating null as passing
made those rules
// fire only on a description explicitly set to "".
- return condition == RuleCondition.NOT_NULL || condition ==
RuleCondition.NOT_EMPTY;
+ if (condition != RuleCondition.NOT_MATCHES_PATTERN) {
+ return condition == RuleCondition.NOT_NULL || condition ==
RuleCondition.NOT_EMPTY;
+ }
+ // A pattern that forbids blank values, such as SQL-002's "no row
limit", must also see an
+ // unset value, so NOT_MATCHES_PATTERN reads null as "".
+ fieldValue = "";
}
// Handle null condition value for conditions that don't need it
@@ -1024,6 +1104,12 @@ public class CustomRuleExecutor {
case NOT_EMPTY:
return Utils.isEmpty(fieldValue.toString());
+ case IS_EMPTY:
+ // The opposite of NOT_EMPTY, so that a clause can require that a
field is not set. Under
+ // allOf, url MATCHES_PATTERN ^https://.* and httpLogin IS_EMPTY
report a plain http:// URL
+ // with a login.
+ return !Utils.isEmpty(fieldValue.toString());
+
case NOT_NULL:
return fieldValue == null;
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
index f481f76ae8..223e6320a1 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
@@ -532,6 +532,18 @@ public class LintCommand implements Callable<Integer>,
IHopCommand, IHasHopMetad
System.out.printf(" %-40s %s%n", entry.getKey(), entry.getValue());
}
System.out.println();
+ // hasDefaultName and the like are worked out by the linter, not read from
the plugin, and
+ // were missing from this list although a rule can use them on any
transform or action.
+ Map<String, String> common =
+ "transform".equals(kind)
+ ? CustomRuleExecutor.TRANSFORM_FIELDS
+ : CustomRuleExecutor.ACTION_FIELDS;
+ System.out.println("Fields on every " + kind + ":");
+ System.out.println();
+ for (Map.Entry<String, String> entry : common.entrySet()) {
+ System.out.printf(" %-40s %s%n", entry.getKey(), entry.getValue());
+ }
+ System.out.println();
System.out.println(
"Nested values are reached with a dotted path, for example
fileSettings.fileName.");
return 0;
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleCondition.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleCondition.java
index 36e7a29083..5ac953f67e 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleCondition.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleCondition.java
@@ -26,6 +26,7 @@ public enum RuleCondition {
// String conditions
NOT_EMPTY("Not Empty", "Field must not be empty or null", false),
NOT_NULL("Not Null", "Field must not be null", false),
+ IS_EMPTY("Is Empty", "Field must be empty or not set", false),
NO_HARDCODED("No Hardcoded Values", "Field must use variables, not hardcoded
values", false),
MATCHES_PATTERN("Matches Pattern", "Field must match specified regex
pattern", true),
NOT_MATCHES_PATTERN(
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleManagerDialog.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleManagerDialog.java
index 101b8a3b59..a9f673b649 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleManagerDialog.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleManagerDialog.java
@@ -294,7 +294,9 @@ public class RuleManagerDialog extends Dialog {
if (rule.isComposed()) {
item.setText(
4, rule.getClauses().size() + " fields (" +
rule.getCombinator().getYamlKey() + ")");
- item.setText(5, rule.getCombinator() == RuleCombinator.ALL_OF ? "All
of" : "Any of");
+ // A condition says what is required; the rule reports when the
conditions are not met.
+ item.setText(
+ 5, rule.getCombinator() == RuleCombinator.ALL_OF ? "All not met" :
"Any not met");
item.setText(6, "");
} else {
item.setText(5, rule.getCondition() != null ?
rule.getCondition().getDisplayName() : "");
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleTargetFields.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleTargetFields.java
index 4c7185f444..c0faa829d8 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleTargetFields.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/RuleTargetFields.java
@@ -225,6 +225,7 @@ public class RuleTargetFields {
return Arrays.asList(
RuleCondition.NOT_EMPTY,
RuleCondition.NOT_NULL,
+ RuleCondition.IS_EMPTY,
RuleCondition.NO_HARDCODED,
RuleCondition.MATCHES_PATTERN,
RuleCondition.NOT_MATCHES_PATTERN,
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/registry/YamlRulePackParser.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/registry/YamlRulePackParser.java
index 9e00b9e16b..d809b28e1d 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/registry/YamlRulePackParser.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/registry/YamlRulePackParser.java
@@ -399,7 +399,20 @@ public final class YamlRulePackParser {
if ("custom".equals(type)) {
return true;
}
- return ruleData.containsKey("target") && ruleData.containsKey("condition");
+ if (!ruleData.containsKey("target")) {
+ return false;
+ }
+ // A composed rule has its conditions inside allOf or anyOf rather than at
the top. Without
+ // type: custom it was taken for an override of a rule that does not
exist, and dropped.
+ if (ruleData.containsKey("condition")) {
+ return true;
+ }
+ for (RuleCombinator combinator : RuleCombinator.values()) {
+ if (ruleData.containsKey(combinator.getYamlKey())) {
+ return true;
+ }
+ }
+ return false;
}
public static boolean isNativeRuleDefinition(Map<String, Object> ruleData) {
diff --git a/plugins/misc/lint/src/main/resources/hop-lint-core.yml
b/plugins/misc/lint/src/main/resources/hop-lint-core.yml
index 423f93a716..629626f6e6 100644
--- a/plugins/misc/lint/src/main/resources/hop-lint-core.yml
+++ b/plugins/misc/lint/src/main/resources/hop-lint-core.yml
@@ -137,7 +137,7 @@ rules:
targetField: hasDefaultName
condition: MUST_BE_FALSE
name: "Default Transform Name"
- description: "Auto-generated names such as 'Transform 1' make logs and
error messages hard to trace back to the pipeline"
+ description: "Names Hop Gui assigns, such as 'Table input' or 'Table input
2', make logs and error messages hard to trace back to the pipeline"
# ------------------------------------------------------------------
# Hop's own verify remarks.
@@ -303,9 +303,10 @@ rules:
name: "Workflow Naming Convention"
description: "Example naming rule: UPPER_SNAKE_CASE workflow names.
Replace conditionValue with your own pattern"
- # A worked example of a rule that checks more than one thing. allOf reports
only when every
- # clause is broken, so a SELECT * that is bounded by a row limit is left
alone; anyOf would
- # report when either was. Each clause names its own field, condition and
value.
+ # A worked example of a rule that checks more than one thing. Each condition
says what is
+ # required, and allOf reports only when every one of them is not met: here,
when the SQL selects
+ # every column and the row limit is unset, 0 or negative, which Hop reads as
no limit. anyOf would report
+ # when either was not met. Each clause names its own field, condition and
value.
SQL-002:
type: custom
enabled: false
@@ -315,10 +316,11 @@ rules:
- TableInput
allOf:
- targetField: sql
- condition: MATCHES_PATTERN
+ condition: NOT_MATCHES_PATTERN
conditionValue: "(?is).*\\bselect\\s+\\*.*"
- targetField: rowLimit
- condition: NOT_EMPTY
+ condition: NOT_MATCHES_PATTERN
+ conditionValue: "^\\s*([+-]?0*|-\\d+)\\s*$"
name: "Unbounded SELECT *"
description: "A Table Input that selects every column and sets no row
limit reads more than it needs, and breaks when the table gains a column"
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 a1b319030a..94a2ec2775 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
@@ -122,9 +122,9 @@ LintResultsPanel.Details.GroupSelected=Group selected.
Select a specific issue t
# ---------------------------------------------------------------------------
PreCommitLintExtension.Dialog.CommitBlocked.Title=Commit Blocked - Lint {0}+
Issues
PreCommitLintExtension.CommitBlocked.Reason={0} lint issue(s) at {1} or above.
Open Tools > Lint > Show Lint Results for the full list.
-RuleBuilderDialog.Label.Match=Match:
-RuleBuilderDialog.Combinator.AllOf=All of the clauses below
-RuleBuilderDialog.Combinator.AnyOf=Any of the clauses below
+RuleBuilderDialog.Label.Match=Report when:
+RuleBuilderDialog.Combinator.AllOf=every condition below is not met
+RuleBuilderDialog.Combinator.AnyOf=any condition below is not met
RuleBuilderDialog.Button.AddClause=Add clause
RuleBuilderDialog.Button.RemoveClause=Remove clause
LintResultsPanel.Shell.TitleForFolder=Hop Lint Results \u2014 folder {0}
diff --git
a/plugins/misc/lint/src/test/java/org/apache/hop/lint/CoreRulePackFixesTest.java
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/CoreRulePackFixesTest.java
new file mode 100644
index 0000000000..f74a5674a4
--- /dev/null
+++
b/plugins/misc/lint/src/test/java/org/apache/hop/lint/CoreRulePackFixesTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.plugins.TransformPluginType;
+import org.apache.hop.lint.registry.RuleRegistry;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.apache.hop.workflow.actions.start.ActionStart;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+/**
+ * Fixes to the core rule pack and to how rules are read and evaluated.
+ *
+ * @see <a href="https://github.com/apache/hop/issues/8733">#8733</a>
+ */
+public class CoreRulePackFixesTest {
+
+ @BeforeAll
+ static void loadPlugins() throws Exception {
+ HopEnvironment.init();
+ }
+
+ private static CustomLintRule coreRule(String id) {
+ CustomLintRule rule =
+ RuleRegistry.getInstance().resolve(null).getRules().stream()
+ .filter(r -> id.equals(r.generateRuleId()))
+ .findFirst()
+ .orElseThrow()
+ .copy();
+ rule.setEnabled(true);
+ return rule;
+ }
+
+ // ------------------------------------------------------------------
NAMING-004
+
+ /**
+ * Hop Gui names a new transform after its plugin, with a number when the
name is taken. Only
+ * "Transform 1" counted, which Hop Gui never generates.
+ */
+ @Test
+ public void theNamesHopGuiGeneratesAreDefaultNames() {
+ for (String name :
+ List.of(
+ "Dummy (do nothing)", "Dummy (do nothing) 2", "dummy (DO nothing)
12", "Transform 1")) {
+ assertTrue(
+ CustomRuleExecutor.hasDefaultGeneratedName(name, "Dummy",
TransformPluginType.class),
+ name);
+ }
+ for (String name : List.of("Load customers", "Dummy (do nothing) copy",
"Dummy")) {
+ assertFalse(
+ CustomRuleExecutor.hasDefaultGeneratedName(name, "Dummy",
TransformPluginType.class),
+ name);
+ }
+ }
+
+ @Test
+ public void naming004ReportsADefaultTransformName() {
+ TransformMeta dummy = new TransformMeta();
+ dummy.setName("Dummy (do nothing) 2");
+ dummy.setTransformPluginId("Dummy");
+
+ assertEquals(
+ 1, CustomRuleExecutor.executeRule(coreRule("NAMING-004"), dummy,
"/tmp/p.hpl").size());
+ }
+
+ /** Every workflow starts at Start, and there is no better name for it. */
+ @Test
+ public void theStartActionNeverHasADefaultName() {
+ ActionMeta start = new ActionMeta(new ActionStart());
+ start.setName("Start");
+ CustomLintRule rule = coreRule("NAMING-004");
+ rule.setTarget(RuleTarget.ACTION);
+
+ assertTrue(CustomRuleExecutor.executeRule(rule, start,
"/tmp/w.hwf").isEmpty());
+ }
+
+ @Test
+ public void listFieldsShowsTheFieldsEveryTransformHas() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PrintStream systemOut = System.out;
+ try {
+ System.setOut(new PrintStream(out, true, StandardCharsets.UTF_8));
+ new CommandLine(new LintCommand()).execute("--list-fields", "Dummy");
+ } finally {
+ System.setOut(systemOut);
+ }
+
+ String listing = out.toString(StandardCharsets.UTF_8);
+ assertTrue(listing.contains("Fields on every transform:"), listing);
+ assertTrue(listing.contains("hasDefaultName"), listing);
+ assertTrue(listing.contains("isOrphaned"), listing);
+ }
+
+ // ------------------------------------------------------------------
SQL-002 and allOf
+
+ /** Stands in for a Table Input: the two fields SQL-002 reads. */
+ public static class FakeTableInputMeta extends BaseTransformMeta {
+ private String sql;
+ private String rowLimit;
+
+ FakeTableInputMeta(String sql, String rowLimit) {
+ this.sql = sql;
+ this.rowLimit = rowLimit;
+ }
+ }
+
+ private static boolean sql002Reports(String sql, String rowLimit) {
+ TransformMeta tableInput =
+ new TransformMeta("TableInput", "Read customers", new
FakeTableInputMeta(sql, rowLimit));
+ return !CustomRuleExecutor.executeRule(coreRule("SQL-002"), tableInput,
"/tmp/p.hpl").isEmpty();
+ }
+
+ /** Both clauses were the wrong way round, so the rule could not fire on
what it describes. */
+ @Test
+ public void sql002ReportsOnlyAnUnboundedSelectStar() {
+ assertTrue(sql002Reports("SELECT * FROM customers", "0"), "SELECT *, no
limit");
+ assertTrue(sql002Reports("select *\nfrom customers", ""), "SELECT *, empty
limit");
+ assertTrue(sql002Reports("SELECT * FROM customers", null), "SELECT *,
limit never set");
+ assertTrue(sql002Reports("SELECT * FROM customers", "-1"), "SELECT *,
negative limit");
+ assertTrue(sql002Reports("SELECT * FROM customers", "+0"), "SELECT *,
limit +0");
+ assertFalse(sql002Reports("SELECT * FROM customers", "100"), "SELECT *,
limit 100");
+ assertFalse(sql002Reports("SELECT * FROM customers", "${LIMIT}"), "limit
from a variable");
+ assertFalse(sql002Reports("SELECT id, name FROM customers", "0"), "named
columns");
+ }
+
+ @Test
+ public void isEmptyRequiresTheFieldToBeUnset() {
+ CustomLintRule rule = new CustomLintRule();
+ rule.setEnabled(true);
+ rule.setSeverity("WARNING");
+ rule.setTarget(RuleTarget.PIPELINE);
+ rule.setTargetField("description");
+ rule.setCondition(RuleCondition.IS_EMPTY);
+
+ PipelineMeta pipeline = new PipelineMeta();
+ pipeline.setName("load");
+ assertTrue(CustomRuleExecutor.executeRule(rule, pipeline,
"/tmp/p.hpl").isEmpty(), "unset");
+
+ pipeline.setDescription("Loads the customers");
+ assertEquals(1, CustomRuleExecutor.executeRule(rule, pipeline,
"/tmp/p.hpl").size(), "set");
+ }
+
+ /**
+ * An allOf rule written without type: custom was taken for an override of a
rule that does not
+ * exist, and never ran.
+ */
+ @Test
+ public void aComposedRuleNeedsNoTypeCustom() throws Exception {
+ File projectYaml = File.createTempFile("hop-lint", ".yml");
+ Files.writeString(
+ projectYaml.toPath(),
+ """
+ rules:
+ HTTP-001:
+ target: TRANSFORM
+ severity: WARNING
+ allOf:
+ - targetField: url
+ condition: MATCHES_PATTERN
+ conditionValue: "^https://.*"
+ - targetField: httpLogin
+ condition: IS_EMPTY
+ """);
+ try {
+ CustomLintRule rule =
+ RuleRegistry.getInstance().resolve(projectYaml).getRules().stream()
+ .filter(r -> "HTTP-001".equals(r.generateRuleId()))
+ .findFirst()
+ .orElseThrow();
+
+ assertEquals(2, rule.getClauses().size());
+ assertEquals(RuleCombinator.ALL_OF, rule.getCombinator());
+ } finally {
+ Files.deleteIfExists(projectYaml.toPath());
+ }
+ }
+}