From c7f9a97892dc6eabda7853b0c32a44bc5184f567 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Wed, 3 Jun 2020 12:05:00 -0400 Subject: [PATCH] TargetNamespace.1/2 error range 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 #704, Fixes #703 Signed-off-by: David Thompson --- .../participants/XMLSchemaErrorCode.java | 10 ++- .../participants/XMLSyntaxErrorCode.java | 1 + .../TargetNamespace_1CodeAction.java | 70 ++++++++++++++++ .../TargetNamespace_2CodeAction.java | 80 +++++++++++++++++++ .../lemminx/utils/XMLPositionUtility.java | 15 ++++ .../XMLSchemaDiagnosticsTest.java | 66 +++++++++++++++ .../XMLSyntaxDiagnosticsTest.java | 1 + .../test/resources/xsd/two-letter-name.xsd | 10 +++ 8 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_1CodeAction.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java create mode 100644 org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java index ebc900d46c..51be2d5502 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java @@ -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; @@ -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"); @@ -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]); @@ -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; @@ -260,5 +266,7 @@ public static void registerCodeActionParticipants(Map 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 ""; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java new file mode 100644 index 0000000000..b6bf2e7505 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/codeactions/TargetNamespace_2CodeAction.java @@ -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 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 ""; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java index 5f1749faf7..e5c354b267 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/XMLPositionUtility.java @@ -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) { diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java index e6d811ad1b..b2fa045655 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSchemaDiagnosticsTest.java @@ -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; @@ -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 = "\n" + // + "\n" + // + "Io"; + 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 = "\n" + // + "\n" + // + "Io"; + 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 = "\n" + // + "\n" + // + "Io"; + 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 = "\n" + // + "\n" + // + "Io"; + 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 = "\n" + // + "\n" + // + "Io"; + 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); } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java index f2932f912f..0db6fc2dda 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLSyntaxDiagnosticsTest.java @@ -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; diff --git a/org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd b/org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd new file mode 100644 index 0000000000..83ede5f3a1 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xsd/two-letter-name.xsd @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file