Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the dependency path exception message #11405

Merged
merged 4 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 63 additions & 26 deletions core/src/main/java/io/micronaut/core/naming/NameUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package io.micronaut.core.naming;

import io.micronaut.core.annotation.AccessorsStyle;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.StringUtils;

Expand Down Expand Up @@ -47,7 +49,7 @@ public class NameUtils {
* @param name The name
* @return True if it is
*/
public static boolean isHyphenatedLowerCase(String name) {
public static boolean isHyphenatedLowerCase(@Nullable String name) {
if (name == null || name.isEmpty() || !Character.isLetter(name.charAt(0))) {
return false;
}
Expand All @@ -67,7 +69,7 @@ public static boolean isHyphenatedLowerCase(String name) {
* @param suffixes The suffix to remove
* @return The decapitalized name
*/
public static String decapitalizeWithoutSuffix(String name, String... suffixes) {
public static @NonNull String decapitalizeWithoutSuffix(@NonNull String name, String... suffixes) {
String decapitalized = decapitalize(name);
return trimSuffix(decapitalized, suffixes);
}
Expand All @@ -79,7 +81,7 @@ public static String decapitalizeWithoutSuffix(String name, String... suffixes)
* @param suffixes The suffixes
* @return The trimmed string
*/
public static String trimSuffix(String string, String... suffixes) {
public static @NonNull String trimSuffix(@NonNull String string, String... suffixes) {
if (suffixes != null) {
for (String suffix : suffixes) {
if (string.endsWith(suffix)) {
Expand All @@ -96,7 +98,7 @@ public static String trimSuffix(String string, String... suffixes) {
* @param name The property name
* @return The class name
*/
public static String capitalize(String name) {
public static @NonNull String capitalize(@NonNull String name) {
final String rest = name.substring(1);

// Funky rule so that names like 'pNAME' will still work.
Expand All @@ -113,7 +115,7 @@ public static String capitalize(String name) {
* @param name The name
* @return The hyphenated string
*/
public static String hyphenate(String name) {
public static @NonNull String hyphenate(@NonNull String name) {
return hyphenate(name, true);
}

Expand All @@ -124,7 +126,7 @@ public static String hyphenate(String name) {
* @param lowerCase Whether the result should be converted to lower case
* @return The hyphenated string
*/
public static String hyphenate(String name, boolean lowerCase) {
public static @NonNull String hyphenate(@NonNull String name, boolean lowerCase) {
String kebabReplaced = name.replace('_', '-').replace(' ', '-');
if (isHyphenatedLowerCase(name)) {
return kebabReplaced;
Expand All @@ -139,7 +141,7 @@ public static String hyphenate(String name, boolean lowerCase) {
* @param name The hyphenated string
* @return The camel case form
*/
public static String dehyphenate(String name) {
public static @NonNull String dehyphenate(@NonNull String name) {
StringBuilder sb = new StringBuilder(name.length());
for (String token : StringUtils.splitOmitEmptyStrings(name, '-')) {
if (!token.isEmpty() && Character.isLetter(token.charAt(0))) {
Expand All @@ -158,7 +160,7 @@ public static String dehyphenate(String name) {
* @param className The class name
* @return The package name
*/
public static String getPackageName(String className) {
public static @NonNull String getPackageName(@NonNull String className) {
Matcher matcher = DOT_UPPER.matcher(className);
if (matcher.find()) {
int position = matcher.start();
Expand All @@ -173,7 +175,7 @@ public static String getPackageName(String className) {
* @param camelCase The camel case name
* @return The underscore separated version
*/
public static String underscoreSeparate(String camelCase) {
public static @NonNull String underscoreSeparate(@NonNull String camelCase) {
return underscoreSeparate(camelCase, false);
}

Expand All @@ -184,7 +186,7 @@ public static String underscoreSeparate(String camelCase) {
* @param lowercase true to lowercase the result
* @return The underscore separated version
*/
public static String underscoreSeparate(String camelCase, boolean lowercase) {
public static @NonNull String underscoreSeparate(@NonNull String camelCase, boolean lowercase) {
return separateCamelCase(camelCase.replace('-', '_'), lowercase, '_');
}

Expand All @@ -194,7 +196,7 @@ public static String underscoreSeparate(String camelCase, boolean lowercase) {
* @param camelCase The camel case name
* @return The underscore separated version
*/
public static String environmentName(String camelCase) {
public static @NonNull String environmentName(@NonNull String camelCase) {
return separateCamelCase(camelCase.replace('-', '_').replace('.', '_'), false, '_')
.toUpperCase(Locale.ENGLISH);
}
Expand All @@ -205,7 +207,7 @@ public static String environmentName(String camelCase) {
* @param className The class name
* @return The simple name of the class
*/
public static String getSimpleName(String className) {
public static @NonNull String getSimpleName(@NonNull String className) {
Matcher matcher = DOT_UPPER.matcher(className);
if (matcher.find()) {
int position = matcher.start();
Expand All @@ -214,13 +216,48 @@ public static String getSimpleName(String className) {
return className;
}

/**
* Returns the shortened fully-qualified name for a class represented as a string.
* Shortened name would have package names and owner objects reduced to a single letter.
* For example, {@code com.example.Owner$Inner} would become {@code c.e.O$Inner}.
* IDEs would still be able to recognize these types, but they would take less space
* visually.
*
* @since 4.8.x
* @param typeName The fully-qualified type name
* @return The shortened type name
*/
@Experimental
public static @NonNull String getShortenedName(@NonNull String typeName) {
int nameStart = typeName.lastIndexOf('$');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check type name doesn't == null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is not checked in other methods, so I assume it is not typical for typeName to be null. Added @NonNull though.

if (nameStart < 0) {
nameStart = typeName.lastIndexOf('.');
}
if (nameStart < 0) {
nameStart = 0;
}
StringBuilder shortened = new StringBuilder();
boolean segmentStart = true;
for (int i = 0; i < nameStart; i++) {
char c = typeName.charAt(i);
if (segmentStart) {
shortened.append(c);
segmentStart = false;
} else if (c == '.' || c == '$') {
shortened.append(c);
segmentStart = true;
}
}
return shortened.append(typeName.substring(nameStart)).toString();
}

/**
* Is the given method name a valid setter name.
*
* @param methodName The method name
* @return True if it is a valid setter name
*/
public static boolean isSetterName(String methodName) {
public static boolean isSetterName(@NonNull String methodName) {
return isWriterName(methodName, AccessorsStyle.DEFAULT_WRITE_PREFIX);
}

Expand Down Expand Up @@ -271,7 +308,7 @@ public static boolean isWriterName(@NonNull String methodName, @NonNull String[]
* @param setterName The setter
* @return The property name
*/
public static String getPropertyNameForSetter(String setterName) {
public static @NonNull String getPropertyNameForSetter(@NonNull String setterName) {
return getPropertyNameForSetter(setterName, AccessorsStyle.DEFAULT_WRITE_PREFIX);
}

Expand Down Expand Up @@ -351,7 +388,7 @@ public static String getPropertyNameForSetter(String setterName) {
* @param methodName The method name
* @return True if it is a valid getter name
*/
public static boolean isGetterName(String methodName) {
public static boolean isGetterName(@NonNull String methodName) {
return isReaderName(methodName, AccessorsStyle.DEFAULT_READ_PREFIX);
}

Expand Down Expand Up @@ -410,7 +447,7 @@ private static boolean isValidCharacterAfterReaderWriterPrefix(char c) {
* @param getterName The getter
* @return The property name
*/
public static String getPropertyNameForGetter(String getterName) {
public static @NonNull String getPropertyNameForGetter(@NonNull String getterName) {
return getPropertyNameForGetter(getterName, AccessorsStyle.DEFAULT_READ_PREFIX);
}

Expand Down Expand Up @@ -514,8 +551,8 @@ public static String getterNameFor(@NonNull String propertyName, boolean isBoole
return nameFor(isBoolean ? PREFIX_IS : PREFIX_GET, propertyName);
}

private static String nameFor(String prefix, @NonNull String propertyName) {
if (prefix.isEmpty()) {
private static @NonNull String nameFor(@Nullable String prefix, @NonNull String propertyName) {
if (StringUtils.isEmpty(prefix)) {
return propertyName;
}

Expand All @@ -541,7 +578,7 @@ private static String nameFor(String prefix, @NonNull String propertyName) {
* @param name The String to decapitalize
* @return The decapitalized version of the String
*/
public static String decapitalize(String name) {
public static @Nullable String decapitalize(@Nullable String name) {
if (name == null) {
return null;
}
Expand Down Expand Up @@ -572,7 +609,7 @@ public static String decapitalize(String name) {
return name;
}

static String separateCamelCase(String name, boolean lowerCase, char separatorChar) {
static @NonNull String separateCamelCase(@NonNull String name, boolean lowerCase, char separatorChar) {
StringBuilder newName = new StringBuilder(name.length() + 4);
if (!lowerCase) {
boolean first = true;
Expand Down Expand Up @@ -650,7 +687,7 @@ static String separateCamelCase(String name, boolean lowerCase, char separatorCh
* @param filename The name of the file
* @return The file extension
*/
public static String extension(String filename) {
public static @NonNull String extension(@NonNull String filename) {
int extensionPos = filename.lastIndexOf('.');
int lastUnixPos = filename.lastIndexOf('/');
int lastWindowsPos = filename.lastIndexOf('\\');
Expand All @@ -669,7 +706,7 @@ public static String extension(String filename) {
* @param str The string
* @return The new string in camel case
*/
public static String camelCase(String str) {
public static @NonNull String camelCase(@NonNull String str) {
return camelCase(str, true);
}

Expand All @@ -680,7 +717,7 @@ public static String camelCase(String str) {
* @param lowerCaseFirstLetter Whether the first letter is in upper case or lower case
* @return The new string in camel case
*/
public static String camelCase(String str, boolean lowerCaseFirstLetter) {
public static @NonNull String camelCase(@NonNull String str, boolean lowerCaseFirstLetter) {
StringBuilder sb = new StringBuilder(str.length());
for (String s : str.split("[\\s_-]")) {
String capitalize = capitalize(s);
Expand All @@ -700,7 +737,7 @@ public static String camelCase(String str, boolean lowerCaseFirstLetter) {
* @param path The path of the file
* @return The file name without extension
*/
public static String filename(String path) {
public static @NonNull String filename(@NonNull String path) {
int extensionPos = path.lastIndexOf('.');
int lastUnixPos = path.lastIndexOf('/');
int lastWindowsPos = path.lastIndexOf('\\');
Expand All @@ -719,7 +756,7 @@ public static String filename(String path) {
* @param str The string to check
* @return Whether is valid kebab-case or not
*/
public static boolean isValidHyphenatedPropertyName(String str) {
public static boolean isValidHyphenatedPropertyName(@NonNull String str) {
return KEBAB_CASE_SEQUENCE.matcher(str).matches();
}

Expand All @@ -729,7 +766,7 @@ public static boolean isValidHyphenatedPropertyName(String str) {
* @param str The string to check
* @return Whether is valid environment-style property name or not
*/
public static boolean isEnvironmentName(String str) {
public static boolean isEnvironmentName(@NonNull String str) {
return ENVIRONMENT_VAR_SEQUENCE.matcher(str).matches();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ConstructorCircularDependencyFailureSpec extends Specification {
ApplicationContext context = ApplicationContext.run()

when:"A bean is obtained that has a setter with @Inject"
MyClassB b = context.getBean(MyClassB)
context.getBean(MyClassB)

then:"The implementation is injected"
def e = thrown(CircularDependencyException)
Expand All @@ -40,10 +40,10 @@ Failed to inject value for field [propA] of class: io.micronaut.inject.failures.

Message: Circular dependency detected
Path Taken:
new MyClassB()
\\---> MyClassB.propA
^ \\---> new MyClassA([MyClassC propC])
| \\---> new MyClassC([MyClassB propB])
new i.m.i.f.C$MyClassB()
\\---> i.m.i.f.C$MyClassB#propA
^ \\---> new i.m.i.f.C$MyClassA([MyClassC propC])
| \\---> new i.m.i.f.C$MyClassC([MyClassB propB])
| |
+--------------+'''

Expand All @@ -65,11 +65,11 @@ Failed to inject value for field [propA] of class: io.micronaut.inject.failures.

Message: Circular dependency detected
Path Taken:
new MyClassD(MyClassB propB)
\\---> new MyClassD([MyClassB propB])
\\---> MyClassB.propA
^ \\---> new MyClassA([MyClassC propC])
| \\---> new MyClassC([MyClassB propB])
new i.m.i.f.C$MyClassD(MyClassB propB)
\\---> new i.m.i.f.C$MyClassD([MyClassB propB])
\\---> i.m.i.f.C$MyClassB#propA
^ \\---> new i.m.i.f.C$MyClassA([MyClassC propC])
| \\---> new i.m.i.f.C$MyClassC([MyClassB propB])
| |
+--------------+'''
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,35 @@ class ConstructorDependencyFailureSpec extends Specification {
void "test a useful exception is thrown when a dependency injection failure occurs"() {
given:
ApplicationContext context = ApplicationContext.run()
var space = " "

when:"A bean that defines a constructor dependency on a missing bean"
B b = context.getBean(B)
context.getBean(MyClassB)

then:"The correct error is thrown"
def e = thrown(DependencyInjectionException)
e.message.normalize().contains('''\
Failed to inject value for parameter [a] of class: io.micronaut.inject.failures.ConstructorDependencyFailureSpec$B
e.message.normalize() == """\
Failed to inject value for parameter [propA] of class: io.micronaut.inject.failures.ConstructorDependencyFailureSpec\$MyClassB

Message: No bean of type [io.micronaut.inject.failures.ConstructorDependencyFailureSpec$A] exists.''')

e.message.normalize().contains('Path Taken: new B(A a) --> new B([A a])')
Message: No bean of type [io.micronaut.inject.failures.ConstructorDependencyFailureSpec\$MyClassA] exists.$space
Path Taken:$space
new i.m.i.f.C\$MyClassB(MyClassA propA)
\\---> new i.m.i.f.C\$MyClassB([MyClassA propA])"""

cleanup:
context.close()
}

static interface A {
static interface MyClassA {

}

static class B {
private final A a
static class MyClassB {
private final MyClassA propA

@Inject
B(A a) {
this.a = a
MyClassB(MyClassA propA) {
this.propA = propA
}
}

Expand Down
Loading
Loading