Arguments.java

/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript;

/**
 * This class implements the "arguments" object.
 *
 * <p>See ECMA 10.1.8
 *
 * @see org.mozilla.javascript.NativeCall
 * @author Norris Boyd
 */
class Arguments extends ScriptableObject {
    private static final long serialVersionUID = 4275508002492040609L;

    private static final String CLASS_NAME = "Arguments";

    // Fields to hold caller, callee and length properties,
    // where NOT_FOUND value tags deleted properties.
    // In addition, the 'caller' arguments is only available in JS <= 1.3 scripts
    private Object calleeObj;
    private Object lengthObj;

    private NativeCall activation;

    // Initially args holds activation.getOriginalArgs(), but any modification
    // of its elements triggers creation of a copy. If its element holds NOT_FOUND,
    // it indicates deleted index, in which case super class is queried.
    private Object[] args;

    public Arguments(NativeCall activation, Context cx) {
        this.activation = activation;

        Scriptable parent = activation.getParentScope();
        setParentScope(parent);
        setPrototype(ScriptableObject.getObjectPrototype(parent));

        args = activation.originalArgs;
        lengthObj = Integer.valueOf(args.length);

        JSFunction f = activation.function;
        calleeObj = f;

        defineProperty(
                SymbolKey.ITERATOR,
                TopLevel.getBuiltinPrototype(
                                ScriptableObject.getTopLevelScope(parent), TopLevel.Builtins.Array)
                        .get("values", parent),
                ScriptableObject.DONTENUM);
        defineProperty("length", lengthObj, ScriptableObject.DONTENUM);

        if (activation.isStrict) {
            // ECMAScript2015
            // 9.4.4.6 CreateUnmappedArgumentsObject(argumentsList)
            //   8. Perform DefinePropertyOrThrow(obj, "caller", PropertyDescriptor {[[Get]]:
            // %ThrowTypeError%,
            //      [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, [[Configurable]]: false}).
            //   9. Perform DefinePropertyOrThrow(obj, "callee", PropertyDescriptor {[[Get]]:
            // %ThrowTypeError%,
            //      [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, [[Configurable]]: false}).
            BaseFunction typeErrorThrower = ScriptRuntime.typeErrorThrower(cx);
            int version = cx.getLanguageVersion();
            if (version <= Context.VERSION_1_8) {
                setGetterOrSetter("caller", 0, typeErrorThrower, true);
                setGetterOrSetter("caller", 0, typeErrorThrower, false);
                setGetterOrSetter("callee", 0, typeErrorThrower, true);
                setGetterOrSetter("callee", 0, typeErrorThrower, false);
                setAttributes("caller", DONTENUM | PERMANENT);
                setAttributes("callee", DONTENUM | PERMANENT);
            } else {
                setGetterOrSetter("callee", 0, typeErrorThrower, true);
                setGetterOrSetter("callee", 0, typeErrorThrower, false);
                setAttributes("callee", DONTENUM | PERMANENT);
            }
            calleeObj = null;
        } else {
            defineProperty("callee", calleeObj, ScriptableObject.DONTENUM);

            int version = cx.getLanguageVersion();
            if (version <= Context.VERSION_1_3 && version != Context.VERSION_DEFAULT) {
                defineProperty("caller", (Object) null, ScriptableObject.DONTENUM);
            }
        }
    }

    @Override
    public String getClassName() {
        return CLASS_NAME;
    }

    private Object arg(int index) {
        if (index < 0 || args.length <= index) return NOT_FOUND;
        return args[index];
    }

    // the following helper methods assume that 0 < index < args.length

    private void putIntoActivation(int index, Object value) {
        String argName = activation.function.getParamOrVarName(index);
        activation.put(argName, activation, value);
    }

    private Object getFromActivation(int index) {
        String argName = activation.function.getParamOrVarName(index);
        return activation.get(argName, activation);
    }

    private void replaceArg(int index, Object value) {
        if (sharedWithActivation(index)) {
            putIntoActivation(index, value);
        }
        synchronized (this) {
            if (args == activation.originalArgs) {
                args = args.clone();
            }
            args[index] = value;
        }
    }

    private void removeArg(int index) {
        synchronized (this) {
            if (args[index] != NOT_FOUND) {
                if (args == activation.originalArgs) {
                    args = args.clone();
                }
                args[index] = NOT_FOUND;
            }
        }
    }

    // end helpers

    @Override
    public boolean has(int index, Scriptable start) {
        if (arg(index) != NOT_FOUND) {
            return true;
        }
        return super.has(index, start);
    }

    @Override
    public Object get(int index, Scriptable start) {
        final Object value = arg(index);
        if (value == NOT_FOUND) {
            return super.get(index, start);
        }
        if (sharedWithActivation(index)) {
            return getFromActivation(index);
        }
        return value;
    }

