From 310248563405af29fa7563f6096d0f2d1f13e1a7 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Tue, 24 Sep 2024 14:16:23 +0200 Subject: [PATCH] Allow to use wildcard characters in certain places within a package part --- readme/README.md | 13 +- readme/RELEASE_NOTES.md | 9 +- .../analyze/PackagePattern.java | 26 +++- .../analyze/PackagePatternSpecifityTest.java | 12 +- .../analyze/PackagePatternTest.java | 139 ++++++++++++------ 5 files changed, 132 insertions(+), 67 deletions(-) diff --git a/readme/README.md b/readme/README.md index 861fc626..189f0829 100644 --- a/readme/README.md +++ b/readme/README.md @@ -715,19 +715,20 @@ considerably slower. ## Package Patterns Package patterns are dot separated strings that can be compared case sensitively part by part. Every part must adhere to -the java identifier rules with the exception of a some special literals: +the java identifier rules except some special literals: 1. `*` matches every package part but exactly one. 2. `**` matches multiple package parts but at least one. 3. `'*'` matches a literal `*` in an import statement. +4. `*SomeString` matches every package part that ends with `SomeString`. +5. `SomeString*` matches every package part that starts with `SomeString`. +6. `*SomeString*` matches every package part that contains `SomeString`. The pattern `java.util.*` matches `java.util.ArrayList` but not `java.util.regex.Pattern`. -Likewise the pattern `java.util.**` matches all classes and subclasses contained in -`java.util`. Double wildcards are supported everywhere within a pattern. `**.DumbName` -would match every import which ends in `DumbName`. Wildcards are forbidden to be used in -combination with other characters within a single part, like in `com.foo**`. Also parts -within a package must not be empty like in `foo..bar`. +Likewise, the pattern `java.util.**` matches all classes and subclasses contained in`java.util`. +Double wildcards are supported everywhere within a pattern. `**.*DumbName` would match every import which ends in `DumbName`. +Parts within a package must not be empty like in `foo..bar`. If a pattern does not contain any wildcards, matching degrades to a simple String comparison. diff --git a/readme/RELEASE_NOTES.md b/readme/RELEASE_NOTES.md index dffbea9f..ac1ff1dc 100644 --- a/readme/RELEASE_NOTES.md +++ b/readme/RELEASE_NOTES.md @@ -1,14 +1,9 @@ [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=@project.version@&color=blue)](https://search.maven.org/artifact/@project.groupId@/restrict-imports-enforcer-rule/@project.version@/jar) [![Gradle Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/@project.pluginId@?versionSuffix=@project.version@)](https://plugins.gradle.org/plugin/@project.pluginId@/@project.version@) -> [!NOTE] -> This is the first release after migrating our build to Gradle and which uses shaded dependencies. -> If you encounter any irregularities with this version, please do not hesitate to file an issue. - ### Features -* [#38](https://github.com/skuzzle/restrict-imports-enforcer-rule/issues/38) Dependencies are shaded into plugin artifacts -* [#59](https://github.com/skuzzle/restrict-imports-enforcer-rule/issues/59) Provide a Gradle plugin -* [#118](https://github.com/skuzzle/restrict-imports-enforcer-rule/issues/118) Print absolute paths in exception messages to make IntelliJ render clickable links +* [#177](https://github.com/skuzzle/restrict-imports-enforcer-rule/issues/177) Support matching prefix, suffix and infix +parts within a package pattern. ### Dependency coordinates
diff --git a/restrict-imports-enforcer-rule-core/src/main/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePattern.java b/restrict-imports-enforcer-rule-core/src/main/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePattern.java index 08d18c72..e0ca3455 100644 --- a/restrict-imports-enforcer-rule-core/src/main/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePattern.java +++ b/restrict-imports-enforcer-rule-core/src/main/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePattern.java @@ -68,9 +68,9 @@ private void checkCharacters(String original, String part, int partIndex) { if (part.isEmpty()) { throw new IllegalArgumentException(String.format("The pattern '%s' contains an empty part", original)); - } else if ("*".equals(part) || "**".equals(part) || "'*'".equals(part)) { + } else if (isValidWildcardPart(part)) { return; - } else if (part.contains("*")) { + } else if (part.contains("*") && (part.indexOf('*') > 1 || part.indexOf('*') < part.length() - 2)) { throw new IllegalArgumentException(String.format( "The pattern '%s' contains a part which mixes wildcards and normal characters", original)); } else if (partIndex == 0 && "static".equals(part)) { @@ -91,6 +91,17 @@ private void checkCharacters(String original, String part, int partIndex) { } } + private boolean isValidWildcardPart(String patternPart) { + if ("*".equals(patternPart) || "**".equals(patternPart) || "'*'".equals(patternPart)) { + return true; + } + if (patternPart.startsWith("*") || patternPart.endsWith("*")) { + final String substring = patternPart.substring(1, patternPart.length() - 1); + return !substring.contains("*"); + } + return false; + } + /** * Parses each string of the given collection into a {@link PackagePattern} and * returns them in a list. @@ -193,6 +204,15 @@ private static boolean matchParts(String patternPart, String matchPart) { return true; } else if ("'*'".equals(patternPart)) { return matchPart.equals("*"); + } else if (patternPart.startsWith("*") && patternPart.endsWith("*")) { + final String infix = patternPart.substring(1, patternPart.length() - 1); + return matchPart.contains(infix); + } else if (patternPart.startsWith("*")) { + final String suffix = patternPart.substring(1); + return matchPart.endsWith(suffix); + } else if (patternPart.endsWith("*")) { + final String prefix = patternPart.substring(0, patternPart.length() - 1); + return matchPart.startsWith(prefix); } return patternPart.equals(matchPart); } @@ -228,7 +248,7 @@ public int compareTo(PackagePattern other) { private int specificityOf(String part) { if (part.equals("**")) { return 0; - } else if (part.equals("*")) { + } else if (part.contains("*")) { return 1; } return 2; diff --git a/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternSpecifityTest.java b/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternSpecifityTest.java index c0f9a6ca..a3f0741f 100644 --- a/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternSpecifityTest.java +++ b/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternSpecifityTest.java @@ -35,6 +35,12 @@ private static SpecifityTest expect(String moreSpecific) { expect("*").toBeMoreSpecificThan("**"), expect("de.**").toBeMoreSpecificThan("**"), expect("de.xyz.*").toBeMoreSpecificThan("de.xyz.**"), + expect("de.xyz.Foo").toBeMoreSpecificThan("de.xyz.Foo*"), + expect("de.xyz.Foo").toBeMoreSpecificThan("de.xyz.*Foo"), + expect("de.xyz.Foo").toBeMoreSpecificThan("de.xyz.*Foo*"), + expect("de.xyz.*Foo").toBeMoreSpecificThan("de.*xyz.*Foo"), + expect("de.x.y.z.*Foo").toBeMoreSpecificThan("de.*xyz.*Foo"), + expect("de.*.xyz").toBeMoreSpecificThan("de.**.xyz"), expect("de").toBeMoreSpecificThan("*"), expect("de").toBeMoreSpecificThan("**"), @@ -50,7 +56,7 @@ private static SpecifityTest expect(String moreSpecific) { expect("*.xyz").toBeMoreSpecificThan("**.xyz")); @TestFactory - Stream testCompareToSelf() throws Exception { + Stream testCompareToSelf() { return patterns.stream() .map(pattern -> DynamicTest.dynamicTest(String.format( "%s should be more specific than itself", pattern.moreSpecific), @@ -64,7 +70,7 @@ Stream testCompareToSelf() throws Exception { } @TestFactory - Stream testCompareSpecificty() throws Exception { + Stream testCompareSpecificty() { return patterns.stream() .map(pattern -> DynamicTest.dynamicTest(String.format( "%s should be more specific than %s", pattern.moreSpecific, @@ -80,7 +86,7 @@ Stream testCompareSpecificty() throws Exception { } @TestFactory - Stream testCompareSpecifictyReverse() throws Exception { + Stream testCompareSpecifictyReverse() { return patterns.stream() .map(pattern -> DynamicTest.dynamicTest(String.format( "%s should not be more specific than %s", pattern.lessSpecific, diff --git a/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternTest.java b/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternTest.java index 452c732f..e1cd737e 100644 --- a/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternTest.java +++ b/restrict-imports-enforcer-rule-core/src/test/java/de/skuzzle/enforcer/restrictimports/analyze/PackagePatternTest.java @@ -32,167 +32,161 @@ void testMatchLiteralAsteriskWithWildCard() { } @Test - public void testNull() throws Exception { + void testNull() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse(null)); } @Test - public void testNull2() throws Exception { + void testNull2() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parseAll(null)); } @Test - public void testMisplacedDoubleWildcardInfix() throws Exception { + void testMisplacedDoubleWildcardInfix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("foo.xyz**abc")); } @Test - public void testMisplacedSingleWildcardInfix() throws Exception { + void testMisplacedSingleWildcardInfix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("foo.xyz*abc")); } @Test - public void testMisplacedDoubleWildcardPrefix() throws Exception { + void testMisplacedDoubleWildcardPrefix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("foo.**abc")); } @Test - public void testMisplacedDoubleWildcardSuffix() throws Exception { + void testMisplacedDoubleWildcardSuffix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("foo.abc**")); } @Test - public void testMisplacedSingleWildcardPrefix() throws Exception { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> PackagePattern.parse("foo.*abc")); - } - - @Test - public void testMisplacedSingleWildcardSuffix() throws Exception { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> PackagePattern.parse("foo.abc*")); - } - - @Test - public void testEmptyPartInfix() throws Exception { + void testEmptyPartInfix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("foo..bar")); } @Test - public void testEmptyPartPrefix() throws Exception { + void testEmptyPartPrefix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse(".bar")); } @Test - public void testEmptyPartSuffix() throws Exception { + void testEmptyPartSuffix() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("bar.")); } @Test - public void testIllegalWhitespace() throws Exception { + void testIllegalWhitespace() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("com foo")); } @Test - public void testToString() throws Exception { + void testToString() { assertThat(PackagePattern.parse("de.skuzzle.**").toString()) .isEqualTo("de.skuzzle.**"); } @Test - public void testToStringStatic() throws Exception { + void testToStringStatic() { assertThat(PackagePattern.parse("static de.skuzzle.**").toString()) .isEqualTo("static de.skuzzle.**"); } @Test - public void testVerifyEquals() throws Exception { + void testVerifyEquals() { EqualsVerifier.forClass(PackagePattern.class).verify(); } @Test - public void testNotEquals() throws Exception { + void testNotEquals() { assertThat(PackagePattern.parse("de.skuzzle.**")).isNotEqualTo( PackagePattern.parse("de.skuzzle.*")); } @Test - public void testMatchDefaultPackage() throws Exception { + void testMatchDefaultPackage() { final PackagePattern pattern = PackagePattern.parse("**"); assertThat(pattern.matches("")).isTrue(); } @Test - public void testMatchesDefaultPackage2() throws Exception { + void testMatchesDefaultPackage2() { final PackagePattern pattern = PackagePattern.parse("*"); assertThat(pattern.matches("")).isTrue(); } @Test - public void testMatchExactly() throws Exception { + void testMatchesDefaultPackage3() { + final PackagePattern pattern = PackagePattern.parse("*Foo*"); + assertThat(pattern.matches("NotFooType")).isTrue(); + } + + @Test + void testMatchExactly() { final PackagePattern pattern = PackagePattern.parse("de.skuzzle.SomeClass"); assertThat(pattern.matches("de.skuzzle.SomeClass")).isTrue(); assertThat(pattern.matches("de.skuzzle.SomeClass2")).isFalse(); } @Test - public void testMatchWildCardSuffix() throws Exception { + void testMatchWildCardSuffix() { final PackagePattern pattern = PackagePattern.parse("de.skuzzle.*"); assertThat(pattern.matches("de.skuzzle.TestClass")).isTrue(); assertThat(pattern.matches("de.skuzzle.TestClass2")).isTrue(); } @Test - public void testWildCardMatchesSingle() throws Exception { + void testWildCardMatchesSingle() { final PackagePattern pattern = PackagePattern.parse("de.skuzzle.*"); assertThat(pattern.matches("de.skuzzle.sub.TestClass")).isFalse(); } @Test - public void testMatchInfix() throws Exception { + void testMatchInfix() { final PackagePattern pattern = PackagePattern.parse("de.skuzzle.*.test"); assertThat(pattern.matches("de.skuzzle.foo.test")).isTrue(); assertThat(pattern.matches("de.skuzzle.test")).isFalse(); } @Test - public void testMatchMultipleInfix() throws Exception { + void testMatchMultipleInfix() { final PackagePattern pattern = PackagePattern.parse("de.skuzzle.*.xx.*.test"); assertThat(pattern.matches("de.skuzzle.foo.xx.bar.test")).isTrue(); assertThat(pattern.matches("de.skuzzle.foo.xx.bar")).isFalse(); } @Test - public void testWildcardMatchMultiple() throws Exception { + void testWildcardMatchMultiple() { final PackagePattern pattern = PackagePattern.parse("de.skuzzle.**"); assertThat(pattern.matches("de.skuzzle.sub.TestClass")).isTrue(); assertThat(pattern.matches("de.skuzzle.TestClass2")).isTrue(); } @Test - public void testLogger() throws Exception { + void testLogger() { final PackagePattern pattern = PackagePattern.parse("java.util.**"); assertThat(pattern.matches("java.util.logging.Logger")).isTrue(); } @Test - public void testWildcardInStringToTest() throws Exception { + void testWildcardInStringToTest() { final PackagePattern pattern = PackagePattern.parse("java.util.ArrayList"); assertThat(pattern.matches("java.util.*")).isFalse(); } @Test - public void testDoubleWildcardInBetween() throws Exception { + void testDoubleWildcardInBetween() { final PackagePattern pattern = PackagePattern.parse("com.**.bar.ClassName"); assertThat(pattern.matches("com.foo.bar.ClassName")).isTrue(); assertThat(pattern.matches("com.xyz.foo.bar")).isFalse(); @@ -201,40 +195,40 @@ public void testDoubleWildcardInBetween() throws Exception { } @Test - public void testDoubleWildcardInBetweenSkipMultiple() throws Exception { + void testDoubleWildcardInBetweenSkipMultiple() { final PackagePattern pattern = PackagePattern.parse("com.**.bar.ClassName"); assertThat(pattern.matches("com.xyz.foo.bar.ClassName")).isTrue(); assertThat(pattern.matches("com.xyz.foo.bar")).isFalse(); } @Test - public void testConsecutiveDoubleWildcard() throws Exception { + void testConsecutiveDoubleWildcard() { final PackagePattern pattern = PackagePattern.parse("com.**.**.ClassName"); assertThat(pattern.matches("com.xyz.foo.bar.ClassName")).isTrue(); assertThat(pattern.matches("com.xyz.foo.bar")).isFalse(); } @Test - public void testDoubleWildcard() throws Exception { + void testDoubleWildcard() { final PackagePattern pattern = PackagePattern.parse("com.**.xx.**.ClassName"); assertThat(pattern.matches("com.xyz.foo.yy.xx.bar.ClassName")).isTrue(); assertThat(pattern.matches("com.xyz.foo.bar")).isFalse(); } @Test - public void testDoubleWildCartBeginning() throws Exception { + void testDoubleWildCartBeginning() { final PackagePattern pattern = PackagePattern.parse("**.ClassName"); assertThat(pattern.matches("com.xyz.foo.bar.ClassName")).isTrue(); } @Test - public void test() throws Exception { + void test() { final PackagePattern pattern = PackagePattern.parse("com.foo.**"); assertThat(pattern.matches("java.util.ArrayList")).isFalse(); } @Test - public void testPatternMatchesPattern() throws Exception { + void testPatternMatchesPattern() { assertThat(PackagePattern.parse("com.foo.**") .matches(PackagePattern.parse("com.foo.*"))).isTrue(); assertThat(PackagePattern.parse("com.foo.*") @@ -244,32 +238,81 @@ public void testPatternMatchesPattern() throws Exception { } @Test - public void testStaticImport() throws Exception { + void testStaticImport() { assertThat(PackagePattern.parse("static com.foo.bar.*") .matches("static com.foo.bar.Test")).isTrue(); } @Test - public void testImplicitStaticImport() throws Exception { + void testImplicitStaticImport() { assertThat(PackagePattern.parse("com.foo.bar.*") .matches("static com.foo.bar.Test")).isTrue(); } @Test - public void testStaticImportWithWildWhitespaces() throws Exception { + void testStaticImportWithWildWhitespaces() { assertThat(PackagePattern.parse("\n \tstatic \t \n \r com.foo.bar.*\t ") .matches(" static \r \t com.foo.bar.Test \n ")).isTrue(); } @Test - public void testRealPackageNameStartswithStatic() throws Exception { + void testRealPackageNameStartswithStatic() { final PackagePattern pattern = PackagePattern.parse("staticc.foo.Bar"); assertThat(pattern.toString()).isEqualTo("staticc.foo.Bar"); } @Test - public void testParseEmptyString() throws Exception { + void testParseEmptyString() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> PackagePattern.parse("")); } + + @Test + void testMatchSuffixWithinLastPart() { + final PackagePattern pattern = PackagePattern.parse("com.console.model.*Entity"); + assertThat(pattern.matches("com.console.model.UserEntity")).isTrue(); + } + + @Test + void testMatchSuffixWithinMiddlePart() { + final PackagePattern pattern = PackagePattern.parse("com.console.*model.UserEntity"); + assertThat(pattern.matches("com.console.whatevermodel.UserEntity")).isTrue(); + } + + @Test + void testMatchPrefixWithinLastPart() { + final PackagePattern pattern = PackagePattern.parse("com.console.model.Entity*"); + assertThat(pattern.matches("com.console.model.EntityWhatever")).isTrue(); + } + + @Test + void testMatchPrefixWithinMiddlePart() { + final PackagePattern pattern = PackagePattern.parse("com.console.model*.UserEntity"); + assertThat(pattern.matches("com.console.modelwhatever.UserEntity")).isTrue(); + } + + @Test + void testMatchContainingStringWithinLastPart() { + final PackagePattern pattern = PackagePattern.parse("com.console.model.*Foo*"); + assertThat(pattern.matches("com.console.model.XxFooBar")).isTrue(); + } + + @Test + void testMatchContainingStringWithinMiddlePart() { + final PackagePattern pattern = PackagePattern.parse("com.console.*model*.Foo"); + assertThat(pattern.matches("com.console.foomodelfoo.Foo")).isTrue(); + } + + @Test + void testMatchContainingStringExactMatch() { + final PackagePattern pattern = PackagePattern.parse("com.console.*model*.Foo"); + assertThat(pattern.matches("com.console.model.Foo")).isTrue(); + } + + @Test + void testDumbNameExample() { + final PackagePattern pattern = PackagePattern.parse("**.*DumbName"); + assertThat(pattern.matches("com.console.model.ThisIsADumbName")).isTrue(); + assertThat(pattern.matches("com.foo.bar.ThisIsADumbName")).isTrue(); + } }