Integrate properties parser into boot properties extension

This commit is contained in:
BoykoAlex
2016-10-28 17:17:39 -04:00
parent 28fed6ffcb
commit 146ae7ee61
18 changed files with 1498 additions and 60 deletions

View File

@@ -1,9 +1,5 @@
package org.springframework.ide.vscode.application.yaml;
package org.springframework.ide.vscode.application.properties.metadata;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertiesIndexManager;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.ValueProviderRegistry;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;

View File

@@ -19,6 +19,7 @@ import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepos
import org.springframework.boot.configurationmetadata.ConfigurationMetadataSource;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.commons.java.IClasspath;
import org.springframework.ide.vscode.commons.util.StringUtil;
public class SpringPropertyIndex extends FuzzyMap<PropertyInfo> {
@@ -125,4 +126,31 @@ public class SpringPropertyIndex extends FuzzyMap<PropertyInfo> {
protected String getKey(PropertyInfo entry) {
return entry.getId();
}
/**
* Find the longest known property that is a prefix of the given name. Here prefix does not mean
* 'string prefix' but a prefix in the sense of treating '.' as a kind of separators. So
* 'prefix' is not allowed to end in the middle of a 'segment'.
*/
public static PropertyInfo findLongestValidProperty(FuzzyMap<PropertyInfo> index, String name) {
int bracketPos = name.indexOf('[');
int endPos = bracketPos>=0?bracketPos:name.length();
PropertyInfo prop = null;
String prefix = null;
while (endPos>0 && prop==null) {
prefix = name.substring(0, endPos);
String canonicalPrefix = StringUtil.camelCaseToHyphens(prefix);
prop = index.get(canonicalPrefix);
if (prop==null) {
endPos = name.lastIndexOf('.', endPos-1);
}
}
if (prop!=null) {
//We should meet caller's expectation that matched properties returned by this method
// match the names exactly even if we found them using relaxed name matching.
return prop.withId(prefix);
}
return null;
}
}

View File

@@ -0,0 +1,250 @@
package org.springframework.ide.vscode.commons.languageserver.util;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.commons.util.Assert;
/**
* A non-sucky alternative to {@link IRegion}. Represents a region of text in a document.
* <p>
* Caution: assumes the underlying document is not mutated during the lifetime of the
* region object (otherwise start/end positions may no longer be valid).
* <p>
* Implements {@link CharSequence} for convenience (e.g you can use {@link DocumentRegion} as
* input to a {@link Pattern} and other standard JRE functions which expect a {@link CharSequence}.
*
* @author Kris De Volder
*/
public class DocumentRegion implements CharSequence {
final IDocument doc;
final int start;
final int end;
public DocumentRegion(IDocument doc, IRegion r) {
this(doc,
r.getOffset(),
r.getOffset()+r.getLength()
);
}
/**
* Constructs a {@link DocumentRegion} on a given document. Tries its
* best to behave sensibly when passed 'strange' coordinates by
* adjusting them logically rather than throw an Exception.
* <p>
* A position before the start of the document is moved to be the start
* of the document.
* <p>
* A position after the end of the document is moved to the end
* of the document.
* <p>
* If 'end' position is before the start position it is moved be
* exactly at the start position (this avoids region with
* negative length).
*/
public DocumentRegion(IDocument doc, int start, int end) {
this.doc = doc;
this.start = limitRange(start, 0, doc.getLength());
this.end = limitRange(end, start, doc.getLength());
}
private int limitRange(int offset, int min, int max) {
if (offset<min) {
return min;
}
if (offset>max) {
return max;
}
return offset;
}
@Override
public String toString() {
return DocumentUtil.textBetween(doc, start, end);
}
public DocumentRegion trim() {
return trimEnd().trimStart();
}
public DocumentRegion trimStart() {
int howMany = 0;
int len = length();
while (howMany<len && Character.isWhitespace(charAt(howMany))) {
howMany++;
}
return subSequence(howMany, len);
}
public DocumentRegion trimEnd() {
int howMany = 0; //how many chars to remove from the end
int len = length();
int lastChar = len-1;
while (howMany<len && Character.isWhitespace(charAt(lastChar-howMany))) {
howMany++;
}
if (howMany>0) {
return subSequence(0, len-howMany);
}
return this;
}
/**
* Gets character from the region, offset from the start of the region
* @return the character from the document (char)0 if the offset is outside the region.
*/
@Override
public char charAt(int offset) {
if (offset<0 || offset>=length()) {
throw new IndexOutOfBoundsException(""+offset);
}
try {
return doc.getChar(start+offset);
} catch (BadLocationException e) {
throw new IndexOutOfBoundsException(""+offset);
}
}
@Override
public int length() {
return end-start;
}
@Override
public DocumentRegion subSequence(int start, int end) {
int len = length();
Assert.isLegal(start>=0);
Assert.isLegal(end<=len);
if (start==0 && end==len) {
return this;
}
return new DocumentRegion(doc, this.start+start, this.start+end);
}
public boolean isEmpty() {
return length()==0;
}
public DocumentRegion subSequence(int start) {
return subSequence(start, length());
}
public IRegion asRegion() {
return new Region(start, end-start);
}
public int indexOf(char ch, int fromIndex) {
while (fromIndex < length()) {
if (charAt(fromIndex)==ch) {
return fromIndex;
}
fromIndex++;
}
return -1;
}
public DocumentRegion[] split(char c) {
List<DocumentRegion> pieces = new ArrayList<>();
int start = 0;
int end;
while ((end=indexOf(c, start))>=0) {
pieces.add(subSequence(start, end));
start = end+1;
}
// Do not forget the last piece!
pieces.add(subSequence(start, length()));
return pieces.toArray(new DocumentRegion[pieces.size()]);
}
public DocumentRegion[] split(Pattern delimiter) {
List<DocumentRegion> pieces = new ArrayList<>();
int start = 0;
Matcher matcher = delimiter.matcher(this);
while (matcher.find(start)) {
int end = matcher.start();
pieces.add(subSequence(start, end));
start = matcher.end();
}
// Do not forget the last piece!
pieces.add(subSequence(start, length()));
return pieces.toArray(new DocumentRegion[pieces.size()]);
}
/**
* Removes a single occurrence of pat from the start of this region.
*/
public DocumentRegion trimStart(Pattern pat) {
pat = Pattern.compile("^("+pat.pattern()+")");
Matcher matcher = pat.matcher(this);
if (matcher.find()) {
return subSequence(matcher.end());
}
return this;
}
/**
* Removes a single occurrence of pat from the end of this region.
*/
public DocumentRegion trimEnd(Pattern pat) {
pat = Pattern.compile("("+pat.pattern()+")$");
Matcher matcher = pat.matcher(this);
if (matcher.find()) {
return subSequence(0, matcher.start());
}
return this;
}
/**
* Get the region after this one with a given lenght.
* <p>
* If the document is too short to provide the requested lenght
* then the region is truncated to end of the document.
*/
public DocumentRegion textAfter(int len) {
Assert.isLegal(len>=0);
return new DocumentRegion(doc, end, end+len);
}
/**
* Get the region before this one with a given lenght.
* <p>
* If the requested region extends before the start of the document,
* then the region is shortened so its start coincides with document start.
*/
public DocumentRegion textBefore(int len) {
Assert.isLegal(len>=0);
return new DocumentRegion(doc, start-len, start);
}
public IDocument getDocument() {
return doc;
}
/**
* Get the start of this region in 'absolute' terms (i.e. relative to the document).
*/
public int getStart() {
return start;
}
/**
* Get the end of this region in 'absolute' terms (i.e. relative to the document).
*/
public int getEnd() {
return end;
}
/**
* Convert the given document offset into an offset relative to this region.
*/
public int toRelative(int offset) {
return offset-start;
}
public int getLength() {
return getEnd() - getStart();
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.ide.vscode.java.properties.parser.Parser;
import org.springframework.ide.vscode.java.properties.parser.Problem;
import org.springframework.ide.vscode.java.properties.parser.ProblemCodes;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst;
import org.springframework.ide.vscode.java.properties.parser.PropertiesFileEscapes;
import com.google.common.collect.ImmutableList;
@@ -230,7 +231,12 @@ public class AntlrParser implements Parser {
@Override
public String decode() {
return context.getText().replace("\\:", ":").replace("\\=", "=");
// return context.getText().replace("\\:", ":").replace("\\=", "=");
try {
return PropertiesFileEscapes.unescape(context.getText());
} catch (Exception e) {
return context.getText().replace("\\:", ":").replace("\\=", "=");
}
}
}
@@ -249,8 +255,13 @@ public class AntlrParser implements Parser {
private void init() {
// Remove the separator, if it exists
value = context.getText().replaceAll("^\\s*[:=]?\\s*", "");
// Remove all escaped line breaks with trailing spaces
// Remove all escaped line breaks with trailing spaces
decoded = value.replaceAll("\\\\(\r?\n|\r)[ \t\f]*", "");
try {
decoded = PropertiesFileEscapes.unescape(decoded);
} catch (Exception e) {
// ignore
}
}
@Override

View File

@@ -0,0 +1,317 @@
package org.springframework.ide.vscode.java.properties.parser;
/**
* Helper class to convert between Java chars and the escaped form that must be used in .properties
* files.
*
* @since 3.7
*/
public class PropertiesFileEscapes {
private static final char[] HEX_DIGITS= { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
private static char toHex(int halfByte) {
return HEX_DIGITS[(halfByte & 0xF)];
}
/**
* Returns the decimal value of the Hex digit, or -1 if the digit is not a valid Hex digit.
*
* @param digit the Hex digit
* @return the decimal value of digit, or -1 if digit is not a valid Hex digit.
*/
private static int getHexDigitValue(char digit) {
switch (digit) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return digit - '0';
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
return 10 + digit - 'a';
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
return 10 + digit - 'A';
default:
return -1;
}
}
/**
* Convert a Java char to the escaped form that must be used in .properties files.
*
* @param c the Java char
* @return escaped string
*/
public static String escape(char c) {
return escape(c, true, true, true);
}
/**
* Convert characters in a Java string to the escaped form that must be used in .properties
* files.
*
* @param s the Java string
* @param escapeWhitespaceChars if <code>true</code>, escape whitespace characters
* @param escapeBackslash if <code>true</code>, escape backslash characters
* @param escapeUnicodeChars if <code>true</code>, escape unicode characters
* @return escaped string
*/
public static String escape(String s, boolean escapeWhitespaceChars, boolean escapeBackslash, boolean escapeUnicodeChars) {
StringBuffer sb= new StringBuffer(s.length());
int length= s.length();
for (int i= 0; i < length; i++) {
char c= s.charAt(i);
sb.append(escape(c, escapeWhitespaceChars, escapeBackslash, escapeUnicodeChars));
}
return sb.toString();
}
/**
* Convert a Java char to the escaped form that must be used in .properties files.
*
* @param c the Java char
* @param escapeWhitespaceChars if <code>true</code>, escape whitespace characters
* @param escapeBackslash if <code>true</code>, escape backslash characters
* @param escapeUnicodeChars if <code>true</code>, escape unicode characters
* @return escaped string
*/
public static String escape(char c, boolean escapeWhitespaceChars, boolean escapeBackslash, boolean escapeUnicodeChars) {
switch (c) {
case '\t':
return escapeWhitespaceChars ? "\\t" : "\t"; //$NON-NLS-1$//$NON-NLS-2$
case '\n':
return escapeWhitespaceChars ? "\\n" : "\n"; //$NON-NLS-1$//$NON-NLS-2$
case '\f':
return escapeWhitespaceChars ? "\\f" : "\r"; //$NON-NLS-1$//$NON-NLS-2$
case '\r':
return escapeWhitespaceChars ? "\\r" : "\r"; //$NON-NLS-1$//$NON-NLS-2$
case '\\':
return escapeBackslash ? "\\\\" : "\\"; //$NON-NLS-1$ //$NON-NLS-2$
default:
if (escapeUnicodeChars && ((c < 0x0020) || (c > 0x007e && c <= 0x00a0) || (c > 0x00ff))) {
//NBSP (0x00a0) is escaped to differentiate from normal space character
return new StringBuffer()
.append('\\')
.append('u')
.append(toHex((c >> 12) & 0xF))
.append(toHex((c >> 8) & 0xF))
.append(toHex((c >> 4) & 0xF))
.append(toHex(c & 0xF)).toString();
} else
return String.valueOf(c);
}
}
/**
* Convert an escaped string to a string composed of Java characters.
*
* @param s the escaped string
* @return string composed of Java characters
* @throws CoreException if the escaped string has a malformed \\uxxx sequence
*/
public static String unescape(String s) throws Exception {
boolean isValidEscapedString= true;
if (s == null)
return null;
char aChar;
int len= s.length();
StringBuffer outBuffer= new StringBuffer(len);
for (int x= 0; x < len;) {
aChar= s.charAt(x++);
if (aChar == '\\') {
if (x > len - 1) {
return outBuffer.toString(); // silently ignore the \
}
aChar= s.charAt(x++);
if (aChar == 'u') {
// Read the xxxx
int value= 0;
if (x > len - 4) {
throw new Exception("Malformed encoding for properties file");
}
StringBuffer buf= new StringBuffer("\\u"); //$NON-NLS-1$
int digit= 0;
for (int i= 0; i < 4; i++) {
aChar= s.charAt(x++);
digit= getHexDigitValue(aChar);
if (digit == -1) {
isValidEscapedString= false;
x--;
break;
}
value= (value << 4) + digit;
buf.append(aChar);
}
outBuffer.append(digit == -1 ? buf.toString() : String.valueOf((char)value));
} else if (aChar == 't') {
outBuffer.append('\t');
} else if (aChar == 'n') {
outBuffer.append('\n');
} else if (aChar == 'f') {
outBuffer.append('\f');
} else if (aChar == 'r') {
outBuffer.append('\r');
} else {
outBuffer.append(aChar); // silently ignore the \
}
} else
outBuffer.append(aChar);
}
if (isValidEscapedString) {
return outBuffer.toString();
} else {
throw new Exception("Malformed encoding for properties file");
}
}
/**
* Unescape backslash characters in a string.
*
* @param s the escaped string
* @return string with backslash characters unescaped
*/
public static String unescapeBackslashes(String s) {
if (s == null)
return null;
char c;
int length= s.length();
StringBuffer outBuffer= new StringBuffer(length);
for (int i= 0; i < length;) {
c= s.charAt(i++);
if (c == '\\') {
c= s.charAt(i++);
}
outBuffer.append(c);
}
return outBuffer.toString();
}
/**
* Tests if the given text contains any invalid escape sequence.
*
* @param text the text
* @return <code>true</code> if text contains an invalid escape sequence, <code>false</code>
* otherwise
*/
public static boolean containsInvalidEscapeSequence(String text) {
try {
//check for invalid unicode escapes
unescape(text);
} catch (Exception e) {
return true;
}
int length= text.length();
for (int i= 0; i < length; i++) {
char c= text.charAt(i);
if (c == '\\') {
if (i < length - 1) {
char nextC= text.charAt(i + 1);
switch (nextC) {
case 't':
case 'n':
case 'f':
case 'r':
case 'u':
case '\n':
case '\r':
case '=':
case ':':
break;
case '\\':
i++;
break;
default:
return true;
}
} else {
return true;
}
}
}
return false;
}
/**
* Tests if the given text contains an unescaped backslash character.
*
* @param text the text
* @return <code>true</code> if text contains an unescaped backslash character,
* <code>false</code> otherwise
*/
public static boolean containsUnescapedBackslash(String text) {
int length= text.length();
for (int i= 0; i < length; i++) {
char c= text.charAt(i);
if (c == '\\') {
if (i < length - 1) {
char nextC= text.charAt(i + 1);
switch (nextC) {
case '\\':
i++;
break;
default:
return true;
}
} else {
return true;
}
}
}
return false;
}
/**
* Tests if the given text contains only escaped backslash characters and no unescaped backslash
* character.
*
* @param text the text
* @return <code>true</code> if text contains only escaped backslash characters,
* <code>false</code> otherwise
*/
public static boolean containsEscapedBackslashes(String text) {
boolean result= false;
int length= text.length();
for (int i= 0; i < length; i++) {
char c= text.charAt(i);
if (c == '\\') {
if (i < length - 1) {
char nextC= text.charAt(i + 1);
switch (nextC) {
case '\\':
i++;
result= true;
break;
default:
return false;
}
} else {
return false;
}
}
}
return result;
}
}

View File

@@ -1,6 +1,5 @@
package org.springframework.ide.vscode.properties.editor.test.harness;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

View File

@@ -10,8 +10,11 @@
*******************************************************************************/
package org.springframework.ide.vscode.application.properties;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.application.properties.reconcile.SpringPropertiesReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.reconcile.BadWordReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocument;
@@ -19,11 +22,7 @@ import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.Parser;
import io.typefox.lsapi.Diagnostic;
import io.typefox.lsapi.DiagnosticSeverity;
import io.typefox.lsapi.ServerCapabilities;
import io.typefox.lsapi.TextDocumentSyncKind;
import io.typefox.lsapi.impl.DiagnosticImpl;
import io.typefox.lsapi.impl.ServerCapabilitiesImpl;
/**
@@ -40,41 +39,53 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
private ParseResults parseResults;
private Parser parser;
private SpringPropertyIndexProvider indexProvider;
private TypeUtilProvider typeUtilProvider;
public ApplicationPropertiesLanguageServer() {
public ApplicationPropertiesLanguageServer(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider) {
this.indexProvider = indexProvider;
this.typeUtilProvider = typeUtilProvider;
this.parser = new AntlrParser();
SimpleTextDocumentService documents = getTextDocumentService();
IReconcileEngine reconcileEngine = getReconcileEngine();
documents.onDidChangeContent(params -> {
System.out.println("Document changed: "+params);
TextDocument doc = params.getDocument();
parseResults = parser.parse(doc.getText());
validateDocument(documents, doc);
validateWith(doc, reconcileEngine);
});
// documents.onDidChangeContent(params -> {
// System.out.println("Document changed: "+params);
// TextDocument doc = params.getDocument();
// parseResults = parser.parse(doc.getText());
// validateDocument(documents, doc);
// });
}
private void validateDocument(SimpleTextDocumentService documents, TextDocument doc) {
documents.publishDiagnostics(doc, parseResults.syntaxErrors.stream().map(problem -> {
DiagnosticImpl diagnostic = new DiagnosticImpl();
diagnostic.setMessage(createSyntaxErrorMessage(problem.getMessage()));
diagnostic.setCode(problem.getCode());
diagnostic.setSeverity(DiagnosticSeverity.Error);
diagnostic.setSource("java-properties");
diagnostic.setRange(doc.toRange(problem.getOffset(), problem.getLength()));
return diagnostic;
}).collect(Collectors.toList()));
}
private static String createSyntaxErrorMessage(String parserMessage) {
String message = parserMessage;
if (parserMessage.contains("extraneous input '\\n' expecting")) {
message = SYNTAX_ERROR_MSG__UNEXPECTED_END_OF_LINE;
} else if (parserMessage.contains("mismatched input '<EOF>' expecting")) {
message = YNTAX_ERROR_MSG__UNEXPECTED_END_OF_INPUT;
}
return SYNTAX_ERROR_HEADER_MSG + message;
}
// private void validateDocument(SimpleTextDocumentService documents, TextDocument doc) {
// documents.publishDiagnostics(doc, parseResults.syntaxErrors.stream().map(problem -> {
// DiagnosticImpl diagnostic = new DiagnosticImpl();
// diagnostic.setMessage(createSyntaxErrorMessage(problem.getMessage()));
// diagnostic.setCode(problem.getCode());
// diagnostic.setSeverity(DiagnosticSeverity.Error);
// diagnostic.setSource("java-properties");
// diagnostic.setRange(doc.toRange(problem.getOffset(), problem.getLength()));
// return diagnostic;
// }).collect(Collectors.toList()));
// }
//
// private static String createSyntaxErrorMessage(String parserMessage) {
// String message = parserMessage;
// if (parserMessage.contains("extraneous input '\\n' expecting")) {
// message = SYNTAX_ERROR_MSG__UNEXPECTED_END_OF_LINE;
// } else if (parserMessage.contains("mismatched input '<EOF>' expecting")) {
// message = YNTAX_ERROR_MSG__UNEXPECTED_END_OF_INPUT;
// }
// return SYNTAX_ERROR_HEADER_MSG + message;
// }
@Override
protected ServerCapabilitiesImpl getServerCapabilities() {
@@ -84,4 +95,10 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
return c;
}
protected IReconcileEngine getReconcileEngine() {
return new SpringPropertiesReconcileEngine(indexProvider, typeUtilProvider);
}
}

View File

@@ -20,6 +20,11 @@ import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.ide.vscode.application.properties.metadata.DefaultSpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.LoggingFormat;
import io.typefox.lsapi.services.json.LoggingJsonAdapter;
@@ -125,7 +130,12 @@ public class Main {
* When the request stream is closed, wait for 5s for all outstanding responses to compute, then return.
*/
public static void run(Connection connection) {
ApplicationPropertiesLanguageServer server = new ApplicationPropertiesLanguageServer();
SpringPropertyIndexProvider indexProvider = new DefaultSpringPropertyIndexProvider();
TypeUtil typeUtil = new TypeUtil(null);
TypeUtilProvider typeUtilProvider = (IDocument doc) -> typeUtil;
ApplicationPropertiesLanguageServer server = new ApplicationPropertiesLanguageServer(indexProvider, typeUtilProvider);
LoggingJsonAdapter jsonServer = new LoggingJsonAdapter(server);
jsonServer.setMessageLog(new PrintWriter(System.out));

View File

@@ -0,0 +1,155 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.application.properties.quickfix;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.quickfix.ProblemFixer;
import io.typefox.lsapi.CompletionItemKind;
public class ReplaceDeprecatedPropertyQuickfix implements ICompletionProposal {
public static ProblemFixer FIXER = (context, problem, proposals) -> {
throw new UnsupportedOperationException("Not yet implemented");
// PropertyInfo metadata = problem.getMetadata();
// if (metadata!=null) {
// String replacement = metadata.getDeprecationReplacement();
// if (replacement!=null) {
// //No need to check problem type... we only attach this fixer to problems of applicable type.
// proposals.add(new ReplaceDeprecatedYamlQuickfix(context, problem));
// }
// }
};
@Override
public ICompletionProposal deemphasize() {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getLabel() {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public CompletionItemKind getKind() {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public DocumentEdits getTextEdit() {
throw new UnsupportedOperationException("Not yet implemented");
}
// private final QuickfixContext context;
// private final SpringPropertyProblem problem;
//
// private LazyProposalApplier applier = new LazyProposalApplier() {
// protected ProposalApplier create() throws Exception {
// String newName = problem.getMetadata().getDeprecationReplacement();
// String oldName = problem.getPropertyName();
// YamlPath newPath = YamlPath.fromProperty(newName);
// YamlPath oldPath = YamlPath.fromProperty(oldName);
// YamlPath prefix = newPath.commonPrefix(oldPath);
// if (prefix.size()==newPath.size()-1 && newPath.size()==oldPath.size()) {
// //only the last segment has changed. We can do a simple 'in-place' replace
// // of just the change segment.
// DocumentEdits edits = new DocumentEdits(context.getDocument());
// edits.replace(problem.getOffset(), problem.getEnd(), newPath.getLastSegment().toPropString());
// return edits;
// }
// YamlDocument doc = new YamlDocument(context.getDocument(), YamlStructureProvider.DEFAULT);
// SNode problemNode = doc.getStructure().find(problem.getOffset());
// if (problemNode.getNodeType()==SNodeType.KEY) {
// SKeyNode problemKey = (SKeyNode) problemNode;
// if (problemKey.isInKey(problem.getOffset())) {
// YamlPathEdits edits = new YamlPathEdits(doc);
//// print(doc, edits);
// String valueText = problemKey.getValueWithRelativeIndent();
// edits.deleteNode(problemKey);
// int maxParentDeletions = oldPath.size() - prefix.size() - 1; // don't delete bits of the common prefix!
// SChildBearingNode parent = problemNode.getParent();
// while (maxParentDeletions>0 && parent!=null && parent.getChildren().size()==1) {
// edits.deleteNode(parent);
// parent = parent.getParent();
// maxParentDeletions--;
// }
//// print(doc, edits);
// SDocNode docRoot = problemNode.getDocNode(); //edits should stay within the same 'document' for yaml file that has multiple documents inside of it.
// edits.createPath(docRoot, YamlPath.fromProperty(newName), valueText);
//// print(doc, edits);
// return edits;
// }
// }
// //Not sure what to do... case not covered... so do nothing but tell the user.
// context.getUI().error("Yaml file too complex",
// "Sorry, but the yaml file is too complex for this quickfix. " +
// "Please make the change manually."
// );
// return ProposalApplier.NULL;
// }
//
//// private void print(YamlDocument doc, YamlPathEdits edits) throws Exception {
//// Document workingCopy = new Document(doc.getDocument().get());
//// edits.apply(workingCopy);
//// System.out.println("==============");
//// System.out.println(workingCopy.get());
//// System.out.println("==============");
//// }
// };
//
// public ReplaceDeprecatedYamlQuickfix(QuickfixContext context, SpringPropertyProblem problem) {
// this.context = context;
// this.problem = problem;
// }
//
// @Override
// public void apply(IDocument doc) {
// try {
// applier.apply(doc);
// } catch (Exception e) {
// Log.log(e);
// }
// }
//
// private String getReplacementProperty() {
// return problem.getMetadata().getDeprecationReplacement();
// }
//
// @Override
// public Point getSelection(IDocument doc) {
// try {
// return applier.getSelection(doc);
// } catch (Exception e) {
// Log.log(e);
// return null;
// }
// }
//
// @Override
// public String getAdditionalProposalInfo() {
// return null;
// }
//
// @Override
// public String getDisplayString() {
// return "Change to '"+getReplacementProperty()+"'";
// }
//
// @Override
// public Image getImage() {
// return JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE);
// }
//
// @Override
// public IContextInformation getContextInformation() {
// return null;
// }
}

View File

@@ -30,7 +30,8 @@ public enum ApplicationPropertiesProblemType implements ProblemType {
PROP_INVALID_BEAN_PROPERTY("Accessing a named property in a type that doesn't provide a property accessor with that name"),
PROP_UNKNOWN_PROPERTY(WARNING, "Property-key not found in any configuration metadata on the project's classpath"),
PROP_DEPRECATED(WARNING, "Property is marked as Deprecated"),
PROP_DUPLICATE_KEY("Multiple assignments to the same property value");
PROP_DUPLICATE_KEY("Multiple assignments to the same property value"),
PROP_SYNTAX_ERROR("Syntax Error");
private final ProblemSeverity defaultSeverity;
private String description;

View File

@@ -0,0 +1,52 @@
package org.springframework.ide.vscode.application.properties.reconcile;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
/**
* Helper class to reconcile text contained in a document region as a comma-separated list.
*
* @author Kris De Volder
*/
public class DelimitedListReconciler {
interface TypeBasedReconciler {
void reconcile(DocumentRegion region, Type expectType, IProblemCollector problems);
}
private final TypeBasedReconciler valueReconciler;
private final Pattern delimiter;
public DelimitedListReconciler(Pattern delimiter, TypeBasedReconciler valueReconciler) {
this.valueReconciler = valueReconciler;
this.delimiter = delimiter;
}
public void reconcile(DocumentRegion region, Type listType, IProblemCollector problems) {
Type elType = getElementType(listType);
//Its pointless to reconcile list of we can't determine value type.
if (elType!=null) {
Arrays.stream(region.split(delimiter)).forEach(entry -> {
valueReconciler.reconcile(entry, elType, problems);
});
}
}
private Type getElementType(Type listType) {
Type elType = TypeUtil.getDomainType(listType);
if (elType!=null) {
Type nestedElType = getElementType(elType);
if (nestedElType!=null) {
return nestedElType;
}
return elType;
}
return null;
}
}

View File

@@ -0,0 +1,57 @@
package org.springframework.ide.vscode.application.properties.reconcile;
import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_DUPLICATE_KEY;
import static org.springframework.ide.vscode.application.properties.reconcile.SpringPropertyProblem.problem;
import java.util.HashMap;
import java.util.Map;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key;
/**
* Instance of this class is fed the regions of names in a properties file, checks them for duplicates and
* reports the duplicates to {@link IProblemCollector}.
*
* @author Kris De Volder
*/
public class DuplicateNameChecker {
/**
* Keep track of seen names. The value in the map entries is either null
* or the Region for the first time the name was seen.
* <p>
* This is used so that the first occurrence can still be reported retroactively
* when the second occurrence is encountered.
*/
private Map<String, Key> seen = new HashMap<>();
IProblemCollector problems;
public DuplicateNameChecker(IProblemCollector problems) {
this.problems = problems;
}
public void check(Key nameRegion) {
String name = nameRegion.decode();
if (!name.isEmpty()) {
if (seen.containsKey(name)) {
Key pending = seen.get(name);
if (pending!=null) {
reportDuplicate(pending);
seen.put(name, null);
}
reportDuplicate(nameRegion);
} else {
seen.put(name, nameRegion);
}
}
}
private void reportDuplicate(Key nameRegion) {
String decodedKey = nameRegion.decode();
problems.accept(problem(PROP_DUPLICATE_KEY,
"Duplicate property '"+decodedKey+"'", nameRegion));
}
}

View File

@@ -0,0 +1,254 @@
package org.springframework.ide.vscode.application.properties.reconcile;
import static org.springframework.ide.vscode.application.properties.reconcile.SpringPropertyProblem.problem;
import static org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.isBracketable;
import java.util.List;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.BeanPropertyNameMode;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.types.TypedProperty;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
/**
* Helper class for {@link SpringPropertiesReconcileEngine} and {@link SpringPropertiesCompletionEngine}.
* <p>
* This class provides a means to 'navigate' a chain of bracket and dot navigation operations down
* from a typed property into its value type.
*
* @author Kris De Volder
*/
public class PropertyNavigator {
private static final char EOF = 0;
/**
* If problem collector is not null, then problems detected in the navigation chain are added to the
* collector.
*/
private IProblemCollector problemCollector;
/**
* Document in which navigation chain text is contained.
*/
private IDocument doc;
private TypeUtil typeUtil;
private Node region;
private String regionText;
public PropertyNavigator(IDocument doc, IProblemCollector problemCollector, TypeUtil typeUtil, Node region) throws BadLocationException {
this.doc = doc;
this.problemCollector = problemCollector==null?IProblemCollector.NULL:problemCollector;
this.typeUtil = typeUtil;
this.region = region;
this.regionText = doc.get(region.getOffset(), region.getLength());
}
/**
* @param offset current position in the nav chain. Text before this offset is already 'processed'.
* @param type The type at the end of the already processed nav chain. The next nav op in the chain
* should go deeper into this type.
* @param r The entire region of the navchain, including both the already processed portion as well
* as the remaining text.
* @return Type at the end of the whole nav chain, or null if the type could not be determined.
*/
public Type navigate(int offset, Type type) {
if (type!=null) {
if (offset<getEnd(region)) {
char navOp = getChar(offset);
if (navOp=='.') {
if (typeUtil.isDotable(type)) {
return dotNavigate(offset, type);
} else {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_INVALID_BEAN_NAVIGATION,
"Can't use '.' navigation for property '"+textBetween(region.getOffset(), offset)+"' of type "+type,
offset, getEnd(region)-offset));
}
} else if (navOp=='[') {
if (isBracketable(type)) {
return bracketNavigate(offset, type);
} else {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_INVALID_INDEXED_NAVIGATION,
"Can't use '[..]' navigation for property '"+textBetween(region.getOffset(), offset)+"' of type "+type,
offset, getEnd(region)-offset));
}
} else {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_EXPECTED_DOT_OR_LBRACK, "Expecting either a '.' or '['", offset, getEnd(region)-offset));
}
} else {
//end of nav chain
return type;
}
}
//Something we can't handle...
return null;
}
private String textBetween(int start, int end) {
try {
if (end>start) {
return doc.get(start, end-start);
}
} catch (BadLocationException e) {
//ignore
}
return "";
}
private int indexOf(char c, int from) {
int offset = region.getOffset();
int found = regionText.indexOf(c, from-offset);
if (found>=0) {
return found+offset;
}
return -1;
}
/**
* Handle bracket navigation into given type, after a bracket at
* was found at given offset. Assumes the type has already been checked to
* be 'bracketable'.
*/
private Type bracketNavigate(int offset, Type type) {
int lbrack = offset;
int rbrack = indexOf(']', lbrack);
if (rbrack<0) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_NO_MATCHING_RBRACK,
"No matching ']'",
offset, 1));
} else {
String indexStr = textBetween(lbrack+1, rbrack);
if (!indexStr.contains("${")) {
try {
Integer.parseInt(indexStr);
} catch (Exception e) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_NON_INTEGER_IN_BRACKETS,
"Expecting 'Integer' for '[...]' notation '"+textBetween(region.getOffset(), lbrack)+"'",
lbrack+1, rbrack-lbrack-1
));
}
}
Type domainType = TypeUtil.getDomainType(type);
return navigate(rbrack+1, domainType);
}
return null;
}
/**
* Handle dot navigation into given type, after a '.' was
* was found at given offset. Assumes the type has already been
* checked to be 'dotable'.
*/
private Type dotNavigate(int offset, Type type) {
if (TypeUtil.isMap(type)) {
int keyStart = offset+1;
Type domainType = TypeUtil.getDomainType(type);
int keyEnd = -1;
if (typeUtil.isDotable(domainType)) {
//'.' should be interpreted as navigation.
keyEnd = nextNavOp(".[", offset+1);
} else {
//'.' should *not* be interpreted as navigation.
keyEnd = nextNavOp("[", offset+1);
}
String key = textBetween(keyStart, keyEnd);
Type keyType = typeUtil.getKeyType(type);
if (keyType!=null) {
ValueParser keyParser = typeUtil.getValueParser(keyType);
if (keyParser!=null) {
try {
keyParser.parse(key);
} catch (Exception e) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH,
"Expecting "+typeUtil.niceTypeName(keyType),
keyStart, keyEnd-keyStart));
}
}
}
return navigate(keyEnd, domainType);
} else {
// dot navigation into object properties
int keyStart = offset+1;
int keyEnd = nextNavOp(".[", offset+1);
if (keyEnd<0) {
keyEnd = getEnd(region);
}
String key = StringUtil.camelCaseToHyphens(textBetween(keyStart, keyEnd));
List<TypedProperty> properties = typeUtil.getProperties(type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED);
if (properties!=null) {
TypedProperty prop = null;
for (TypedProperty p : properties) {
if (p.getName().equals(key)) {
prop = p;
break;
}
}
if (prop==null) {
problemCollector.accept(problem(ApplicationPropertiesProblemType.PROP_INVALID_BEAN_PROPERTY,
"Type '"+typeUtil.niceTypeName(type)+"' has no property '"+key+"'",
keyStart, keyEnd-keyStart));
} else {
if (prop.isDeprecated()) {
problemCollector.accept(problemDeprecated(type, prop, keyStart, keyEnd-keyStart));
}
return navigate(keyEnd, prop.getType());
}
}
}
return null;
}
private ReconcileProblem problemDeprecated(Type contextType, TypedProperty prop, int offset, int len) {
SpringPropertyProblem p = problem(ApplicationPropertiesProblemType.PROP_DEPRECATED,
TypeUtil.deprecatedPropertyMessage(
prop.getName(), typeUtil.niceTypeName(contextType),
prop.getDeprecationReplacement(), prop.getDeprecationReason()
),
offset, len
);
p.setPropertyName(prop.getName());
return p;
}
/**
* Skip ahead from give position until reaching the next 'navigation' operator (or the end
* of the navigation chain region).
*
* @param navops Each character in this string is considered a 'navigation operator'.
* @param pos current position in the document.
* @return position of next navop if found, or the position at the end of the region if not found.
*/
private int nextNavOp(String navops, int pos) {
int end = getEnd(region);
while (pos < end && navops.indexOf(getChar(pos))<0) {
pos++;
}
return Math.min(pos, end); //ensure never past the end
}
private char getChar(int offset) {
try {
return doc.getChar(offset);
} catch (BadLocationException e) {
//outside doc, return something anyways.
return EOF;
}
}
private int getEnd(Node region) {
return region.getOffset()+region.getLength();
}
}

