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

feat: Adds unique serializable path for each CtElement from Root Element. #1874

Merged
merged 28 commits into from
Feb 26, 2018
Merged

feat: Adds unique serializable path for each CtElement from Root Element. #1874

merged 28 commits into from
Feb 26, 2018

Conversation

nharrand
Copy link
Collaborator

The goal of this PR is to add support for getting an unique CtPath for each CtElement of a model. In particular this could be used for persistence. Any given CtElement can be access through an unique deterministic serializable path. All the following transformations are supported: CtElement -> CtPath -> String -> CtPath -> CtElement.

Main content:

  • Method CtPath CtElement.getPath(): Returns the path from root element to this.
  • Class CtElementPathBuilder: Build a CtPath from a given CtElement
  • Class CtUniqueRolePathElement : Extends CtRolePathElement with a new behavior and a new syntax.
  • Method MainTest.testElementToPathToElementEquivalency(): Test CtElement -> CtPath -> String -> CtPath -> CtElement

In order to integrate this to CtPathStringBuilder without conflict, the char '@' is used for this new path element.

The syntax for paths is the following

  • For unique CtElement -> @role
  • For elements in a List -> @role[index=ID]
  • For elements in a Set of CtElement for which name exists and is unique (Either CtNamedElement or CtReference) -> @role[name=element.getSimpleName()]
  • For elements in a Map<String, CtElement> -> @role[key=String]

@subPackage[name=spoon]@subPackage[name=compiler]@containedType[name=Environment]@typeMember[index=0] -> spoon.compiler.Environment 's first type member ie: int getComplianceLevel();

