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

Add Code Actions for Managed Dependency Version override #366

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ private DOMConstants() {
public static final String VERSION_ELT = "version";
public static final String PLUGINS_ELT = "plugins";
public static final String PLUGIN_ELT = "plugin";
public static final String PROFILE_ELT = "profile";
public static final String PROFILES_ELT = "profiles";
public static final String PARENT_ELT = "parent";
public static final String SCOPE_ELT = "scope";
public static final String PHASE_ELT = "phase";
Expand All @@ -40,6 +42,7 @@ private DOMConstants() {
public static final String CLASSIFIER_ELT = "classifier";
public static final String EXCLUSION_ELT = "exclusion";
public static final String EXCLUSIONS_ELT = "exclusions";
public static final String ID_ELT = "id";

public static final String TARGET_PATH_ELT = "targetPath";
public static final String DIRECTORY_ELT = "directory";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import org.eclipse.aether.repository.WorkspaceReader;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.MavenIdPartRemovalCodeAction;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.MavenManagedVersionRemovalCodeAction;
import org.eclipse.lemminx.extensions.maven.participants.codeaction.MavenNoGrammarConstraintsCodeAction;
import org.eclipse.lemminx.extensions.maven.participants.completion.MavenCompletionParticipant;
import org.eclipse.lemminx.extensions.maven.participants.definition.MavenDefinitionParticipant;
Expand Down Expand Up @@ -482,6 +483,7 @@ private void registerCodeActionParticipants(XMLExtensionsRegistry registry) {
}
codeActionParticipants.add(new MavenNoGrammarConstraintsCodeAction());
codeActionParticipants.add(new MavenIdPartRemovalCodeAction());
codeActionParticipants.add(new MavenManagedVersionRemovalCodeAction());

codeActionParticipants.stream().forEach(registry::registerCodeActionParticipant);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

public enum MavenSyntaxErrorCode implements IXMLErrorCode {
DuplicationOfParentGroupId,
DuplicationOfParentVersion;
DuplicationOfParentVersion,
OverridingOfManagedDependency;

private final String code;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*******************************************************************************
* Copyright (c) 2023 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.lemminx.extensions.maven.participants.codeaction;

import static org.eclipse.lemminx.client.ClientCommands.OPEN_URI;
import static org.eclipse.lemminx.extensions.maven.participants.diagnostics.ProjectValidator.ATTR_MANAGED_VERSION_LOCATION;
import static org.eclipse.lemminx.extensions.maven.participants.diagnostics.ProjectValidator.ATTR_MANAGED_VERSION_LINE;
import static org.eclipse.lemminx.extensions.maven.participants.diagnostics.ProjectValidator.ATTR_MANAGED_VERSION_COLUMN;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.commons.TextDocument;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.extensions.maven.MavenSyntaxErrorCode;
import org.eclipse.lemminx.extensions.maven.utils.ParticipantUtils;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.codeaction.ICodeActionRequest;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

public class MavenManagedVersionRemovalCodeAction implements ICodeActionParticipant {
private static final Logger LOGGER = Logger.getLogger(MavenIdPartRemovalCodeAction.class.getName());

/**
* string that gets included in pom.xml file comments and makes the marker
* manager to ignore the managed version override marker
*/
String MARKER_IGNORE_MANAGED = "$NO-MVN-MAN-VER$";//$NON-NLS-1$

public MavenManagedVersionRemovalCodeAction() {
}

@Override
public void doCodeAction(ICodeActionRequest request, List<CodeAction> codeActions, CancelChecker cancelChecker) {
Diagnostic diagnostic = request.getDiagnostic();
if (!ParticipantUtils.match(diagnostic, MavenSyntaxErrorCode.OverridingOfManagedDependency.getCode())) {
return;
}

DOMDocument document = request.getDocument();
Range diagnosticRange = diagnostic.getRange();

TextDocument textDocument = document.getTextDocument();
try {
int startOffset = document.offsetAt(diagnosticRange.getStart());
DOMNode version = document.findNodeAt(startOffset);
if (version != null) {
// "Adds comment markup next to the affected element. No longer shows the warning afterwards
Range valueRange = new Range(textDocument.positionAt(version.getEnd()), textDocument.positionAt(version.getEnd()));
CodeAction addIgnoreMarkup = CodeActionFactory.insert("Ignore this warning", valueRange.getEnd(),
"<!--" + MARKER_IGNORE_MANAGED + "-->", document.getTextDocument(), diagnostic);
codeActions.add(addIgnoreMarkup);

// It removes the current definition to rely on value inherited from parent
valueRange = new Range(textDocument.positionAt(version.getStart()), textDocument.positionAt(version.getEnd()));
CodeAction addSchemaAction = CodeActionFactory.remove(
"Remove version declaration",
valueRange, document.getTextDocument(), diagnostic);
codeActions.add(addSchemaAction);

// Opens the declaration of managed version
if (diagnostic.getData() instanceof Map<?, ?> dataMap) {
Object locationObj = dataMap.get(ATTR_MANAGED_VERSION_LOCATION);
if (locationObj instanceof String location) {
int line = -1;
int column = -1;
try {
Object lineObj = dataMap.get(ATTR_MANAGED_VERSION_LINE);
if (lineObj instanceof String lineString) {
line = Integer.valueOf(lineString);
}
Object columnObj = dataMap.get(ATTR_MANAGED_VERSION_COLUMN);
if (columnObj instanceof String columnString) {
column = Integer.valueOf(columnString);
}
} catch (NumberFormatException e) {
// Ignore
}

boolean canSupportOpenUri = true; // TODO: Should be asked from ICodeActionRequest?
String uri = location;
if (line > -1) {
uri += "#L" + line;
if (column > -1) {
uri += "," + column;
}
}
CodeAction openLocation = CodeActionFactory.createCommand("Open declaration of managed version",
canSupportOpenUri ? OPEN_URI : "",
canSupportOpenUri ? Arrays.asList(uri) : null,
diagnostic);
codeActions.add(openLocation);
}
}
}
} catch (BadLocationException e) {
LOGGER.log(Level.SEVERE, "Unable to remove specified element", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,31 @@
*******************************************************************************/
package org.eclipse.lemminx.extensions.maven.participants.diagnostics;

import static org.eclipse.lemminx.extensions.maven.DOMConstants.ARTIFACT_ID_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.CLASSIFIER_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.DEPENDENCY_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.DEPENDENCIES_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.GROUP_ID_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.ID_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.PARENT_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROFILE_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROFILES_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.TYPE_ELT;
import static org.eclipse.lemminx.extensions.maven.DOMConstants.VERSION_ELT;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;

import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.InputLocation;
import org.apache.maven.model.InputLocationTracker;
import org.apache.maven.model.InputSource;
import org.apache.maven.project.MavenProject;

import org.eclipse.lemminx.dom.DOMDocument;
Expand All @@ -32,6 +48,10 @@
public class ProjectValidator {
private static final Logger LOGGER = Logger.getLogger(ProjectValidator.class.getName());

public static final String ATTR_MANAGED_VERSION_LOCATION = "managedVersionLocation"; //$NON-NLS-1$
public static final String ATTR_MANAGED_VERSION_LINE = "managedVersionLine"; //$NON-NLS-1$
public static final String ATTR_MANAGED_VERSION_COLUMN = "managedVersionColumn"; //$NON-NLS-1$

private MavenLemminxExtension plugin;

public ProjectValidator(MavenLemminxExtension plugin) {
Expand Down Expand Up @@ -114,7 +134,7 @@ private Optional<List<Diagnostic>> validateParentMatchingGroupIdVersion(Diagnost
}

/**
* TODO: Validates if a dependency version duplicates or overrides a managed dependency version
* Validates if a dependency version duplicates or overrides a managed dependency version
*
* @param diagnosticRequest
* @return
Expand All @@ -125,7 +145,84 @@ private Optional<List<Diagnostic>> validateManagedDependencies(DiagnosticRequest
return Optional.empty();
}

List<DOMElement> candidates = new ArrayList<>();
Optional<DOMElement> dependencies = DOMUtils.findChildElement(node, DEPENDENCIES_ELT);
dependencies.ifPresent(dependenciesElement -> {
DOMUtils.findChildElements(dependenciesElement, DEPENDENCY_ELT).stream()
.filter(dependency -> DOMUtils.findChildElement(dependency, VERSION_ELT).isPresent())
.forEach(candidates::add);
});

// we should also consider <dependencies> section in the profiles, but profile
// are optional and so is their
// dependencyManagement section.. that makes handling our markers more complex.
// see MavenProject.getInjectedProfileIds() for a list of currently active
// profiles in effective pom
String currentProjectKey = mavenProject.getGroupId() + ":" + mavenProject.getArtifactId() + ":" //$NON-NLS-1$//$NON-NLS-2$
+ mavenProject.getVersion();
List<String> activeprofiles = mavenProject.getInjectedProfileIds().get(currentProjectKey);
// remember what profile we found the dependency in.
Optional<DOMElement> profiles = DOMUtils.findChildElement(node, PROFILES_ELT);
profiles.ifPresent(profilesElement -> {
DOMUtils.findChildElements(profilesElement, PROFILE_ELT).stream()
.forEach(profile -> {
Optional<String> idString = DOMUtils.findChildElementText(profile, ID_ELT);
if (idString.isPresent() && activeprofiles.contains(idString.get())) {
Optional<DOMElement> profileDependencies = DOMUtils.findChildElement(profile, DEPENDENCIES_ELT);
profileDependencies.ifPresent(dependenciesElement -> {
DOMUtils.findChildElements(dependenciesElement, DEPENDENCY_ELT).stream()
.filter(dependency -> DOMUtils.findChildElement(dependency, VERSION_ELT).isPresent())
.forEach(dependency -> {
candidates.add(dependency);
});
});
}
});
});

//collect the managed dep ids
Map<String, Dependency> managed = new HashMap<>();
DependencyManagement dm = mavenProject.getDependencyManagement();
if(dm != null) {
List<Dependency> deps = dm.getDependencies();
deps.stream().filter(dep -> dep.getVersion() != null)
.forEach(dep -> managed.put(dep.getManagementKey(), dep));
}

//now we have all the candidates, match them against the effective managed set
List<Diagnostic> diagnostics = new ArrayList<>();
candidates.stream().forEach(dep -> {
Optional<DOMElement> version = DOMUtils.findChildElement(dep, VERSION_ELT);
version.ifPresent(v -> {
String grpString = DOMUtils.findChildElementText(dep, GROUP_ID_ELT).orElse(null);
String artString = DOMUtils.findChildElementText(dep, ARTIFACT_ID_ELT).orElse(null);
String versionString = DOMUtils.findElementText(v).orElse(null);
if(grpString != null && artString != null && versionString != null) {
String typeString = DOMUtils.findChildElementText(dep, TYPE_ELT).orElse(null);
String classifier = DOMUtils.findChildElementText(dep, CLASSIFIER_ELT).orElse(null);
String id = getDependencyKey(grpString, artString, typeString, classifier);
if(managed.containsKey(id)) {
Dependency managedDep = managed.get(id);
String managedVersion = managedDep == null ? null : managedDep.getVersion();
String msg = versionString.equals(managedVersion)
? "Duplicating managed version %s for %s"
: "Overriding managed version %s for %s";

DOMDocument xmlDocument = diagnosticRequest.getXMLDocument();
Diagnostic diagnostic = new Diagnostic(
XMLPositionUtility.createRange(v.getStartTagCloseOffset() + 1,
v.getEndTagOpenOffset(), xmlDocument),
String.format(msg, managedVersion, artString),
DiagnosticSeverity.Warning,
xmlDocument.getDocumentURI(),
MavenSyntaxErrorCode.OverridingOfManagedDependency.getCode());
setManagedVersionAttributes(diagnostic, mavenProject, managedDep);
diagnostics.add(diagnostic);
}
}
});
});

return Optional.of(diagnostics);
}

Expand All @@ -142,4 +239,40 @@ private Optional<List<Diagnostic>> validateManagedPlugins(DiagnosticRequest diag
List<Diagnostic> diagnostics = new ArrayList<>();
return Optional.of(diagnostics);
}

private static String getDependencyKey(String groupId, String artifactId, String type, String classifier) {
StringBuilder key = new StringBuilder(groupId).append(":").append(artifactId).append(":") //$NON-NLS-1$ //$NON-NLS-2$
.append(type == null ? "jar" : type);//$NON-NLS-1$
if (classifier != null) {
key.append(":").append(classifier);//$NON-NLS-1$
}
return key.toString();
}

private static void setManagedVersionAttributes(Diagnostic diagnostic, MavenProject mavenproject,
InputLocationTracker dependencyOrPlugin) {
InputLocation location = dependencyOrPlugin == null ? null : dependencyOrPlugin.getLocation("version");
if (location != null) {
InputSource source = location.getSource();
if (source != null) {
String loc = source.getLocation();
if (loc != null) {
File file = new File(loc);
int lineNumber = location != null ? location.getLineNumber() : -1;
int columnNumber = location != null ? location.getColumnNumber() : -1;
if (file != null) {
Map<String, String> managedLocationData = new HashMap<>();
managedLocationData.put(ATTR_MANAGED_VERSION_LOCATION, file.toURI().toString());
if (lineNumber > -1) {
managedLocationData.put(ATTR_MANAGED_VERSION_LINE, String.valueOf(lineNumber));
if (columnNumber > -1) {
managedLocationData.put(ATTR_MANAGED_VERSION_COLUMN, String.valueOf(columnNumber));
}
}
diagnostic.setData(managedLocationData);
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public static Optional<DOMElement> findChildElement(DOMNode parent, String eleme
.filter(DOMElement.class::isInstance).map(DOMElement.class::cast).findAny();
}

public static List<DOMElement> findChildElements(DOMElement parent, String elementName) {
return parent.getChildren().stream().filter(node -> elementName.equals(node.getLocalName()))
.filter(DOMElement.class::isInstance).map(DOMElement.class::cast).collect(Collectors.toList());
}

public static Optional<String> findChildElementText(DOMNode rootNode, final String elementName) {
return findChildElement(rootNode, elementName) //
.stream() //
Expand Down
Loading