View File

@@ -0,0 +1,247 @@
/*******************************************************************************
* Copyright (c) 2014-2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.application.properties.reconcile;
//import static org.springframework.ide.eclipse.boot.properties.editor.SpringPropertiesCompletionEngine.isAssign;
import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_DEPRECATED;
import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_SYNTAX_ERROR;
import static org.springframework.ide.vscode.application.properties.reconcile.ApplicationPropertiesProblemType.PROP_UNKNOWN_PROPERTY;
import static org.springframework.ide.vscode.application.properties.reconcile.SpringPropertyProblem.problem;
import static org.springframework.ide.vscode.commons.util.StringUtil.commonPrefix;
import java.util.regex.Pattern;
import javax.inject.Provider;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndex;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeParser;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.application.properties.quickfix.ReplaceDeprecatedPropertyQuickfix;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.Parser;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.KeyValuePair;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
import org.springframework.ide.vscode.java.properties.parser.PropertiesFileEscapes;
/**
* Implements reconciling algorithm for {@link SpringPropertiesReconcileStrategy}.
* <p>
* The code in here could have been also part of the {@link SpringPropertiesReconcileStrategy}
* itself, however isolating it here allows it to me more easily unit tested (no dependencies
* on ISourceViewer which is difficult to 'mock' in testing harness.
*
* @author Kris De Volder
*/
@SuppressWarnings("restriction")
public class SpringPropertiesReconcileEngine implements IReconcileEngine {
/**
* Regexp that matches a ',' surrounded by whitespace, including escaped whitespace / newlines
*/
private static final Pattern COMMA = Pattern.compile(
"(\\s|\\\\\\s)*,(\\s|\\\\\\s)*"
);
// private static final Pattern SPACES = Pattern.compile(
// "(\\s|\\\\\\s)*"
// );
/**
* Regexp that matches a whitespace, including escaped whitespace
*/
// private static final Pattern ASSIGN = SpringPropertiesCompletionEngine.ASSIGN;
private SpringPropertyIndexProvider fIndexProvider;
private TypeUtilProvider typeUtilProvider;
private final DelimitedListReconciler commaListReconciler = new DelimitedListReconciler(COMMA, this::reconcileType);
private Parser parser = new AntlrParser();
public SpringPropertiesReconcileEngine(SpringPropertyIndexProvider provider, TypeUtilProvider typeUtilProvider) {
this.fIndexProvider = provider;
this.typeUtilProvider = typeUtilProvider;
}
public void reconcile(IDocument doc, IProblemCollector problemCollector) {
FuzzyMap<PropertyInfo> index = fIndexProvider.getIndex(doc);
problemCollector.beginCollecting();
try {
ParseResults results = parser.parse(doc.get());
DuplicateNameChecker duplicateNameChecker = new DuplicateNameChecker(problemCollector);
results.syntaxErrors.forEach(syntaxError -> {
problemCollector.accept(problem(PROP_SYNTAX_ERROR, syntaxError.getMessage(), syntaxError.getOffset(),
syntaxError.getLength()));
});
if (index==null || index.isEmpty()) {
//don't report errors when index is empty, simply don't check (otherwise we will just reprot
// all properties as errors, but this not really useful information since the cause is
// some problem putting information about properties into the index.
return;
}
results.ast.getNodes(KeyValuePair.class).forEach(pair -> {
Key fullName = pair.getKey();
String keyName = fullName.decode();
duplicateNameChecker.check(fullName);
PropertyInfo validProperty = SpringPropertyIndex.findLongestValidProperty(index, keyName);
try {
if (validProperty!=null) {
//TODO: Remove last remnants of 'IRegion trimmedRegion' here and replace
// it all with just passing around 'fullName' DocumentRegion. This may require changes
// in PropertyNavigator (probably these changes are also for the better making it simpler as well)
if (validProperty.isDeprecated()) {
problemCollector.accept(problemDeprecated(fullName, validProperty));
}
int offset = validProperty.getId().length() + fullName.getOffset();
PropertyNavigator navigator = new PropertyNavigator(doc, problemCollector, typeUtilProvider.getTypeUtil(doc), fullName);
Type valueType = navigator.navigate(offset, TypeParser.parse(validProperty.getType()));
if (valueType!=null) {
reconcileType(doc, valueType, pair.getValue(), problemCollector);
}
} else { //validProperty==null
//The name is invalid, with no 'prefix' of the name being a valid property name.
PropertyInfo similarEntry = index.findLongestCommonPrefixEntry(fullName.toString());
CharSequence validPrefix = commonPrefix(similarEntry.getId(), keyName);
problemCollector.accept(problemUnkownProperty(createRegion(doc, fullName), similarEntry, validPrefix));
} //end: validProperty==null
} catch (Exception e) {
Log.log(e);
}
});
} catch (Throwable e2) {
Log.log(e2);
} finally {
problemCollector.endCollecting();
}
}
protected SpringPropertyProblem problemDeprecated(Key key, PropertyInfo property) {
SpringPropertyProblem p = problem(PROP_DEPRECATED,
TypeUtil.deprecatedPropertyMessage(
property.getId(), null,
property.getDeprecationReplacement(),
property.getDeprecationReason()
),
key
);
p.setPropertyName(property.getId());
p.setMetadata(property);
p.setProblemFixer(ReplaceDeprecatedPropertyQuickfix.FIXER);
return p;
}
protected SpringPropertyProblem problemUnkownProperty(DocumentRegion fullNameRegion,
PropertyInfo similarEntry, CharSequence validPrefix) {
String fullName = fullNameRegion.toString();
SpringPropertyProblem p = problem(PROP_UNKNOWN_PROPERTY,
"'"+fullName+"' is an unknown property."+suggestSimilar(similarEntry, validPrefix, fullName),
fullNameRegion.subSequence(validPrefix.length())
);
p.setPropertyName(fullName);
return p;
}
private void reconcileType(IDocument doc, Type expectType, Node value, IProblemCollector problems) {
// DocumentRegion escapedValue = getAssignedValue(doc, regions, i);
// if (escapedValue==null) {
// int charPos = DocumentUtil.lastNonWhitespaceCharOfRegion(doc, regions[i]);
// if (charPos>=0) {
// problems.accept(problem(SpringPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH,
// "Expecting '"+typeUtil.niceTypeName(expectType)+"'",
// charPos, 1));
// }
// } else {
// reconcileType(escapedValue, expectType, problems);
// }
reconcileType(createRegion(doc, value), expectType,
problems);
}
private DocumentRegion createRegion(IDocument doc, Node value) {
return new DocumentRegion(doc, value.getOffset(), value.getOffset() + value.getLength());
}
private void reconcileType(DocumentRegion region, Type expectType, IProblemCollector problems) {
TypeUtil typeUtil = typeUtilProvider.getTypeUtil(region.getDocument());
ValueParser parser = typeUtil.getValueParser(expectType);
if (parser!=null) {
try {
String valueStr = PropertiesFileEscapes.unescape(region.toString());
if (!valueStr.contains("${")) {
//Don't check strings that look like they use variable substitution.
parser.parse(valueStr);
}
} catch (Exception e) {
problems.accept(problem(ApplicationPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH,
"Expecting '"+typeUtil.niceTypeName(expectType)+"'",
region));
}
} else if (TypeUtil.isList(expectType)||TypeUtil.isArray(expectType)) {
commaListReconciler.reconcile(region, expectType, problems);
}
}
// private DocumentRegion getAssignedValue(IDocument doc, ITypedRegion[] regions, int i) {
// int valueRegionIndex = i+1;
// if (valueRegionIndex<regions.length) {
// String valueRegionType = regions[valueRegionIndex].getType();
// DocumentRegion valueRegion = new DocumentRegion(doc, regions[valueRegionIndex]);
// if (IPropertiesFilePartitions.PROPERTY_VALUE.equals(valueRegionType)) {
// //Need to remove the 'ASSIGN' bit from the start
// valueRegion = valueRegion.trimStart(ASSIGN).trimEnd(SPACES);
// //region text includes
// // potential padding with whitespace.
// // the ':' or '=' (if its there).
// return valueRegion;
// }
// }
// return null;
// }
private String suggestSimilar(PropertyInfo similarEntry, CharSequence validPrefix, CharSequence fullName) {
int matchedChars = validPrefix.length();
int wrongChars = fullName.length()-matchedChars;
if (wrongChars<matchedChars) {
return " Did you mean '"+similarEntry.getId()+"'?";
} else {
return "";
}
}
/**
* Check that there is an assignment char directly following the given region.
*/
// private boolean isAssigned(IDocument doc, IRegion r) {
// try {
// char c = doc.getChar(r.getOffset()+r.getLength());
// //Note either a '=' or a ':' can be used to assign properties.
// return isAssign(c);
// } catch (BadLocationException e) {
// //happens if looking for assignment char outside the document
// return false;
// }
// }
}

