blob: 7ae31a7eb695980af55e5a47785a46042926c642 [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;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.json.parser.JsonBaseVisitor;
import org.netbeans.modules.javascript2.json.parser.JsonLexer;
import org.netbeans.modules.javascript2.json.parser.JsonParser;
import org.netbeans.modules.javascript2.json.parser.ParseTreeToXml;
import org.netbeans.modules.javascript2.model.ModelBuilder;
import org.netbeans.modules.javascript2.model.api.JsElement;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.spi.ModelElementFactory;
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.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Pair;
import org.openide.util.Parameters;
import org.openide.util.lookup.ServiceProvider;
import org.w3c.dom.Document;
/**
*
* @author Tomas Zezula
*/
public final class JsonModelResolver extends JsonBaseVisitor<Boolean> implements ModelResolver {
private static final Logger LOG = Logger.getLogger(JsonModelResolver.class.getName());
private final ParserResult parserResult;
private final OccurrenceBuilder occurrenceBuilder;
private final ModelBuilder modelBuilder;
private final Deque<Pair<ParserRuleContext,JsObject>> path = new ArrayDeque<>();
boolean importantTerminalExpected;
private JsonModelResolver(
@NonNull final ParserResult parserResult,
@NonNull final OccurrenceBuilder occurrenceBuilder) {
Parameters.notNull("parserResult", parserResult); //NOI18N
Parameters.notNull("occurrenceBuilder", occurrenceBuilder); //NOI18N
this.parserResult = parserResult;
this.occurrenceBuilder = occurrenceBuilder;
final FileObject fileObject = parserResult.getSnapshot().getSource().getFileObject();
this.modelBuilder = new ModelBuilder(JsFunctionImpl.createGlobal(
fileObject,
Integer.MAX_VALUE,
parserResult.getSnapshot().getMimeType()));
}
@Override
public void init() {
final JsonParser.JsonContext parseTree = parserResult.getLookup().lookup(JsonParser.JsonContext.class);
if (LOG.isLoggable(Level.FINEST)) {
final FileObject file = parserResult.getSnapshot().getSource().getFileObject();
if (parseTree != null) {
try {
final JsonLexer l = new JsonLexer(new ANTLRInputStream());
JsonBaseVisitor<Document> visitor = new ParseTreeToXml(
l,
new JsonParser(new CommonTokenStream(l)));
LOG.log(Level.FINEST,
"Parse tree for file: {0}\n{1}", //NOI18N
new Object[]{
file == null ? null : FileUtil.getFileDisplayName(file),
ParseTreeToXml.stringify(visitor.visit(parseTree))
});
} catch (IOException ioe) {
LOG.log(
Level.FINEST,
"Error dumping parse tree for file: {0} : {1}", //NOI18N
new Object[] {
file == null ? null : FileUtil.getFileDisplayName(file),
ioe.getMessage()
});
}
} else {
LOG.log(
Level.FINEST,
"No parse tree for file: {0}", //NOI18N
file == null ? null : FileUtil.getFileDisplayName(file));
}
}
if (parseTree != null) {
parseTree.accept(this);
}
}
@Override
public JsObject getGlobalObject() {
return modelBuilder.getGlobal();
}
@Override
public JsObject resolveThis(JsObject where) {
return where;
}
@Override
public void processCalls(
ModelElementFactory elementFactory,
Map<String, Map<Integer, List<TypeUsage>>> returnTypesFromFrameworks) {
}
@Override
public List<Identifier> getASTNodeName(Object astNode) {
return Collections.emptyList();
}
@Override
public Boolean visitObject(JsonParser.ObjectContext ctx) {
JsObjectImpl object = null;
if (!path.isEmpty()) {
final Pair<ParserRuleContext,JsObject> top = path.peek();
if (top.first() instanceof JsonParser.PairContext) {
object = createJSObject(ctx, (JsonParser.PairContext) top.first());
} else if (top.first() instanceof JsonParser.ArrayContext) {
object = createJSObject(ctx, null);
((JsArrayImpl)top.second()).addTypeInArray(
new TypeUsage(object.getFullyQualifiedName(), ctx.start.getStartIndex()));
}
}
if (object == null) {
//Top level object or broken source
object = createJSObject(ctx, null);
}
modelBuilder.setCurrentObject(object);
path.push(Pair.<ParserRuleContext,JsObject>of(ctx,object));
super.visitObject(ctx);
path.pop();
modelBuilder.reset();
return true;
}
@Override
public Boolean visitArray(JsonParser.ArrayContext ctx) {
JsObjectImpl object = null;
if (!path.isEmpty()) {
final Pair<ParserRuleContext,JsObject> top = path.peek();
if (top.first() instanceof JsonParser.PairContext) {
object = createJsArray(ctx, (JsonParser.PairContext)top.first());
} else if (top.first() instanceof JsonParser.ArrayContext) {
object = createJsArray(ctx, null);
((JsArrayImpl)top.second()).addTypeInArray(
new TypeUsage(object.getFullyQualifiedName(), ctx.start.getStartIndex()));
}
}
if (object == null) {
//Top level array or broken source
object = createJsArray(ctx, null);
}
modelBuilder.setCurrentObject(object);
path.push(Pair.<ParserRuleContext,JsObject>of(ctx,object));
super.visitArray(ctx);
path.pop();
modelBuilder.reset();
return true;
}
@Override
public Boolean visitTerminal(TerminalNode node) {
if (!importantTerminalExpected) {
return false;
}
if (!path.isEmpty()) {
final Pair<ParserRuleContext,JsObject> top = path.peek();
if (top.first() instanceof JsonParser.PairContext) {
createJSObject(node, (JsonParser.PairContext) top.first());
} else if (top.first() instanceof JsonParser.ArrayContext) {
((JsArrayImpl)top.second()).addTypeInArray(getLiteralType(node));
}
} else {
createJSObject(node, null);
}
super.visitTerminal(node);
return true;
}
@Override
public Boolean visitValue(JsonParser.ValueContext ctx) {
importantTerminalExpected = hasImportantTerminal(ctx);
final Boolean res = super.visitValue(ctx);
importantTerminalExpected = false;
return res;
}
@Override
public Boolean visitPair(JsonParser.PairContext ctx) {
path.push(Pair.<ParserRuleContext,JsObject>of(ctx,null));
super.visitPair(ctx);
path.pop();
return true;
}
@NonNull
private JsObjectImpl createJSObject(
@NonNull final JsonParser.ObjectContext objLit,
@NullAllowed final JsonParser.PairContext property) {
final JsObjectImpl declarationScope = modelBuilder.getCurrentObject();
final Identifier name = property != null ?
new Identifier(
stringValue(property.key().getText()),
createOffsetRange(property.key())) :
new Identifier(modelBuilder.getUnigueNameForAnonymObject(parserResult), OffsetRange.NONE);
JsObjectImpl object = new JsObjectImpl(
declarationScope,
name,
createOffsetRange(property != null ? property : objLit),
true,
declarationScope.getMimeType(),
declarationScope.getSourceLabel());
declarationScope.addProperty(object.getName(), object);
object.setJsKind(JsElement.Kind.OBJECT_LITERAL);
if (property == null) {
object.setAnonymous(true);
} else {
object.addOccurrence(name.getOffsetRange());
}
return object;
}
@NonNull
private JsObjectImpl createJSObject(
@NonNull final TerminalNode literal,
@NullAllowed final JsonParser.PairContext property) {
final JsObjectImpl declarationScope = modelBuilder.getCurrentObject();
final Identifier name = property != null ?
new Identifier(
stringValue(property.key().getText()),
createOffsetRange(property.key())) :
new Identifier(modelBuilder.getUnigueNameForAnonymObject(parserResult), OffsetRange.NONE);
JsObjectImpl object = new JsObjectImpl(
declarationScope,
name,
property != null ?
createOffsetRange(property) :
createOffsetRange(literal),
true,
declarationScope.getMimeType(),
declarationScope.getSourceLabel());
declarationScope.addProperty(object.getName(), object);
object.addAssignment(
getLiteralType(literal),
object.getOffset());
if (property == null) {
object.setAnonymous(true);
object.setJsKind(JsElement.Kind.OBJECT);
} else {
object.setJsKind(JsElement.Kind.PROPERTY);
object.addOccurrence(name.getOffsetRange());
}
return object;
}
@NonNull
private JsArrayImpl createJsArray(
@NonNull final JsonParser.ArrayContext arrayLit,
@NullAllowed final JsonParser.PairContext property) {
JsObjectImpl declarationScope = modelBuilder.getCurrentObject();
final Identifier name = property != null ?
new Identifier(
stringValue(property.key().getText()),
createOffsetRange(property.key())) :
new Identifier(modelBuilder.getUnigueNameForAnonymObject(parserResult), OffsetRange.NONE);
JsArrayImpl array = new JsArrayImpl(
declarationScope,
name,
createOffsetRange(property != null ? property : arrayLit),
declarationScope.getMimeType(),
declarationScope.getSourceLabel());
declarationScope.addProperty(array.getName(), array);
array.addAssignment(
new TypeUsage(TypeUsage.ARRAY, -1, true),
array.getOffset());
array.setDeclared(true); //Todo: Why? but when not set it's not displayed by navigator
if (property == null) {
array.setAnonymous(true);
array.setJsKind(JsElement.Kind.OBJECT_LITERAL);
} else {
array.setJsKind(JsElement.Kind.PROPERTY);
array.addOccurrence(name.getOffsetRange());
}
return array;
}
@NonNull
private static OffsetRange createOffsetRange(@NonNull final ParserRuleContext parseTree) {
int start = parseTree.start.getStartIndex();
int end = parseTree.stop.getStopIndex() +1;
if (end <= start) {
end = start + 1;
LOG.log(
Level.FINE,
"ParseTree.start offset is bigger then end offset [{0}, {1}]", //NOI18N
new Object[]{parseTree.start.getStartIndex(), parseTree.stop.getStopIndex()});
}
return new OffsetRange( start, end);
}
@NonNull
private static OffsetRange createOffsetRange(@NonNull final TerminalNode terminal) {
return new OffsetRange(
terminal.getSymbol().getStartIndex(),
terminal.getSymbol().getStopIndex() +1);
}
@NonNull
private String stringValue(@NonNull final String strTknVal) {
return strTknVal.substring(1, strTknVal.length()-1);
}
private static boolean hasImportantTerminal(@NonNull final JsonParser.ValueContext valCtx) {
return valCtx.array() == null && valCtx.object() == null;
}
private static TypeUsage getLiteralType(@NonNull final TerminalNode t) {
switch (t.getSymbol().getType()) {
case JsonLexer.FALSE:
case JsonLexer.TRUE:
return new TypeUsage(TypeUsage.BOOLEAN, -1, true);
case JsonLexer.NUMBER:
return new TypeUsage(TypeUsage.NUMBER, -1, true);
case JsonLexer.STRING:
return new TypeUsage(TypeUsage.STRING, -1, true);
case JsonLexer.NULL:
return new TypeUsage(TypeUsage.OBJECT, -1, true);
default:
throw new IllegalArgumentException(t.toString());
}
}
@ServiceProvider(service = ModelResolver.Provider.class, position = 100)
public static final class Provider implements ModelResolver.Provider {
@CheckForNull
@Override
public ModelResolver create(
@NonNull final ParserResult result,
@NonNull final OccurrenceBuilder occurrenceBuilder) {
final JsonParser.JsonContext parseTree = result.getLookup().lookup(JsonParser.JsonContext.class);
if (parseTree == null) {
return null;
}
return new JsonModelResolver(result, occurrenceBuilder);
}
}
}