Skip to content

Commit

Permalink
Model and completion for property expressions
Browse files Browse the repository at this point in the history
In many MicroProfile implementations, `${other.property}` can be
used to refer to the value of an already defined property when defining
the value of another property.

This PR modifies the property file model to allow representing these
property expressions.
It also adds completion for defined properties when completion is
opened after typing `${` as a part of a property value.

This PR was migrated from
[quarkus-ls](redhat-developer/quarkus-ls#340).

Signed-off-by: David Thompson <davthomp@redhat.com>
  • Loading branch information
datho7561 authored and angelozerr committed Sep 1, 2020
1 parent 7901de7 commit 8512c09
Show file tree
Hide file tree
Showing 18 changed files with 982 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public abstract class Node {
*
*/
public static enum NodeType {
DOCUMENT, PROPERTY, PROPERTY_KEY, PROPERTY_VALUE, COMMENTS, ASSIGN;
DOCUMENT, PROPERTY, PROPERTY_KEY, PROPERTY_VALUE, PROPERTY_VALUE_LITERAL, PROPERTY_VALUE_EXPRESSION, COMMENTS, ASSIGN;
}

private int start, end;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*******************************************************************************/
package org.eclipse.lsp4mp.model;

import java.util.List;

import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4mp.ls.commons.BadLocationException;
import org.eclipse.lsp4mp.ls.commons.TextDocument;
Expand Down Expand Up @@ -124,6 +126,32 @@ public void delimiterAssign(ParseContext context) {
public void blankLine(ParseContext context) {

}

@Override
public void startPropertyValueLiteral(ParseContext context) {
Node valLiteral = new PropertyValueLiteral();
valLiteral.setStart(context.getLocationOffset());
property.getValue().addNode(valLiteral);
}

@Override
public void endPropertyValueLiteral(ParseContext context) {
List<Node> propFragments = property.getValue().getChildren();
propFragments.get(propFragments.size() - 1).setEnd(context.getLocationOffset());
}

@Override
public void startPropertyValueExpression(ParseContext context) {
Node expression = new PropertyValueExpression();
expression.setStart(context.getLocationOffset());
property.getValue().addNode(expression);
}

@Override
public void endPropertyValueExpression(ParseContext context) {
List<Node> propFragments = property.getValue().getChildren();
propFragments.get(propFragments.size() - 1).setEnd(context.getLocationOffset());
}
}

private final TextDocument document;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public Node findNodeAt(int offset) {
}
if (offset >= assign.getStart()) {
Node value = getValue();
return value != null ? value : assign;
return value != null ? value.findNodeAt(offset) : assign;
}
return key;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. 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, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/

package org.eclipse.lsp4mp.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.common.graph.EndpointPair;
import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import com.google.common.graph.Traverser;

import org.eclipse.lsp4mp.model.Node.NodeType;

/**
* Represents the graph of dependencies between properties defined in a
* MicroProfile properties file.
*/
public class PropertyGraph {

private Graph<String> graph;

/**
* Build a PropertyGraph for the given properties model
*
* @param model the properties model to build the graph for
*/
public PropertyGraph(PropertiesModel model) {
MutableGraph<String> graph = GraphBuilder.directed().allowsSelfLoops(true).build();
// Add nodes
for (Node modelChild : model.getChildren()) {
if (modelChild.getNodeType() == NodeType.PROPERTY) {
Property property = (Property) modelChild;
String propertyName = property.getPropertyName();
if (!(propertyName.isEmpty() || graph.nodes().contains(propertyName))) {
graph.addNode(propertyName);
}
}
}
// Add edges
for (Node modelChild : model.getChildren()) {
if (modelChild.getNodeType() == NodeType.PROPERTY) {
Property property = (Property) modelChild;
if (property.getValue() != null) {
for (Node valueNode : property.getValue().getChildren()) {
if (valueNode.getNodeType() == NodeType.PROPERTY_VALUE_EXPRESSION) {
PropertyValueExpression propExpr = (PropertyValueExpression) valueNode;
String propName = property.getPropertyName();
String refPropName = propExpr.getReferencedPropertyName();
if (graph.nodes().containsAll(Arrays.asList(propName, refPropName))) {
graph.putEdge(propName, refPropName);
}
}
}
}
}
}
this.graph = graph;
}

/**
* Returns true if the given property is in the PropertyGraph, and false
* otherwise
*
* @param property the property that is being checked
* @return true if the given property is in the PropertyGraph, and false
* otherwise
*/
public boolean hasNode(String property) {
return this.graph.nodes().contains(property);
}

/**
* Gets a list of properties that do not depend on <code>property</code>.
*
* These are the properties that, if a dependency of <code>property</code> on
* them were introduced, wouldn't introduce a circular dependency.
*
* @param property The property the find the independent properties of.
* @return A list of all the properties whose value do not depend on this
* property.
*/
public List<String> getIndependentProperties(String property) {
Graph<String> reversed = getReversed();
Set<String> reachable = new HashSet<>();
List<String> unreachable = new ArrayList<>(graph.nodes().size());
for (String reached : Traverser.forGraph(reversed).breadthFirst(property)) {
reachable.add(reached);
}
for (String node : graph.nodes()) {
if (!reachable.contains(node)) {
unreachable.add(node);
}
}
return unreachable;
}

/**
* Get a copy of this property graph with all the edges reversed.
*
* @return a copy of this property graph with all the edges reversed.
*/
private Graph<String> getReversed() {
MutableGraph<String> mutableReversed = GraphBuilder.directed().allowsSelfLoops(true).build();
for (String node : graph.nodes()) {
mutableReversed.addNode(node);
}
for (EndpointPair<String> edge : graph.edges()) {
mutableReversed.putEdge(edge.nodeV(), edge.nodeU());
}
return mutableReversed;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. 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, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.lsp4mp.model;

/**
* Represents a portion of the property value that refers to the value of
* another property.
*
* When properties file is processed, the reference is replaced with the value
* of the other property. In the properties file, it has the form:
* <code>${other.property.name}</code>
*/
public class PropertyValueExpression extends Node {

@Override
public NodeType getNodeType() {
return NodeType.PROPERTY_VALUE_EXPRESSION;
}

/**
* Returns the text that this Node contains.
*
* Removes backslashes, and newlines. Doesn't not resolve the reference to
* another property.
*/
public String getValue() {
String text = getText(true);
return text != null ? text.trim() : null;
}

/**
* Get the name of the referenced property, or null if the opening bracket is
* missing in the property expression.
*
* Does not check if the referenced property exists.
*
* @return the name of the referenced property, or null if brackets were missing
* in the property expression.
*/
public String getReferencedPropertyName() {
String value = getValue();
if (value.length() < 2 || !"${".equals(value.substring(0, 2))) {
return null;
}
int end = value.indexOf("}");
end = end == -1 ? value.length() : end;
return value.substring(2, end);
}

/**
* Returns true if the last character in this node is a '}', and false
* otherwise.
*
* @return true if the last character in this node is a '}', and false
* otherwise.
*/
public boolean isClosed() {
String text = getText();
return text.charAt(text.length() - 1) == '}';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. 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, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.lsp4mp.model;

/**
* Represents text in a property value that should be interpreted literally.
*/
public class PropertyValueLiteral extends Node {

@Override
public NodeType getNodeType() {
return NodeType.PROPERTY_VALUE_LITERAL;
}

/**
* Returns the text this node contains and null otherwise.
*
* If this node covers more than one line, the backslashes and newlines are
* removed.
*
* @return the text this node contains and null otherwise
*/
public String getValue() {
String text = getText(true);
return text != null ? text.trim() : null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,43 @@ public interface PropertiesHandler {
void startPropertyValue(ParseContext context);

/**
* Start of a property name
* End of a property name
*
* @param context the {@link ParseContext}
*/
void endPropertyValue(ParseContext context);

/**
* Start of a text literal section in the property value
*
* @param context the {@link ParseContext}
*/
void startPropertyValueLiteral(ParseContext context);

/**
* End of a text literal section in the property value
*
* @param context the {@link ParseContext}
*/
void endPropertyValueLiteral(ParseContext context);

/**
* Start of a property value expression, which is a portion of a property value
* that refers to the value of another property
*
* @param context the {@link ParseContext}
*/
void startPropertyValueExpression(ParseContext context);

/**
* End of a property value expression, which is a portion of a property value
* that refers to the value of another property
*
* @param context the {@link ParseContext}
*/
void endPropertyValueExpression(ParseContext context);


/**
* Start of a comment line
*
Expand Down
Loading

0 comments on commit 8512c09

Please sign in to comment.