blob: baeb7dfe8ac9318cdee9c807fd487025f52d3be9 [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.model.api;
import com.oracle.js.parser.ir.Node;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.Documentation;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.doc.api.JsDocumentationSupport;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.doc.spi.JsDocumentationHolder;
import org.netbeans.modules.javascript2.model.AnonymousObject;
import org.netbeans.modules.javascript2.model.JsFunctionImpl;
import org.netbeans.modules.javascript2.model.JsFunctionReference;
import org.netbeans.modules.javascript2.model.JsObjectImpl;
import org.netbeans.modules.javascript2.model.JsObjectReference;
import org.netbeans.modules.javascript2.model.JsWithObjectImpl;
import org.netbeans.modules.javascript2.model.ModelAccessor;
import org.netbeans.modules.javascript2.model.ModelElementFactoryAccessor;
import org.netbeans.modules.javascript2.model.ModelExtender;
import org.netbeans.modules.javascript2.model.OccurrenceBuilder;
import org.netbeans.modules.javascript2.model.ParameterObject;
import org.netbeans.modules.javascript2.model.spi.ModelContainer;
import org.netbeans.modules.javascript2.model.spi.ModelElementFactory;
import org.netbeans.modules.javascript2.model.ModelResolver;
import org.netbeans.modules.javascript2.model.spi.ObjectInterceptor;
import org.netbeans.modules.javascript2.types.api.DeclarationScope;
import org.netbeans.modules.javascript2.types.api.Identifier;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.javascript2.types.spi.ParserResult;
import org.openide.util.NbBundle;
/**
*
* @author Petr Pisl
*/
@NbBundle.Messages("LBL_DefaultDocContentForURL=To view documentation for this function, press the browser button in the toolbar above this text.")
public final class Model {
private static final AtomicBoolean assertFired = new AtomicBoolean(false);
private static final Logger LOGGER = Logger.getLogger(Model.class.getName());
private static final Comparator<Map.Entry<String, ? extends JsObject>> PROPERTIES_COMPARATOR = new Comparator<Map.Entry<String, ? extends JsObject>>() {
@Override
public int compare(Entry<String, ? extends JsObject> o1, Entry<String, ? extends JsObject> o2) {
return o1.getKey().compareTo(o2.getKey());
}
};
private static final Comparator<TypeUsage> RETURN_TYPES_COMPARATOR = new Comparator<TypeUsage>() {
@Override
public int compare(TypeUsage o1, TypeUsage o2) {
return o1.getType().compareTo(o2.getType());
}
};
private static final Pattern OBJECT_PATTERN = Pattern.compile(
"(FUNCTION|OBJECT) (\\S+) \\[ANONYMOUS: (true|false), DECLARED: (true|false)( - (\\S+))?" // NOI18N
+ "(, MODIFIERS: ((PUBLIC|STATIC|PROTECTED|PRIVATE|DEPRECATED|ABSTRACT)" // NOI18N
+ "(, (PUBLIC|STATIC|PROTECTED|PRIVATE|DEPRECATED|ABSTRACT))*))?" // NOI18N
+ ", (FUNCTION|METHOD|CONSTRUCTOR|OBJECT|PROPERTY|VARIABLE|FIELD|FILE|PARAMETER|ANONYMOUS_OBJECT|PROPERTY_GETTER|PROPERTY_SETTER|OBJECT_LITERAL|CATCH_BLOCK)\\]"); // NOI18N
private static final Pattern RETURN_TYPE_PATTERN = Pattern.compile("(\\S+), RESOLVED: (true|false)");
private static enum ParsingState {
DOCUMETATION_URL, RETURN, PARAMETER, PROPERTY
}
static {
ModelAccessor.setDefault(new ModelAccessor() {
@Override
public Model createModel(ParserResult result) {
return new Model(result);
}
});
}
private final ParserResult parserResult;
private final OccurrenceBuilder occurrenceBuilder;
private ModelResolver visitor;
private Map<String, Map<Integer, List<TypeUsage>>> returnTypesFromFrameworks;
/**
* contains with expression?
*/
private boolean resolveWithObjects;
private Model(ParserResult parserResult) {
this.parserResult = parserResult;
this.occurrenceBuilder = new OccurrenceBuilder(parserResult);
this.resolveWithObjects = false;
this.returnTypesFromFrameworks = new HashMap<String, Map<Integer, List<TypeUsage>>>();
}
/**
*
* @param parserResult
* @return the model, if the parser result is created on top of a js source
*/
public static Model getModel(org.netbeans.modules.csl.spi.ParserResult parserResult, boolean reload) {
if (parserResult instanceof ParserResult) {
ParserResult r = (ParserResult) parserResult;
ModelContainer c = r.getLookup().lookup(ModelContainer.class);
if (c != null) {
return c.getModel(r, reload);
} else {
return new Model(r);
}
}
return null;
}
private synchronized ModelResolver getModelVisitor() {
boolean resolveWindowProperties = false;
if (visitor == null) {
long start = System.currentTimeMillis();
visitor = ModelResolver.create(parserResult, occurrenceBuilder);
if (visitor == null) {
throw new IllegalStateException("No ModelResolver for result: " + parserResult); //NOI18N
}
visitor.init();
long startResolve = System.currentTimeMillis();
// create all occurrences
occurrenceBuilder.processOccurrences(visitor.getGlobalObject());
resolveLocalTypes(visitor.getGlobalObject(), JsDocumentationSupport.getDocumentationHolder(parserResult));
ModelElementFactory elementFactory = ModelElementFactoryAccessor.getDefault().createModelElementFactory();
long startCallingME = System.currentTimeMillis();
visitor.processCalls(elementFactory, returnTypesFromFrameworks);
for (ObjectInterceptor objectInterceptor : ModelExtender.getDefault().getObjectInterceptors()) {
objectInterceptor.interceptGlobal(visitor.getGlobalObject(), elementFactory);
}
resolveWindowProperties = !resolveWithObjects;
long end = System.currentTimeMillis();
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(MessageFormat.format("Building model took {0}ms. Resolving types took {1}ms. Extending model took {2}", new Object[]{(end - start), (startCallingME - startResolve), (end - startCallingME)}));
}
} else if (resolveWithObjects) {
//long start = System.currentTimeMillis();
resolveWithObjects = false;
resolveWindowProperties = true;
Index jsIndex = Index.get(parserResult.getSnapshot().getSource().getFileObject());
processWithObjectIn(visitor.getGlobalObject(), jsIndex);
//long end = System.currentTimeMillis();
//System.out.println("resolving with took: " + (end - start));
}
if (resolveWindowProperties) {
processWindowsProperties(visitor.getGlobalObject());
}
return visitor;
}
private void processWithObjectIn(JsObject where, Index jsIndex) {
if (where.getProperties().isEmpty()) {
return;
}
List<JsObject> properties = new ArrayList<>(where.getProperties().values());
for (JsObject property : properties) {
if (property instanceof JsWith) {
processWithObject((JsWith)property, jsIndex, null);
} else {
processWithObjectIn(property, jsIndex);
}
}
}
private void processWithObject(JsWith with, Index jsIndex, List<String> outerExpression) {
Collection<TypeUsage> withTypes = with.getTypes();
withTypes.clear();
Collection<TypeUsage> resolveTypeFromExpression = new ArrayList<TypeUsage>();
int offset = ((JsWithObjectImpl)with).getExpressionRange().getEnd();
List<String> ech = ModelUtils.resolveExpressionChain(parserResult.getSnapshot(), offset, false);
List<String> originalExp = new ArrayList<String>(ech);
JsObject fromType = null;
if (outerExpression == null) {
outerExpression = ech;
resolveTypeFromExpression.addAll(ModelUtils.resolveTypeFromExpression(this, jsIndex, ech, offset, true));
resolveTypeFromExpression = ModelUtils.resolveTypes(resolveTypeFromExpression, this, jsIndex, true);
withTypes.addAll(resolveTypeFromExpression);
} else {
ech.addAll(outerExpression);
boolean resolved = false;
resolveTypeFromExpression.addAll(ModelUtils.resolveTypeFromExpression(this, jsIndex, ech, offset, true));
resolveTypeFromExpression = ModelUtils.resolveTypes(resolveTypeFromExpression, this, jsIndex, true);
for(TypeUsage type : resolveTypeFromExpression) {
fromType = ModelUtils.findJsObjectByName(visitor.getGlobalObject(), type.getType());
if (fromType != null) {
resolved = true;
outerExpression = ech;
withTypes.add(type);
break;
}
}
if (!resolved) {
resolveTypeFromExpression.clear();
resolveTypeFromExpression.addAll(ModelUtils.resolveTypeFromExpression(this, jsIndex, originalExp, offset, true));
resolveTypeFromExpression = ModelUtils.resolveTypes(resolveTypeFromExpression, this, jsIndex, true);
for (TypeUsage type : resolveTypeFromExpression) {
fromType = ModelUtils.findJsObjectByName(visitor.getGlobalObject(), type.getType());
if (fromType != null) {
resolved = true;
outerExpression = originalExp;
withTypes.add(type);
break;
}
}
}
}
for (JsWith innerWith : with.getInnerWiths()) {
processWithObject(innerWith, jsIndex, outerExpression);
}
for (TypeUsage type : resolveTypeFromExpression) {
fromType = ModelUtils.findJsObjectByName(visitor.getGlobalObject(), type.getType());
if (fromType != null) {
processWithExpressionOccurrences(fromType, ((JsWithObjectImpl)with).getExpressionRange(), originalExp);
Collection<TypeUsage> assignments = ModelUtils.resolveTypes(fromType.getAssignments(), this, jsIndex, true);
for (TypeUsage assignment : assignments) {
Collection<IndexedElement> properties = jsIndex.getProperties(assignment.getType());
for (IndexedElement indexedElement : properties) {
JsObject jsWithProperty = with.getProperty(indexedElement.getName());
if (jsWithProperty != null) {
moveProperty(fromType, jsWithProperty);
if (jsWithProperty.isDeclared()) {
((JsObjectImpl)jsWithProperty).setDeclared(false);
}
}
}
}
for (JsObject fromTypeProperty : fromType.getProperties().values()) {
JsObject jsWithProperty = with.getProperty(fromTypeProperty.getName());
if (jsWithProperty != null) {
moveProperty(fromType, jsWithProperty);
if (jsWithProperty.isDeclared()) {
((JsObjectImpl)jsWithProperty).setDeclared(false);
}
}
}
// the object needs to be resolved again to handle right occurrences
resolveLocalTypes(fromType, JsDocumentationSupport.getDocumentationHolder(parserResult));
} else {
Collection<IndexedElement> properties = jsIndex.getProperties(type.getType());
if (!properties.isEmpty()) {
StringBuilder fqn = new StringBuilder();
for (int i =outerExpression.size() - 1; i > -1; i--) {
fqn.append(outerExpression.get(--i));
fqn.append('.');
}
if (fqn.length() > 0) {
DeclarationScope ds = ModelUtils.getDeclarationScope(with);
JsObject fromExpression = ModelUtils.findJsObjectByName((JsObject)ds, fqn.toString());
if (fromExpression == null) {
int position = ((JsWithObjectImpl)with).getExpressionRange().getStart();
JsObject parent = visitor.getGlobalObject();
for (StringTokenizer stringTokenizer = new StringTokenizer( type.getType(), "."); stringTokenizer.hasMoreTokens();) {
String name = stringTokenizer.nextToken();
JsObject newObject = parent.getProperty(name);
if (newObject == null) {
newObject = new JsObjectImpl(parent, new Identifier(name, position), new OffsetRange(position, position + name.length()), false, null, null);
parent.addProperty(name, newObject);
}
position = position + name.length() + 1; // 1 is the dot
parent = newObject;
}
fromExpression = parent;
}
if (fromExpression != null) {
for (IndexedElement indexedElement : properties) {
JsObject jsWithProperty = with.getProperty(indexedElement.getName());
if (jsWithProperty != null) {
moveProperty(fromExpression, jsWithProperty);
}
}
processWithExpressionOccurrences(fromExpression, ((JsWithObjectImpl)with).getExpressionRange(), originalExp);
}
}
}
}
}
boolean hasOuter = with.getOuterWith() != null;
List<JsObject> withProperties = new ArrayList<>(with.getProperties().values());
DeclarationScope withDS = ModelUtils.getDeclarationScope(with);
for (JsObject jsWithProperty : withProperties) {
if (!(jsWithProperty instanceof JsWith)) {
String name = jsWithProperty.getName();
boolean moved = false;
if (hasOuter) {
// move the property for one level up
moveProperty(with.getOuterWith(), jsWithProperty);
moved = true;
} else {
JsObject variable = ModelUtils.getScopeVariable(withDS, name);
if (variable != null && variable.getParent() != null) {
moveProperty(variable.getParent(), jsWithProperty);
moved = true;
}
}
if (!moved) {
// move the property to the global space
moveProperty(visitor.getGlobalObject(), jsWithProperty);
}
}
}
}
private void processWithExpressionOccurrences(JsObject jsObject, OffsetRange expRange, List<String> expression) {
JsObject parent = jsObject.getParent();
boolean isThis = false;
if ((expression.size() > 1) && expression.get(expression.size() - 2).equals(ModelUtils.THIS)) { //NOI18N
parent = ModelUtils.findJsObject(this, expRange.getStart());
if (parent instanceof JsWith) {
parent = parent.getParent();
}
parent = visitor.resolveThis(parent);
isThis = true;
}
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(parserResult.getSnapshot(), expRange.getEnd());
if (ts == null) {
return;
}
if (isThis) {
ts.move(expRange.getStart());
} else {
ts.move(expRange.getEnd());
}
if (isThis && !ts.moveNext()) {
return;
}
if(!isThis && !ts.movePrevious()) {
return;
}
Token<? extends JsTokenId> token = ts.token();
if (isThis) {
for (int i = expression.size() - 4; i > - 1; i--) {
String name = expression.get(i--);
while ((token.id() != JsTokenId.IDENTIFIER || !(token.id() == JsTokenId.IDENTIFIER && token.text().toString().equals(name))) && ts.offset() < expRange.getEnd() && ts.moveNext()) {
token = ts.token();
}
if (parent != null && token.id() == JsTokenId.IDENTIFIER && token.text().toString().equals(name)) {
JsObject property = parent.getProperty(name);
if (property != null) {
property.addOccurrence(new OffsetRange(ts.offset(), ts.offset() + name.length()));
}
parent = property;
}
}
} else {
for (int i = 0; i < expression.size() - 1; i++) {
String name = expression.get(i++);
while ((token.id() != JsTokenId.IDENTIFIER || !(token.id() == JsTokenId.IDENTIFIER && token.text().toString().equals(name))) && ts.offset() > expRange.getStart() && ts.movePrevious()) {
token = ts.token();
}
if (parent != null && token.id() == JsTokenId.IDENTIFIER && token.text().toString().equals(name)) {
JsObject property = parent.getProperty(name);
if (property != null) {
property.addOccurrence(new OffsetRange(ts.offset(), ts.offset() + name.length()));
}
parent = parent.getParent();
}
}
}
}
private void moveProperty (JsObject newParent, JsObject property) {
JsObject newProperty = newParent.getProperty(property.getName());
if (property.getParent() != null) {
property.getParent().getProperties().remove(property.getName());
}
if (newProperty == null) {
((JsObjectImpl)property).setParent(newParent);
newParent.addProperty(property.getName(), property);
} else {
if (property.isDeclared() && !newProperty.isDeclared()) {
JsObject tmpProperty = newProperty;
newParent.addProperty(property.getName(), property);
((JsObjectImpl)property).setParent(newParent);
newProperty = property;
property = tmpProperty;
}
JsObjectImpl.moveOccurrenceOfProperties((JsObjectImpl) newProperty, property);
for (Occurrence occurrence : property.getOccurrences()) {
newProperty.addOccurrence(occurrence.getOffsetRange());
}
List<JsObject>propertiesToMove = new ArrayList<>(property.getProperties().values());
for (JsObject propOfProperty: propertiesToMove) {
moveProperty(newProperty, propOfProperty);
}
// the property needs to be resolved again to handle right occurrences
resolveLocalTypes(newProperty, JsDocumentationSupport.getDocumentationHolder(parserResult));
}
}
private void processWindowsProperties(JsObject globalObject) {
JsObject window = globalObject.getProperty("window");
if (window != null) {
// JsObjectImpl.moveOccurrenceOfProperties((JsObjectImpl)window, globalObject);
for (JsObject winProp: window.getProperties().values()) {
JsObject globalVar = globalObject.getProperty(winProp.getName());
if (globalVar != null) {
JsObjectImpl.moveOccurrence((JsObjectImpl)globalVar, winProp);
JsObjectImpl.moveOccurrenceOfProperties((JsObjectImpl) winProp, globalVar);
}
}
}
}
/**
* If you need to be sure that the model is fully build, call resolve method before.
* @return the gobal object representing the global space of the file
*/
public JsObject getGlobalObject() {
return getModelVisitor().getGlobalObject();
}
/**
* This returns types of a function call that starts on the offsetCall.
* These return types can be influenced by the call arguments and are obtained
* from the function interceptors defined in the frameworks.
* @param name the name of the function
* @param offsetCall offset where the call starts (the first letter of the method name)
* @return
*/
public Collection<TypeUsage> getReturnTypesFromFrameworks(String name, int offsetCall) {
Map<Integer, List<TypeUsage>> returnTypes = returnTypesFromFrameworks.get(name);
return (returnTypes != null) ? returnTypes.get(offsetCall) : null;
}
public synchronized void resolve() {
if (visitor == null) {
getModelVisitor();
}
if (resolveWithObjects) {
getModelVisitor();
}
}
public Collection<? extends JsObject> getVariables(int offset) {
List<JsObject> result = new ArrayList<JsObject>();
DeclarationScope scope = ModelUtils.getDeclarationScope(this, offset);
while (scope != null) {
for (JsObject object : ((JsObject)scope).getProperties().values()) {
if (!object.isAnonymous()) {
result.add(object);
}
}
if (scope instanceof JsFunction) {
for (JsObject object : ((JsFunction)scope).getParameters()) {
result.add(object);
}
}
scope = scope.getParentScope();
}
return result;
}
/**
*
* @param name can not be null. Single name of the variable
* @param offset the variable is defined in the context (or higher) of this offset
* @return
*/
public JsObject findVariable(final String name, final int offset) {
if (name == null || name.isEmpty()) {
return null;
}
Collection<? extends JsObject> variables = getVariables(offset);
for (JsObject jsObject: variables) {
if (name.equals(jsObject.getName())) {
return jsObject;
}
}
return null;
}
/**
* Get the declaration object of the given offset position.
* @param offset the offset position
* @return an object that declares code at the offset
*/
public JsObject getDeclarationObject(int offset) {
DeclarationScope scope = ModelUtils.getDeclarationScope(this, offset);
return (JsObject) scope;
}
private void resolveLocalTypes(JsObject object, JsDocumentationHolder docHolder) {
Set<String> alreadyResolved = new HashSet<String>();
resolveLocalTypes(object, docHolder, alreadyResolved);
}
private void resolveLocalTypes(JsObject object, JsDocumentationHolder docHolder, Set<String> alreadyResolvedObjects) {
if (object instanceof JsFunctionReference && !object.isAnonymous()) {
return;
}
if (object instanceof JsObjectReference && object.getJSKind() == JsElement.Kind.CLASS) {
return;
}
String fqn = object.getFullyQualifiedName();
boolean isTopObject = object.getJSKind() == JsElement.Kind.FILE;
if (alreadyResolvedObjects.contains(fqn)) {
if (!assertFired.get()) {
assertFired.set(true);
assert false: "Probably cycle in the javascript model of file: " + object.getFileObject().getPath(); //NOI18N
}
System.out.println("alreadyResolved: " + fqn);
return;
}
if (!isTopObject) {
alreadyResolvedObjects.add(fqn);
}
if(object instanceof JsFunctionImpl) {
((JsFunctionImpl)object).resolveTypes(docHolder);
} else {
((JsObjectImpl)object).resolveTypes(docHolder);
if (object instanceof JsWith) {
resolveWithObjects = true;
}
}
ArrayList<JsObject> copy = new ArrayList<>(object.getProperties().values());
ArrayList<String> namesBefore = new ArrayList<>(object.getProperties().keySet());
Collections.reverse(copy); // resolve the properties in revers order (how was added)
for(JsObject property: copy) {
resolveLocalTypes(property, docHolder, alreadyResolvedObjects);
}
ArrayList<String> namesAfter = new ArrayList<>(object.getProperties().keySet());
// it's possible that some properties was moved to the object, then resolve them.
for (String propertyName : namesAfter) {
if (!namesBefore.contains(propertyName)) {
resolveLocalTypes(object.getProperty(propertyName), docHolder, alreadyResolvedObjects);
}
}
if (!isTopObject) {
alreadyResolvedObjects.remove(fqn);
}
}
/**
* Gets the node name if it has any (case of AccessNode, BinaryNode, VarNode, PropertyNode).
*
* @param node examined node for getting its name
* @return name of the node if it supports it
*/
public List<Identifier> getNodeName(Node node) {
final ModelResolver resolver = ModelResolver.create(parserResult, occurrenceBuilder);
if (resolver == null) {
throw new IllegalStateException("No ModelResolver for result: " + parserResult); //NOI18N
}
return resolver.getASTNodeName(node);
}
public void writeModel(Printer printer) {
writeObject(printer, getGlobalObject(), null);
}
public void writeModel(Printer printer, boolean resolve) {
writeObject(printer, getGlobalObject(), resolve ? parserResult : null);
}
public void writeObject(Printer printer, JsObject object, boolean resolve) {
writeObject(printer, object, resolve ? parserResult : null);
}
public static void writeObject(Printer printer, JsObject object, @NullAllowed ParserResult parseResult) {
StringBuilder sb = new StringBuilder();
writeObject(printer, object, parseResult, sb, "", new HashSet<JsObject>()); // NOI18N
String rest = sb.toString();
if (!rest.isEmpty()) {
printer.println(rest);
}
}
public static Collection<JsObject> readModel(BufferedReader reader, JsObject parent,
@NullAllowed String sourceLabel, URL defaultDocUrl) throws IOException {
String line = null;
StringBuilder pushback = new StringBuilder();
List<JsObject> ret = new ArrayList<JsObject>();
while (pushback.length() > 0 || (line = reader.readLine()) != null) {
if (pushback.length() > 0) {
line = pushback.toString();
pushback.setLength(0);
}
if (line.trim().isEmpty()) {
continue;
}
ret.add(readObject(parent, line, 0, reader, pushback, false, sourceLabel,
defaultDocUrl == null ? null : Documentation.create(Bundle.LBL_DefaultDocContentForURL(), defaultDocUrl)));
}
return ret;
}
private static JsObject readObject(JsObject parent, String firstLine, int indent,
BufferedReader reader, StringBuilder pushback, boolean parameter,
String sourceLabel, Documentation defaultDoc) throws IOException {
JsObject object = readObject(parent, firstLine, parameter, sourceLabel);
ParsingState state = null;
String line = null;
StringBuilder innerPushback = new StringBuilder();
while (innerPushback.length() > 0 || (line = reader.readLine()) != null) {
if (innerPushback.length() > 0) {
line = innerPushback.toString();
innerPushback.setLength(0);
}
if (line.length() < indent || !line.substring(0, indent).trim().isEmpty()) {
pushback.append(line);
break;
}
if ("# DOCUMENTATION URL".equals(line.trim())) { // NOI18N
state = ParsingState.DOCUMETATION_URL;
continue;
} else if ("# RETURN TYPES".equals(line.trim())) { // NOI18N
state = ParsingState.RETURN;
continue;
} else if ("# PARAMETERS".equals(line.trim())) { // NOI18N
state = ParsingState.PARAMETER;
continue;
} else if ("# PROPERTIES".equals(line.trim())) { // NOI18N
state = ParsingState.PROPERTY;
continue;
} else if ("# SEPARATOR".equals(line.trim())) { // NOI18N
break;
}
if (state == null) {
pushback.append(line);
break;
}
switch (state) {
case DOCUMETATION_URL:
((JsObjectImpl) object).setDocumentation(
Documentation.create(Bundle.LBL_DefaultDocContentForURL(), new URL(line.trim())));
break;
case RETURN:
Matcher matcher = RETURN_TYPE_PATTERN.matcher(line.trim());
if (!matcher.matches()) {
throw new IOException("Unexpected line: " + line);
}
((JsFunctionImpl) object).addReturnType(
new TypeUsage(matcher.group(1), -1, Boolean.parseBoolean(matcher.group(2))));
break;
case PARAMETER:
JsObject parameterObject = readObject(object, line.trim(),
indent + 8, reader, innerPushback, true, sourceLabel, null);
((JsFunctionImpl) object).addParameter(parameterObject);
break;
case PROPERTY:
int index = line.indexOf(':');
assert index > 0 && index < line.length() : line;
String name = line.substring(0, index);
String value = line.substring(index + 1);
int newIndent = name.length();
name = name.trim();
JsObject property = readObject(object, value.trim(), newIndent,
reader, innerPushback, false, sourceLabel, defaultDoc);
object.addProperty(name, property);
break;
default:
throw new IOException("Unexpected line: " + line);
}
}
if (defaultDoc != null && object.getDocumentation() == null) {
((JsObjectImpl) object).setDocumentation(defaultDoc);
}
return object;
}
private static JsObject readObject(JsObject parent, String line, boolean parameter, String sourceLabel) throws IOException {
Matcher m = OBJECT_PATTERN.matcher(line);
if (!m.matches()) {
throw new IOException("Malformed line: " + line);
}
boolean function = "FUNCTION".equals(m.group(1)); // NOI18N
String name = m.group(2);
boolean anonymous = Boolean.valueOf(m.group(3));
boolean declared = Boolean.valueOf(m.group(4));
// Decalartion name is not used actually
//String declarationName = m.group(6);
String strModifiers = m.group(8);
// Kind is not used actually
JsElement.Kind kind = JsElement.Kind.valueOf(m.group(12));
EnumSet<Modifier> modifiers = EnumSet.noneOf(Modifier.class);
if (modifiers != null) {
String[] parts = strModifiers.split(", ");
for (String part : parts) {
modifiers.add(Modifier.valueOf(part));
}
}
JsObjectImpl ret;
if (parameter) {
ret = new ParameterObject(parent, new Identifier(name, OffsetRange.NONE), null, sourceLabel);
} else if (function) {
JsFunctionImpl functionImpl = new JsFunctionImpl(null, parent,
new Identifier(name, OffsetRange.NONE), Collections.<Identifier>emptyList(), OffsetRange.NONE, null, sourceLabel);
functionImpl.setAnonymous(anonymous);
ret = functionImpl;
} else {
if (anonymous) {
ret = new AnonymousObject(parent, name, OffsetRange.NONE, null, sourceLabel);
} else {
ret = new JsObjectImpl(parent, new Identifier(name, OffsetRange.NONE),
OffsetRange.NONE, null, sourceLabel);
}
}
//System.out.println("===" + declarationName + ":" + ret.getDeclarationName() + " " + name);
//assert declarationName == null || declarationName.equals(ret.getDeclarationName().getName());
ret.setJsKind(kind);
ret.setDeclared(declared);
ret.getModifiers().clear();
for (Modifier modifier : modifiers) {
ret.addModifier(modifier);
}
ret.getProperties().clear();
return ret;
}
private static void writeObject(Printer printer, JsObject jsObject, ParserResult parseResult,
StringBuilder sb, String ident, Set<JsObject> path) {
if (jsObject instanceof JsFunction) {
sb.append("FUNCTION "); // NOI18N
} else {
sb.append("OBJECT "); // NOI18N
}
sb.append(jsObject.getName());
sb.append(" ["); // NOI18N
sb.append("ANONYMOUS: "); // NOI18N
sb.append(jsObject.isAnonymous());
sb.append(", DECLARED: "); // NOI18N
sb.append(jsObject.isDeclared());
if (jsObject.getDeclarationName() != null) {
sb.append(" - ").append(jsObject.getDeclarationName().getName());
}
if (!jsObject.getModifiers().isEmpty()) {
sb.append(", MODIFIERS: "); // NOI18N
for (Modifier m : jsObject.getModifiers()) {
sb.append(m.toString());
sb.append(", "); // NOI18N
}
sb.setLength(sb.length() - 2);
}
sb.append(", "); // NOI18N
sb.append(jsObject.getJSKind());
sb.append("]"); // NOI18N
path.add(jsObject);
if (jsObject instanceof JsFunction) {
JsFunction function = ((JsFunction) jsObject);
if (!function.getReturnTypes().isEmpty()) {
newLine(printer, sb, ident);
sb.append("# RETURN TYPES"); // NOI18N
Collection<? extends TypeUsage> ret = function.getReturnTypes();
if (parseResult != null) {
// FIXME model
ret = ModelUtils.resolveTypes(ret, Model.getModel(parseResult, false),
Index.get(parseResult.getSnapshot().getSource().getFileObject()), true);
}
List<TypeUsage> returnTypes = new ArrayList<TypeUsage>(ret);
Collections.sort(returnTypes, RETURN_TYPES_COMPARATOR);
for (TypeUsage type : returnTypes) {
newLine(printer, sb, ident);
sb.append(type.getType());
sb.append(", RESOLVED: ");
sb.append(type.isResolved());
}
}
if (!function.getParameters().isEmpty()) {
newLine(printer, sb, ident);
sb.append("# PARAMETERS"); // NOI18N
for (JsObject param : function.getParameters()) {
newLine(printer, sb, ident);
if (path.contains(param)) {
sb.append("CYCLE ").append(param.getFullyQualifiedName()); // NOI18N
} else {
writeObject(printer, param, parseResult, sb, ident + " ", path);
}
}
}
}
int length = 0;
for (String str : jsObject.getProperties().keySet()) {
if (str.length() > length) {
length = str.length();
}
}
StringBuilder identBuilder = new StringBuilder(ident);
identBuilder.append(' '); // NOI18N
for (int i = 0; i < length; i++) {
identBuilder.append(' '); // NOI18N
}
List<Map.Entry<String, ? extends JsObject>> entries =
new ArrayList<Entry<String, ? extends JsObject>>(jsObject.getProperties().entrySet());
if (!entries.isEmpty()) {
newLine(printer, sb, ident);
sb.append("# PROPERTIES"); // NOI18N
Collections.sort(entries, PROPERTIES_COMPARATOR);
for (Map.Entry<String, ? extends JsObject> entry : entries) {
newLine(printer, sb, ident);
sb.append(entry.getKey());
for (int i = entry.getKey().length(); i < length; i++) {
sb.append(' '); // NOI18N
}
sb.append(" : "); // NOI18N
if (path.contains(entry.getValue())) {
sb.append("CYCLE ").append(entry.getValue().getFullyQualifiedName()); // NOI18N
} else {
writeObject(printer, entry.getValue(), parseResult, sb, identBuilder.toString(), path);
}
}
}
path.remove(jsObject);
}
private static void newLine(Printer printer, StringBuilder sb, String ident) {
printer.println(sb.toString());
sb.setLength(0);
sb.append(ident);
}
public static interface Printer {
void println(String str);
}
}