eclipse-xtext / eclipse-xtext/xtext

Quickfix NullPointerException in PartialSerializer

Open
#2,565 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug confirmed
Dominant language
Java
Stars
831
Forks
330
Avg merge
3d 7h
Merged PRs (30d)
12

Description

https://www.eclipse.org/forums/index.php/m/1779172

I'm having problems in Xtext 2.13 with quickfixes that i didn't have in 2.12, i suppose related to the changes applied to support multiple quickfix resolutions.

The problem appears when a node A is added to the AST tree and then another node is added as a child of A

java.lang.NullPointerException
	at org.eclipse.xtext.ide.serializer.impl.PartialSerializer$SerializeRecursiveStrategy.serialize(PartialSerializer.java:155)
	at org.eclipse.xtext.ide.serializer.impl.PartialSerializer.serializeChanges(PartialSerializer.java:280)
	at org.eclipse.xtext.ide.serializer.impl.RecordingXtextResourceUpdater.applyChange(RecordingXtextResourceUpdater.java:80)
	at org.eclipse.xtext.ide.serializer.impl.ChangeSerializer.endRecordChanges(ChangeSerializer.java:162)
	at org.eclipse.xtext.ide.serializer.impl.ChangeSerializer.applyModifications(ChangeSerializer.java:93)
	at org.eclipse.xtext.ui.editor.model.edit.BatchModification.applyInWorkspace(BatchModification.java:142)

The problem happens with a single quickfix, but it can be avoided somehow waiting to add the node A until it's fully set.
The problem is much more difficult to solve if several quickfixes are applied together and the business rules are complex

As an example, i've made a simple test case

grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals

generate myDsl "http://www.xtext.org/example/mydsl/MyDsl"

Model:
	classes+=Class* statements+=Statement+;
	
Class:
	'class' name=ID '{' vars+=Variable+ '}';
	
Variable: name=ID;
	
Statement: var=[Variable|VariableQN] '=' INT;	

VariableQN hidden(): ID '.' ID;
@Fix(Diagnostic.LINKING_DIAGNOSTIC)
def void fixMissingEntity(Issue issue, IssueResolutionAcceptor acceptor) {
	if (issue.message.contains("Variable")) {
		fixMissingVariable(issue, acceptor);
	}
}

private def fixMissingVariable(Issue issue, IssueResolutionAcceptor acceptor) {
	acceptor.acceptMulti(
		issue,
		"Create missing variable definition",
		"Create missing variable definition",
		"Entity.gif",
		[ Statement element, ICompositeModificationContext<Statement> context |
			val fullReferenceName = getIssueMissingReferenceName(context.issue)
			val nameArray = fullReferenceName.split("\\.")
			val className = nameArray.get(0)
			val fieldName = nameArray.get(1)
			
			val root = element.eContainer as Model
			
			context.addModification(root, [ctx |
				var cl = root.classes.findFirst[it.name == className]
				
				if (cl === null) {
					// The class didn't exist
					cl =  MyDslFactory::eINSTANCE.createClass() => [
						name = className
					];
					
					// before adding the class, we can add the field
					addVariable(cl, fieldName)
					
					ctx.classes.add(cl)

					// after adding the class, adding the field will cause NullPointerException in Serialization
					// addVariable(cl, fieldName)
				} else {
					// The class existed (maybe created in a previous multiquickfix)
					// NullPointerException if 2 fixs are applied together and the parent class was added in the first fix
					addVariable(cl, fieldName)
				}
			])
			
		]
	)
}

protected def boolean addVariable(Class cl, String fieldName) {
	val atr = MyDslFactory::eINSTANCE.createVariable() => [
		name = fieldName
	];
	cl.vars.add(atr)
}

private def String getIssueMissingReferenceName(Issue issue) {
	// Not the best way ...
	
	val issueMessage = issue.message
	val j = issueMessage.lastIndexOf("'")
	val i = issueMessage.lastIndexOf("'", j - 1)
	
	issueMessage.substring(i+1, j)
}

Example code:

// aplying the fix to this 2 problems together works because they create their own classes
Class1.test1 = 1 
Class2.test2 = 1

// aplying the fix to this 2 problems together don't work because the first adds the class plus field and the second adds another field to the new class
Class3.test3 = 1 
Class3.test4 = 1

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at org.eclipse.xtext.ide.serializer.impl.PartialSerializer.java:155 and trace serializeChanges through RecordingXtextResourceUpdater.java and ChangeSerializer.java. Reproduce the provided multi-quickfix example where a class and child variable are added, then verify that applying single and combined quickfixes no longer causes a NullPointerException.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.