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