| /* |
| * 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); |
| } |
| } |