/* * Copyright (c) 2014, 2018, 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.ArrayDeque; import java.util.Queue; import com.sun.source.tree.LambdaExpressionTree; import com.sun.tools.javac.code.Source; import com.sun.tools.javac.code.Source.Feature; 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.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.Options; import com.sun.tools.javac.util.Position; 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.FOREACHLOOP; 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 AnalyzerCopier copier; 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); copier = new AnalyzerCopier(); Options options = Options.instance(context); String findOpt = options.get("find"); //parse modes Source source = Source.instance(context); allowDiamondWithAnonymousClassCreation = Feature.DIAMOND_WITH_ANONYMOUS_CLASS_CREATION.allowedInSource(source); 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", Feature.DIAMOND), LAMBDA("lambda", Feature.LAMBDA), METHOD("method", Feature.GRAPH_INFERENCE), LOCAL("local", Feature.LOCAL_VARIABLE_TYPE_INFERENCE); final String opt; final Feature feature; AnalyzerMode(String opt, Feature feature) { this.opt = opt; this.feature = feature; } /** * 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.feature.allowedInSource(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(s) */ abstract List rewrite(S oldTree); /** * 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 List rewrite(JCNewClass oldTree) { if (oldTree.clazz.hasTag(TYPEAPPLY)) { JCNewClass nc = copier.copy(oldTree); ((JCTypeApply)nc.clazz).arguments = List.nil(); return List.of(nc); } else { return List.of(oldTree); } } @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 List rewrite(JCNewClass oldTree){ JCMethodDecl md = (JCMethodDecl)copier.copy(decls(oldTree.def).head); List params = md.params; JCBlock body = md.body; JCLambda newTree = make.at(oldTree).Lambda(params, body); return List.of(newTree); } @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 List rewrite(JCMethodInvocation oldTree){ JCMethodInvocation app = copier.copy(oldTree); app.typeargs = List.nil(); return List.of(app); } @Override void process (JCMethodInvocation oldTree, JCMethodInvocation newTree, boolean hasErrors){ if (!hasErrors) { //exact match log.warning(oldTree, Warnings.MethodRedundantTypeargs); } } } /** * Base class for local variable inference analyzers. */ abstract class RedundantLocalVarTypeAnalyzerBase extends StatementAnalyzer { RedundantLocalVarTypeAnalyzerBase(JCTree.Tag tag) { super(AnalyzerMode.LOCAL, tag); } boolean isImplicitlyTyped(JCVariableDecl decl) { return decl.vartype.pos == Position.NOPOS; } /** * Map a variable tree into a new declaration using implicit type. */ JCVariableDecl rewriteVarType(JCVariableDecl oldTree) { JCVariableDecl newTree = copier.copy(oldTree); newTree.vartype = null; return newTree; } /** * Analyze results of local variable inference. */ void processVar(JCVariableDecl oldTree, JCVariableDecl newTree, boolean hasErrors) { if (!hasErrors) { if (types.isSameType(oldTree.type, newTree.type)) { log.warning(oldTree, Warnings.LocalRedundantType); } } } } /** * This analyzer checks if a local variable declaration has redundant type. */ class RedundantLocalVarTypeAnalyzer extends RedundantLocalVarTypeAnalyzerBase { RedundantLocalVarTypeAnalyzer() { super(VARDEF); } boolean match(JCVariableDecl tree){ return tree.sym.owner.kind == Kind.MTH && tree.init != null && !isImplicitlyTyped(tree) && attr.canInferLocalVarType(tree) == null; } @Override List rewrite(JCVariableDecl oldTree) { return List.of(rewriteVarType(oldTree)); } @Override void process(JCVariableDecl oldTree, JCVariableDecl newTree, boolean hasErrors){ processVar(oldTree, newTree, hasErrors); } } /** * This analyzer checks if a for each variable declaration has redundant type. */ class RedundantLocalVarTypeAnalyzerForEach extends RedundantLocalVarTypeAnalyzerBase { RedundantLocalVarTypeAnalyzerForEach() { super(FOREACHLOOP); } @Override boolean match(JCEnhancedForLoop tree){ return !isImplicitlyTyped(tree.var); } @Override List rewrite(JCEnhancedForLoop oldTree) { JCEnhancedForLoop newTree = copier.copy(oldTree); newTree.var = rewriteVarType(oldTree.var); newTree.body = make.at(oldTree.body).Block(0, List.nil()); return List.of(newTree); } @Override void process(JCEnhancedForLoop oldTree, JCEnhancedForLoop newTree, boolean hasErrors){ processVar(oldTree.var, newTree.var, hasErrors); } } @SuppressWarnings({"unchecked", "rawtypes"}) StatementAnalyzer[] analyzers = new StatementAnalyzer[] { new DiamondInitializer(), new LambdaAnalyzer(), new RedundantTypeArgAnalyzer(), new RedundantLocalVarTypeAnalyzer(), new RedundantLocalVarTypeAnalyzerForEach() }; /** * 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) { StatementScanner statementScanner = new StatementScanner(statement, env); statementScanner.scan(); if (!statementScanner.rewritings.isEmpty()) { for (RewritingContext rewriting : statementScanner.rewritings) { deferredAnalysisHelper.queue(rewriting); } } } /** * Helper interface to handle deferral of analysis tasks. */ interface DeferredAnalysisHelper { /** * Add a new analysis task to the queue. */ void queue(RewritingContext rewriting); /** * Flush queue with given attribution env. */ void flush(Env flushEnv); } /** * Dummy deferral handler. */ DeferredAnalysisHelper flushDeferredHelper = new DeferredAnalysisHelper() { @Override public void queue(RewritingContext rewriting) { //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(RewritingContext rewriting) { Queue s = Q.computeIfAbsent(rewriting.env.enclClass.sym.outermostClass(), k -> new ArrayDeque<>()); s.add(rewriting); } @Override public void flush(Env flushEnv) { if (!Q.isEmpty()) { DeferredAnalysisHelper prevHelper = deferredAnalysisHelper; try { deferredAnalysisHelper = flushDeferredHelper; Queue rewritings = Q.get(flushEnv.enclClass.sym.outermostClass()); while (rewritings != null && !rewritings.isEmpty()) { doAnalysis(rewritings.remove()); } } finally { deferredAnalysisHelper = prevHelper; } } } }; DeferredAnalysisHelper deferredAnalysisHelper = queueDeferredHelper; void doAnalysis(RewritingContext rewriting) { DiagnosticSource prevSource = log.currentSource(); LocalCacheContext localCacheContext = argumentAttr.withLocalCacheContext(); try { log.useSource(rewriting.env.toplevel.getSourceFile()); JCStatement treeToAnalyze = (JCStatement)rewriting.originalTree; if (rewriting.env.info.scope.owner.kind == Kind.TYP) { //add a block to hoist potential dangling variable declarations treeToAnalyze = make.at(Position.NOPOS) .Block(Flags.SYNTHETIC, List.of((JCStatement)rewriting.originalTree)); } //TODO: to further refine the analysis, try all rewriting combinations deferredAttr.attribSpeculative(treeToAnalyze, rewriting.env, attr.statInfo, new TreeRewriter(rewriting), t -> rewriting.diagHandler(), argumentAttr.withLocalCacheContext()); rewriting.analyzer.process(rewriting.oldTree, rewriting.replacement, rewriting.erroneous); } catch (Throwable ex) { Assert.error("Analyzer error when processing: " + rewriting.originalTree); } finally { log.useSource(prevSource.getFile()); localCacheContext.leave(); } } public void flush(Env flushEnv) { deferredAnalysisHelper.flush(flushEnv); } /** * Subclass of {@link com.sun.tools.javac.tree.TreeScanner} which visit AST-nodes w/o crossing * statement boundaries. */ class StatementScanner extends TreeScanner { /** Tree rewritings (generated by analyzers). */ ListBuffer rewritings = new ListBuffer<>(); JCTree originalTree; Env env; StatementScanner(JCTree originalTree, Env env) { this.originalTree = originalTree; this.env = attr.copyEnv(env); } public void scan() { scan(originalTree); } @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)) { for (JCTree t : analyzer.rewrite(tree)) { rewritings.add(new RewritingContext(originalTree, tree, t, analyzer, env)); } 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()); } } class RewritingContext { // the whole tree being analyzed JCTree originalTree; // a subtree, old tree, that will be rewritten JCTree oldTree; // the replacement for the old tree JCTree replacement; // did the compiler find any error boolean erroneous; // the env Env env; // the corresponding analyzer StatementAnalyzer analyzer; RewritingContext( JCTree originalTree, JCTree oldTree, JCTree replacement, StatementAnalyzer analyzer, Env env) { this.originalTree = originalTree; this.oldTree = oldTree; this.replacement = replacement; this.analyzer = analyzer; this.env = attr.copyEnv(env); /* this is a temporary workaround that should be removed once we have a truly independent * clone operation */ if (originalTree.hasTag(VARDEF)) { // avoid redefinition clashes this.env.info.scope.remove(((JCVariableDecl)originalTree).sym); } } /** * Simple deferred diagnostic handler which filters out all messages and keep track of errors. */ Log.DeferredDiagnosticHandler diagHandler() { return new Log.DeferredDiagnosticHandler(log, d -> { if (d.getType() == DiagnosticType.ERROR) { erroneous = true; } return true; }); } } /** * Subclass of TreeCopier that maps nodes matched by analyzers onto new AST nodes. */ class AnalyzerCopier extends TreeCopier { public AnalyzerCopier() { super(make); } @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; } } class TreeRewriter extends AnalyzerCopier { RewritingContext rewriting; TreeRewriter(RewritingContext rewriting) { this.rewriting = rewriting; } @Override @SuppressWarnings("unchecked") public Z copy(Z tree, Void _unused) { Z newTree = super.copy(tree, null); if (tree != null && tree == rewriting.oldTree) { Assert.checkNonNull(rewriting.replacement); newTree = (Z)rewriting.replacement; } return newTree; } } }