Skip to content

Commit

Permalink
Completion provides invalid suggestions for xsd:sequence
Browse files Browse the repository at this point in the history
Fix #347

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jul 8, 2019
1 parent f7f18e9 commit 1222e4b
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Collection;

import org.eclipse.lsp4xml.dom.DOMElement;
import org.w3c.dom.Element;

/**
* Content model document which abstracts element declaration from a given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import java.util.Collection;

import org.eclipse.lsp4xml.dom.DOMElement;

/**
* Content model element which abstracts element declaration from a given
* grammar (XML Schema, DTD).
Expand Down Expand Up @@ -59,6 +61,14 @@ default String getName(String prefix) {
*/
Collection<CMElementDeclaration> getElements();

/**
* Returns the possible declared elements for the given DOM after element.
*
* @param afterElement the after element
* @return the possible declared elements for the given DOM after element.
*/
Collection<CMElementDeclaration> getPossibleElements(DOMElement afterElement);

/**
* Returns the declared element which matches the given XML tag name / namespace
* and null otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.lsp4xml.commons.BadLocationException;
import org.eclipse.lsp4xml.dom.DOMDocument;
import org.eclipse.lsp4xml.dom.DOMElement;
import org.eclipse.lsp4xml.dom.DOMNode;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMAttributeDeclaration;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMDocument;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMElementDeclaration;
Expand Down Expand Up @@ -66,10 +67,18 @@ public void onTagOpen(ICompletionRequest request, ICompletionResponse response)
CMElementDeclaration cmElement = contentModelManager.findCMElement(parentElement);
String defaultPrefix = null;

// get the previous sibling element to filter completion according XML Schema
// constraints.
DOMElement previousSiblingElement = null;
DOMNode previousSibling = request.getNode();// .getPreviousSibling();
if (previousSibling != null && previousSibling.isElement() && !previousSibling.equals(parentElement)
&& previousSibling.isClosed()) {
previousSiblingElement = (DOMElement) previousSibling;
}
if (cmElement != null) {
defaultPrefix = parentElement.getPrefix();
fillWithChildrenElementDeclaration(parentElement, cmElement.getElements(), defaultPrefix, false,
request, response, schemaURI);
fillWithChildrenElementDeclaration(parentElement, cmElement.getPossibleElements(previousSiblingElement),
defaultPrefix, false, request, response, schemaURI);
}
if (parentElement.isDocumentElement()) {
// completion on root document element
Expand All @@ -95,8 +104,9 @@ public void onTagOpen(ICompletionRequest request, ICompletionResponse response)
CMElementDeclaration cmInternalElement = contentModelManager.findInternalCMElement(parentElement);
if (cmInternalElement != null) {
defaultPrefix = parentElement.getPrefix();
fillWithChildrenElementDeclaration(parentElement, cmInternalElement.getElements(), defaultPrefix, false,
request, response, schemaURI);
fillWithChildrenElementDeclaration(parentElement,
cmInternalElement.getPossibleElements(previousSiblingElement), defaultPrefix, false, request,
response, schemaURI);
}
} catch (CacheResourceDownloadingException e) {
// XML Schema, DTD is loading, ignore this error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.List;

import org.apache.xerces.impl.dtd.XMLElementDecl;
import org.eclipse.lsp4xml.dom.DOMElement;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMAttributeDeclaration;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMElementDeclaration;

Expand Down Expand Up @@ -62,6 +63,12 @@ public Collection<CMElementDeclaration> getElements() {
return elements;
}

@Override
public Collection<CMElementDeclaration> getPossibleElements(DOMElement afterElement) {
// TODO: support valid element declaration for DTD
return getElements();
}

@Override
public CMElementDeclaration findCMElement(String tag, String namespace) {
for (CMElementDeclaration cmElement : getElements()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import java.util.Map;

import org.apache.xerces.impl.dv.XSSimpleType;
import org.apache.xerces.impl.xs.XSElementDecl;
import org.apache.xerces.impl.xs.XSElementDeclHelper;
import org.apache.xerces.xni.QName;
import org.apache.xerces.xs.StringList;
import org.apache.xerces.xs.XSConstants;
import org.apache.xerces.xs.XSElementDeclaration;
Expand All @@ -35,7 +38,7 @@
* XSD document implementation.
*
*/
public class CMXSDDocument implements CMDocument {
public class CMXSDDocument implements CMDocument, XSElementDeclHelper {

private final XSModel model;

Expand Down Expand Up @@ -173,4 +176,9 @@ static boolean isBooleanType(XSSimpleTypeDefinition typeDefinition) {
}
return false;
}

@Override
public XSElementDecl getGlobalElementDecl(QName element) {
return (XSElementDecl) model.getElementDeclaration(element.localpart, element.uri);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

import org.apache.xerces.impl.dv.xs.XSSimpleTypeDecl;
import org.apache.xerces.impl.xs.SubstitutionGroupHandler;
import org.apache.xerces.impl.xs.XSComplexTypeDecl;
import org.apache.xerces.impl.xs.models.CMBuilder;
import org.apache.xerces.impl.xs.models.CMNodeFactory;
import org.apache.xerces.impl.xs.models.XSCMValidator;
import org.apache.xerces.xni.QName;
import org.apache.xerces.xs.XSAttributeUse;
import org.apache.xerces.xs.XSComplexTypeDefinition;
import org.apache.xerces.xs.XSConstants;
Expand All @@ -27,15 +34,38 @@
import org.apache.xerces.xs.XSSimpleTypeDefinition;
import org.apache.xerces.xs.XSTerm;
import org.apache.xerces.xs.XSTypeDefinition;
import org.eclipse.lsp4xml.dom.DOMElement;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMAttributeDeclaration;
import org.eclipse.lsp4xml.extensions.contentmodel.model.CMElementDeclaration;
import org.eclipse.lsp4xml.utils.StringUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
* XSD element declaration implementation.
*
*/
public class CMXSDElementDeclaration implements CMElementDeclaration {

private static class QNameInfo extends QName {

private int count;

public QNameInfo(String prefix, String localpart, String rawname, String uri) {
super(prefix, localpart, rawname, uri);
increment();
}

public void increment() {
count++;
}

public int getCount() {
return count;
}
}

private final CMXSDDocument document;

private final XSElementDeclaration elementDeclaration;
Expand Down Expand Up @@ -107,6 +137,99 @@ public Collection<CMElementDeclaration> getElements() {
return elements;
}

@Override
public Collection<CMElementDeclaration> getPossibleElements(DOMElement afterElement) {
XSTypeDefinition typeDefinition = elementDeclaration.getTypeDefinition();
if (typeDefinition != null && typeDefinition.getTypeCategory() == XSTypeDefinition.COMPLEX_TYPE) {
// The type definition is complex (ex: xs:all; xs:sequence), returns list of
// element declaration according those constraint

// compute list of QName from the parent element of after element to the after
// element
List<QNameInfo> qnames = new ArrayList<>();
if (afterElement != null) {
QNameInfo last = null;
DOMElement parent = afterElement.getParentElement();
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) child;
if (last != null && last.localpart.equals(element.getLocalName())) {
last.increment();
} else {
last = createQName((Element) child);
qnames.add(last);
}
if (child.equals(afterElement)) {
break;
}
}
}
}

// Compoute list of possible elements
Collection<CMElementDeclaration> possibleElements = new ArrayList<>();
CMBuilder cmBuilder = new CMBuilder(new CMNodeFactory());
XSCMValidator validator = cmBuilder.getContentModel((XSComplexTypeDecl) typeDefinition, true);
int[] states = validator.startContentModel();
// Get list of XSElemenTDeclaration according the XML Schema constraints and
// QNames list
Vector<?> result = whatCanGoHere(validator, states, qnames, 0);
if (result != null) {
QNameInfo last = qnames.size() > 0 ? qnames.get(qnames.size() - 1) : null;
for (Object object : result) {
if (object instanceof XSElementDeclaration) {
XSElementDeclaration elementDecl = (XSElementDeclaration) object;
if (last != null && last.localpart.equals(elementDecl.getName())) {
final int[] occurenceInfo = validator.occurenceInfo(states);
if (occurenceInfo != null && occurenceInfo[1] <= last.count) {
continue;
}
}
document.collectElement(elementDecl, possibleElements);
}
}
}
return possibleElements;
}
return getElements();
}

