/* * Copyright (c) 2014, 2015, 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.source.tree.LambdaExpressionTree; 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.comp.ArgumentAttr.LocalCacheContext; 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.JCLambda.ParameterKind; 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.DefinedBy; import com.sun.tools.javac.util.DefinedBy.Api; 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.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 ArgumentAttr argumentAttr; final TreeMaker make; final Names names; private final boolean allowDiamondWithAnonymousClassCreation; 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); argumentAttr = ArgumentAttr.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); allowDiamondWithAnonymousClassCreation = source.allowDiamondWithAnonymousClassCreation(); 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 || allowDiamondWithAnonymousClassCreation); } @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, explicitArgs; if (oldTree.def != null) { inferredArgs = newTree.def.implementing.nonEmpty() ? newTree.def.implementing.get(0).type.getTypeArguments() : newTree.def.extending.type.getTypeArguments(); explicitArgs = oldTree.def.implementing.nonEmpty() ? oldTree.def.implementing.get(0).type.getTypeArguments() : oldTree.def.extending.type.getTypeArguments(); } else { inferredArgs = newTree.type.getTypeArguments(); explicitArgs = oldTree.type.getTypeArguments(); } for (Type t : inferredArgs) { if (!types.isSameType(t, explicitArgs.head)) { 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 LocalCacheContext localCacheContext = argumentAttr.withLocalCacheContext(); try { deferredAttr.attribSpeculative(fakeBlock, env, attr.statInfo, treeMapper, t -> new AnalyzeDeferredDiagHandler(context)); } finally { localCacheContext.leave(); } 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; } @Override @DefinedBy(Api.COMPILER_TREE) public JCTree visitLambdaExpression(LambdaExpressionTree node, Void _unused) { JCLambda oldLambda = (JCLambda)node; JCLambda newLambda = (JCLambda)super.visitLambdaExpression(node, _unused); if (oldLambda.paramKind == ParameterKind.IMPLICIT) { //reset implicit lambda parameters (whose type might have been set during attr) newLambda.paramKind = ParameterKind.IMPLICIT; newLambda.params.forEach(p -> p.vartype = null); } return newLambda; } } }