langtools/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Analyzer.java
changeset 28142 32a6b1af81b1
child 28146 adf3a039cb7d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Analyzer.java	Fri Dec 12 18:07:24 2014 +0000
@@ -0,0 +1,493 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.tools.javac.comp;
+
+import com.sun.tools.javac.code.Source;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.Types;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCBlock;
+import com.sun.tools.javac.tree.JCTree.JCClassDecl;
+import com.sun.tools.javac.tree.JCTree.JCDoWhileLoop;
+import com.sun.tools.javac.tree.JCTree.JCEnhancedForLoop;
+import com.sun.tools.javac.tree.JCTree.JCForLoop;
+import com.sun.tools.javac.tree.JCTree.JCIf;
+import com.sun.tools.javac.tree.JCTree.JCLambda;
+import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
+import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
+import com.sun.tools.javac.tree.JCTree.JCNewClass;
+import com.sun.tools.javac.tree.JCTree.JCStatement;
+import com.sun.tools.javac.tree.JCTree.JCSwitch;
+import com.sun.tools.javac.tree.JCTree.JCTypeApply;
+import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
+import com.sun.tools.javac.tree.JCTree.JCWhileLoop;
+import com.sun.tools.javac.tree.JCTree.Tag;
+import com.sun.tools.javac.tree.TreeCopier;
+import com.sun.tools.javac.tree.TreeInfo;
+import com.sun.tools.javac.tree.TreeMaker;
+import com.sun.tools.javac.tree.TreeScanner;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Filter;
+import com.sun.tools.javac.util.JCDiagnostic;
+import com.sun.tools.javac.util.JCDiagnostic.DiagnosticType;
+import com.sun.tools.javac.util.List;
+import com.sun.tools.javac.util.ListBuffer;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Names;
+import com.sun.tools.javac.util.Options;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import static com.sun.tools.javac.code.Flags.GENERATEDCONSTR;
+import static com.sun.tools.javac.code.Flags.SYNTHETIC;
+import static com.sun.tools.javac.code.TypeTag.CLASS;
+import static com.sun.tools.javac.tree.JCTree.Tag.APPLY;
+import static com.sun.tools.javac.tree.JCTree.Tag.CLASSDEF;
+import static com.sun.tools.javac.tree.JCTree.Tag.METHODDEF;
+import static com.sun.tools.javac.tree.JCTree.Tag.NEWCLASS;
+import static com.sun.tools.javac.tree.JCTree.Tag.TYPEAPPLY;
+
+/**
+ * Helper class for defining custom code analysis, such as finding instance creation expression
+ * that can benefit from diamond syntax.
+ */
+public class Analyzer {
+    protected static final Context.Key<Analyzer> analyzerKey = new Context.Key<>();
+
+    final Types types;
+    final Log log;
+    final Attr attr;
+    final DeferredAttr deferredAttr;
+    final TreeMaker make;
+    final Names names;
+
+    final EnumSet<AnalyzerMode> analyzerModes;
+
+    public static Analyzer instance(Context context) {
+        Analyzer instance = context.get(analyzerKey);
+        if (instance == null)
+            instance = new Analyzer(context);
+        return instance;
+    }
+
+    protected Analyzer(Context context) {
+        context.put(analyzerKey, this);
+        types = Types.instance(context);
+        log = Log.instance(context);
+        attr = Attr.instance(context);
+        deferredAttr = DeferredAttr.instance(context);
+        make = TreeMaker.instance(context);
+        names = Names.instance(context);
+        Options options = Options.instance(context);
+        String findOpt = options.get("find");
+        //parse modes
+        Source source = Source.instance(context);
+        analyzerModes = AnalyzerMode.getAnalyzerModes(findOpt, source);
+    }
+
+    /**
+     * This enum defines supported analyzer modes, as well as defining the logic for decoding
+     * the {@code -XDfind} option.
+     */
+    enum AnalyzerMode {
+        DIAMOND("diamond", Source::allowDiamond),
+        LAMBDA("lambda", Source::allowLambda),
+        METHOD("method", Source::allowGraphInference);
+
+        final String opt;
+        final Predicate<Source> sourceFilter;
+
+        AnalyzerMode(String opt, Predicate<Source> sourceFilter) {
+            this.opt = opt;
+            this.sourceFilter = sourceFilter;
+        }
+
+        /**
+         * This method is used to parse the {@code find} option.
+         * Possible modes are separated by colon; a mode can be excluded by
+         * prepending '-' to its name. Finally, the special mode 'all' can be used to
+         * add all modes to the resulting enum.
+         */
+        static EnumSet<AnalyzerMode> getAnalyzerModes(String opt, Source source) {
+            if (opt == null) {
+                return EnumSet.noneOf(AnalyzerMode.class);
+            }
+            List<String> modes = List.from(opt.split(","));
+            EnumSet<AnalyzerMode> res = EnumSet.noneOf(AnalyzerMode.class);
+            if (modes.contains("all")) {
+                res = EnumSet.allOf(AnalyzerMode.class);
+            }
+            for (AnalyzerMode mode : values()) {
+                if (modes.contains(mode.opt)) {
+                    res.add(mode);
+                } else if (modes.contains("-" + mode.opt) || !mode.sourceFilter.test(source)) {
+                    res.remove(mode);
+                }
+            }
+            return res;
+        }
+    }
+
+    /**
+     * A statement analyzer is a work-unit that matches certain AST nodes (of given type {@code S}),
+     * rewrites them to different AST nodes (of type {@code T}) and then generates some meaningful
+     * messages in case the analysis has been successful.
+     */
+    abstract class StatementAnalyzer<S extends JCTree, T extends JCTree> {
+
+        AnalyzerMode mode;
+        JCTree.Tag tag;
+
+        StatementAnalyzer(AnalyzerMode mode, Tag tag) {
+            this.mode = mode;
+            this.tag = tag;
+        }
+
+        /**
+         * Is this analyzer allowed to run?
+         */
+        boolean isEnabled() {
+            return analyzerModes.contains(mode);
+        }
+
+        /**
+         * Should this analyzer be rewriting the given tree?
+         */
+        abstract boolean match(S tree);
+
+        /**
+         * Rewrite a given AST node into a new one
+         */
+        abstract T map(S oldTree, S newTree);
+
+        /**
+         * Entry-point for comparing results and generating diagnostics.
+         */
+        abstract void process(S oldTree, T newTree, boolean hasErrors);
+
+    }
+
+    /**
+     * This analyzer checks if generic instance creation expression can use diamond syntax.
+     */
+    class DiamondInitializer extends StatementAnalyzer<JCNewClass, JCNewClass> {
+
+        DiamondInitializer() {
+            super(AnalyzerMode.DIAMOND, NEWCLASS);
+        }
+
+        @Override
+        boolean match(JCNewClass tree) {
+            return tree.clazz.hasTag(TYPEAPPLY) &&
+                    !TreeInfo.isDiamond(tree) &&
+                    tree.def == null;
+        }
+
+        @Override
+        JCNewClass map(JCNewClass oldTree, JCNewClass newTree) {
+            if (newTree.clazz.hasTag(TYPEAPPLY)) {
+                ((JCTypeApply)newTree.clazz).arguments = List.nil();
+            }
+            return newTree;
+        }
+
+        @Override
+        void process(JCNewClass oldTree, JCNewClass newTree, boolean hasErrors) {
+            if (!hasErrors) {
+                List<Type> inferredArgs = newTree.type.getTypeArguments();
+                List<Type> explicitArgs = oldTree.type.getTypeArguments();
+                for (Type t : inferredArgs) {
+                    if (!types.isSameType(t, explicitArgs.head)) {
+                        log.warning(oldTree.clazz, "diamond.redundant.args.1",
+                                oldTree.clazz.type, newTree.clazz.type);
+                        return;
+                    }
+                    explicitArgs = explicitArgs.tail;
+                }
+                //exact match
+                log.warning(oldTree.clazz, "diamond.redundant.args");
+            }
+        }
+    }
+
+    /**
+     * This analyzer checks if anonymous instance creation expression can replaced by lambda.
+     */
+    class LambdaAnalyzer extends StatementAnalyzer<JCNewClass, JCLambda> {
+
+        LambdaAnalyzer() {
+            super(AnalyzerMode.LAMBDA, NEWCLASS);
+        }
+
+        @Override
+        boolean match (JCNewClass tree){
+            Type clazztype = tree.clazz.type;
+            return tree.def != null &&
+                    clazztype.hasTag(CLASS) &&
+                    types.isFunctionalInterface(clazztype.tsym) &&
+                    decls(tree.def).length() == 1;
+        }
+        //where
+            private List<JCTree> decls(JCClassDecl decl) {
+                ListBuffer<JCTree> decls = new ListBuffer<>();
+                for (JCTree t : decl.defs) {
+                    if (t.hasTag(METHODDEF)) {
+                        JCMethodDecl md = (JCMethodDecl)t;
+                        if ((md.getModifiers().flags & GENERATEDCONSTR) == 0) {
+                            decls.add(md);
+                        }
+                    } else {
+                        decls.add(t);
+                    }
+                }
+                return decls.toList();
+            }
+
+        @Override
+        JCLambda map (JCNewClass oldTree, JCNewClass newTree){
+            JCMethodDecl md = (JCMethodDecl)decls(newTree.def).head;
+            List<JCVariableDecl> params = md.params;
+            JCBlock body = md.body;
+            return make.Lambda(params, body);
+        }
+        @Override
+        void process (JCNewClass oldTree, JCLambda newTree, boolean hasErrors){
+            if (!hasErrors) {
+                log.warning(oldTree.def, "potential.lambda.found");
+            }
+        }
+    }
+
+    /**
+     * This analyzer checks if generic method call has redundant type arguments.
+     */
+    class RedundantTypeArgAnalyzer extends StatementAnalyzer<JCMethodInvocation, JCMethodInvocation> {
+
+        RedundantTypeArgAnalyzer() {
+            super(AnalyzerMode.METHOD, APPLY);
+        }
+
+        @Override
+        boolean match (JCMethodInvocation tree){
+            return tree.typeargs != null &&
+                    tree.typeargs.nonEmpty();
+        }
+        @Override
+        JCMethodInvocation map (JCMethodInvocation oldTree, JCMethodInvocation newTree){
+            newTree.typeargs = List.nil();
+            return newTree;
+        }
+        @Override
+        void process (JCMethodInvocation oldTree, JCMethodInvocation newTree, boolean hasErrors){
+            if (!hasErrors) {
+                //exact match
+                log.warning(oldTree, "method.redundant.typeargs");
+            }
+        }
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    StatementAnalyzer<JCTree, JCTree>[] analyzers = new StatementAnalyzer[] {
+            new DiamondInitializer(),
+            new LambdaAnalyzer(),
+            new RedundantTypeArgAnalyzer()
+    };
+
+    /**
+     * Analyze an AST node if needed.
+     */
+    void analyzeIfNeeded(JCTree tree, Env<AttrContext> env) {
+        if (!analyzerModes.isEmpty() &&
+                !env.info.isSpeculative &&
+                TreeInfo.isStatement(tree)) {
+            JCStatement stmt = (JCStatement)tree;
+            analyze(stmt, env);
+        }
+    }
+
+    /**
+     * Analyze an AST node; this involves collecting a list of all the nodes that needs rewriting,
+     * and speculatively type-check the rewritten code to compare results against previously attributed code.
+     */
+    void analyze(JCStatement statement, Env<AttrContext> env) {
+        AnalysisContext context = new AnalysisContext();
+        StatementScanner statementScanner = new StatementScanner(context);
+        statementScanner.scan(statement);
+
+        if (!context.treesToAnalyzer.isEmpty()) {
+
+            //add a block to hoist potential dangling variable declarations
+            JCBlock fakeBlock = make.Block(SYNTHETIC, List.of(statement));
+
+            TreeMapper treeMapper = new TreeMapper(context);
+            //TODO: to further refine the analysis, try all rewriting combinations
+            deferredAttr.attribSpeculative(fakeBlock, env, attr.statInfo, treeMapper,
+                    t -> new AnalyzeDeferredDiagHandler(context));
+
+            context.treeMap.entrySet().forEach(e -> {
+                context.treesToAnalyzer.get(e.getKey())
+                        .process(e.getKey(), e.getValue(), context.errors.nonEmpty());
+            });
+        }
+    }
+
+    /**
+     * Simple deferred diagnostic handler which filters out all messages and keep track of errors.
+     */
+    class AnalyzeDeferredDiagHandler extends Log.DeferredDiagnosticHandler {
+        AnalysisContext context;
+
+        public AnalyzeDeferredDiagHandler(AnalysisContext context) {
+            super(log, d -> {
+                if (d.getType() == DiagnosticType.ERROR) {
+                    context.errors.add(d);
+                }
+                return true;
+            });
+            this.context = context;
+        }
+    }
+
+    /**
+     * This class is used to pass around contextual information bewteen analyzer classes, such as
+     * trees to be rewritten, errors occurred during the speculative attribution step, etc.
+     */
+    class AnalysisContext {
+        /** Map from trees to analyzers. */
+        Map<JCTree, StatementAnalyzer<JCTree, JCTree>> treesToAnalyzer = new HashMap<>();
+
+        /** Map from original AST nodes to rewritten AST nodes */
+        Map<JCTree, JCTree> treeMap = new HashMap<>();
+
+        /** Errors in rewritten tree */
+        ListBuffer<JCDiagnostic> errors = new ListBuffer<>();
+    }
+
+    /**
+     * Subclass of {@link com.sun.tools.javac.tree.TreeScanner} which visit AST-nodes w/o crossing
+     * statement boundaries.
+     */
+    class StatementScanner extends TreeScanner {
+
+        /** context */
+        AnalysisContext context;
+
+        StatementScanner(AnalysisContext context) {
+            this.context = context;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public void scan(JCTree tree) {
+            if (tree != null) {
+                for (StatementAnalyzer<JCTree, JCTree> analyzer : analyzers) {
+                    if (analyzer.isEnabled() &&
+                            tree.hasTag(analyzer.tag) &&
+                            analyzer.match(tree)) {
+                        context.treesToAnalyzer.put(tree, analyzer);
+                        break; //TODO: cover cases where multiple matching analyzers are found
+                    }
+                }
+            }
+            super.scan(tree);
+        }
+
+        @Override
+        public void visitClassDef(JCClassDecl tree) {
+            //do nothing (prevents seeing same stuff twice
+        }
+
+        @Override
+        public void visitMethodDef(JCMethodDecl tree) {
+            //do nothing (prevents seeing same stuff twice
+        }
+
+        @Override
+        public void visitBlock(JCBlock tree) {
+            //do nothing (prevents seeing same stuff twice
+        }
+
+        @Override
+        public void visitSwitch(JCSwitch tree) {
+            scan(tree.getExpression());
+        }
+
+        @Override
+        public void visitForLoop(JCForLoop tree) {
+            scan(tree.getInitializer());
+            scan(tree.getCondition());
+            scan(tree.getUpdate());
+        }
+
+        @Override
+        public void visitForeachLoop(JCEnhancedForLoop tree) {
+            scan(tree.getExpression());
+        }
+
+        @Override
+        public void visitWhileLoop(JCWhileLoop tree) {
+            scan(tree.getCondition());
+        }
+
+        @Override
+        public void visitDoLoop(JCDoWhileLoop tree) {
+            scan(tree.getCondition());
+        }
+
+        @Override
+        public void visitIf(JCIf tree) {
+            scan(tree.getCondition());
+        }
+    }
+
+    /**
+     * Subclass of TreeCopier that maps nodes matched by analyzers onto new AST nodes.
+     */
+    class TreeMapper extends TreeCopier<Void> {
+
+        AnalysisContext context;
+
+        TreeMapper(AnalysisContext context) {
+            super(make);
+            this.context = context;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <Z extends JCTree> Z copy(Z tree, Void _unused) {
+            Z newTree = super.copy(tree, _unused);
+            StatementAnalyzer<JCTree, JCTree> analyzer = context.treesToAnalyzer.get(tree);
+            if (analyzer != null) {
+                newTree = (Z)analyzer.map(tree, newTree);
+                context.treeMap.put(tree, newTree);
+            }
+            return newTree;
+        }
+    }
+}