/*******************************************************************************
* Copyright (c) 2015 Bruno Medeiros and other Contributors.
* 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:
* Bruno Medeiros - initial API and implementation
*******************************************************************************/
package com.googlecode.goclipse.tooling.oracle;
import static melnorme.lang.tooling.structure.StructureElementKind.STRUCT;
import static melnorme.utilbox.core.CoreUtil.list;
import java.util.Collections;
import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import melnorme.lang.tooling.EProtection;
import melnorme.lang.tooling.ElementAttributes;
import melnorme.lang.tooling.ast.ParserErrorTypes;
import melnorme.lang.tooling.ast.SourceRange;
import melnorme.lang.tooling.common.LineColumnPosition;
import melnorme.lang.tooling.common.ParserError;
import melnorme.lang.tooling.common.SourceFileLocation;
import melnorme.lang.tooling.structure.AbstractStructureParser;
import melnorme.lang.tooling.structure.SourceFileStructure;
import melnorme.lang.tooling.structure.StructureElement;
import melnorme.lang.tooling.structure.StructureElementKind;
import melnorme.lang.tooling.toolchain.ops.ToolOutputParseHelper;
import melnorme.lang.utils.gson.GsonHelper;
import melnorme.lang.utils.gson.JsonParserX;
import melnorme.lang.utils.parse.LexingUtils;
import melnorme.lang.utils.parse.StringCharSource;
import melnorme.utilbox.collections.ArrayList2;
import melnorme.utilbox.collections.Indexable;
import melnorme.utilbox.core.CommonException;
import melnorme.utilbox.misc.Location;
import melnorme.utilbox.misc.StringUtil;
import melnorme.utilbox.process.ExternalProcessHelper.ExternalProcessResult;
public class GuruPackageDescribeParser extends AbstractStructureParser {
protected final GsonHelper helper = new GsonHelper();
public GuruPackageDescribeParser(Location location, String goSource) {
super(location, goSource);
}
public SourceFileStructure parse(ExternalProcessResult result) throws CommonException {
if(result.exitValue != 0) {
String errorMsg = result.getStdErrBytes().toString(StringUtil.UTF8);
return parseErrorMessage(errorMsg);
}
return parse(result.getStdOutBytes().toString(StringUtil.UTF8));
}
@Override
public SourceFileStructure parse(String describeOutput) throws CommonException {
ArrayList2<StructureElement> elements = doParseJsonResult(describeOutput);
return new SourceFileStructure(location, elements, null);
}
protected ArrayList2<StructureElement> doParseJsonResult(String output)
throws CommonException {
JsonObject describe = new JsonParserX().parseObject(output, true);
JsonObject packageObj = helper.getObject(describe, "package");
JsonArray members = helper.getOptionalArray(packageObj, "members");
return parseElements(members, false);
}
protected ArrayList2<StructureElement> parseElements(JsonArray members, boolean parsingMethods)
throws CommonException {
ArrayList2<StructureElement> elements = new ArrayList2<>();
if(members != null) {
for(int i = 0; i < members.size(); i++) {
JsonElement arrayElement = members.get(i);
if(arrayElement.isJsonObject()) {
JsonObject jsonObject = arrayElement.getAsJsonObject();
StructureElement structureElement = parseStructureElement(jsonObject, parsingMethods);
if(structureElement == null) {
continue; // Can happen for external elements
}
elements.add(structureElement);
} else {
throw new CommonException("'members' array element is not a JSONObject: " + arrayElement);
}
}
}
Collections.sort(elements, new Comparator<StructureElement>() {
@Override
public int compare(StructureElement o1, StructureElement o2) {
SourceRange sr1 = o1.getSourceRange();
SourceRange sr2 = o2.getSourceRange();
int cmp = sr1.getOffset() - sr2.getOffset();
if(cmp == 0) {
int offset1 = o1.getNameSourceRange2() == null ? 0 : o1.getNameSourceRange2().getOffset();
int offset2 = o2.getNameSourceRange2() == null ? 0 : o2.getNameSourceRange2().getOffset();
return offset1 - offset2;
}
return cmp;
}
});
return elements;
}
protected StructureElement parseStructureElement(JsonObject object, boolean parsingMethods)
throws CommonException {
String name = helper.getString(object, "name");
String posString = helper.getString(object, "pos");
SourceFileLocation elementSourceFileLoc = SourceFileLocation.parseSourceRange(posString, ':');
SourceRange nameSourceRange;
SourceRange sourceRange;
if(!isSourceElementLocation(elementSourceFileLoc.getFileLocation())) {
sourceRange = nameSourceRange = null;
} else {
sourceRange = nameSourceRange = elementSourceFileLoc.parseSourceRangeFrom1BasedIndex(sourceLinesInfo);
}
String type = helper.getOptionalString(object, "type");
String kindString = helper.getOptionalString(object, "kind");
StructureElementKind elementKind;
if(parsingMethods) {
elementKind = StructureElementKind.METHOD;
} else {
if(kindString == null) {
throw new CommonException("No `kind` field for element: " + name);
}
elementKind = parseKind(kindString, type);
}
if(elementKind == STRUCT || elementKind == StructureElementKind.INTERFACE) {
type = null;
} else if(elementKind == StructureElementKind.METHOD) {
if(name.startsWith("method ")) {
name = name.substring("method ".length());
if(name.startsWith("(")) {
String fullName = StringUtil.segmentAfterMatch(name, ")");
if(fullName != null) {
fullName = fullName.trim();
int idLength = parseIdentifierStart(fullName);
if(idLength > 0) {
name = fullName.substring(0, idLength);
type = "func" + fullName.substring(idLength);
}
}
}
}
}
if(name.length() == 0) {
throw new CommonException("No name provided");
}
EProtection protection = EProtection.PUBLIC;
if(!parsingMethods && Character.isLowerCase(name.charAt(0))) {
protection = EProtection.PRIVATE;
}
ElementAttributes elementAttributes = new ElementAttributes(protection);
JsonArray methods = helper.getOptionalArray(object, "methods");
Indexable<StructureElement> children = parseElements(methods, true);
if(!isSourceElementLocation(elementSourceFileLoc.getFileLocation())) {
// Fix source range to children range.
if(children.size() == 0) {
return null; // Shouldn't even happen
}
nameSourceRange = null;
int startPos = children.get(0).getSourceRange().getStartPos();
int endPos = children.get(children.size()-1).getSourceRange().getEndPos();
sourceRange = SourceRange.srStartToEnd(startPos, endPos);
}
return new StructureElement(name, nameSourceRange, sourceRange,
elementKind, elementAttributes, type, children);
}
protected boolean isSourceElementLocation(Location sourceFileLoc) throws CommonException {
return location == null || location.equals(sourceFileLoc);
}
protected int parseIdentifierStart(String source) {
StringCharSource parser = new StringCharSource(source);
return LexingUtils.matchJavaIdentifier(parser);
}
protected StructureElementKind parseKind(String kind, String type) {
if(kind == null) {
return null;
}
switch (kind.toLowerCase()) {
case "func": return StructureElementKind.FUNCTION;
case "var": return StructureElementKind.VARIABLE;
case "const": return StructureElementKind.CONST;
case "type":
if(type.startsWith("struct")) {
return StructureElementKind.STRUCT;
}
if(type.startsWith("interface")) {
return StructureElementKind.INTERFACE;
}
return StructureElementKind.TYPE_DECL;
default:
return StructureElementKind.VARIABLE;
}
}
protected static final Pattern GO_MESSAGE_LINE_Regex = Pattern.compile(
"^([^:\\n]*):" + // file
"(\\d*):((\\d*):)?" +// line:column
// "( (\\d*):(\\d*))?" + // end line:column
// "()" + // column-end
"\\s(.*)$" // error message
);
public SourceFileStructure parseErrorMessage(String errorMsg) throws CommonException {
errorMsg = StringUtil.substringUntilLastMatch(errorMsg, "\n");
errorMsg = StringUtil.trimStart(errorMsg, "oracle: ");
errorMsg = StringUtil.trimStart(errorMsg, "guru: ");
ArrayList2<ParserError> parserProblems = new ArrayList2<>();
if(errorMsg.length() > 2 && errorMsg.charAt(1) == ':') {
// The path has a Windows driver letter, we need to remove it so it doesn't mess up the regex
errorMsg = errorMsg.substring(2);
}
Matcher matcher = GO_MESSAGE_LINE_Regex.matcher(errorMsg);
if(!matcher.matches()) {
throw new CommonException(errorMsg);
}
String lineStr = matcher.group(2);
String columnStr = matcher.group(4);
String message = matcher.group(5);
LineColumnPosition lcPost = ToolOutputParseHelper.parseLineColumn(lineStr, columnStr, 1, 1);
int offset = sourceLinesInfo.getValidatedOffset_1(lcPost.line, lcPost.column);
String source = sourceLinesInfo.getSource();
int length = 1;
if(offset == source.length()) {
length = 0;
}
else if(offset < source.length()) {
length = heuristic_determinTokenLength(offset, source);
}
SourceRange sr = new SourceRange(offset, length);
parserProblems.add(new ParserError(ParserErrorTypes.GENERIC_ERROR, sr, message, null));
return new SourceFileStructure(location, list(), parserProblems);
}
/**
* Try to guess the length of the token at given offset, from given source.
* Doesn't have to be accurate measurement. (used to get a better range for error msgs)
*/
protected int heuristic_determinTokenLength(int offset, String source) {
StringCharSource charSource = new StringCharSource(source);
charSource.consumeAmount(offset);
int length = LexingUtils.matchJavaIdentifier(charSource);
if (length == 0) {
return 1;
}
return length;
}
}