blob: b428a6fcc6860dd8ab697d4fa51dd0620f87e59a [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.editor.index;
import org.netbeans.modules.javascript2.model.api.IndexedElement;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.model.api.JsElement;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.Model;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.javascript2.model.api.ModelUtils;
import org.netbeans.modules.javascript2.editor.parser.JsParserResult;
import org.netbeans.modules.javascript2.editor.parser.SanitizingParser;
import org.netbeans.modules.javascript2.model.api.Index;
import static org.netbeans.modules.javascript2.model.api.IndexedElement.ANONYMOUS_POSFIX;
import static org.netbeans.modules.javascript2.model.api.IndexedElement.OBJECT_POSFIX;
import static org.netbeans.modules.javascript2.model.api.IndexedElement.PARAMETER_POSTFIX;
import org.netbeans.modules.javascript2.model.api.JsArray;
import org.netbeans.modules.javascript2.model.api.JsReference;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.indexing.Context;
import org.netbeans.modules.parsing.spi.indexing.EmbeddingIndexer;
import org.netbeans.modules.parsing.spi.indexing.EmbeddingIndexerFactory;
import org.netbeans.modules.parsing.spi.indexing.Indexable;
import org.netbeans.modules.parsing.spi.indexing.support.IndexDocument;
import org.netbeans.modules.parsing.spi.indexing.support.IndexingSupport;
import org.openide.util.ChangeSupport;
import org.openide.util.Lookup;
import org.openide.util.Parameters;
import org.openide.util.lookup.ServiceProvider;
/**
*
* @author Petr Pisl
*/
public class JsIndexer extends EmbeddingIndexer {
private static final Logger LOG = Logger.getLogger(JsIndexer.class.getName());
private static final ChangeSupport changeSupport = new ChangeSupport(JsIndexer.class);
@Override
protected void index(Indexable indexable, Result result, Context context) {
LOG.log(Level.FINE, "Indexing: {0}, fullPath: {1}", new Object[]{indexable.getRelativePath(), result.getSnapshot().getSource().getFileObject().getPath()});
if (!(result instanceof JsParserResult)) {
return;
}
if (!context.checkForEditorModifications()) {
// FIXME
//JsIndex.changeInIndex();
}
JsParserResult parserResult = (JsParserResult) result;
Model model = Model.getModel(parserResult, true);
IndexingSupport support;
try {
support = IndexingSupport.getInstance(context);
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
return;
}
// we need to remove old documents (document per object, not file)
support.removeDocuments(indexable);
JsObject globalObject = model.getGlobalObject();
for (JsObject object : globalObject.getProperties().values()) {
if (object.getParent() != null) {
IdentityHashMap<JsObject,Integer> visited = new IdentityHashMap<>();
storeObject(object, object.getName(), support, indexable, visited);
}
}
IndexDocument document = support.createDocument(indexable);
for (JsObject object : globalObject.getProperties().values()) {
if (object.getParent() != null) {
IdentityHashMap<JsObject,Integer> visited = new IdentityHashMap<>();
storeUsages(object, object.getName(), document, visited);
}
}
support.addDocument(document);
}
protected static IndexDocument createDocument(JsObject object, String fqn, IndexingSupport support, Indexable indexable) {
IndexDocument elementDocument = support.createDocument(indexable);
elementDocument.addPair(Index.FIELD_BASE_NAME, object.getName(), true, true);
elementDocument.addPair(Index.FIELD_BASE_NAME_INSENSITIVE, object.getName().toLowerCase(), true, false);
elementDocument.addPair(Index.FIELD_FQ_NAME, fqn + (object.isAnonymous() ? ANONYMOUS_POSFIX
: object.getJSKind() == JsElement.Kind.PARAMETER ? PARAMETER_POSTFIX : OBJECT_POSFIX), true, true);
// boolean isGlobal = object.getParent() != null ? ModelUtils.isGlobal(object.getParent()) : ModelUtils.isGlobal(object);
// elementDocument.addPair(JsIndex.FIELD_IS_GLOBAL, (isGlobal ? "1" : "0"), true, true);
elementDocument.addPair(Index.FIELD_OFFSET, Integer.toString(object.getOffset()), true, true);
elementDocument.addPair(Index.FIELD_FLAG, Integer.toString(IndexedElement.Flag.getFlag(object)), false, true);
// StringBuilder sb = new StringBuilder();
// for (JsObject property : object.getProperties().values()) {
// if (!property.getModifiers().contains(Modifier.PRIVATE)) {
// sb.append(codeProperty(property)).append("#@#");
// }
// }
// elementDocument.addPair(JsIndex.FIELD_PROPERTY, sb.toString(), false, true);
StringBuilder sb = new StringBuilder();
for (TypeUsage type : object.getAssignments()) {
sb.append(type.getType());
sb.append(":"); //NOI18N
sb.append(type.getOffset());
sb.append(":"); //NOI18N
sb.append(type.isResolved() ? "1" : "0"); //NOI18N
sb.append("|");
}
elementDocument.addPair(Index.FIELD_ASSIGNMENTS, sb.toString(), false, true);
if (object.getJSKind().isFunction()) {
sb = new StringBuilder();
for(TypeUsage type : ((JsFunction)object).getReturnTypes()) {
sb.append(type.getType());
sb.append(","); //NOI18N
sb.append(type.getOffset());
sb.append(","); //NOI18N
sb.append(type.isResolved() ? "1" : "0"); //NOI18N
sb.append("|");
}
elementDocument.addPair(Index.FIELD_RETURN_TYPES, sb.toString(), false, true);
elementDocument.addPair(Index.FIELD_PARAMETERS, codeParameters(((JsFunction)object).getParameters()), false, true);
}
if (object instanceof JsArray) {
sb = new StringBuilder();
for(TypeUsage type : ((JsArray)object).getTypesInArray()) {
sb.append(type.getType());
sb.append(","); //NOI18N
sb.append(type.getOffset());
sb.append(","); //NOI18N
sb.append(type.isResolved() ? "1" : "0"); //NOI18N
sb.append("|");
}
elementDocument.addPair(Index.FIELD_ARRAY_TYPES, sb.toString(), false, true);
}
return elementDocument;
}
protected static IndexDocument createDocumentForReference(JsReference object, String fqn, IndexingSupport support, Indexable indexable) {
IndexDocument elementDocument = support.createDocument(indexable);
elementDocument.addPair(Index.FIELD_BASE_NAME, object.getName(), true, true);
elementDocument.addPair(Index.FIELD_BASE_NAME_INSENSITIVE, object.getName(), true, false);
elementDocument.addPair(Index.FIELD_FQ_NAME, fqn + (object.isAnonymous() ? ANONYMOUS_POSFIX
: object.getJSKind() == JsElement.Kind.PARAMETER ? PARAMETER_POSTFIX : OBJECT_POSFIX), true, true);
elementDocument.addPair(Index.FIELD_OFFSET, Integer.toString(object.getOffset()), true, true);
elementDocument.addPair(Index.FIELD_FLAG, Integer.toString(IndexedElement.Flag.getFlag(object)), false, true);
StringBuilder sb = new StringBuilder();
sb.append(object.getOriginal().getFullyQualifiedName());
sb.append(":"); //NOI18N
sb.append(object.getOffset());
sb.append(":"); //NOI18N
sb.append("1"); //NOI18N
elementDocument.addPair(Index.FIELD_ASSIGNMENTS, sb.toString(), false, true);
if (object.getJSKind().isFunction()) {
sb = new StringBuilder();
for(TypeUsage type : ((JsFunction)object).getReturnTypes()) {
sb.append(type.getType());
sb.append(","); //NOI18N
sb.append(type.getOffset());
sb.append(","); //NOI18N
sb.append(type.isResolved() ? "1" : "0"); //NOI18N
sb.append("|");
}
elementDocument.addPair(Index.FIELD_RETURN_TYPES, sb.toString(), false, true);
elementDocument.addPair(Index.FIELD_PARAMETERS, codeParameters(((JsFunction)object).getParameters()), false, true);
}
if (object instanceof JsArray) {
sb = new StringBuilder();
for(TypeUsage type : ((JsArray)object).getTypesInArray()) {
sb.append(type.getType());
sb.append(","); //NOI18N
sb.append(type.getOffset());
sb.append(","); //NOI18N
sb.append(type.isResolved() ? "1" : "0"); //NOI18N
sb.append("|");
}
elementDocument.addPair(Index.FIELD_ARRAY_TYPES, sb.toString(), false, true);
}
return elementDocument;
}
private void storeObject(JsObject object, String fqn, IndexingSupport support, Indexable indexable, IdentityHashMap<JsObject,Integer> visited) {
// @todo: This prevents unlimited recursion when self referencing strucures
// are scanned. It is necessary to rework the index users so that
// this is not necessary
if(visited.containsKey(object)) {
return;
}
IdentityHashMap<JsObject,Integer> childVisited = new IdentityHashMap<>(visited);
childVisited.compute(object, (k, v) -> v == null ? 1 : v+1);
if (!isInvisibleFunction(object) && object != null && object.getName() != null) {
if (object.isDeclared() || ModelUtils.PROTOTYPE.equals(object.getName())) {
// if it's delcared, then store in the index as new document.
IndexDocument document = createDocument(object, fqn, support, indexable);
support.addDocument(document);
}
if (!(object instanceof JsReference && ModelUtils.isDescendant(object, ((JsReference)object).getOriginal()))) {
// look for all other properties. Even if the object doesn't have to be delcared in the file
// there can be declared it's properties or methods
for (JsObject property : object.getProperties().values()) {
if (!(property instanceof JsReference && !((JsReference)property).getOriginal().isAnonymous())) {
storeObject(property, fqn + '.' + property.getName(), support, indexable, childVisited);
} else {
IndexDocument document = createDocumentForReference((JsReference)property, fqn + '.' + property.getName(), support, indexable);
//// IndexDocument document = IndexedElement.createDocument(property, fqn + '.' + property.getName(), support, indexable);
support.addDocument(document);
}
}
if (object instanceof JsFunction) {
// store parameters
for (JsObject parameter : ((JsFunction)object).getParameters()) {
storeObject(parameter, fqn + '.' + parameter.getName(), support, indexable, childVisited);
}
}
}
}
}
private boolean isInvisibleFunction(JsObject object) {
if (object.getJSKind().isFunction() && (object.isAnonymous() || object.getModifiers().contains(Modifier.PRIVATE))) {
JsObject parent = object.getParent();
if (parent != null && parent.getJSKind() == JsElement.Kind.FILE) {
return false;
}
if (parent != null && parent instanceof JsFunction) {
Collection<? extends TypeUsage> returnTypes = ((JsFunction) parent).getReturnTypes();
String fqn = object.getFullyQualifiedName();
for (TypeUsage returnType : returnTypes) {
if (returnType.getType().equals(fqn)) {
return false;
}
}
}
Collection<? extends TypeUsage> returnTypes = ((JsFunction) object).getReturnTypes();
if (returnTypes.size() == 1 && (returnTypes.iterator().next()).getType().equals("undefined")) {
return true;
}
}
return false;
}
private static String codeParameters(Collection<? extends JsObject> params) {
StringBuilder result = new StringBuilder();
for (Iterator<? extends JsObject> it = params.iterator(); it.hasNext();) {
JsObject parametr = it.next();
result.append(parametr.getName());
result.append(":");
for (Iterator<? extends TypeUsage> itType = parametr.getAssignmentForOffset(parametr.getOffset() + 1).iterator(); itType.hasNext();) {
TypeUsage type = itType.next();
result.append(type.getType());
if (itType.hasNext()) {
result.append("|");
}
}
if (it.hasNext()) {
result.append(',');
}
}
return result.toString();
}
private void storeUsages(JsObject object, String name, IndexDocument document, IdentityHashMap<JsObject,Integer> visited) {
// @todo: This prevents unlimited recursion when self referencing strucures
// are scanned. It is necessary to rework the index users so that
// this is not necessary
if(visited.containsKey(object)) {
return;
}
IdentityHashMap<JsObject,Integer> childVisited = new IdentityHashMap<>(visited);
childVisited.compute(object, (k, v) -> v == null ? 1 : v+1);
StringBuilder sb = new StringBuilder();
sb.append(object.getName());
for (JsObject property : object.getProperties().values()) {
if (storeUsage(property)) {
sb.append(':');
sb.append(property.getName()).append('#');
if (property.getJSKind().isFunction()) {
sb.append('F');
} else {
sb.append('P');
}
}
}
document.addPair(Index.FIELD_USAGE, sb.toString(), true, true);
if (object instanceof JsFunction) {
// store parameters
for (JsObject parameter : ((JsFunction) object).getParameters()) {
storeUsages(parameter, parameter.getName(), document, childVisited);
}
}
for (JsObject property : object.getProperties().values()) {
if (storeUsage(property) && (!(property instanceof JsReference && !((JsReference)property).getOriginal().isAnonymous()))) {
storeUsages(property, property.getName(), document, childVisited);
}
}
}
private boolean storeUsage(JsObject object) {
boolean result = true;
if ("arguments".equals(object.getName()) || object.getJSKind() == JsElement.Kind.ANONYMOUS_OBJECT
|| object.getModifiers().contains(Modifier.PRIVATE)) {
result = false;
}
return result;
}
public static final class Factory extends EmbeddingIndexerFactory {
public static final String NAME = "js"; // NOI18N
public static final int VERSION = 16;
private static final int PRIORITY = 100;
private static final ThreadLocal<Collection<Runnable>> postScanTasks = new ThreadLocal<>();
@Override
public EmbeddingIndexer createIndexer(final Indexable indexable, final Snapshot snapshot) {
if (isIndexable(indexable, snapshot)) {
return new JsIndexer();
} else {
return null;
}
}
@Override
public String getIndexerName() {
return NAME;
}
@Override
public int getIndexVersion() {
return VERSION;
}
private boolean isIndexable(Indexable indexable, Snapshot snapshot) {
if ((snapshot.getText().length() > SanitizingParser.MAX_FILE_SIZE_TO_PARSE) && !SanitizingParser.PARSE_BIG_FILES) {
return false;
}
return JsTokenId.JAVASCRIPT_MIME_TYPE.equals(snapshot.getMimeType());
}
@Override
public void filesDeleted(Iterable<? extends Indexable> deleted, Context context) {
try {
IndexingSupport is = IndexingSupport.getInstance(context);
for (Indexable i : deleted) {
is.removeDocuments(i);
}
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
}
@Override
public void rootsRemoved(final Iterable<? extends URL> removedRoots) {
}
@Override
public void filesDirty(Iterable<? extends Indexable> dirty, Context context) {
try {
IndexingSupport is = IndexingSupport.getInstance(context);
for (Indexable i : dirty) {
is.markDirtyDocuments(i);
}
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
}
@Override
public boolean scanStarted(Context context) {
postScanTasks.set(new LinkedList<>());
return super.scanStarted(context);
}
@Override
public void scanFinished(Context context) {
if (context.isAllFilesIndexing()) {
IndexChangeSupport changeSupport = Lookup.getDefault().lookup(IndexChangeSupport.class);
if (changeSupport != null) {
// when the scan is finished, clear the index cache
changeSupport.fireChange();
}
}
try {
for (Runnable task : postScanTasks.get()) {
task.run();
}
} finally {
postScanTasks.remove();
super.scanFinished(context);
}
}
public static boolean isScannerThread() {
return postScanTasks.get() != null;
}
public static void addPostScanTask(@NonNull final Runnable task) {
Parameters.notNull("task", task); //NOI18N
final Collection<Runnable> tasks = postScanTasks.get();
if (tasks == null) {
throw new IllegalStateException("JsIndexer.postScanTask can be called only from scanner thread."); //NOI18N
}
tasks.add(task);
}
@Override
public int getPriority() {
return PRIORITY;
}
} // End of Factory class
@ServiceProvider(service = org.netbeans.modules.javascript2.model.spi.IndexChangeSupport.class)
public static final class IndexChangeSupport implements org.netbeans.modules.javascript2.model.spi.IndexChangeSupport {
@Override
public void addChangeListener(ChangeListener listener) {
changeSupport.addChangeListener(listener);
}
@Override
public void removeChangeListener(ChangeListener listener) {
changeSupport.removeChangeListener(listener);
}
public void fireChange() {
changeSupport.fireChange();
}
}
@ServiceProvider(service = org.netbeans.modules.javascript2.editor.spi.PostScanProvider.class)
public static final class PostScanProvider implements org.netbeans.modules.javascript2.editor.spi.PostScanProvider {
@Override
public void addPostScanTask(Runnable task) {
Factory.addPostScanTask(task);
}
}
}