blob: 60ffd576a0f1d98b3e00eeb6d8733681ff75c0b3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.javascript2.knockout.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.types.api.DeclarationScope;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.Occurrence;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.javascript2.model.spi.FunctionArgument;
import org.netbeans.modules.javascript2.model.spi.FunctionInterceptor;
import org.netbeans.modules.javascript2.model.spi.ModelElementFactory;
import org.netbeans.modules.parsing.api.Snapshot;
/**
*
* @author Petr Hejl
*/
@FunctionInterceptor.Registration(priority = 200)
public class KnockoutExportInterceptor implements FunctionInterceptor {
private static final Logger LOGGER = Logger.getLogger(KnockoutExportInterceptor.class.getName());
private static final String GLOBAL_KO_OBJECT = "ko"; // NOI18N
private static final Pattern NAME_PATTERN = Pattern.compile("ko\\.(exportSymbol|exportProperty)"); // NOI18N
@Override
public Pattern getNamePattern() {
return NAME_PATTERN;
}
@Override
public Collection<TypeUsage> intercept(Snapshot snapshot, String functionName, JsObject globalObject, DeclarationScope scope,
ModelElementFactory factory, Collection<FunctionArgument> args) {
if (args.size() == 3) {
Iterator<FunctionArgument> iterator = args.iterator();
FunctionArgument objectArgument = iterator.next();
FunctionArgument nameArgument = iterator.next();
FunctionArgument valueArgument = iterator.next();
int offset = nameArgument.getOffset();
JsObject object = null;
if (objectArgument.getKind() == FunctionArgument.Kind.REFERENCE) {
List<String> identifiers = (List<String>) objectArgument.getValue();
JsObject ref = getReference(scope, identifiers, false);
if (ref != null) {
JsObject found = findJsObjectByAssignment(globalObject, ref, offset);
if (found != null) {
ref = found;
}
}
object = ref;
}
JsObject value = null;
if (valueArgument.getKind() == FunctionArgument.Kind.REFERENCE) {
List<String> identifiers = (List<String>) valueArgument.getValue();
JsObject ref = getReference(scope, identifiers, true);
if (ref != null) {
JsObject found = findJsObjectByAssignment(globalObject, ref, offset);
if (found != null) {
ref = found;
}
}
value = ref;
}
String name = (String) nameArgument.getValue();
OffsetRange offsetRange = new OffsetRange(offset, offset + name.length());
if (object != null && value != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Exporting property {0} to {1} as {2}",
new Object[] {name, object.getFullyQualifiedName(), value.getFullyQualifiedName()});
}
EnumSet<Modifier> modifiers = EnumSet.copyOf(value.getModifiers());
modifiers.remove(Modifier.STATIC);
object.addProperty(name,
factory.newReference(object, name, offsetRange, value, true, modifiers));
}
} else if (args.size() == 2) {
JsObject ko = globalObject.getProperty(GLOBAL_KO_OBJECT); // NOI18N
if (ko == null) {
ko = factory.newObject(globalObject, GLOBAL_KO_OBJECT, OffsetRange.NONE, true);
globalObject.addProperty(GLOBAL_KO_OBJECT, ko);
}
Iterator<FunctionArgument> iterator = args.iterator();
FunctionArgument nameArgument = iterator.next();
FunctionArgument valueArgument = iterator.next();
int offset = nameArgument.getOffset();
if (nameArgument.getKind() == FunctionArgument.Kind.STRING) { // NOI18N
JsObject parent = ko;
String[] names = ((String) nameArgument.getValue()).split("\\."); // NOI18N
for (int i = 0; i < names.length - 1; i++) {
String name = names[i];
if (i == 0 && GLOBAL_KO_OBJECT.equals(name)) {
continue;
}
JsObject child = parent.getProperty(name);
OffsetRange offsetRange = new OffsetRange(offset, offset + name.length());
if (child == null) {
child = factory.newObject(parent, name, offsetRange, true);
parent.addProperty(name, child);
} else if (!child.isDeclared()) {
JsObject newJsObject = factory.newObject(parent, name, offsetRange, true);
parent.addProperty(name, newJsObject);
for (Occurrence occurrence : child.getOccurrences()) {
newJsObject.addOccurrence(occurrence.getOffsetRange());
}
newJsObject.addOccurrence(child.getDeclarationName().getOffsetRange());
child = newJsObject;
}
parent = child;
}
String name = names[names.length - 1];
if (valueArgument.getKind() == FunctionArgument.Kind.REFERENCE) {
List<String> identifiers = (List<String>) valueArgument.getValue();
JsObject value = getReference(scope, identifiers, false);
if (value != null) {
JsObject found = findJsObjectByAssignment(globalObject, value, offset);
if (found != null && found.isDeclared()) {
value = found;
} else {
int levelUp = identifiers.size() - 1;
JsObject foundParent = null;
JsObject valueParent = value.getParent();
while (valueParent != null) {
foundParent = findJsObjectByAssignment(globalObject, valueParent, offset);
if (foundParent != null) {
break;
}
levelUp--;
valueParent = valueParent.getParent();
}
if (foundParent != null) {
boolean skip = true;
for (int i = levelUp; i < identifiers.size(); i++) {
JsObject property = foundParent.getProperty(identifiers.get(i));
if (property == null) {
skip = false;
break;
}
foundParent = property;
}
if (skip) {
return Collections.emptyList();
}
}
}
OffsetRange offsetRange = new OffsetRange(offset, offset + name.length());
JsObject property = parent.getProperty(name);
if (property != null) {
// XXX is looks like value is artificial
if ((property instanceof JsFunction) && !(value instanceof JsFunction)) {
value = property;
} else {
Map<String, JsObject> current = new HashMap<String, JsObject>(property.getProperties());
current.keySet().removeAll(value.getProperties().keySet());
for (Map.Entry<String, JsObject> entry : current.entrySet()) {
// XXX reference ?
value.addProperty(entry.getKey(), entry.getValue());
}
}
}
parent.addProperty(name, factory.newReference(
parent, name, offsetRange, value, true, null));
}
}
}
}
return Collections.emptyList();
}
private static JsObject getReference(DeclarationScope scope,
List<String> identifier, boolean searchPrototype) {
if ("this".equals(identifier.get(0))) { // NOI18N
// XXX this is not exactly right as it is evaluated at runtime
return (JsObject) scope;
}
DeclarationScope currentScope = scope;
while (currentScope != null) {
JsObject ret = getReference((JsObject) currentScope, identifier);
if (ret != null) {
return ret;
}
currentScope = currentScope.getParentScope();
}
if (searchPrototype && identifier.size() > 1) {
List<String> prototype = new ArrayList<String>(identifier);
prototype.add(prototype.size() - 1, "prototype"); // NOI18N
return getReference(scope, prototype, false);
}
return null;
}
private static JsObject getReference(JsObject object, List<String> identifier) {
// XXX performance
if (object == null) {
return null;
}
if (identifier.isEmpty()) {
return object;
}
return getReference(object.getProperty(identifier.get(0)),
identifier.subList(1, identifier.size()));
}
private static JsObject findJsObjectByAssignment(JsObject globalObject,
JsObject value, int offset) {
return findJsObjectByAssignment(globalObject, value, offset, true);
}
private static JsObject findJsObjectByAssignment(JsObject globalObject, JsObject value,
int offset, boolean searchPrototype) {
if (value == null) {
return null;
}
JsObject ret = null;
Collection<? extends TypeUsage> assigments = value.getAssignmentForOffset(offset);
if (assigments.size() == 1) {
ret = findJsObjectByName(globalObject,
assigments.iterator().next().getType());
}
// XXX multiple assignments
if (ret == null && searchPrototype) {
String fqn = value.getFullyQualifiedName();
int index = fqn.lastIndexOf('.');
if (index > 0) {
fqn = fqn.substring(0, index) + ".prototype" + fqn.substring(index);
JsObject obj = findJsObjectByName(globalObject, fqn);
if (obj != null) {
ret = findJsObjectByAssignment(globalObject, obj, offset, false);
}
}
}
return ret;
}
private static JsObject findJsObjectByName(JsObject global, String fqName) {
JsObject result = global;
JsObject property = result;
for (StringTokenizer stringTokenizer = new StringTokenizer(fqName, "."); stringTokenizer.hasMoreTokens() && result != null;) {
String token = stringTokenizer.nextToken();
property = result.getProperty(token);
if (property == null) {
result = (result instanceof JsFunction)
? ((JsFunction)result).getParameter(token)
: null;
if (result == null) {
break;
}
} else {
result = property;
}
}
return result;
}
}