View File

@@ -0,0 +1,46 @@
package org.springframework.ide.vscode.application.properties.reconcile;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.commons.languageserver.quickfix.ProblemFixer;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
// TODO: Move to a common project shared between YAML and Properties
public class SpringPropertyProblem extends ReconcileProblemImpl {
private PropertyInfo property = null;
private ProblemFixer fixer;
private String propertyName;
public SpringPropertyProblem(ProblemType type, String msg, int offset, int len) {
super(type, msg, offset, len);
}
public static SpringPropertyProblem problem(ApplicationPropertiesProblemType type, String msg, int offset, int len) {
return new SpringPropertyProblem(type, msg, offset, len);
}
public static SpringPropertyProblem problem(ApplicationPropertiesProblemType type, String msg, Node region) {
return new SpringPropertyProblem(type, msg, region.getOffset(), region.getLength());
}
public static SpringPropertyProblem problem(ApplicationPropertiesProblemType type, String msg, DocumentRegion region) {
return new SpringPropertyProblem(type, msg, region.getStart(), region.getLength());
}
public void setMetadata(PropertyInfo property) {
this.property = property;
}
public void setProblemFixer(ProblemFixer fixer) {
this.fixer = fixer;
}
public void setPropertyName(String name) {
propertyName = name;
}
}

