All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nodes.functions.Argument Maven / Gradle / Ivy

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