/* * Copyright (c) 2014, 2017, 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 java.util.ArrayList; 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.resources.CompilerProperties.Warnings; 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 com.sun.source.tree.NewClassTree; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Kinds.Kind; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.tree.JCTree.JCTry; import com.sun.tools.javac.tree.JCTree.JCUnary; import com.sun.tools.javac.util.Assert; import com.sun.tools.javac.util.DiagnosticSource; import static com.sun.tools.javac.code.Flags.GENERATEDCONSTR; 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.LABELLED; 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.NULLCHK; import static com.sun.tools.javac.tree.JCTree.Tag.TYPEAPPLY; import static com.sun.tools.javac.tree.JCTree.Tag.VARDEF; /** * 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 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 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 sourceFilter; AnalyzerMode(String opt, Predicate 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 getAnalyzerModes(String opt, Source source) { if (opt == null) { return EnumSet.noneOf(AnalyzerMode.class); } List modes = List.from(opt.split(",")); EnumSet 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 { 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 { 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 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, Warnings.DiamondRedundantArgs); } } } /** * This analyzer checks if anonymous instance creation expression can replaced by lambda. */ class LambdaAnalyzer extends StatementAnalyzer { 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 decls(JCClassDecl decl) { ListBuffer 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 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, Warnings.PotentialLambdaFound); } } } /** * This analyzer checks if generic method call has redundant type arguments. */ class RedundantTypeArgAnalyzer extends StatementAnalyzer { 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, Warnings.MethodRedundantTypeargs); } } } @SuppressWarnings({"unchecked", "rawtypes"}) StatementAnalyzer[] analyzers = new StatementAnalyzer[] { new DiamondInitializer(), new LambdaAnalyzer(), new RedundantTypeArgAnalyzer() }; /** * Create a copy of Env if needed. */ Env copyEnvIfNeeded(JCTree tree, Env env) { if (!analyzerModes.isEmpty() && !env.info.isSpeculative && TreeInfo.isStatement(tree) && !tree.hasTag(LABELLED)) { Env analyzeEnv = env.dup(env.tree, env.info.dup(env.info.scope.dupUnshared(env.info.scope.owner))); analyzeEnv.info.returnResult = analyzeEnv.info.returnResult != null ? attr.new ResultInfo(analyzeEnv.info.returnResult.pkind, analyzeEnv.info.returnResult.pt) : null; return analyzeEnv; } else { return null; } } /** * Analyze an AST node if needed. */ void analyzeIfNeeded(JCTree tree, Env env) { if (env != null) { 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 env) { AnalysisContext context = new AnalysisContext(statement, env); StatementScanner statementScanner = new StatementScanner(context); statementScanner.scan(statement); if (!context.treesToAnalyzer.isEmpty()) { deferredAnalysisHelper.queue(context); } } /** * Helper interface to handle deferral of analysis tasks. */ interface DeferredAnalysisHelper { /** * Add a new analysis task to the queue. */ void queue(AnalysisContext context); /** * Flush queue with given attribution env. */ void flush(Env flushEnv); } /** * Dummy deferral handler. */ DeferredAnalysisHelper flushDeferredHelper = new DeferredAnalysisHelper() { @Override public void queue(AnalysisContext context) { //do nothing } @Override public void flush(Env flushEnv) { //do nothing } }; /** * Simple deferral handler. All tasks belonging to the same outermost class are added to * the same queue. The queue is flushed after flow analysis (only if no error occurred). */ DeferredAnalysisHelper queueDeferredHelper = new DeferredAnalysisHelper() { Map> Q = new HashMap<>(); @Override public void queue(AnalysisContext context) { ArrayList s = Q.computeIfAbsent(context.env.enclClass.sym.outermostClass(), k -> new ArrayList<>()); s.add(context); } @Override public void flush(Env flushEnv) { if (!Q.isEmpty()) { DeferredAnalysisHelper prevHelper = deferredAnalysisHelper; try { deferredAnalysisHelper = flushDeferredHelper; ArrayList s = Q.get(flushEnv.enclClass.sym.outermostClass()); while (s != null && !s.isEmpty()) { doAnalysis(s.remove(0)); } } finally { deferredAnalysisHelper = prevHelper; } } } }; DeferredAnalysisHelper deferredAnalysisHelper = queueDeferredHelper; void doAnalysis(AnalysisContext context) { DiagnosticSource prevSource = log.currentSource(); LocalCacheContext localCacheContext = argumentAttr.withLocalCacheContext(); try { log.useSource(context.env.toplevel.getSourceFile()); JCStatement treeToAnalyze = (JCStatement)context.tree; if (context.env.info.scope.owner.kind == Kind.TYP) { //add a block to hoist potential dangling variable declarations treeToAnalyze = make.Block(Flags.SYNTHETIC, List.of((JCStatement)context.tree)); } TreeMapper treeMapper = new TreeMapper(context); //TODO: to further refine the analysis, try all rewriting combinations deferredAttr.attribSpeculative(treeToAnalyze, context.env, attr.statInfo, treeMapper, t -> new AnalyzeDeferredDiagHandler(context), argumentAttr.withLocalCacheContext()); context.treeMap.entrySet().forEach(e -> { context.treesToAnalyzer.get(e.getKey()) .process(e.getKey(), e.getValue(), context.errors.nonEmpty()); }); } catch (Throwable ex) { Assert.error("Analyzer error when processing: " + context.tree); } finally { log.useSource(prevSource.getFile()); localCacheContext.leave(); } } public void flush(Env flushEnv) { deferredAnalysisHelper.flush(flushEnv); } /** * 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 { JCTree tree; Env env; AnalysisContext(JCTree tree, Env env) { this.tree = tree; this.env = attr.copyEnv(env); /* this is a temporary workaround that should be removed once we have a truly independent * clone operation */ if (tree.hasTag(VARDEF)) { // avoid redefinition clashes this.env.info.scope.remove(((JCVariableDecl)tree).sym); } } /** Map from trees to analyzers. */ Map> treesToAnalyzer = new HashMap<>(); /** Map from original AST nodes to rewritten AST nodes */ Map treeMap = new HashMap<>(); /** Errors in rewritten tree */ ListBuffer 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 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) { //skip body and var decl (to prevents same statements to be analyzed twice) scan(tree.getCondition()); scan(tree.getUpdate()); } @Override public void visitTry(JCTry tree) { //skip resources (to prevents same statements to be analyzed twice) scan(tree.getBlock()); scan(tree.getCatches()); scan(tree.getFinallyBlock()); } @Override public void visitForeachLoop(JCEnhancedForLoop tree) { //skip body (to prevents same statements to be analyzed twice) scan(tree.getExpression()); } @Override public void visitWhileLoop(JCWhileLoop tree) { //skip body (to prevents same statements to be analyzed twice) scan(tree.getCondition()); } @Override public void visitDoLoop(JCDoWhileLoop tree) { //skip body (to prevents same statements to be analyzed twice) scan(tree.getCondition()); } @Override public void visitIf(JCIf tree) { //skip body (to prevents same statements to be analyzed twice) scan(tree.getCondition()); } } /** * Subclass of TreeCopier that maps nodes matched by analyzers onto new AST nodes. */ class TreeMapper extends TreeCopier { AnalysisContext context; TreeMapper(AnalysisContext context) { super(make); this.context = context; } @Override @SuppressWarnings("unchecked") public Z copy(Z tree, Void _unused) { Z newTree = super.copy(tree, _unused); StatementAnalyzer 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; } @Override @DefinedBy(Api.COMPILER_TREE) public JCTree visitNewClass(NewClassTree node, Void aVoid) { JCNewClass oldNewClazz = (JCNewClass)node; JCNewClass newNewClazz = (JCNewClass)super.visitNewClass(node, aVoid); if (!oldNewClazz.args.isEmpty() && oldNewClazz.args.head.hasTag(NULLCHK)) { //workaround to Attr generating trees newNewClazz.encl = ((JCUnary)newNewClazz.args.head).arg; newNewClazz.args = newNewClazz.args.tail; } return newNewClazz; } } }