View File

@@ -48,24 +48,15 @@ import io.typefox.lsapi.Diagnostic;
*/
public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
private static final String SYNTAX_ERROR__UNEXPECTED_END_OF_INPUT = "Unexpected end of input, value identifier is expected";
private static final String SYNTAX_ERROR__UNEXPECTED_END_OF_LINE = "Unexpected end of line, value identifier is expected";
@Test public void testReconcileCatchesParseError() throws Exception {
LanguageServerHarness harness = new LanguageServerHarness(ApplicationPropertiesLanguageServer::new);
harness.intialize(null);
Editor editor = harness.newEditor("key\n");
editor.assertProblems("key|" + SYNTAX_ERROR__UNEXPECTED_END_OF_LINE);
Editor editor = newEditor("key\n");
editor.assertProblems("key|extraneous input");
}
@Test public void linterRunsOnDocumentOpenAndChange() throws Exception {
LanguageServerHarness harness = new LanguageServerHarness(ApplicationPropertiesLanguageServer::new);
harness.intialize(null);
Editor editor = newEditor("key");
Editor editor = harness.newEditor("key");
editor.assertProblems("key|" + SYNTAX_ERROR__UNEXPECTED_END_OF_INPUT);
editor.assertProblems("key|mismatched input");
editor.setText(
"problem\n" +
@@ -73,7 +64,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
"another"
);
editor.assertProblems("problem|" + SYNTAX_ERROR__UNEXPECTED_END_OF_LINE, "another|" + SYNTAX_ERROR__UNEXPECTED_END_OF_INPUT);
editor.assertProblems("problem|extraneous input", "another|mismatched input");
}
@@ -1563,7 +1554,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
@Override
protected SimpleLanguageServer newLanguageServer() {
return new ApplicationPropertiesLanguageServer();
return new ApplicationPropertiesLanguageServer(md.getIndexProvider(), typeUtilProvider);
}
/**

View File

@@ -15,14 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.concurrent.Callable;
import org.junit.Test;
import org.springframework.ide.vscode.application.properties.ApplicationPropertiesLanguageServer;
import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness;
import io.typefox.lsapi.InitializeResult;
import io.typefox.lsapi.ServerCapabilities;
import io.typefox.lsapi.TextDocumentSyncKind;
import io.typefox.lsapi.services.LanguageServer;
/**
* Boot app properties file language server tests
@@ -36,9 +37,14 @@ public class ApplicationPropertiesLanguageServerTest {
return Paths.get(ApplicationPropertiesLanguageServer.class.getResource(name).toURI()).toFile();
}
private LanguageServerHarness newHarness() throws Exception {
Callable<? extends LanguageServer> f = () -> new ApplicationPropertiesLanguageServer((d) -> null, (d) -> null);
return new LanguageServerHarness(f);
}
@Test
public void createAndInitializeServerWithWorkspace() throws Exception {
LanguageServerHarness harness = new LanguageServerHarness(ApplicationPropertiesLanguageServer::new);
LanguageServerHarness harness = newHarness();
File workspaceRoot = getTestResource("/workspace/");
assertExpectedInitResult(harness.intialize(workspaceRoot));
}
@@ -46,7 +52,7 @@ public class ApplicationPropertiesLanguageServerTest {
@Test
public void createAndInitializeServerWithoutWorkspace() throws Exception {
File workspaceRoot = null;
LanguageServerHarness harness = new LanguageServerHarness(ApplicationPropertiesLanguageServer::new);
LanguageServerHarness harness = newHarness();
assertExpectedInitResult(harness.intialize(workspaceRoot));
}

View File

@@ -9,6 +9,7 @@ import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.ide.vscode.application.properties.metadata.DefaultSpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;