Analyzer.java revision 2868:816bd88d33a8
1/*
2 * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.tools.javac.comp;
27
28import com.sun.source.tree.LambdaExpressionTree;
29import com.sun.tools.javac.code.Source;
30import com.sun.tools.javac.code.Type;
31import com.sun.tools.javac.code.Types;
32import com.sun.tools.javac.tree.JCTree;
33import com.sun.tools.javac.tree.JCTree.JCBlock;
34import com.sun.tools.javac.tree.JCTree.JCClassDecl;
35import com.sun.tools.javac.tree.JCTree.JCDoWhileLoop;
36import com.sun.tools.javac.tree.JCTree.JCEnhancedForLoop;
37import com.sun.tools.javac.tree.JCTree.JCForLoop;
38import com.sun.tools.javac.tree.JCTree.JCIf;
39import com.sun.tools.javac.tree.JCTree.JCLambda;
40import com.sun.tools.javac.tree.JCTree.JCLambda.ParameterKind;
41import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
42import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
43import com.sun.tools.javac.tree.JCTree.JCNewClass;
44import com.sun.tools.javac.tree.JCTree.JCStatement;
45import com.sun.tools.javac.tree.JCTree.JCSwitch;
46import com.sun.tools.javac.tree.JCTree.JCTypeApply;
47import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
48import com.sun.tools.javac.tree.JCTree.JCWhileLoop;
49import com.sun.tools.javac.tree.JCTree.Tag;
50import com.sun.tools.javac.tree.TreeCopier;
51import com.sun.tools.javac.tree.TreeInfo;
52import com.sun.tools.javac.tree.TreeMaker;
53import com.sun.tools.javac.tree.TreeScanner;
54import com.sun.tools.javac.util.Context;
55import com.sun.tools.javac.util.DefinedBy;
56import com.sun.tools.javac.util.DefinedBy.Api;
57import com.sun.tools.javac.util.Filter;
58import com.sun.tools.javac.util.JCDiagnostic;
59import com.sun.tools.javac.util.JCDiagnostic.DiagnosticType;
60import com.sun.tools.javac.util.List;
61import com.sun.tools.javac.util.ListBuffer;
62import com.sun.tools.javac.util.Log;
63import com.sun.tools.javac.util.Names;
64import com.sun.tools.javac.util.Options;
65
66import java.util.EnumSet;
67import java.util.HashMap;
68import java.util.Map;
69import java.util.function.Predicate;
70
71import static com.sun.tools.javac.code.Flags.GENERATEDCONSTR;
72import static com.sun.tools.javac.code.Flags.SYNTHETIC;
73import static com.sun.tools.javac.code.TypeTag.CLASS;
74import static com.sun.tools.javac.tree.JCTree.Tag.APPLY;
75import static com.sun.tools.javac.tree.JCTree.Tag.CLASSDEF;
76import static com.sun.tools.javac.tree.JCTree.Tag.METHODDEF;
77import static com.sun.tools.javac.tree.JCTree.Tag.NEWCLASS;
78import static com.sun.tools.javac.tree.JCTree.Tag.TYPEAPPLY;
79
80/**
81 * Helper class for defining custom code analysis, such as finding instance creation expression
82 * that can benefit from diamond syntax.
83 */
84public class Analyzer {
85    protected static final Context.Key<Analyzer> analyzerKey = new Context.Key<>();
86
87    final Types types;
88    final Log log;
89    final Attr attr;
90    final DeferredAttr deferredAttr;
91    final TreeMaker make;
92    final Names names;
93    private final boolean allowDiamondWithAnonymousClassCreation;
94
95    final EnumSet<AnalyzerMode> analyzerModes;
96
97    public static Analyzer instance(Context context) {
98        Analyzer instance = context.get(analyzerKey);
99        if (instance == null)
100            instance = new Analyzer(context);
101        return instance;
102    }
103
104    protected Analyzer(Context context) {
105        context.put(analyzerKey, this);
106        types = Types.instance(context);
107        log = Log.instance(context);
108        attr = Attr.instance(context);
109        deferredAttr = DeferredAttr.instance(context);
110        make = TreeMaker.instance(context);
111        names = Names.instance(context);
112        Options options = Options.instance(context);
113        String findOpt = options.get("find");
114        //parse modes
115        Source source = Source.instance(context);
116        allowDiamondWithAnonymousClassCreation = source.allowDiamondWithAnonymousClassCreation();
117        analyzerModes = AnalyzerMode.getAnalyzerModes(findOpt, source);
118    }
119
120    /**
121     * This enum defines supported analyzer modes, as well as defining the logic for decoding
122     * the {@code -XDfind} option.
123     */
124    enum AnalyzerMode {
125        DIAMOND("diamond", Source::allowDiamond),
126        LAMBDA("lambda", Source::allowLambda),
127        METHOD("method", Source::allowGraphInference);
128
129        final String opt;
130        final Predicate<Source> sourceFilter;
131
132        AnalyzerMode(String opt, Predicate<Source> sourceFilter) {
133            this.opt = opt;
134            this.sourceFilter = sourceFilter;
135        }
136
137        /**
138         * This method is used to parse the {@code find} option.
139         * Possible modes are separated by colon; a mode can be excluded by
140         * prepending '-' to its name. Finally, the special mode 'all' can be used to
141         * add all modes to the resulting enum.
142         */
143        static EnumSet<AnalyzerMode> getAnalyzerModes(String opt, Source source) {
144            if (opt == null) {
145                return EnumSet.noneOf(AnalyzerMode.class);
146            }
147            List<String> modes = List.from(opt.split(","));
148            EnumSet<AnalyzerMode> res = EnumSet.noneOf(AnalyzerMode.class);
149            if (modes.contains("all")) {
150                res = EnumSet.allOf(AnalyzerMode.class);
151            }
152            for (AnalyzerMode mode : values()) {
153                if (modes.contains(mode.opt)) {
154                    res.add(mode);
155                } else if (modes.contains("-" + mode.opt) || !mode.sourceFilter.test(source)) {
156                    res.remove(mode);
157                }
158            }
159            return res;
160        }
161    }
162
163    /**
164     * A statement analyzer is a work-unit that matches certain AST nodes (of given type {@code S}),
165     * rewrites them to different AST nodes (of type {@code T}) and then generates some meaningful
166     * messages in case the analysis has been successful.
167     */
168    abstract class StatementAnalyzer<S extends JCTree, T extends JCTree> {
169
170        AnalyzerMode mode;
171        JCTree.Tag tag;
172
173        StatementAnalyzer(AnalyzerMode mode, Tag tag) {
174            this.mode = mode;
175            this.tag = tag;
176        }
177
178        /**
179         * Is this analyzer allowed to run?
180         */
181        boolean isEnabled() {
182            return analyzerModes.contains(mode);
183        }
184
185        /**
186         * Should this analyzer be rewriting the given tree?
187         */
188        abstract boolean match(S tree);
189
190        /**
191         * Rewrite a given AST node into a new one
192         */
193        abstract T map(S oldTree, S newTree);
194
195        /**
196         * Entry-point for comparing results and generating diagnostics.
197         */
198        abstract void process(S oldTree, T newTree, boolean hasErrors);
199
200    }
201
202    /**
203     * This analyzer checks if generic instance creation expression can use diamond syntax.
204     */
205    class DiamondInitializer extends StatementAnalyzer<JCNewClass, JCNewClass> {
206
207        DiamondInitializer() {
208            super(AnalyzerMode.DIAMOND, NEWCLASS);
209        }
210
211        @Override
212        boolean match(JCNewClass tree) {
213            return tree.clazz.hasTag(TYPEAPPLY) &&
214                    !TreeInfo.isDiamond(tree) &&
215                    (tree.def == null || allowDiamondWithAnonymousClassCreation);
216        }
217
218        @Override
219        JCNewClass map(JCNewClass oldTree, JCNewClass newTree) {
220            if (newTree.clazz.hasTag(TYPEAPPLY)) {
221                ((JCTypeApply)newTree.clazz).arguments = List.nil();
222            }
223            return newTree;
224        }
225
226        @Override
227        void process(JCNewClass oldTree, JCNewClass newTree, boolean hasErrors) {
228            if (!hasErrors) {
229                List<Type> inferredArgs = newTree.type.getTypeArguments();
230                List<Type> explicitArgs = oldTree.type.getTypeArguments();
231                for (Type t : inferredArgs) {
232                    if (!types.isSameType(t, explicitArgs.head)) {
233                        log.warning(oldTree.clazz, "diamond.redundant.args.1",
234                                oldTree.clazz.type, newTree.clazz.type);
235                        return;
236                    }
237                    explicitArgs = explicitArgs.tail;
238                }
239                //exact match
240                log.warning(oldTree.clazz, "diamond.redundant.args");
241            }
242        }
243    }
244
245    /**
246     * This analyzer checks if anonymous instance creation expression can replaced by lambda.
247     */
248    class LambdaAnalyzer extends StatementAnalyzer<JCNewClass, JCLambda> {
249
250        LambdaAnalyzer() {
251            super(AnalyzerMode.LAMBDA, NEWCLASS);
252        }
253
254        @Override
255        boolean match (JCNewClass tree){
256            Type clazztype = tree.clazz.type;
257            return tree.def != null &&
258                    clazztype.hasTag(CLASS) &&
259                    types.isFunctionalInterface(clazztype.tsym) &&
260                    decls(tree.def).length() == 1;
261        }
262        //where
263            private List<JCTree> decls(JCClassDecl decl) {
264                ListBuffer<JCTree> decls = new ListBuffer<>();
265                for (JCTree t : decl.defs) {
266                    if (t.hasTag(METHODDEF)) {
267                        JCMethodDecl md = (JCMethodDecl)t;
268                        if ((md.getModifiers().flags & GENERATEDCONSTR) == 0) {
269                            decls.add(md);
270                        }
271                    } else {
272                        decls.add(t);
273                    }
274                }
275                return decls.toList();
276            }
277
278        @Override
279        JCLambda map (JCNewClass oldTree, JCNewClass newTree){
280            JCMethodDecl md = (JCMethodDecl)decls(newTree.def).head;
281            List<JCVariableDecl> params = md.params;
282            JCBlock body = md.body;
283            return make.Lambda(params, body);
284        }
285        @Override
286        void process (JCNewClass oldTree, JCLambda newTree, boolean hasErrors){
287            if (!hasErrors) {
288                log.warning(oldTree.def, "potential.lambda.found");
289            }
290        }
291    }
292
293    /**
294     * This analyzer checks if generic method call has redundant type arguments.
295     */
296    class RedundantTypeArgAnalyzer extends StatementAnalyzer<JCMethodInvocation, JCMethodInvocation> {
297
298        RedundantTypeArgAnalyzer() {
299            super(AnalyzerMode.METHOD, APPLY);
300        }
301
302        @Override
303        boolean match (JCMethodInvocation tree){
304            return tree.typeargs != null &&
305                    tree.typeargs.nonEmpty();
306        }
307        @Override
308        JCMethodInvocation map (JCMethodInvocation oldTree, JCMethodInvocation newTree){
309            newTree.typeargs = List.nil();
310            return newTree;
311        }
312        @Override
313        void process (JCMethodInvocation oldTree, JCMethodInvocation newTree, boolean hasErrors){
314            if (!hasErrors) {
315                //exact match
316                log.warning(oldTree, "method.redundant.typeargs");
317            }
318        }
319    }
320
321    @SuppressWarnings({"unchecked", "rawtypes"})
322    StatementAnalyzer<JCTree, JCTree>[] analyzers = new StatementAnalyzer[] {
323            new DiamondInitializer(),
324            new LambdaAnalyzer(),
325            new RedundantTypeArgAnalyzer()
326    };
327
328    /**
329     * Analyze an AST node if needed.
330     */
331    void analyzeIfNeeded(JCTree tree, Env<AttrContext> env) {
332        if (!analyzerModes.isEmpty() &&
333                !env.info.isSpeculative &&
334                TreeInfo.isStatement(tree)) {
335            JCStatement stmt = (JCStatement)tree;
336            analyze(stmt, env);
337        }
338    }
339
340    /**
341     * Analyze an AST node; this involves collecting a list of all the nodes that needs rewriting,
342     * and speculatively type-check the rewritten code to compare results against previously attributed code.
343     */
344    void analyze(JCStatement statement, Env<AttrContext> env) {
345        AnalysisContext context = new AnalysisContext();
346        StatementScanner statementScanner = new StatementScanner(context);
347        statementScanner.scan(statement);
348
349        if (!context.treesToAnalyzer.isEmpty()) {
350
351            //add a block to hoist potential dangling variable declarations
352            JCBlock fakeBlock = make.Block(SYNTHETIC, List.of(statement));
353
354            TreeMapper treeMapper = new TreeMapper(context);
355            //TODO: to further refine the analysis, try all rewriting combinations
356            deferredAttr.attribSpeculative(fakeBlock, env, attr.statInfo, treeMapper,
357                    t -> new AnalyzeDeferredDiagHandler(context));
358
359            context.treeMap.entrySet().forEach(e -> {
360                context.treesToAnalyzer.get(e.getKey())
361                        .process(e.getKey(), e.getValue(), context.errors.nonEmpty());
362            });
363        }
364    }
365
366    /**
367     * Simple deferred diagnostic handler which filters out all messages and keep track of errors.
368     */
369    class AnalyzeDeferredDiagHandler extends Log.DeferredDiagnosticHandler {
370        AnalysisContext context;
371
372        public AnalyzeDeferredDiagHandler(AnalysisContext context) {
373            super(log, d -> {
374                if (d.getType() == DiagnosticType.ERROR) {
375                    context.errors.add(d);
376                }
377                return true;
378            });
379            this.context = context;
380        }
381    }
382
383    /**
384     * This class is used to pass around contextual information bewteen analyzer classes, such as
385     * trees to be rewritten, errors occurred during the speculative attribution step, etc.
386     */
387    class AnalysisContext {
388        /** Map from trees to analyzers. */
389        Map<JCTree, StatementAnalyzer<JCTree, JCTree>> treesToAnalyzer = new HashMap<>();
390
391        /** Map from original AST nodes to rewritten AST nodes */
392        Map<JCTree, JCTree> treeMap = new HashMap<>();
393
394        /** Errors in rewritten tree */
395        ListBuffer<JCDiagnostic> errors = new ListBuffer<>();
396    }
397
398    /**
399     * Subclass of {@link com.sun.tools.javac.tree.TreeScanner} which visit AST-nodes w/o crossing
400     * statement boundaries.
401     */
402    class StatementScanner extends TreeScanner {
403
404        /** context */
405        AnalysisContext context;
406
407        StatementScanner(AnalysisContext context) {
408            this.context = context;
409        }
410
411        @Override
412        @SuppressWarnings("unchecked")
413        public void scan(JCTree tree) {
414            if (tree != null) {
415                for (StatementAnalyzer<JCTree, JCTree> analyzer : analyzers) {
416                    if (analyzer.isEnabled() &&
417                            tree.hasTag(analyzer.tag) &&
418                            analyzer.match(tree)) {
419                        context.treesToAnalyzer.put(tree, analyzer);
420                        break; //TODO: cover cases where multiple matching analyzers are found
421                    }
422                }
423            }
424            super.scan(tree);
425        }
426
427        @Override
428        public void visitClassDef(JCClassDecl tree) {
429            //do nothing (prevents seeing same stuff twice
430        }
431
432        @Override
433        public void visitMethodDef(JCMethodDecl tree) {
434            //do nothing (prevents seeing same stuff twice
435        }
436
437        @Override
438        public void visitBlock(JCBlock tree) {
439            //do nothing (prevents seeing same stuff twice
440        }
441
442        @Override
443        public void visitSwitch(JCSwitch tree) {
444            scan(tree.getExpression());
445        }
446
447        @Override
448        public void visitForLoop(JCForLoop tree) {
449            scan(tree.getInitializer());
450            scan(tree.getCondition());
451            scan(tree.getUpdate());
452        }
453
454        @Override
455        public void visitForeachLoop(JCEnhancedForLoop tree) {
456            scan(tree.getExpression());
457        }
458
459        @Override
460        public void visitWhileLoop(JCWhileLoop tree) {
461            scan(tree.getCondition());
462        }
463
464        @Override
465        public void visitDoLoop(JCDoWhileLoop tree) {
466            scan(tree.getCondition());
467        }
468
469        @Override
470        public void visitIf(JCIf tree) {
471            scan(tree.getCondition());
472        }
473    }
474
475    /**
476     * Subclass of TreeCopier that maps nodes matched by analyzers onto new AST nodes.
477     */
478    class TreeMapper extends TreeCopier<Void> {
479
480        AnalysisContext context;
481
482        TreeMapper(AnalysisContext context) {
483            super(make);
484            this.context = context;
485        }
486
487        @Override
488        @SuppressWarnings("unchecked")
489        public <Z extends JCTree> Z copy(Z tree, Void _unused) {
490            Z newTree = super.copy(tree, _unused);
491            StatementAnalyzer<JCTree, JCTree> analyzer = context.treesToAnalyzer.get(tree);
492            if (analyzer != null) {
493                newTree = (Z)analyzer.map(tree, newTree);
494                context.treeMap.put(tree, newTree);
495            }
496            return newTree;
497        }
498
499        @Override @DefinedBy(Api.COMPILER_TREE)
500        public JCTree visitLambdaExpression(LambdaExpressionTree node, Void _unused) {
501            JCLambda oldLambda = (JCLambda)node;
502            JCLambda newLambda = (JCLambda)super.visitLambdaExpression(node, _unused);
503            if (oldLambda.paramKind == ParameterKind.IMPLICIT) {
504                //reset implicit lambda parameters (whose type might have been set during attr)
505                newLambda.paramKind = ParameterKind.IMPLICIT;
506                newLambda.params.forEach(p -> p.vartype = null);
507            }
508            return newLambda;
509        }
510    }
511}
512