    private boolean sharedWithActivation(int index) {
        Context cx = Context.getContext();
        if (cx.isStrictMode()) {
            return false;
        }
        JSFunction f = activation.function;

        // Check if default arguments are present
        if (f == null || f.hasDefaultParameters()) {
            return false;
        }

        int definedCount = f.getParamCount();
        if (index < definedCount) {
            // Check if argument is not hidden by later argument with the same
            // name as hidden arguments are not shared with activation
            if (index < definedCount - 1) {
                String argName = f.getParamOrVarName(index);
                for (int i = index + 1; i < definedCount; i++) {
                    if (argName.equals(f.getParamOrVarName(i))) {
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public void put(int index, Scriptable start, Object value) {
        if (arg(index) == NOT_FOUND) {
            super.put(index, start, value);
        } else {
            replaceArg(index, value);
        }
    }

    @Override
    public void put(String name, Scriptable start, Object value) {
        super.put(name, start, value);
    }

    @Override
    public void put(Symbol key, Scriptable start, Object value) {
        super.put(key, start, value);
    }

    @Override
    public void delete(int index) {
        if (0 <= index && index < args.length) {
            removeArg(index);
        }
        super.delete(index);
    }

    @Override
    Object[] getIds(CompoundOperationMap map, boolean getNonEnumerable, boolean getSymbols) {
        Object[] ids = super.getIds(map, getNonEnumerable, getSymbols);
        if (args.length != 0) {
            boolean[] present = new boolean[args.length];
            int extraCount = args.length;
            for (int i = 0; i != ids.length; ++i) {
                Object id = ids[i];
                if (id instanceof Integer) {
                    int index = ((Integer) id).intValue();
                    if (0 <= index && index < args.length) {
                        if (!present[index]) {
                            present[index] = true;
                            extraCount--;
                        }
                    }
                }
            }
            if (!getNonEnumerable) { // avoid adding args which were redefined to non-enumerable
                for (int i = 0; i < present.length; i++) {
                    if (!present[i] && super.has(i, this)) {
                        present[i] = true;
                        extraCount--;
                    }
                }
            }
            if (extraCount != 0) {
                Object[] tmp = new Object[extraCount + ids.length];
                System.arraycopy(ids, 0, tmp, extraCount, ids.length);
                ids = tmp;
                int offset = 0;
                for (int i = 0; i != args.length; ++i) {
                    if (!present[i]) {
                        ids[offset] = Integer.valueOf(i);
                        ++offset;
                    }
                }
                if (offset != extraCount) Kit.codeBug();
            }
        }
        return ids;
    }

    @Override
    protected DescriptorInfo getOwnPropertyDescriptor(Context cx, Object id) {
        if (ScriptRuntime.isSymbol(id) || id instanceof Scriptable) {
            return super.getOwnPropertyDescriptor(cx, id);
        }

        double d = ScriptRuntime.toNumber(id);
        int index = (int) d;
        if (d != index) {
            return super.getOwnPropertyDescriptor(cx, id);
        }
        Object value = arg(index);
        if (value == NOT_FOUND) {
            return super.getOwnPropertyDescriptor(cx, id);
        }
        if (sharedWithActivation(index)) {
            value = getFromActivation(index);
        }
        if (super.has(index, this)) { // the descriptor has been redefined
            DescriptorInfo desc = super.getOwnPropertyDescriptor(cx, id);
            desc.value = value;
            return desc;
        }
        return buildDataDescriptor(value, EMPTY);
    }

    @Override
    protected boolean defineOwnProperty(
            Context cx, Object id, DescriptorInfo desc, boolean checkValid) {
        super.defineOwnProperty(cx, id, desc, checkValid);
        if (ScriptRuntime.isSymbol(id)) {
            return true;
        }

        double d = ScriptRuntime.toNumber(id);
        int index = (int) d;
        if (d != index) return true;

        Object value = arg(index);
        if (value == NOT_FOUND) return true;

        if (desc.isAccessorDescriptor()) {
            removeArg(index);
            return true;
        }

        Object newValue = desc.value;
        if (newValue == NOT_FOUND) return true;

        replaceArg(index, newValue);

        if (isFalse(desc.writable)) {
            removeArg(index);
        }
        return true;
    }

    static final class ReadonlyArguments extends Arguments {
        private boolean initialized;

        public ReadonlyArguments(Arguments arguments, Context cx) {
            super(arguments.activation, cx);
            initialized = true;
        }

        @Override
        public void put(int index, Scriptable start, Object value) {
            if (initialized) {
                return;
            }
            super.put(index, start, value);
        }

        @Override
        public void put(String name, Scriptable start, Object value) {
            if (initialized) {
                return;
            }
            super.put(name, start, value);
        }

        @Override
        public void put(Symbol key, Scriptable start, Object value) {
            if (initialized) {
                return;
            }
            super.put(key, start, value);
        }

        @Override
        public void delete(int index) {
            if (initialized) {
                return;
            }
            super.delete(index);
        }

        @Override
        public void delete(String name) {
            if (initialized) {
                return;
            }
            super.delete(name);
        }

        @Override
        public void delete(Symbol key) {
            if (initialized) {
                return;
            }
            super.delete(key);
        }
    }
}