private Vector<?> whatCanGoHere(XSCMValidator validator, int[] states, List<QNameInfo> qnames, int index) {
if (qnames.size() <= index) {
return validator.whatCanGoHere(states);
}
QNameInfo current = qnames.get(index);
Vector<?> result = validator.whatCanGoHere(states);
for (Object object : result) {
if (object instanceof XSElementDeclaration) {
XSElementDeclaration elementDecl = (XSElementDeclaration) object;
if (!current.localpart.equals(elementDecl.getName())) {
continue;
}
final int[] occurenceInfo = validator.occurenceInfo(states);
if (occurenceInfo != null
&& !(occurenceInfo[0] <= current.count && occurenceInfo[1] >= current.count)) {
return null;
}
index++;
if (qnames.size() < index) {
return null;
}
validator.oneTransition(current, states, new SubstitutionGroupHandler(document));
return whatCanGoHere(validator, states, qnames, index);
}
}
return null;
}

private static QNameInfo createQName(Element tag) {
String namespace = tag.getNamespaceURI();
return new QNameInfo(tag.getPrefix(), tag.getLocalName().intern(), tag.getTagName().intern(),
StringUtils.isEmpty(namespace) ? null : namespace.intern());
}

private void collectElementsDeclaration(XSElementDeclaration elementDecl,
Collection<CMElementDeclaration> elements) {
XSTypeDefinition typeDefinition = elementDecl.getTypeDefinition();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,17 @@ public DOMNode getNode() {
@Override
public DOMElement getParentElement() {
DOMNode currentNode = getNode();
if (!currentNode.isElement() || currentNode.getEnd() < offset) {
if (!currentNode.isElement()) {
// Node is not an element, search parent element.
return currentNode.getParentElement();
}
DOMElement element = (DOMElement) currentNode;
// node is an element, there are 2 cases
// case 1: <| or <bean | > --> in this case we must search parent of bean
// element
if (element.isInStartTag(offset) || element.isInEndTag(offset)) {
// node is an element, there are 3 cases
// - case 1: <|
// - case 2: <bean | >
// - case 3: <bean /> | or <bean></bean> |
// --> in thoses cases we must search parent of bean element
if (element.isInStartTag(offset) || element.isInEndTag(offset) || (element.hasEndTag() && element.getEnd() <= offset)) {
return element.getParentElement();
}
// case 2: <bean> | --> in this case, parent element is the bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ private void collectOpenTagSuggestions(boolean hasOpenBracket, Range replaceRang
}
}
DOMElement parentNode = completionRequest.getParentElement();
if (parentNode != null && !completionResponse.hasSomeItemFromGrammar()) {
if (parentNode != null && !parentNode.getOwnerDocument().hasGrammar()) {
// no grammar, collect similar tags from the parent node
Set<String> seenElements = new HashSet<>();
if (parentNode != null && parentNode.isElement() && parentNode.hasChildNodes()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void completion() throws BadLocationException {
"<catalog xmlns=\"urn:oasis:names:tc:entity:xmlns:xml:catalog\">\r\n" + //
" |";

XMLAssert.testCompletionFor(xml, 16, c("public", "<public publicId=\"\" uri=\"\" />"));
XMLAssert.testCompletionFor(xml, 15, c("public", "<public publicId=\"\" uri=\"\" />"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,18 +198,39 @@ public void noNamespaceSchemaLocationCompletion() throws BadLocationException {
+ //
" <ViewDefinitions>\r\n" + //
" <View><|";
XMLAssert.testCompletionFor(xml, null, "src/test/resources/Format.xml", null, c("Name", "<Name></Name>"),
c("ViewSelectedBy", "<ViewSelectedBy></ViewSelectedBy>"));
// Completion only with Name
XMLAssert.testCompletionFor(xml, null, "src/test/resources/Format.xml", 4, c("Name", "<Name></Name>"),
c("End with '</Configuration>'", "/Configuration>"), c("End with '</ViewDefinitions>'", "/ViewDefinitions>"),
c("End with '</View>'", "/View>"));

xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" + //
"<Configuration xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"xsd/Format.xsd\">\r\n"
+ //
" <ViewDefinitions>\r\n" + //
" <View><Name /><|";
// Completion only with Name
XMLAssert.testCompletionFor(xml, null, "src/test/resources/Format.xml", 4, c("Name", "<Name></Name>"),
c("End with '</Configuration>'", "/Configuration>"), c("End with '</ViewDefinitions>'", "/ViewDefinitions>"),
c("End with '</View>'", "/View>"));
}

@Test
public void schemaLocationWithXSDFileSystemCompletion() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" + //
"<invoice xmlns=\"http://invoice\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n"
+ " xsi:schemaLocation=\"http://invoice xsd/invoice.xsd \">\r\n" + //
+ " xsi:schemaLocation=\"http://invoice xsd/invoice-ns.xsd \">\r\n" + //
" <|";
XMLAssert.testCompletionFor(xml, null, "src/test/resources/invoice.xml", null, c("date", "<date></date>"),
c("number", "<number></number>"));
// Completion only for date
XMLAssert.testCompletionFor(xml, null, "src/test/resources/invoice.xml", 2, c("date", "<date></date>"),
c("End with '</invoice>'", "</invoice>"));

// Completion only for number
xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" + //
"<invoice xmlns=\"http://invoice\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n"
+ " xsi:schemaLocation=\"http://invoice xsd/invoice-ns.xsd \">\r\n" + //
" <date></date>|";
XMLAssert.testCompletionFor(xml, null, "src/test/resources/invoice.xml", 2, c("number", "<number></number>"),
c("End with '</invoice>'", "</invoice>"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ public void completionWithSourceDescriptionAndDetail() throws BadLocationExcepti
String lineSeparator = System.getProperty("line.separator");
XMLAssert.testCompletionFor(xml, null, "src/test/resources/invoice.xml", null,
c("date", te(3, 2, 3, 3, "<date></date>"), "<date",
"Date Description" + lineSeparator + lineSeparator + "Source: invoice.xsd"),
c("number", "<number></number>"));
"Date Description" + lineSeparator + lineSeparator + "Source: invoice.xsd"));
}

@Test
Expand Down
Loading

0 comments on commit 1222e4b

Please sign in to comment.