Skip to content

Commit

Permalink
TargetNamespace.1/2 error range
Browse files Browse the repository at this point in the history
If the declared namespace of the root element is not the same as
the namespace declared in the schema, the `xmlns` attribute of the root
element is highlighted.

If there is no namespace declared in the root element, but one is
declared in the schema, the root element is highlighted.

Added a CodeAction attached to `TargetNamespace.1` that replaces
the namespace declared in the instance with the namespace declared in
the schema.

Added a CodeAction attached to `TargetNamespace.2` that adds the
`xmlns` attribute to the root element and fills it with the namespace
used in the schema.

Fixes eclipse-lemminx#704, Fixes eclipse-lemminx#703

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Jun 4, 2020
1 parent b7be86b commit c7f9a97
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.dom.NoNamespaceSchemaLocation;
import org.eclipse.lemminx.dom.SchemaLocation;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.TargetNamespace_1CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.TargetNamespace_2CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_attribute_3CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_1CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_3CodeAction;
Expand Down Expand Up @@ -74,6 +76,7 @@ public enum XMLSchemaErrorCode implements IXMLErrorCode {
cvc_maxInclusive_valid("cvc-maxInclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-maxinclusive-valid
cvc_minExclusive_valid("cvc-minExclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-minexclusive-valid
cvc_minInclusive_valid("cvc-minInclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-mininclusive-valid
TargetNamespace_1("TargetNamespace.1"), //
TargetNamespace_2("TargetNamespace.2"), SchemaLocation("SchemaLocation"), schema_reference_4("schema_reference.4"), //
src_element_3("src-element.3");

Expand Down Expand Up @@ -138,7 +141,6 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj
case cvc_elt_1_a:
case cvc_complex_type_4:
case src_element_3:
case TargetNamespace_2:
return XMLPositionUtility.selectStartTagName(offset, document);
case cvc_complex_type_3_2_2: {
String attrName = getString(arguments[1]);
Expand Down Expand Up @@ -245,6 +247,10 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj
}
case cvc_type_3_1_2:
return XMLPositionUtility.selectStartTagName(offset, document);
case TargetNamespace_1:
return XMLPositionUtility.selectRootAttributeValue("xmlns", document);
case TargetNamespace_2:
return XMLPositionUtility.selectRootStartTag(document);
default:
}
return null;
Expand All @@ -260,5 +266,7 @@ public static void registerCodeActionParticipants(Map<String, ICodeActionPartici
codeActions.put(cvc_complex_type_3_2_2.getCode(), new cvc_complex_type_3_2_2CodeAction());
codeActions.put(cvc_enumeration_valid.getCode(), new cvc_enumeration_validCodeAction());
codeActions.put(cvc_complex_type_2_1.getCode(), new cvc_complex_type_2_1CodeAction());
codeActions.put(TargetNamespace_1.getCode(), new TargetNamespace_1CodeAction());
codeActions.put(TargetNamespace_2.getCode(), new TargetNamespace_2CodeAction());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.xerces.xni.XMLLocator;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ETagRequiredCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementUnterminatedCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.EqRequiredInAttributeCodeAction;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2020 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*/
package org.eclipse.lemminx.extensions.contentmodel.participants.codeactions;

import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.EnforceQuoteStyle;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;

/**
* CodeAction to replace an incorrect namespace in an .xml document.
*
* Changes the value of the xmlns attribute of the root element of the .xml
* document to the declared namespace of the referenced .xsd document.
*/
public class TargetNamespace_1CodeAction implements ICodeActionParticipant {

private static final Pattern NAMESPACE_EXTRACTOR = Pattern.compile("'([^']+)'\\.");

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {

String namespace = extractNamespace(diagnostic.getMessage());
String quote = "\"";
if (sharedSettings.getFormattingSettings().getEnforceQuoteStyle() == EnforceQuoteStyle.preferred) {
quote = sharedSettings.getPreferences().getQuotationAsString();
}
// @formatter:off
CodeAction replaceNamespace = CodeActionFactory.replace(
"Replace with '" + namespace + "'",
diagnostic.getRange(),
quote + namespace + quote,
document.getTextDocument(), diagnostic);
// @formatter:on
codeActions.add(replaceNamespace);
}

private static String extractNamespace(String diagnosticMessage) {
// The error message has this form:
// TargetNamespace.1: Expecting namespace 'NaN', but the target namespace of the
// schema document is 'http://two-letter-name'.
Matcher nsMatcher = NAMESPACE_EXTRACTOR.matcher(diagnosticMessage);
if (nsMatcher.find()) {
String namespace = nsMatcher.group(1);
return namespace == null ? "" : namespace;
}
return "";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2020 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*/
package org.eclipse.lemminx.extensions.contentmodel.participants.codeactions;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.EnforceQuoteStyle;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;

/**
* Code action to add the missing xmlns declaration to the root element in an
* .xml document.
*
* Finds the namespace of the referenced .xsd. Adds the xmlns attribute
* to the root of .xml, and sets its value to the .xsd namespace.
*/
public class TargetNamespace_2CodeAction implements ICodeActionParticipant {

private static final Pattern NAMESPACE_EXTRACTOR = Pattern.compile("'([^']+)'\\.");

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {

DOMNode root = document.getDocumentElement();
if (root == null) {
return;
}
Position tagEnd = XMLPositionUtility.selectStartTagName(root).getEnd();
String namespace = extractNamespace(diagnostic.getMessage());
String quote = "\"";
if (sharedSettings.getFormattingSettings().getEnforceQuoteStyle() == EnforceQuoteStyle.preferred) {
quote = sharedSettings.getPreferences().getQuotationAsString();
}
// @formatter:off
CodeAction addNamespaceDecl = CodeActionFactory.insert(
"Declare '" + namespace + "' as the namespace",
tagEnd,
" xmlns=" + quote + namespace + quote,
document.getTextDocument(),
diagnostic);
// @formatter:on
codeActions.add(addNamespaceDecl);
}

private static String extractNamespace(String diagnosticMessage) {
// The error message has this form:
// TargetNamespace.2: Expecting no namespace, but the schema document has a
// target namespace of 'http://docbook.org/ns/docbook'.
Matcher nsMatcher = NAMESPACE_EXTRACTOR.matcher(diagnosticMessage);
if (nsMatcher.find()) {
String namespace = nsMatcher.group(1);
return namespace == null ? "" : namespace;
}
return "";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ public static Range selectRootStartTag(DOMDocument document) {
return selectStartTagName(root);
}

public static Range selectRootAttributeValue(String attrName, DOMDocument document) {
DOMNode root = document.getDocumentElement();
if (root == null) {
root = document.getChild(0);
}
if (root == null) {
return null;
}
DOMAttr attr = root.getAttributeNode(attrName);
if (attr == null) {
return null;
}
return selectAttributeValue(attr);
}

public static Range selectStartTagName(int offset, DOMDocument document) {
DOMNode element = document.findNodeAt(offset);
if (element != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import org.eclipse.lemminx.XMLAssert;
import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSchemaErrorCode;
import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings;
import org.eclipse.lemminx.settings.EnforceQuoteStyle;
import org.eclipse.lemminx.settings.QuoteStyle;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.Diagnostic;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -577,6 +580,69 @@ public void cvc_complex_type_2_2_withText() throws Exception {
testDiagnosticsFor(xml, diagnosticBob, diagnostic_cvc_2_2);
}

@Test
public void testTargetNamespace_1Normal() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + //
"<?xml-model href=\"src/test/resources/xsd/two-letter-name.xsd\"?>\n" + //
"<two-letter-name xmlns=\"BAD_NS\">Io</two-letter-name>";
Diagnostic targetNamespace = d(2, 23, 2, 31, XMLSchemaErrorCode.TargetNamespace_1, "TargetNamespace.1: Expecting namespace 'BAD_NS', but the target namespace of the schema document is 'http://two-letter-name'.");
testDiagnosticsFor(xml,
targetNamespace,
d(2, 1, 2, 16, XMLSchemaErrorCode.cvc_elt_1_a, "cvc-elt.1.a: Cannot find the declaration of element 'two-letter-name'.")
);
testCodeActionsFor(xml, targetNamespace, ca(targetNamespace, te(2, 23, 2, 31, "\"http://two-letter-name\"")));
}

@Test
public void testTargetNamespace_1ShortNS() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + //
"<?xml-model href=\"src/test/resources/xsd/two-letter-name.xsd\"?>\n" + //
"<two-letter-name xmlns=\"_\">Io</two-letter-name>";
Diagnostic targetNamespace = d(2, 23, 2, 26, XMLSchemaErrorCode.TargetNamespace_1, "TargetNamespace.1: Expecting namespace '_', but the target namespace of the schema document is 'http://two-letter-name'.");
testDiagnosticsFor(xml,
targetNamespace,
d(2, 1, 2, 16, XMLSchemaErrorCode.cvc_elt_1_a, "cvc-elt.1.a: Cannot find the declaration of element 'two-letter-name'.")
);
testCodeActionsFor(xml, targetNamespace, ca(targetNamespace, te(2, 23, 2, 26, "\"http://two-letter-name\"")));
}

@Test
public void testTargetNamespace_1SingleQuotes() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + //
"<?xml-model href=\"src/test/resources/xsd/two-letter-name.xsd\"?>\n" + //
"<two-letter-name xmlns=\"_\">Io</two-letter-name>";
SharedSettings settings = new SharedSettings();
settings.getFormattingSettings().setEnforceQuoteStyle(EnforceQuoteStyle.preferred);
settings.getPreferences().setQuoteStyle(QuoteStyle.singleQuotes);
Diagnostic targetNamespace = d(2, 23, 2, 26, XMLSchemaErrorCode.TargetNamespace_1, "TargetNamespace.1: Expecting namespace '_', but the target namespace of the schema document is 'http://two-letter-name'.");
testCodeActionsFor(xml, targetNamespace, settings, ca(targetNamespace, te(2, 23, 2, 26, "'http://two-letter-name'")));
}

@Test
public void testTargetNamespace_2() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + //
"<?xml-model href=\"src/test/resources/xsd/two-letter-name.xsd\"?>\n" + //
"<two-letter-name>Io</two-letter-name>";
Diagnostic targetNamespace = d(2, 1, 2, 16, XMLSchemaErrorCode.TargetNamespace_2, "TargetNamespace.2: Expecting no namespace, but the schema document has a target namespace of 'http://two-letter-name'.");
testDiagnosticsFor(xml,
targetNamespace,
d(2, 1, 2, 16, XMLSchemaErrorCode.cvc_elt_1_a, "cvc-elt.1.a: Cannot find the declaration of element 'two-letter-name'.")
);
testCodeActionsFor(xml, targetNamespace, ca(targetNamespace, te(2, 16, 2, 16, " xmlns=\"http://two-letter-name\"")));
}

@Test
public void testTargetNamespace_2SingleQuotes() throws Exception {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + //
"<?xml-model href=\"src/test/resources/xsd/two-letter-name.xsd\"?>\n" + //
"<two-letter-name>Io</two-letter-name>";
SharedSettings settings = new SharedSettings();
settings.getFormattingSettings().setEnforceQuoteStyle(EnforceQuoteStyle.preferred);
settings.getPreferences().setQuoteStyle(QuoteStyle.singleQuotes);
Diagnostic targetNamespace = d(2, 1, 2, 16, XMLSchemaErrorCode.TargetNamespace_2, "TargetNamespace.2: Expecting no namespace, but the schema document has a target namespace of 'http://two-letter-name'.");
testCodeActionsFor(xml, targetNamespace, settings, ca(targetNamespace, te(2, 16, 2, 16, " xmlns='http://two-letter-name'")));
}

private static void testDiagnosticsFor(String xml, Diagnostic... expected) {
XMLAssert.testDiagnosticsFor(xml, "src/test/resources/catalogs/catalog.xml", expected);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static org.eclipse.lemminx.XMLAssert.testDiagnosticsFor;

import org.eclipse.lemminx.XMLAssert;
import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSchemaErrorCode;
import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSyntaxErrorCode;
import org.eclipse.lemminx.settings.EnforceQuoteStyle;
import org.eclipse.lemminx.settings.QuoteStyle;
Expand Down
10 changes: 10 additions & 0 deletions org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://two-letter-name">
<xs:element name="two-letter-name">
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:pattern value="[A-Z][a-z]"></xs:pattern>
</xs:restriction>
</xs:simpleType>
</xs:element>
</xs:schema>

0 comments on commit c7f9a97

Please sign in to comment.