Currently the test fails (related to #1872).

What do you think?

@@ -536,4 +540,12 @@ public CtElement clone() {
rh.setValue(this, value);
return (E) this;
}

public CtPath getPath() {
try {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why not removing the try/catch and let the exception go?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The exception is supposed to be risen from CtElementPathBuilder().fromElement(CtElement el, CtElement from) when from is not a parent of el. So in this case, I think it can't happen and I didn't want to annoy the user with the handling of an exception which is not supposed to happen.
But still, we can let it go.

Copy link
Collaborator

Choose a reason for hiding this comment

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

"let it go" then :-)

Copy link
Collaborator Author

@nharrand nharrand Feb 25, 2018

Choose a reason for hiding this comment

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

I have a counter proposal: Why not catch CtPathException (which should not happen in that context) and throw a SpoonException to show that it's an spoon implementation error. Further more it would avoid that the user has to handle it.

try {
	return new CtElementPathBuilder().fromElement(this, getParent(CtModelImpl.CtRootPackage.class));
} catch (CtPathException e) {
	throw new SpoonException();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK for me.

assertEquals(returnedElements.size(), 1);
CtElement actualElement = (CtElement) returnedElements.toArray()[0];
//contract: Element -> Path -> String -> Path -> Element leads to the original element
assertEquals(element, actualElement);
Copy link
Collaborator

Choose a reason for hiding this comment

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

assertSame?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that sounds better. I'll fix it ASAP.

@monperrus
Copy link
Collaborator

looks super cool, I love the strong generic test.

are you working on fixing #1872?

@nharrand
Copy link
Collaborator Author

nharrand commented Feb 21, 2018

@monperrus I tried to add:

	static class CtCatchVariable_TYPE_RoleHandler extends ListHandler<CtCatchVariable, CtTypeReference<?>> {
		private CtCatchVariable_TYPE_RoleHandler() {
			super(CtRole.TYPE, CtCatchVariable.class, CtTypeReference.class);
		}

		@SuppressWarnings("unchecked")
		@Override
		public <T, U> U getValue(T element) {
			return ((U) ((Object) (castTarget(element).getMultiTypes())));
		}

		@Override
		public <T, U> void setValue(T element, U value) {
			castTarget(element).setMultiTypes(castValue(value));
		}
}

to ModelRoleHandlers, which fixed the issue but broke another test. I realized that this code is actually generated, if I'm not mistaken. So, this is where I got stuck.

@nharrand
Copy link
Collaborator Author

@monperrus I'm not sure why does the Travis CI build fails. It mentions Javadoc errors in files that I haven't changed. Any ideas?

Do you want to keep the current syntax for CtUniqueRolePathElement when transformed to String? Do you want me to merge it to the pre existing syntax?

@monperrus
Copy link
Collaborator

I propose to replace the previous implementation, to keep the old syntax with slash, and thus to update CtPathTest. It means we would drop the support for wildcard temporarily, but I'm OK with that. (This was an experimental partial feature, with no user to my knowledge).

WDYT?

@monperrus
Copy link
Collaborator

Fixed the Javadoc error at CtPathStringBuilder.java line 71 (please pull).

(Maven prefixes "warnings" of the Javadoc linter by [Error] which is super confusing, but later on the line it's clear that it is only a warning)

@monperrus
Copy link
Collaborator

monperrus commented Feb 23, 2018

Do you want me to merge it to the pre existing syntax?

Yes that would be great, see #1874 (comment)

@nharrand
Copy link
Collaborator Author

nharrand commented Feb 24, 2018

We can keep the wildcards, they are a part of CtNamedPathElement, and can already be used in combination of the new syntax.
The only change is that blocks cannot be omitted anymore.
Example:

new CtPathStringBuilder().fromString(".spoon.test.path.Foo.foo#body[index=0]");

becomes

new CtPathStringBuilder().fromString(".spoon.test.path.Foo.foo#body#statement[index=0]");

@monperrus
Copy link
Collaborator

the change is OK for me.

it seems we're close to merge!

CtElement el = root.getValueByRole(getRole());
matchs.add(el);
}
} catch (SpoonException e) { } //When no element are found for a given role, return empty list.
Copy link
Collaborator

Choose a reason for hiding this comment

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

same thing here, do we want this exception to be silent?

Copy link
Collaborator Author

@nharrand nharrand Feb 25, 2018

Choose a reason for hiding this comment

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

Yes, imo, because as it is, if we do not silence this exception, wildcards are broken.
It is thrown when we query a node for children with a given role that does not exist among its children.

Copy link
Collaborator Author

@nharrand nharrand Feb 25, 2018

Choose a reason for hiding this comment

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

I guess one alternative would be adding a method boolean hasChildWith(CtRole role) to CtElement.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is already there. See RoleHandlerHelper#getOptionalRoleHandler
Have a look also at RoleHandler#getContainerKind, which might be used instead of root.getValueByRole(getRole()) instanceof Set and similar

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@pvojtechovsky Thanks for the tip.
Problem solved.

@monperrus
Copy link
Collaborator

OK for me. Will merge it. That's a really nice contribution to Spoon, thanks!

@monperrus
Copy link
Collaborator

what about changing the signature of evaluateOn to evaluateOn(CtElement...) (varargs)?. the API would be easier for clients.

@monperrus
Copy link
Collaborator

sorry @nharrand , I fixed the test by calling super.scan(element), and it seems it now finds a bug :-)

@nharrand
Copy link
Collaborator Author

Yes, I see that! I'm looking into that.

@nharrand
Copy link
Collaborator Author

Ok this should work now.

if (getArguments().containsKey("index")) {
int index = Integer.parseInt(getArguments().get("index"));
matchs.add((CtElement) roleHandler.asList(root).get(index));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

else it should add all items of the into matchs. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

and what about case when index >= size()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, you are right. I only had in mind my Unique path case in mind here, but yes. I'll fix this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, that also means that I should add negative test cases...

Copy link
Collaborator Author

@nharrand nharrand Feb 26, 2018

Choose a reason for hiding this comment

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

else it should add all items of the into matchs.

We should extends that behavior to sets and maps, don't you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I agree. For Set it should add all items of the Set. And for Map it should add all values of the map.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Implemented in 67ec78b

case MAP:
if (getArguments().containsKey("key")) {
String name = getArguments().get("key");
matchs.add((CtElement) roleHandler.asMap(root).get(name));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess it is not correct to add null into matchs, when map doesn't contain a key

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Indeed. I'll fix this too.

try {
return new CtElementPathBuilder().fromElement(this, getParent(CtModelImpl.CtRootPackage.class));
} catch (CtPathException e) {
throw new SpoonException();
Copy link
Collaborator

Choose a reason for hiding this comment

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

throw new SpoonException(e); ... the orgin exception should be used as cause of the SpoonException

Copy link
Collaborator Author

@nharrand nharrand Feb 26, 2018

Choose a reason for hiding this comment

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

Ok. But, my idea was that CtElementPathBuilder().fromElement(CtElement to, CtElement from) would throw a CtPathException if from was no parent of to which means that the user made a mistake. But in the specific context of this call, as the arguments are an element and root package, no exception should happen unless the spoon implementation is at fault, therefor SpoonException.
WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, no exception should happen, but if it happens because of some mistake in spoon code, then your current implementation will avoid efficient detection of the cause, because the cause of the problem is lost. So just type in e into brackets and I am OK with that ;-)

Copy link
Collaborator Author

@nharrand nharrand Feb 26, 2018

Choose a reason for hiding this comment

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

just type in e into brackets

What do you mean? Shall I change throw new SpoonException(); into throw new SpoonException(e); ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes please

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in 8cfc1ad

return STRING + getRole().toString() + getParamString();
}

public CtElement getFromSet(Set set, String name) throws CtPathException {
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we need it public? Note: In Spoon we try to make public API as small as possible. It is easier to maintain and refactor then.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, we don't. I'll change it.

throw new CtPathException();
}
}
throw new CtPathException();
Copy link
Collaborator

Choose a reason for hiding this comment

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

throwing of exception as part of normal program flow is not a good idea. It has a performance impact. So it is much faster (main line debug mode), when you return null and check it in caller.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7ee9921

@pvojtechovsky
Copy link
Collaborator

It looks good to me. I am looking forward to use this new Spoon feature! Thank You @nharrand .

I guess we can merge. @monperrus WDYT?

@spoon-bot
Copy link
Collaborator

API changes: 2 (Detected by Revapi)

Old API: fr.inria.gforge.spoon:spoon-core:jar:6.2.0-20180225.235035-111 / New API: fr.inria.gforge.spoon:spoon-core:jar:6.2.0-SNAPSHOT

Method was added to an interface.
Old none
New method CtElement#getPath()
Breaking binary: non_breaking
The type of the parameter changed from 'java.util.Collection<? extends spoon.reflect.declaration.CtElement>' to 'spoon.reflect.declaration.CtElement[]'.
Old parameter CtPath#evaluateOn(===Collection===)
New parameter CtPath#evaluateOn(===.Array===)
Breaking binary: breaking

@monperrus monperrus merged commit fd9d6c0 into INRIA:master Feb 26, 2018
@monperrus
Copy link
Collaborator

OK to merge! This opens super exciting new usages.

Kudos and thanks to @nharrand

@nharrand
Copy link
Collaborator Author

You are welcome. Thanks for the reviews.

I'm a little disappointed that it counts as only one commit though... ;-)

@nharrand nharrand deleted the feature-unique-path branch February 26, 2018 20:18
@monperrus
Copy link
Collaborator

monperrus commented Feb 27, 2018 via email

monperrus added a commit that referenced this pull request Dec 4, 2018
Dear all,

I'd like to nominate Nicolas Harrand (@nharrand) to become an integrator in Spoon. Nicolas has done several key contributions over the past year:

* he has added support for paths in order to uniquely identify AST nodes (#1874), this feature is unique (to my knowledge, it does not exist in any other source code analysis library for Java), and enables original source code analysis over time and versions.
* he has added support for analysis of binary files with decompilation (#2455)
* he has laid down the foundations of a modularized Spoon, which will likely be key in the future
* he has ported Spoon to JDK 11 (#2789). This kind of infrastructure contribution is also very important in the long term.

In addition, Nicolas has proven his ability to communicate constructively and respectfully over issues and pull-requests.

What do you think?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants