nodes.functions.Argument Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jsyntaxtree Show documentation
Show all versions of jsyntaxtree Show documentation
Syntax tree representation of the JASS language
The newest version!
package nodes.functions;
import exception.ParsingException;
import interfaces.IFunctionRenameable;
import interfaces.IVariableRenameable;
import nodes.AbstractNode;
import tree.TreeContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* Stores an argument for a function call or statement.
* This decomposes the argument into the most basic form possible.
*
* This class logically splits an argument into three basic forms:
*
* - Basic argument: cannot be broken down any further and calls no method
* - Function call: a single, standalone function call
* - Aggregation: other arguments joined by an operator (for example +)
*/
public final class Argument extends AbstractNode implements IFunctionRenameable, IVariableRenameable {
/**
* Basic Argument is something that stands alone, for example
* a literal or a variable reference
* i.e. "5", or "myVariable"
*/
private String basicArgument;
/**
* A function call is an atomic call to a function that
* may or may not return a value.
* i.e. myFunction(5)
*/
private FunctionCall functionCall;
/**
* An array name is the outer part of the call to an array
* Such as ybx[Abc], ybx would be the arrayName
*/
private Argument arrayName;
/**
* An array call is the inner section to an array.
* Such as ybx[Abc], Abc would be the arrayCall
*/
private Argument arrayCall;
/**
* An aggregation is a combination of two or more
* basic arguments and/or function calls.
* i.e. myFunction + 5 + 3
*/
private List aggregation;
/**
* The separator between the aggregation entries.
*/
private String operator;
/**
* If we removed parenthesis to begin with,
* we want to add them back on at the end.
*/
private boolean hasParenthesis;
/**
* Represents the argument of not (notPart)
*/
private Argument notPart;
/**
* Operators that cen separate an aggregation.
* Note the spaces in the operator is important
* Or else we can't differentiate between < and <= for example
*/
private static String[] OPERATORS = {"+", "-", "/", "*", " and ", " or ", "> ", "< ", ">=", "<=", "==", "!=", " not "};
/**
* Some additional characters that aren't really
* separators for an aggregation, but also can't be
* in a function name.
*/
private static String[] INVALID_FUNCTION_CHARACTERS = {"\"", "\\", ")", "(", "[", "]", "\n", " "};
/**
* Sets up this node with a scanner to receive words.
*
* @param inputScanner Scanner containing JASS code
*/
public Argument(Scanner inputScanner, TreeContext context) {
super(inputScanner, context);
}
/**
* No-args constructor used for creating from an existing
*/
public Argument(String basicArgument, FunctionCall functionCall, List aggregation, String operator, Argument notPart, Argument arrayName, Argument arrayCall, boolean hasParenthesis, TreeContext context) {
super(context);
this.basicArgument = basicArgument;
this.functionCall = functionCall;
this.aggregation = aggregation;
this.operator = operator;
this.notPart = notPart;
this.hasParenthesis = hasParenthesis;
this.arrayName = arrayName;
this.arrayCall = arrayCall;
verifyState();
}
/**
* Determines whether or not this line is an
* atomic function call, for example:
*
* myFunction(x) : is a function call.
* myFunction(x) + 5 : is not a function call (it's an aggregation)
*
* @param line Argument line
* @return True if function call; false if not.
*/
private boolean isFunctionCall(String line) {
if (line.contains("(") && line.contains(")")) {
// Extract everything up to the first ( as the functon name
String functionName = line.substring(0, line.indexOf("("));
// Check function name for validity
// If it fails these checks, it wasn't a function call
for (String operator : OPERATORS) {
if (functionName.contains(operator)) {
return false;
}
}
for (String operator : INVALID_FUNCTION_CHARACTERS) {
if (functionName.contains(operator)) {
return false;
}
}
// Everything other than the function name should be the arguments
String argumentsPart = line.substring(functionName.length());
boolean quoted = false;
boolean firstParenthesisFound = false;
int parenthesisLevel = 0; // Keeps track of how many parenthesis deep we are in a line
// Used to handle escape chars
char lastChar1 = ' ';
char lastChar2 = ' ';
boolean shouldBeFinished = false;
for (char c : argumentsPart.toCharArray()) {
if (shouldBeFinished) {
// We should have reached the end of line but didn't
// Therefore this isn't an atomic function call
return false;
}
if (c == '\"') {
// Handle quotes appropriately, taking escape characters into account
if (lastChar1 != '\\') {
quoted = !quoted;
} else {
if (lastChar2 == '\\') {
quoted = !quoted;
}
}
} else if (c == '(' && !quoted) {
// Non-quoted parenthesis means we started a function call
// and went one-parenthesis deeper than before
parenthesisLevel++;
firstParenthesisFound = true;
} else if (c == ')' && !quoted) {
// Non-quoted end-parenthesis means we went up one
// parenthesis level and maybe finished the function call
parenthesisLevel--;
if (parenthesisLevel == 0 && firstParenthesisFound) {
// If we ended our function call, this needs to be the
// last char of the line to be valid.
// If not the last char of the line, this boolean will
// make it return false
shouldBeFinished = true;
}
}
// Update the last 2 characters to handle escape characters correctly
lastChar2 = lastChar1;
lastChar1 = c;
}
// If we reach the end here without returning false, that means it IS atomic
return true;
} else {
// It doesn't have parenthesis at all so it's not a function call
return false;
}
}
/**
* Parse the JASS code contained in the Scanner into a model object
*/
@Override
protected final void readNode() {
String line = readLine();
line = line.replace("<", "< ");
line = line.replaceAll("< =", "<=");
line = line.replace(">", "> ");
line = line.replaceAll("> =", ">=");
readIntoArgument(line);
}
private final void readIntoArgument(String line) {
line = line.trim();
line = formatSpacing(line);
// Handle the annoying "is it < or <=" case by using spacing
line = line.replaceAll("<^[\\=]", "< ");
line = line.replaceAll(">^[\\=]", "> ");
if (shouldTrim(line)) {
// Maintain original parenthesis
hasParenthesis = true;
line = trimParenthesis(line).trim();
} else {
hasParenthesis = false;
}
if(line.startsWith("not ")) {
line = " " + line;
}
List splitParts = new ArrayList<>();
StringBuilder splitPart = new StringBuilder();
if (isFunctionCall(line)) {
this.functionCall = new FunctionCall(new Scanner(line), context);
} else {
// If it's not a function call, try to make it an
// aggregation first. If it's not, then make it a basic arg.
boolean foundOperator = false;
boolean quoted = false;
String whichOperator = "";
int parenthesisLevel = 0;
char lastChar1 = ' ';
char lastChar2 = ' ';
for (char c : line.toCharArray()) {
splitPart.append(c);
// Handle quotes and escapes quotes
if (c == '\"') {
if (lastChar1 != '\\') {
quoted = !quoted;
} else {
if (lastChar2 == '\\') {
quoted = !quoted;
}
}
} else if (c == '(') {
parenthesisLevel++;
} else if (c == ')') {
parenthesisLevel--;
} else if (parenthesisLevel == 0) {
for (String op : OPERATORS) {
// Try to split by every operator
// If we can split by this operator, then split
// the remainder of the entry by it as well.
if (splitPart.toString().endsWith(op) && !quoted && !foundOperator) {
foundOperator = true;
whichOperator = op + "";
removeFinalCharacter(op.length(), splitPart);
splitParts.add(splitPart.toString());
splitPart.setLength(0);
} else if (foundOperator) {
// Split by already known operator
if ((c + "").equals(whichOperator) && !quoted && splitPart.length() > 0) {
removeFinalCharacter(op.length(), splitPart);
splitParts.add(splitPart.toString());
splitPart.setLength(0);
}
}
}
}
// Manage last characters for escaped quotes handling
lastChar2 = lastChar1;
lastChar1 = c;
}
if(splitPart.length()>0) {
splitParts.add(splitPart.toString());
}
if (foundOperator) {
// If we found an operator, then it's an aggregation
this.operator = whichOperator.trim();
int size = splitParts.size();
for (String part : splitParts) {
// Handle empty part, for example "-1" splits into "", "1"
// But this is not an aggregation!
if (part == null || part.isEmpty()) {
size--;
}
}
// This is for empty part handling again.
if (size >= 2) {
for (String part : splitParts) {
// Parse each sub-argument into arguments.
Argument argumentPart = new Argument(new Scanner(part), context);
this.aggregation.add(argumentPart);
}
} else if(size == 1 && this.operator.equals("not")) {
notPart = new Argument(new Scanner(splitParts.get(1)), context);
} else {
// We thought it was an aggregation, but it's actually
// a basic function. The line "-1" triggers this block.
this.basicArgument = line;
}
} else {
// It couldn't be split, so it's not an aggregation.
this.basicArgument = line;
}
}
if(basicArgument != null && basicArgument.matches(".*\\[.*\\]") && basicArgument.endsWith("]")) {
String firstPart = basicArgument.substring(0, basicArgument.indexOf("["));
String secondPart = basicArgument.substring(1+basicArgument.indexOf("["), basicArgument.length()-1);
// Clear out basicArgument and set array parts
arrayName = new Argument(new Scanner(firstPart), context);
arrayCall = new Argument(new Scanner(secondPart), context);
basicArgument = null;
}
verifyState();
}
/**
* Renames the variable and all uses of this variable.
*
* @param oldVariableName Existing variable name
* @param newVariableName Desired variable name
*/
@Override
public final void renameVariable(String oldVariableName, String newVariableName) {
if (functionCall != null) {
functionCall.renameVariable(oldVariableName, newVariableName);
} else if (basicArgument != null && !basicArgument.isEmpty()) {
this.basicArgument = rename(basicArgument, oldVariableName, newVariableName);
} else if (!aggregation.isEmpty()) {
for (Argument arg : aggregation) {
arg.renameVariable(oldVariableName, newVariableName);
}
} else if(arrayCall != null && arrayName != null) {
arrayCall.renameVariable(oldVariableName, newVariableName);
arrayName.renameVariable(oldVariableName, newVariableName);
} else if(notPart != null) {
notPart.renameVariable(oldVariableName, newVariableName);
}
}
/**
* Renames a function and uses to a new name
*
* @param oldFunctionName Existing function name
* @param newFunctionName Desired function name
*/
@Override
public final void renameFunction(String oldFunctionName, String newFunctionName) {
if (functionCall != null) {
this.functionCall.renameFunction(oldFunctionName, newFunctionName);
} else if (basicArgument != null && !basicArgument.isEmpty()) {
this.basicArgument = rename(basicArgument, oldFunctionName, newFunctionName);
} else if (!aggregation.isEmpty()) {
for (Argument arg : aggregation) {
arg.renameFunction(oldFunctionName, newFunctionName);
}
} else if(arrayCall != null && arrayName != null) {
arrayCall.renameFunction(oldFunctionName, newFunctionName);
arrayName.renameFunction(oldFunctionName, newFunctionName);
} else if(notPart != null) {
notPart.renameFunction(oldFunctionName, newFunctionName);
}
}
public Argument inline(String functionName, String newText) {
if (functionCall != null) {
return new Argument(basicArgument, functionCall.inline(functionName, newText), aggregation, operator, notPart, arrayName, arrayCall, hasParenthesis, context);
} else if (basicArgument != null && !basicArgument.isEmpty()) {
return this;
} else if (!aggregation.isEmpty()) {
List newAggregation = new ArrayList<>();
for (Argument arg : aggregation) {
newAggregation.add((Argument) arg.inline(functionName, newText));
}
return new Argument(basicArgument, functionCall, newAggregation, operator, notPart, arrayName, arrayCall, hasParenthesis, context);
} else if(arrayCall != null && arrayName != null) {
return new Argument(basicArgument, functionCall, aggregation, operator, notPart, arrayName, arrayCall, hasParenthesis, context);
} else if(notPart != null) {
return new Argument(basicArgument, functionCall, aggregation, operator, notPart.inline(functionName, newText), arrayName, arrayCall, hasParenthesis, context);
}
return this;
}
public boolean calls(String functionName) {
if (functionCall != null) {
return functionCall.calls(functionName) ||
usesAsFunction(functionName);
} else if(arrayCall != null && arrayName != null) {
return arrayCall.calls(functionName) ||
arrayName.calls(functionName) ||
usesAsFunction(functionName);
}
return false;
}
public boolean usesAsFunction(String functionName) {
if (functionCall != null) {
return functionCall.usesAsFunction(functionName);
} else if (basicArgument != null && !basicArgument.isEmpty()) {
return basicArgument.contains("function " + functionName);
} else if(arrayCall != null && arrayName != null) {
return arrayCall.usesAsFunction(functionName) || arrayName.usesAsFunction(functionName);
} else if (!aggregation.isEmpty()) {
for (Argument arg : aggregation) {
if(arg.usesAsFunction(functionName)) {
return true;
}
}
return false;
}
return false;
}
/**
* Sets up any class-level variables before
* performing the node reading.
*/
@Override
protected final void setupVariables() {
this.aggregation = new ArrayList<>();
}
/**
* Converts this node back to its original form.
* Indentation is not added.
*
* @return Original form of this node (code or string)
*/
@Override
public final String toString() {
StringBuilder built = new StringBuilder();
if (hasParenthesis) {
// Add back on the trimmed parenthesis, if required.
built.append("(");
}
if (basicArgument != null && aggregation.isEmpty() && functionCall == null) {
return basicArgument;
} else if (!aggregation.isEmpty()) {
for (Argument aggregate : aggregation) {
built.append(aggregate.toString()).append(" ").append(operator).append(" ");
}
removeFinalCharacter(operator.length() + 2, built);
} else if (functionCall != null) {
return functionCall.toString();
} else if(arrayCall != null && arrayName != null) {
return arrayName.toString() + "[" + arrayCall + "]";
} else if(notPart != null) {
return "not (" + notPart.toString() + ")";
} else {
return "";
}
if (hasParenthesis) {
// Add back on the trimmed parenthesis, if required.
built.append(")");
}
// Trim spaces from and/or because we added an extra one on.
return built.toString().replace(" and ", " and ").replace(" or ", " or ");
}
/**
* Converts this node back to its original form.
*
* @param indentationLevel Current indentation level
* @return Original form of this node (code or string) with indentation
*/
@Override
public String toFormattedString(int indentationLevel) {
return this.toString();
}
/**
* Ensures the state of this argument is not corrupted.
* Should not happen under regular usage.
*/
private void verifyState() {
if (basicArgument != null) {
if (!(arrayCall == null && arrayName == null && aggregation.isEmpty() && functionCall == null)) {
throw new ParsingException("Failed internal validity (1) check: " + this.toString());
}
} else if (!aggregation.isEmpty()) {
if (!(arrayCall == null && arrayName == null && basicArgument == null) && functionCall == null) {
throw new ParsingException("Failed internal validity (2) check: " + this.toString());
}
} else if (functionCall != null) {
if (!(arrayCall == null && arrayName == null && basicArgument == null && aggregation.isEmpty())) {
throw new ParsingException("Failed internal validity (3) check: " + this.toString());
}
} else if (arrayName != null && arrayCall != null) {
if (!(functionCall == null && basicArgument == null && aggregation.isEmpty())) {
throw new ParsingException("Failed internal validity (4) check: " + this.toString());
}
} else if(notPart == null) {
throw new ParsingException("Failed internal validity (5) check: " + this.toString());
}
}
public boolean isNot() {
return notPart != null;
}
public Argument getNotPart() {
return notPart;
}
public final List getArguments() {
List baseArguments = new ArrayList<>();
if(basicArgument != null) {
baseArguments.add(this);
} else if(aggregation != null && !aggregation.isEmpty()) {
for(Argument arg : aggregation) {
baseArguments.addAll(arg.getArguments());
}
} else if(functionCall != null) {
baseArguments.add(this);
for(Argument arg : functionCall.getArguments()) {
baseArguments.addAll(arg.getArguments());
}
}
return baseArguments;
}
public void setArgument(String line) {
readIntoArgument(line);
}
@Override
public int hashCode() {
return this.toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) { return false; }
if (obj == this) { return true; }
if (obj.getClass() != getClass()) {
return false;
}
Argument other = (Argument) obj;
return this.toString().equals(other.toString());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy