-
Notifications
You must be signed in to change notification settings - Fork 42
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
Use imports instead of FQN #97 #99
base: main
Are you sure you want to change the base?
Conversation
first point is a nice to have but it does not hurt too much to have the import. I might have missed it but we should have a test with classes with the same simple name, ex: I have moved this to the 3.0 version to give use time testing this and because I'm going to release 2.1.0 soonish. |
@@ -319,14 +322,49 @@ private String generateAssertionsEntryPointClassContent(final Set<ClassDescripti | |||
? determineBestEntryPointsAssertionsClassPackage(classDescriptionSet) | |||
: entryPointClassPackage; | |||
entryPointAssertionsClassContent = replace(entryPointAssertionsClassContent, PACKAGE, classPackage); | |||
|
|||
Set<String> imports; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extract imports resolution in a method, maybe resolveImports
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolvedImports
? As they are already resolved once they are in there. However, if we just use the map then we won't need this
entryPointAssertionsClassContent = replace(entryPointAssertionsClassContent, ALL_ASSERTIONS_ENTRY_POINTS, | ||
allEntryPointsAssertionContent); | ||
return entryPointAssertionsClassContent; | ||
} | ||
|
||
private static String listNeededImports(Collection<String> imports) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the name is confusing I was expecting it to return a list, rename to buildImports
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do, just so you know, I used this name because it is already used to create the imports
for the assertion classes. I thought there is some reason so I kept it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking to have the code looking like:
String imports = resolveImports(classDescriptionSet);
entryPointAssertionsClassContent = replace(entryPointAssertionsClassContent, IMPORTS, listNeededImports(imports));
and do whatever we need to do in resolveImports
to get all the imports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that we need the set of imports later in generateAssertionEntryPointMethodsFor
but I think it is better to recompute them in generateAssertionEntryPointMethodsFor
as we are passing the Set<ClassDescription>
which is enough to recompute the imports.
I like when we only pass the minimal parameters to a method, to generate assertThat
methods we only need the template and the Set<ClassDescription>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is also a check that makes sure that there is ${imports}
in the entry point template, if there is not then we don't do anything. Are you willing to enforce this and make this a breaking change?
If we recompute the imports in generateAssertionEntryPointMethodsFor
then we need to pass a parameter that is going to tell whether we need to use imports or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it has been decided to go with the imports, I'm ok with the breaking change, users that have used their own templates would only have to add ${imports}
at the top, it seems reasonable to me if we document that clearly.
private static Set<String> extractImports(Set<ClassDescription> classDescriptionSet) { | ||
Set<String> imports = new TreeSet<>(); | ||
Map<String, String> importedQualifiedName = new HashMap<>(classDescriptionSet.size() * 2); | ||
for (ClassDescription description : new TreeSet<>(classDescriptionSet)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not using a simple TreeSet
instead of a map and just add everything ? the returned set will guarantee there will be no duplicates and the code will be really simple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return a SortedSet
to make it clear that imports are sorted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I add all the imports then we will have collisions when we have classes with the same name but different FQN. We might not need the Set
if we just use the Map
and do the check in the next method.
Will return a SortedSet
to make it clear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in case of collision, we only add the import for the first class but not for the second, correct ?
this method needs comments to explain clearly hoe we deal with colliding class names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes if we have a map then we would only add the FQN for the first simple class later one if an import exists we wouldn't do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a comment with an example to make this clear for the next guy that will read the code ? (likely me in 6 months 😸 )
@@ -362,12 +401,20 @@ private String generateAssertionEntryPointMethodsFor(final Set<ClassDescription> | |||
String assertionEntryPointMethodContent = assertionEntryPointMethodTemplate.getContent(); | |||
// resolve class assert (ex: PlayerAssert) | |||
// in case of inner classes like Movie.PublicCategory, class assert will be MoviePublicCategoryAssert | |||
String customAssertionClass = fullyQualifiedAssertClassName(classDescription); | |||
if (imports.contains(customAssertionClass)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that if the import was not already known, we should add it to the import list but I feel that at this point we should already have it. WDYT ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that makes me think that we should not pas the imports here, if we have done a good job at finding all of them, we can use simple class name everywhere (except for simple class name collision but does it apply here, not sure ...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need it so we can choose whether to use the simple class name or the FQN. Maybe better would be to pass a map from simple class name to FQN and if the simple class name matches and the FQN matches then we use that otherwise we use the FQN (Java 8 would be awesome here 😄).
The imports are created outside of this. I agree with you the best approach would be to compute the imports here (we already are iterating over the ClassDescription
, but in order to do that we need to change the return so we can later add them to the entry point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't understood that last sentence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method here returns a String
which is the content for entry points. If we want to compute the imports here (we can actually do this) then we need to return the content for the entry points and also the imports that need to be added to the entry point class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking to recompute the imports here only to be able to decide whether to use a FQN or not, replacing ${imports}
would still be done in generateAssertionsEntryPointClassContent
.
I prefer to have a simpler method signature (without Set<String> imports
parameter) at the cost of recompute the set of imports which is not an overly expensive operation, I usually don't like to pass parameters when they can be inferred in the method itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about making the generateAssertionEntryPointMethodsFor
return some wrapper over the content and the imports
? We'll need duplicate logic if we handle in in 2 places.
@@ -476,6 +476,24 @@ public static String getSimpleNameWithOuterClass(Class<?> clazz) { | |||
} | |||
|
|||
/** | |||
* Gets the simple name of the outer class, if the class is not nested then this is the same as | |||
* {@link Class#getSimpleName()}. | |||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an example would be welcome
Thanks for the feedback @joel-costigliola. I've replied to your comments inline. And here to your review comment:
I'll see what we can do about this.
From what I saw one can pass the options to the
Yes there is a test it is the
Yes sure, 3.0 makes more sense so we can have t tested good. Looking forward to 2.1.0 |
I think we don't need the toggle feature / configuration flag, if we can make it work it is better to have imports. |
importedQualifiedName.put(simpleAssertClassName(description), toImport); | ||
} | ||
} | ||
return imports; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is that imports = importedQualifiedName.values() ? if so we can get rid of the imports set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes it is, we can actually do this.
// resolve class (ex: Player) | ||
// in case of inner classes like Movie.PublicCategory use class name with outer class i.e. Movie.PublicCategory. | ||
String classToAssert = classDescription.getFullyQualifiedClassName(); | ||
if (imports.contains(classDescription.getPackageName() + "." + classDescription.getOuterClassName())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not if (imports.contains(classToAssert)
? if there is a legitimate case for not using that we need a comment explaining why with an example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we can do imports only for first level classes, nested classes will go via the first level class.
For example for Movie.PublicCategory
. We'll have an import for Movie
and we then we can use Movie.PublicCategory
in the template. What do you prefer more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is better to use Movie.PublicCategory
over PublicCategory
, inner classes name without their outer class can be ambiguous.
@joel-costigliola thanks for all the useful comments on the PR. I would also like to explain my reasoning behind some of the decisions. The reason why I want to consolidate the generation of the imports into one place is due to performance. If we need to compute the imports once in
We can reduce this to 2 times, but then we will need to have the same logic we have in |
@joel-costigliola I update the PR with what we discussed in the comments. While doing the last commit I thought about the templates. Are there some mandatory variables that a user has to use. For example: |
I never had performance issue with the generator, I don't think it is gonna be used on more than 10 000 classes, I favor simple code over more complex but more performant one, so let's avoid premature optimization, keep it as simple as possible and wait until we or someone else raise a performance issue. As for mandatory variables, more or less everything is mandatory, should we verify the templates ? interesting question, I would say that users that change templates should test the generated result, it can't be our job, moreover even if the variables are there you can still screw up the generated code by using them at improper location - not much we can do about it. |
Sorry about premature optimization, I've been having problems with that lately (not with the generator), so I am in that mindset a bit :). I addressed your previous comments. When you have time you can have another look at this. I think it is much cleaner now. I created #107 for a discussion for the mandatory variables. It is just an idea to provide a fail fast alternative when a user does not use a variable. |
*/ | ||
private static SortedSet<String> extractImports(Set<ClassDescription> classDescriptionSet) { | ||
Map<String, String> importedQualifiedName = new HashMap<>(classDescriptionSet.size() * 2); | ||
for (final ClassDescription description : new TreeSet<>(classDescriptionSet)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to create a new TreeSet<>
to iterate on classDescriptionSet
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily, do we want to have some ordering of the imports, i.e. Do we want to first methods to always have a normal import and then the methods below to use FQN in case that is needed? What we can do is to pass a SortedSet<ClassDescription>
to generateAssertionsEntryPointClassContent
as we in any case create those down the line.
* @return a sorted set with all the FQN that can be imported | ||
*/ | ||
private static SortedSet<String> extractImports(Set<ClassDescription> classDescriptionSet) { | ||
Map<String, String> importedQualifiedName = new HashMap<>(classDescriptionSet.size() * 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think importByClassName
would be more expressive than importedQualifiedName
for (final ClassDescription description : new TreeSet<>(classDescriptionSet)) { | ||
final String outerClassName = description.getOuterClassName(); | ||
if (!importedQualifiedName.containsKey(outerClassName)) { | ||
String toImport = description.getPackageName() + "." + outerClassName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description.getPackageName() + "." + outerClassName
should be extracted to a ClassDescription
method (getOuterClassFullyQualifiedName
?), the same code is used later on in generateAssertionEntryPointMethodsFor
.
// resolve class (ex: Player) | ||
// in case of inner classes like Movie.PublicCategory use class name with outer class i.e. Movie.PublicCategory. | ||
String classToAssert = classDescription.getFullyQualifiedClassName(); | ||
if (resolvedImports.contains(classDescription.getPackageName() + "." + classDescription.getOuterClassName())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use the classDescription.getOuterClassFullyQualifiedName()
(see previous comment)
@@ -229,6 +237,11 @@ public void testGetSimpleNameWithOuterClass_notNestedClass() throws Exception { | |||
} | |||
|
|||
@Test | |||
public void testGetSimpleNameOuterClass_notNestedClass() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename the test using an english sentence to express what the test verifies then put _
instead of space.
Note: starting test method is redundant since we already have an @Test
annotation
@joel-costigliola I just saw that #101 has been merged. This overhauls the type system that was used before. Can we say that it is safe to not use generics for this? The generator does not generate generic classes right? |
what do you mean exactly by |
Don't worry about the conflicts, I will fix them when integrating this PR |
I am wrong, the I am not worried about the conflicts the problems are more on a conceptual level. If we have this entry point: public com.example.MyGenericAssert assertThat(MyGeneric<com.example.Test> value) {
...
} I even think that the code that will be created will be wrong. I think that we are going to generate something like Let's that we generate the entry points correctly, now we need to import |
Yes that does not work out of the box in master but I think it is solvable. public static org.assertj.assertions.generator.data.MyGenericAssert<T> assertThat(org.assertj.assertions.generator.data.MyGeneric<Object> actual) {",
return new org.assertj.assertions.generator.data.MyGenericAssert<T>(actual);",
} when giving: public class MyGeneric<T> {
public T field;
public T getValue() {
return null;
}
} I'm using Still lot of work to do though ... |
Generics are quite tricky, especially on runtime when we have access only to the reflection API. I can make the following suggestion, let's wrap up the generics first and then we go back to this one. What do you think? As a direction for the generics I can propose the following: instead of doing everything in the single type we do it recursively, i.e. we have an Object that holds the raw type and the other generic parameters. We can have a factory that creates this types and this factory will know if a type can be imported (use non FQN) or not (use FQN). Then when we create the template we can just take the name for using from the type. Then for the imports we get what we can import from the type themselves. I can point you to another project I am working on (MapStruct) and we have something like this. We generate Java classes with non FQN when possible. |
Postponing this PR was my plan indeed (one thing at a time). I'm refactoring the code to give Is that what you had in mind ? I'm not sure I get why we would need the Factory you are talking about, I think |
Yes something like that. What I had in mind was Let me know once you are done, so I can rebase this and continue working on it 😄 |
Fixes #97
This is a first go in trying to use imports instead of using FQN in the assertions class.
I am not sure if this is complete: