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