/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.ruby; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Map; import org.jrubyparser.ast.AliasNode; import org.jrubyparser.ast.ArgsNode; import org.jrubyparser.ast.ArgumentNode; import org.jrubyparser.ast.BlockArgNode; import org.jrubyparser.ast.DAsgnNode; import org.jrubyparser.ast.DVarNode; import org.jrubyparser.ast.ForNode; import org.jrubyparser.ast.ListNode; import org.jrubyparser.ast.LocalAsgnNode; import org.jrubyparser.ast.LocalVarNode; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.NodeType; import org.jrubyparser.ast.INameNode; import org.netbeans.modules.csl.api.ColoringAttributes; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.csl.api.SemanticAnalyzer; import org.netbeans.modules.parsing.spi.Parser.Result; import org.netbeans.modules.parsing.spi.Scheduler; import org.netbeans.modules.parsing.spi.SchedulerEvent; import org.netbeans.modules.ruby.lexer.LexUtilities; /** * Walk through the JRuby AST and note interesting things * @todo Use the org.jrubyparser.ast.visitor.NodeVisitor interface * @todo Do mixins and includes trip up my unused private method detection code? * @todo Treat toplevel methods as private? * @todo Show unused highlighting for unused class variables: * private_class_method * See section 7.8 in http://www.rubycentral.com/faq/rubyfaq-7.html * @todo Handle java fully packaged names by not bolding "java" and "javax" method * calls in Java projects * @todo I can do faster tree walking with a quick integer set of node types I'm * interested in, or more specifically a set of node types I know I can prune: * ArgNodes etc. * @todo Stash unused variables in a list I can reference from a quickfix! * @author Tor Norbye */ public class RubySemanticAnalyzer extends SemanticAnalyzer { private boolean cancelled; private Map<OffsetRange, Set<ColoringAttributes>> semanticHighlights; private static final Set<String> JAVA_PREFIXES = new HashSet<String>(); /** * Method names for which we should skip highlightning. See #124701 - this * would probably need a more general solution (Operators as method names * are already handled somewhere, but I really can't find where..) */ private static final Set<String> SKIP_HIGHLIGHTNING = new HashSet<String>(); static { JAVA_PREFIXES.add("java"); // NOI18N JAVA_PREFIXES.add("javax"); // NOI18N JAVA_PREFIXES.add("org"); // NOI18N JAVA_PREFIXES.add("com"); // NOI18N // what else than "[]"? SKIP_HIGHLIGHTNING.add("[]"); } public RubySemanticAnalyzer() { } public Map<OffsetRange, Set<ColoringAttributes>> getHighlights() { return semanticHighlights; } protected final synchronized boolean isCancelled() { return cancelled; } protected final synchronized void resume() { cancelled = false; } public final synchronized void cancel() { cancelled = true; } @Override public int getPriority() { throw new UnsupportedOperationException("Not supported yet."); } @Override public Class<? extends Scheduler> getSchedulerClass() { throw new UnsupportedOperationException("Not supported yet."); } @Override public void run(Result info, SchedulerEvent event) { resume(); if (isCancelled()) { return; } RubyParseResult rpr = AstUtilities.getParseResult(info); if (rpr == null) { return; } Node root = rpr.getRootNode(); if (root == null) { return; } Map<OffsetRange, Set<ColoringAttributes>> highlights = new HashMap<OffsetRange, Set<ColoringAttributes>>(100); AstPath path = new AstPath(); path.descend(root); annotate(root, highlights, path, null, false); path.ascend(); if (isCancelled()) { return; } if (highlights.size() > 0) { // XXX - Parsing API // if (rpr.getTranslatedSource() != null) { Map<OffsetRange, Set<ColoringAttributes>> translated = new HashMap<OffsetRange,Set<ColoringAttributes>>(2*highlights.size()); for (Map.Entry<OffsetRange,Set<ColoringAttributes>> entry : highlights.entrySet()) { OffsetRange range = LexUtilities.getLexerOffsets(info, entry.getKey()); if (range != OffsetRange.NONE) { translated.put(range, entry.getValue()); } } highlights = translated; // } this.semanticHighlights = highlights; } else { this.semanticHighlights = null; } } /** Find unused local and dynamic variables */ @SuppressWarnings("fallthrough") private void annotate(Node node, Map<OffsetRange,Set<ColoringAttributes>> highlights, AstPath path, List<String> parameters, boolean isParameter) { switch (node.getNodeType()) { case ARGSNODE: { isParameter = true; break; } case LOCALASGNNODE: { LocalAsgnNode lasgn = (LocalAsgnNode)node; Node method = AstUtilities.findLocalScope(node, path); boolean isUsed = isUsedInMethod(method, lasgn.getName(), isParameter); if (!isUsed) { OffsetRange range = AstUtilities.getLValueRange(lasgn); highlights.put(range, ColoringAttributes.UNUSED_SET); } else if (parameters != null) { String name = ((LocalAsgnNode)node).getName(); if (parameters.contains(name)) { OffsetRange range = AstUtilities.getNameRange(node); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } break; } case DASGNNODE: { DAsgnNode dasgn = (DAsgnNode)node; Node method = AstUtilities.findLocalScope(node, path); boolean isUsed = isUsedInMethod(method, dasgn.getName(), false); if (!isUsed) { OffsetRange range = AstUtilities.getLValueRange(dasgn); highlights.put(range, ColoringAttributes.UNUSED_SET); } break; } case DEFNNODE: case DEFSNODE: { MethodDefNode def = (MethodDefNode)node; parameters = AstUtilities.getDefArgs(def, true); if ((parameters != null) && (parameters.size() > 0)) { List<String> unused = new ArrayList<String>(); for (String parameter : parameters) { boolean isUsed = isUsedInMethod(node, parameter, true); if (!isUsed) { unused.add(parameter); } } if (unused.size() > 0) { annotateUnusedParameters(def, highlights, unused); parameters.removeAll(unused); } if (parameters != null) { if (parameters.size() == 0) { parameters = null; } else { annotateParameters(def, highlights, parameters); } } } if (!SKIP_HIGHLIGHTNING.contains(AstUtilities.getName(node))) { highlightMethodName(node, highlights); } break; } case LOCALVARNODE: { if (parameters != null) { if (parameters.contains(((LocalVarNode)node).getName())) { OffsetRange range = AstUtilities.getRange(node); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } break; } case VCALLNODE: // FALLTHROUGH! if (JAVA_PREFIXES.contains(((INameNode)node).getName())) { // Skip highlighting "org" in "org.foo.Bar" etc. break; } //case CALLNODE: case FCALLNODE: { // CallNode seems overly aggressive - it will show all operators for example OffsetRange range = AstUtilities.getCallRange(node); // see #124701 if (!SKIP_HIGHLIGHTNING.contains(AstUtilities.getName(node))) { highlights.put(range, ColoringAttributes.METHOD_SET); } break; } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } path.descend(child); annotate(child, highlights, path, parameters, isParameter); path.ascend(); } } private void annotateParameters(MethodDefNode node, Map<OffsetRange, Set<ColoringAttributes>> highlights, List<String> usedParameterNames) { List<Node> nodes = node.childNodes(); for (Node c : nodes) { if (c.getNodeType() == NodeType.ARGSNODE) { ArgsNode an = (ArgsNode)c; if (an.getRequiredCount() > 0) { List<Node> args = an.childNodes(); for (Node arg : args) { if (arg instanceof ListNode) { // Many specific types List<Node> args2 = arg.childNodes(); for (Node arg2 : args2) { if (arg2.getNodeType() == NodeType.ARGUMENTNODE) { if (usedParameterNames.contains(((ArgumentNode)arg2).getName())) { OffsetRange range = AstUtilities.getRange(arg2); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } else if (arg2.getNodeType() == NodeType.LOCALASGNNODE) { if (usedParameterNames.contains(((LocalAsgnNode)arg2).getName())) { OffsetRange range = AstUtilities.getNameRange(arg2); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } } } } } // Rest args if (an.getRest() != null) { ArgumentNode bn = an.getRest(); if (usedParameterNames.contains(bn.getName())) { OffsetRange range = AstUtilities.getRange(bn); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } // Block args if (an.getRest() != null) { ArgumentNode bn = an.getRest(); if (usedParameterNames.contains(bn.getName())) { OffsetRange range = AstUtilities.getRange(bn); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } // Block args if (an.getBlock() != null) { BlockArgNode bn = an.getBlock(); if (usedParameterNames.contains(bn.getName())) { OffsetRange range = AstUtilities.getRange(bn); highlights.put(range, ColoringAttributes.PARAMETER_SET); } } } } } private void annotateUnusedParameters(MethodDefNode node, Map<OffsetRange, Set<ColoringAttributes>> highlights, List<String> names) { List<Node> nodes = node.childNodes(); for (Node c : nodes) { if (c.getNodeType() == NodeType.ARGSNODE) { ArgsNode an = (ArgsNode)c; if (an.getRequiredCount() > 0) { List<Node> args = an.childNodes(); for (Node arg : args) { if (arg instanceof ListNode) { // Check subclasses List<Node> args2 = arg.childNodes(); for (Node arg2 : args2) { if (arg2.getNodeType() == NodeType.ARGUMENTNODE) { if (names.contains(((ArgumentNode)arg2).getName())) { OffsetRange range = AstUtilities.getRange(arg2); highlights.put(range, ColoringAttributes.UNUSED_SET); } } else if (arg2.getNodeType() == NodeType.LOCALASGNNODE) { if (names.contains(((LocalAsgnNode)arg2).getName())) { OffsetRange range = AstUtilities.getNameRange(arg2); highlights.put(range, ColoringAttributes.UNUSED_SET); } } } } } } // Rest args if (an.getRest() != null) { ArgumentNode bn = an.getRest(); if (names.contains(bn.getName())) { OffsetRange range = AstUtilities.getRange(bn); highlights.put(range, ColoringAttributes.UNUSED_SET); } } if (an.getBlock() != null) { BlockArgNode bn = an.getBlock(); if (names.contains(bn.getName())) { OffsetRange range = AstUtilities.getRange(bn); highlights.put(range, ColoringAttributes.UNUSED_SET); } } } } } private boolean isUsedInMethod(Node node, String targetName, boolean isParameter) { switch (node.getNodeType()) { case LOCALVARNODE: { if (node.getNodeType() == NodeType.LOCALVARNODE) { String name = ((LocalVarNode)node).getName(); if (targetName.equals(name)) { return true; } } break; } case FORNODE: { // XXX This is no longer necessary, right? // Workaround for the fact that ForNode's childNodes implementation // is wrong - Tom is committing a fix; this is until we pick that // fix (SVN #3561) up Node iterNode = ((ForNode)node).getIterNode(); if (iterNode instanceof INameNode) { if (targetName.equals(((INameNode)iterNode).getName())) { return true; } } break; } case DVARNODE: if (targetName.equals(((DVarNode)node).getName())) { return true; } break; case ALIASNODE: { AliasNode an = (AliasNode)node; if (targetName.equals(AstUtilities.getNameOrValue(an.getOldName()))) { return true; } break; } case ZSUPERNODE: // Super with no arguments passes arguments to parent so consider // the parameters used if (isParameter) { return true; } break; } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } // The "outer" foo here is unused - we shouldn't // recurse into method bodies when doing unused detection // foo = 1; def bar; foo = 2; print foo; end; if (child.getNodeType() == NodeType.DEFSNODE || child.getNodeType() == NodeType.DEFNNODE) { continue; } boolean used = isUsedInMethod(child, targetName, isParameter); if (used) { return true; } } return false; } private void highlightMethodName(Node node, Map<OffsetRange, Set<ColoringAttributes>> highlights) { OffsetRange range = AstUtilities.getFunctionNameRange(node); if (range != OffsetRange.NONE) { if (!highlights.containsKey(range)) { // Don't block out already annotated private methods highlights.put(range, ColoringAttributes.METHOD_SET); } } } }