/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain other free and open source software ("FOSS") code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.js.environment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.editor.js.JSPlugin;
import com.aptana.ide.editor.js.runtime.Environment;
import com.aptana.ide.editor.js.runtime.FunctionBase;
import com.aptana.ide.editor.js.runtime.IFunction;
import com.aptana.ide.editor.js.runtime.IObject;
import com.aptana.ide.editor.js.runtime.IScope;
import com.aptana.ide.editor.js.runtime.JSFunction;
import com.aptana.ide.editor.js.runtime.JSObject;
import com.aptana.ide.editor.js.runtime.ObjectBase;
import com.aptana.ide.editor.js.runtime.OrderedObject;
import com.aptana.ide.editor.js.runtime.OrderedObjectCollection;
import com.aptana.ide.editor.js.runtime.Property;
import com.aptana.ide.editor.js.runtime.Reference;
import com.aptana.ide.editors.UnifiedEditorsPlugin;
import com.aptana.ide.lexer.LexemeList;
import com.aptana.ide.lexer.Range;
import com.aptana.ide.parsing.IParseState;
/**
* Populates the JavaScript environment based on the current set of lexemes. It reloads the environment when files are
* opened or closed. This is the lexeme-based version of the parser in the pro product.
*
* @author Spike Washburn
* @author Kevin Lindsey
*/
public class LexemeBasedEnvironmentLoader
{
private Environment _environment;
private List<ScopeRange> _scopeList = new ArrayList<ScopeRange>();
private IParseState _parseState;
/**
* LexemeBasedEnvironmentLoader
*
* @param env
*/
public LexemeBasedEnvironmentLoader(Environment env)
{
this._environment = env;
ScopeRange sr = new ScopeRange(env.getGlobal(), 0, Integer.MAX_VALUE);
this._scopeList.add(sr);
}
/**
* getFileIndex
*
* @return int
*/
public int getFileIndex()
{
return this._parseState.getFileIndex();
}
/**
* addProperty
*
* @param parentScope
* @param parent
* @param name
* @param offset
* @return IObject
*/
IObject addProperty(IScope parentScope, IObject parent, String name, int offset)
{
IObject propValue = getPropertyValue(parent, name, offset);
if (propValue == ObjectBase.UNDEFINED)
{
propValue = createGuessedObject(new Range(offset, offset + 1));
putPropertyValue(parent, name, propValue);
}
return propValue;
}
/**
* addVariable
*
* @param parentScope
* @param name
* @param offset
* @param isVar
* @return IObject
*/
IObject addVariable(IScope parentScope, String name, int offset, boolean isVar)
{
IObject propValue;
if (isVar)
{
if (name.equals("this") && parentScope.hasLocalProperty("this")) //$NON-NLS-1$ //$NON-NLS-2$
{
// if this is already defined, reuse it, don't let isVar overwrite it.
propValue = parentScope.getLocalProperty("this").getValue(getFileIndex(), offset); //$NON-NLS-1$
}
else
{
propValue = createGuessedObject(new Range(offset, offset + 1));
putVariableValue(parentScope, name, propValue, isVar);
}
}
else
{
propValue = getVariableValue(parentScope, name, offset);
if (propValue == ObjectBase.UNDEFINED)
{
propValue = createGuessedObject(new Range(offset, offset + 1));
putVariableValue(parentScope, name, propValue, isVar);
}
}
return propValue;
}
/**
* createFunctionInstance
*
* @param offset
* @param isInvoking
* @return JSFunction
*/
JSFunction createFunctionInstance(int offset, boolean isInvoking)
{
JSFunction function = (JSFunction) createNewInstance("Function", offset, isInvoking); //$NON-NLS-1$
return function;
}
/**
* createGuessedObject
*
* @param range
* @return IObject
*/
private IObject createGuessedObject(Range range)
{
return new JSGuessedObject(range);
}
/**
* createNewInstance
*
* @param type
* @param offset
* @param isInvoking
* @return IObject
*/
IObject createNewInstance(String type, int offset, boolean isInvoking)
{
boolean needsInstance = false;
if (type.charAt(0) == '+')
{
needsInstance = true;
type = type.substring(1);
}
IObject currentRoot = this._environment.getGlobal();
String[] propertyNames = type.split("\\."); //$NON-NLS-1$
for (int i = 0; currentRoot != ObjectBase.UNDEFINED && i < propertyNames.length; i++)
{
currentRoot = currentRoot.getPropertyValue(propertyNames[i], getFileIndex(), offset);
}
if (currentRoot instanceof JSFunction && !isInvoking)
{
if (needsInstance)
{
return createNewInstance(offset, (IFunction)currentRoot);
}
else
{
// poor mans clone
JSFunction fn = (JSFunction) currentRoot;
JSFunction nw = new JSFunction(new Range(offset, offset + 1));
nw.setBodyScope(fn.getBodyScope());
nw.setDocumentation(fn.getDocumentation());
nw.setGuessedMemberObject(fn.getGuessedMemberObject());
String[] names = fn.getLocalPropertyNames();
for (int i = 0; i < names.length; i++)
{
String name = names[i];
nw.putLocalProperty(name, fn.getLocalProperty(name));
}
nw.setParameterNames(fn.getParameterNames());
return nw;
}
//return createNewInstance(offset, (IFunction)currentRoot);
}
else if (currentRoot instanceof IFunction)
{
return createNewInstance(offset, (IFunction) currentRoot);
}
else
{
// there is no function matching the specified type, so just return an empty object instance
IObject ob = createGuessedObject(new Range(offset, offset + 1));
return ob;
}
}
/**
* createNewInstance note: this one creates instances from functions (x = new foo())
*
* @param offset
* @param function
* @return IObject
*/
IObject createNewInstance(int offset, IFunction function)
{
IObject fnObj = function.construct(this._environment, FunctionBase.EmptyArgs, getFileIndex(), new Range(offset, offset + 1));
return fnObj;
}
/**
* getPropertyValue
*
* @param reference
* @param offset
* @return IObject
*/
IObject getPropertyValue(Reference reference, int offset)
{
IObject result;
if (reference.getObjectBase() instanceof IScope)
{
result = getVariableValue((IScope) reference.getObjectBase(), reference.getPropertyName(), offset);
}
else
{
result = getPropertyValue(reference.getObjectBase(), reference.getPropertyName(), offset);
}
return result;
}
/**
* getPropertyValue
*
* @param parent
* @param name
* @param offset
* @return IObject
*/
IObject getPropertyValue(IObject parent, String name, int offset)
{
IObject propValue;
if (parent instanceof IScope)
{
throw new IllegalArgumentException(Messages.LexemeBasedEnvironmentLoader_ContCallGetPropertyValue);
}
else
{
propValue = parent.getPropertyValue(name, getFileIndex(), offset);
}
return propValue;
}
/**
* getScope
*
* @param offset
* @param defaultScope
* @return IScope
*/
public IScope getScope(int offset, IScope defaultScope)
{
int size = this._scopeList.size();
if (size == 0)
{
return defaultScope; // need to guard on (& figure out why) no scope before first edit
}
try
{
int rangeIndex = 0;
for (int i = 0; i < size; i++)
{
ScopeRange currRange = this._scopeList.get(i);
if (offset > currRange.startOffset)
{
rangeIndex = i;
}
else
{
break;
}
}
for (int j = rangeIndex; j >= 0; j--)
{
ScopeRange range = this._scopeList.get(j);
// invalid functoin end offset
if (range.endOffset != -1 && range.endOffset > offset)
{
return range.scope;
}
}
}
catch (Exception e)
{
IdeLog.logInfo(JSPlugin.getDefault(), Messages.LexemeBasedEnvironmentLoader_GetScopeFailed, e);
}
return defaultScope;
}
/**
* getVariableValue
*
* @param parentScope
* @param name
* @param offset
* @return IObject
*/
IObject getVariableValue(IScope parentScope, String name, int offset)
{
IObject propValue = parentScope.getVariableValue(name, getFileIndex(), offset);
if (propValue == ObjectBase.UNDEFINED)
{
// The global scope is a special case since its an instance of Window too, so
// look to see if this property is defined up the prototype chain
propValue = this._environment.getGlobal().getPropertyValue(name, getFileIndex(), offset);
}
return propValue;
}
/**
* loadEnvironment
*
* @param parseState
*/
private void loadEnvironment(IParseState parseState)
{
try
{
this._parseState = parseState;
// set global as the first scope
ScopeRange sr = new ScopeRange(this._environment.getGlobal(), 0, Integer.MAX_VALUE);
this._scopeList.add(sr);
JSLexemeListWalker walker = new JSLexemeListWalker(this._environment, this._environment.getGlobal(), this);
walker.walkList(parseState, 0);
}
catch (Exception e)
{
IdeLog.logInfo(JSPlugin.getDefault(), Messages.LexemeBasedEnvironmentLoader_LoadEnvironmentFailed, e);
}
// sort the scope list
Collections.sort(this._scopeList);
}
/**
* putPropertyValue
*
* @param ref
* @param value
*/
void putPropertyValue(JSReference ref, IObject value)
{
IObject base = ref.getObjectBase();
String name = ref.getPropertyName();
if (base instanceof IScope)
{
// make sure the prop doesn't exist (can happen with @alias)
IScope scope = (IScope)base;
IObject existing = scope.getVariableValue(name, this.getFileIndex(), value.getStartingOffset());
if(existing != null && existing.getStartingOffset() == value.getStartingOffset())
{
return; // this is a dup variable
}
putVariableValue(scope, name, value, ref.isVar());
}
else if (ref.isVar())
{
throw new IllegalStateException(Messages.LexemeBasedEnvironmentLoader_CantPutVarOnNonScope);
}
else
{
// make sure the prop doesn't exist (can happen with @alias)
IObject existing = base.getPropertyValue(name, getFileIndex(), value.getStartingOffset());
if(existing != null && existing.getStartingOffset() == value.getStartingOffset())
{
return; // this is a dup property
}
putPropertyValue(base, name, value);
}
}
/**
* putPropertyValue
*
* @param parentObject
* @param name
* @param value
*/
void putPropertyValue(IObject parentObject, String name, IObject value)
{
if (parentObject instanceof IScope)
{
throw new IllegalArgumentException(Messages.LexemeBasedEnvironmentLoader_ContCallPutPropertyValue);
}
parentObject.putPropertyValue(name, value, getFileIndex(), Property.NONE);
Reference reference = new Reference(parentObject, name);
Map<Object,Object> updatedProperties = this._parseState.getUpdatedProperties();
updatedProperties.put(reference.getProperty(), reference);
}
/**
* putVariableValue
*
* @param scope
* @param name
* @param value
* @param isVar
*/
void putVariableValue(IScope scope, String name, IObject value, boolean isVar)
{
boolean skipAssignment = false;
if (isVar)
{
if (!scope.hasLocalProperty(name))
{
Property prop = new Property(value, getFileIndex(), Property.NONE);
scope.putLocalProperty(name, prop);
}
else
{
// note: mochikit uses "var MochiKit={};"
if (value instanceof JSObject && value.getLocalPropertyCount() == 0 && value.getPrototype() == null && scope.getVariable(name).hasAssignments())
{
// this is to skip the assignment in cases where code
// insures a namespace by possible reassignment eg:
// if(mochiKit == null){mochiKit = {});
skipAssignment = true;
}
else
{
Property prop = scope.getLocalProperty(name);
prop.setValue(value, getFileIndex());
}
}
}
else if (scope.hasVariable(name))
{
Property existing = scope.getVariable(name);
if ( value instanceof JSObject &&
value.getLocalPropertyCount() == 0 &&
value.getPrototype() == null &&
existing.hasAssignments())
{
// this is to skip the assignment in cases where code
// insures a namespace by possible reassignment eg:
// if(mochiKit == null){mochiKit = {});
skipAssignment = true;
}
else
{
scope.putVariableValue(name, value, getFileIndex());
}
}
else
{
this._environment.getGlobal().putVariableValue(name, value, getFileIndex());
}
if (!skipAssignment)
{
IScope owningScope = scope.getOwningScope(name);
Reference reference = new Reference(owningScope, name);
Map<Object,Object> updatedProperties = this._parseState.getUpdatedProperties();
updatedProperties.put(reference.getProperty(), reference);
}
}
/**
* registerScope
*
* @param scope
* @param startOffset
* @param endOffset
*/
void registerScope(IScope scope, int startOffset, int endOffset)
{
this._scopeList.add(new ScopeRange(scope, startOffset, endOffset));
}
/**
* reloadEnvironment
*
* @param parseState
*/
public void reloadEnvironment(IParseState parseState)
{
this._parseState = parseState;
synchronized (this._environment)
{
LexemeList lexemeList = this._parseState.getLexemeList();
synchronized (lexemeList)
{
try
{
unloadEnvironment();
if (this._parseState.getFileIndex() > -1)
{
loadEnvironment(parseState);
}
}
catch (Exception e)
{
IdeLog.logError(UnifiedEditorsPlugin.getDefault(), Messages.LexemeBasedEnvironmentLoader_ErrorReloading, e);
}
}
}
}
/**
* Replaces an existing definition of a variable in a scope with a function. If the previously assigned value had
* been assigned sub-properties, those properties are transfered to the replacement function.
*
* @param offset
* @param parentScope
* @param functionName
* @param func
*/
public void replaceFunctionDeclaration(int offset, IScope parentScope, String functionName, JSFunction func)
{
// if there is an old definition for this function, transfer any properties that were
// previously added to the object
if (parentScope.hasLocalProperty(functionName))
{
// IObject oldObject = parentScope.getPropertyValue(functionName, getFileIndex(), offset);
Property oldProperty = parentScope.getLocalProperty(functionName);
Map<Object,Object> updatedProperties = this._parseState.getUpdatedProperties();
// remove all assignments that were JSObjects since they were intelliguessed.
OrderedObjectCollection c = oldProperty.getAssignments();
for (int j = 0; j < c.size(); j++)
{
OrderedObject obj = c.get(j);
if (obj.object instanceof JSGuessedObject)
{
IObject oldObject = obj.object;
String[] localProps = oldObject.getLocalPropertyNames();
for (int i = 0; i < localProps.length; i++)
{
Property oldProp = oldObject.getLocalProperty(localProps[i]);
if (!func.hasLocalProperty(localProps[i]))
{
func.putLocalProperty(localProps[i], oldProp);
updatedProperties.put(oldProp, new Reference(func, localProps[i]));
}
else if (updatedProperties.containsKey(oldProp))
{
updatedProperties.remove(oldProp);
}
}
c.remove(obj.fileIndex, obj.object.getStartingOffset());
j--;
}
}
// Minor optimization: now that this property no longer has assignments from this file, we can remove it
// from it from the list of properties that were updated by this file.
updatedProperties.remove(oldProperty);
}
// set the replacement function into the parent scope
this.putVariableValue(parentScope, functionName, func, true);
}
/**
* unloadEnvironment
*/
public void unloadEnvironment()
{
// clear the current environment
synchronized (this._environment)
{
if (this._parseState != null)
{
this._parseState.getParent().unloadFromEnvironment();
// TODO: Remove once scope list is moved to parse state
this._scopeList.clear();
}
}
}
}