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

Diagnostics and Quick fixes for lost config elements #220

Merged
merged 14 commits into from
Oct 10, 2023
2 changes: 1 addition & 1 deletion lemminx-liberty/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<groupId>io.openliberty.tools</groupId>
<artifactId>liberty-langserver-lemminx</artifactId>
<packaging>jar</packaging>
<version>2.0.2-SNAPSHOT</version>
<version>2.1-SNAPSHOT</version>

<name>lemminx-liberty</name>
<url>https://github.com/OpenLiberty/liberty-language-server</url>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

import io.openliberty.tools.langserver.lemminx.codeactions.AddAttribute;
import io.openliberty.tools.langserver.lemminx.codeactions.AddFeature;
import io.openliberty.tools.langserver.lemminx.codeactions.CreateFile;
import io.openliberty.tools.langserver.lemminx.codeactions.EditAttribute;
import io.openliberty.tools.langserver.lemminx.codeactions.ReplaceFeature;
Expand Down Expand Up @@ -55,6 +56,7 @@ private void registerCodeActions() {
codeActionParticipants.put(LibertyDiagnosticParticipant.NOT_OPTIONAL_CODE, new EditAttribute());
codeActionParticipants.put(LibertyDiagnosticParticipant.IMPLICIT_NOT_OPTIONAL_CODE, new AddAttribute());
codeActionParticipants.put(LibertyDiagnosticParticipant.INCORRECT_FEATURE_CODE, new ReplaceFeature());
codeActionParticipants.put(LibertyDiagnosticParticipant.MISSING_CONFIGURED_FEATURE_CODE, new AddFeature());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,39 @@
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

import io.openliberty.tools.langserver.lemminx.data.FeatureListGraph;
import io.openliberty.tools.langserver.lemminx.data.LibertyRuntime;
import io.openliberty.tools.langserver.lemminx.services.FeatureService;
import io.openliberty.tools.langserver.lemminx.services.SettingsService;
import io.openliberty.tools.langserver.lemminx.util.*;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

public class LibertyDiagnosticParticipant implements IDiagnosticsParticipant {
private static final Logger LOGGER = Logger.getLogger(LibertyDiagnosticParticipant.class.getName());

public static final String LIBERTY_LEMMINX_SOURCE = "liberty-lemminx";

public static final String MISSING_FILE_MESSAGE = "The resource at the specified location could not be found.";
public static final String MISSING_FILE_CODE = "missing_file";

public static final String MISSING_CONFIGURED_FEATURE_MESSAGE = "This config element does not configure a feature in the featureManager. Remove this element or add a relevant feature.";
public static final String MISSING_CONFIGURED_FEATURE_CODE = "lost_config_element";

public static final String NOT_OPTIONAL_MESSAGE = "The specified resource cannot be skipped. Check location value or set optional to true.";
public static final String NOT_OPTIONAL_CODE = "not_optional";
public static final String IMPLICIT_NOT_OPTIONAL_MESSAGE = "The specified resource cannot be skipped. Check location value or add optional attribute.";
public static final String IMPLICIT_NOT_OPTIONAL_CODE = "implicit_not_optional";

public static final String INCORRECT_FEATURE_CODE = "incorrect_feature";

private Set<String> includedFeatures = null;

@Override
public void doDiagnostics(DOMDocument domDocument, List<Diagnostic> diagnostics,
Expand All @@ -53,22 +65,35 @@ public void doDiagnostics(DOMDocument domDocument, List<Diagnostic> diagnostics,
try {
validateDom(domDocument, diagnostics);
} catch (IOException e) {
System.err.println("Error validating document " + domDocument.getDocumentURI());
System.err.println(e.getMessage());
LOGGER.severe("Error validating document " + domDocument.getDocumentURI());
LOGGER.severe(e.getMessage());
}
}

private void validateDom(DOMDocument domDocument, List<Diagnostic> list) throws IOException {
private void validateDom(DOMDocument domDocument, List<Diagnostic> diagnosticsList) throws IOException {
List<DOMNode> nodes = domDocument.getDocumentElement().getChildren();
List<Diagnostic> tempDiagnosticsList = new ArrayList<Diagnostic>();

FeatureListGraph featureGraph = FeatureService.getInstance().getFeatureListGraph();
// TODO: Consider adding a cached feature list onto repo to optimize
if (featureGraph.isEmpty()) {
LibertyRuntime runtimeInfo = LibertyUtils.getLibertyRuntimeInfo(domDocument);
String libertyVersion = runtimeInfo == null ? null : runtimeInfo.getRuntimeVersion();
String libertyRuntime = runtimeInfo == null ? null : runtimeInfo.getRuntimeType();
FeatureService.getInstance().getInstalledFeaturesList(domDocument.getDocumentURI(), libertyRuntime, libertyVersion);
}

for (DOMNode node : nodes) {
if (LibertyConstants.FEATURE_MANAGER_ELEMENT.equals(node.getNodeName())) {
validateFeature(domDocument, list, node);
} else if (LibertyConstants.INCLUDE_ELEMENT.equals(node.getNodeName())) {
validateIncludeLocation(domDocument, list, node);
String nodeName = node.getNodeName();
if (LibertyConstants.FEATURE_MANAGER_ELEMENT.equals(nodeName)) {
validateFeature(domDocument, diagnosticsList, node);
} else if (LibertyConstants.INCLUDE_ELEMENT.equals(nodeName)) {
validateIncludeLocation(domDocument, diagnosticsList, node);
} else if (featureGraph.isConfigElement(nodeName)) { // defaults to false
holdConfigElement(domDocument, diagnosticsList, node, tempDiagnosticsList);
}
}

validateConfigElements(diagnosticsList, tempDiagnosticsList, featureGraph);
}

private void validateFeature(DOMDocument domDocument, List<Diagnostic> list, DOMNode featureManager) {
Expand All @@ -93,19 +118,20 @@ private void validateFeature(DOMDocument domDocument, List<Diagnostic> list, DOM
Range range = XMLPositionUtility.createRange(featureTextNode.getStart(), featureTextNode.getEnd(),
domDocument);
String message = "ERROR: The feature \"" + featureName + "\" does not exist.";
list.add(new Diagnostic(range, message, DiagnosticSeverity.Error, "liberty-lemminx", INCORRECT_FEATURE_CODE));
list.add(new Diagnostic(range, message, DiagnosticSeverity.Error, LIBERTY_LEMMINX_SOURCE, INCORRECT_FEATURE_CODE));
} else {
if (includedFeatures.contains(featureName)) {
Range range = XMLPositionUtility.createRange(featureTextNode.getStart(),
featureTextNode.getEnd(), domDocument);
String message = "ERROR: " + featureName + " is already included.";
list.add(new Diagnostic(range, message, DiagnosticSeverity.Error, "liberty-lemminx"));
list.add(new Diagnostic(range, message, DiagnosticSeverity.Error, LIBERTY_LEMMINX_SOURCE));
} else {
includedFeatures.add(featureName);
}
}
}
}
this.includedFeatures = includedFeatures;
}

/**
Expand All @@ -117,7 +143,7 @@ private void validateFeature(DOMDocument domDocument, List<Diagnostic> list, DOM
* 2) performed in isConfigXMLFile
* 4) not yet implemented/determined
*/
private void validateIncludeLocation(DOMDocument domDocument, List<Diagnostic> list, DOMNode node) {
private void validateIncludeLocation(DOMDocument domDocument, List<Diagnostic> diagnosticsList, DOMNode node) {
String locAttribute = node.getAttribute("location");
if (locAttribute == null) {
return;
Expand All @@ -131,7 +157,7 @@ private void validateIncludeLocation(DOMDocument domDocument, List<Diagnostic> l
Range range = XMLPositionUtility.createRange(locNode.getStart(), locNode.getEnd(), domDocument);
if (!locAttribute.endsWith(".xml")) {
String message = "The specified resource is not an XML file.";
list.add(new Diagnostic(range, message, DiagnosticSeverity.Warning, "liberty-lemminx"));
diagnosticsList.add(new Diagnostic(range, message, DiagnosticSeverity.Warning, LIBERTY_LEMMINX_SOURCE));
return;
}

Expand All @@ -144,15 +170,48 @@ private void validateIncludeLocation(DOMDocument domDocument, List<Diagnostic> l
if (!configFile.exists()) {
DOMAttr optNode = node.getAttributeNode("optional");
if (optNode == null) {
list.add(new Diagnostic(range, IMPLICIT_NOT_OPTIONAL_MESSAGE, DiagnosticSeverity.Error, "liberty-lemminx", IMPLICIT_NOT_OPTIONAL_CODE));
diagnosticsList.add(new Diagnostic(range, IMPLICIT_NOT_OPTIONAL_MESSAGE, DiagnosticSeverity.Error, LIBERTY_LEMMINX_SOURCE, IMPLICIT_NOT_OPTIONAL_CODE));
} else if (optNode.getValue().equals("false")) {
Range optRange = XMLPositionUtility.createRange(optNode.getStart(), optNode.getEnd(), domDocument);
list.add(new Diagnostic(optRange, NOT_OPTIONAL_MESSAGE, DiagnosticSeverity.Error, "liberty-lemminx", NOT_OPTIONAL_CODE));
diagnosticsList.add(new Diagnostic(optRange, NOT_OPTIONAL_MESSAGE, DiagnosticSeverity.Error, LIBERTY_LEMMINX_SOURCE, NOT_OPTIONAL_CODE));
}
list.add(new Diagnostic(range, MISSING_FILE_MESSAGE, DiagnosticSeverity.Warning, "liberty-lemminx", MISSING_FILE_CODE));
diagnosticsList.add(new Diagnostic(range, MISSING_FILE_MESSAGE, DiagnosticSeverity.Warning, LIBERTY_LEMMINX_SOURCE, MISSING_FILE_CODE));
}
} catch (IllegalArgumentException e) {
list.add(new Diagnostic(range, MISSING_FILE_MESSAGE, DiagnosticSeverity.Warning, "liberty-lemminx-exception", MISSING_FILE_CODE));
diagnosticsList.add(new Diagnostic(range, MISSING_FILE_MESSAGE, DiagnosticSeverity.Warning, "liberty-lemminx-exception", MISSING_FILE_CODE));
}
}

/**
* Create temporary diagnostics for validation for single pass-through.
* @param domDocument
* @param diagnosticsList
* @param configElementNode
* @param tempDiagnosticsList
*/
private void holdConfigElement(DOMDocument domDocument, List<Diagnostic> diagnosticsList, DOMNode configElementNode, List<Diagnostic> tempDiagnosticsList) {
String configElementName = configElementNode.getNodeName();
Range range = XMLPositionUtility.createRange(configElementNode.getStart(), configElementNode.getEnd(), domDocument);
Diagnostic tempDiagnostic = new Diagnostic(range, MISSING_CONFIGURED_FEATURE_MESSAGE, null, LIBERTY_LEMMINX_SOURCE, MISSING_CONFIGURED_FEATURE_CODE);
tempDiagnostic.setSource(configElementName);
tempDiagnosticsList.add(tempDiagnostic);
}

/**
* Compare the required feature set with included feature set for each config element.
* @param diagnosticsList
* @param tempDiagnosticsList
* @param featureGraph
*/
private void validateConfigElements(List<Diagnostic> diagnosticsList, List<Diagnostic> tempDiagnosticsList, FeatureListGraph featureGraph) {
for (Diagnostic tempDiagnostic : tempDiagnosticsList) {
String configElement = tempDiagnostic.getSource();
Set<String> includedFeaturesCopy = (includedFeatures == null) ? new HashSet<String>() : new HashSet<String>(includedFeatures);
Set<String> compatibleFeaturesList = featureGraph.getAllEnabledBy(configElement);
includedFeaturesCopy.retainAll(compatibleFeaturesList);
if (includedFeaturesCopy.isEmpty()) {
diagnosticsList.add(tempDiagnostic);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*******************************************************************************
* Copyright (c) 2023 IBM Corporation and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package io.openliberty.tools.langserver.lemminx.codeactions;

import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

import io.openliberty.tools.langserver.lemminx.services.FeatureService;
import io.openliberty.tools.langserver.lemminx.util.LibertyConstants;

public class AddFeature implements ICodeActionParticipant {
public static final String FEATURE_FORMAT = "\n<feature>%s</feature>";
public static final String FEATUREMANAGER_FORMAT =
"\n\t<featureManager>" +
"\n\t\t<feature>%s</feature>" +
"\n\t</featureManager>";

@Override
public void doCodeAction(ICodeActionRequest request, List<CodeAction> codeActions, CancelChecker cancelChecker) {
Diagnostic diagnostic = request.getDiagnostic();
DOMDocument document = request.getDocument();
try {
String insertText = "";
DOMNode insertNode = null;
String indent = request.getXMLGenerator().getWhitespacesIndent();

for (DOMNode node : document.getDocumentElement().getChildren()) {
if (LibertyConstants.FEATURE_MANAGER_ELEMENT.equals(node.getNodeName())) {
// If featureManager is empty, add new feature after featureManager start tag.
// Otherwise, add feature after last feature child.
insertNode = node.getLastChild();
insertText = (insertNode == null) ? FEATURE_FORMAT.replace("\n", "\n\t") : FEATURE_FORMAT;
insertNode = (insertNode == null) ? node : insertNode;
break;
}
}

int insertEndOffset = (insertNode == null) ? ((DOMElement)document.getDocumentElement()).getStartTagCloseOffset()+1 : insertNode.getEnd();
// featureManager not found
if (insertNode == null) {
insertNode = document.getDocumentElement();
insertText = FEATUREMANAGER_FORMAT;
}

Range nodeRange = XMLPositionUtility.createRange(insertNode.getStart(), insertEndOffset, document);
Logger.getLogger("dshi").warning("startCharacter" + nodeRange.getStart().getCharacter());
Logger.getLogger("dshi").warning("endCharacter" + nodeRange.getEnd().getCharacter());
insertText = IndentUtil.formatText(insertText, indent, nodeRange.getStart().getCharacter());

// getAllEnabledBy would return all transitive features, but is too many to offer without a filter
Set<String> featureCandidates = FeatureService.getInstance().getFeatureListGraph().get(diagnostic.getSource()).getEnabledBy();
for (String feature : featureCandidates) {
String title = "Add feature " + feature;
codeActions.add(CodeActionFactory.insert(
title, nodeRange.getEnd(), String.format(insertText, feature), document.getTextDocument(), diagnostic));
}
} catch (Exception e) {

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2023 IBM Corporation and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package io.openliberty.tools.langserver.lemminx.codeactions;

public class IndentUtil {
public static final String NEW_LINE = System.lineSeparator();

public static String whitespaceBuffer(String indent, int column) {
String whitespaceBuffer = "";
for (int i = 0; i < column / indent.length(); i++) {
whitespaceBuffer += indent;
}
return whitespaceBuffer;
}

public static String formatText(String text, String indent, int column) {
return text.replace("\n", System.lineSeparator() + whitespaceBuffer(indent, column))
.replace("\t", indent);
}
}
Loading