callgraph/src/main/java/org/netbeans/lib/callgraph/Callgraph.java
author Tim Boudreau <tboudreau@netbeans.org>
Sat, 03 Sep 2016 02:41:36 -0400
changeset 18374 04a79821e760
parent 18373 3527a32a19f0
child 18400 c87c223efe6a
permissions -rw-r--r--
Eliminate duplicates in graph files
     1 /* 
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright (C) 1997-2016 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
    31  * Microsystems, Inc. All Rights Reserved.
    32  *
    33  * If you wish your version of this file to be governed by only the CDDL
    34  * or only the GPL Version 2, indicate your decision by adding
    35  * "[Contributor] elects to include this software in this distribution
    36  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    37  * single choice of license, a recipient has the option to distribute
    38  * your version of this file under either the CDDL, the GPL Version 2 or
    39  * to extend the choice of license to its licensees as provided above.
    40  * However, if you add GPL Version 2 code and therefore, elected the GPL
    41  * Version 2 license, then the option applies only if the new code is
    42  * made subject to such option by the copyright holder.
    43  */
    44 package org.netbeans.lib.callgraph;
    45 
    46 import org.netbeans.lib.callgraph.Arguments.InvalidArgumentsException;
    47 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_CLASSGRAPH;
    48 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_DISABLE_EIGHT_BIT_STRINGS;
    49 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_EXCLUDE;
    50 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_MAVEN;
    51 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_METHODGRAPH;
    52 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_NOSELF;
    53 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_OMIT_ABSTRACT;
    54 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_PACKAGEGRAPH;
    55 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_QUIET;
    56 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_REVERSE;
    57 import static org.netbeans.lib.callgraph.CallgraphControl.CMD_SIMPLE;
    58 import org.netbeans.lib.callgraph.io.JavaFilesIterator;
    59 import org.netbeans.lib.callgraph.javac.JavacRunner;
    60 import org.netbeans.lib.callgraph.javac.SourceElement;
    61 import org.netbeans.lib.callgraph.javac.SourcesInfo;
    62 import org.netbeans.lib.callgraph.util.MergeIterator;
    63 import java.io.File;
    64 import java.io.IOException;
    65 import java.io.PrintStream;
    66 import java.util.ArrayList;
    67 import java.util.Collections;
    68 import java.util.HashSet;
    69 import java.util.Iterator;
    70 import java.util.LinkedList;
    71 import java.util.List;
    72 import java.util.Set;
    73 import java.util.concurrent.atomic.AtomicReference;
    74 import java.util.function.Consumer;
    75 
    76 /**
    77  * Scans a source folder and runs javac against any Java sources present, and
    78  * builds graphs of what calls what within those. The library classpath does not
    79  * need to be set, and the folders do not need to be package roots - we are
    80  * instructing javac to treat errors as non-fatal. The available sources will be
    81  * attributed.
    82  *
    83  * @author Tim Boudreau
    84  */
    85 public final class Callgraph {
    86 
    87     private boolean noself;
    88     private boolean maven;
    89     private boolean quiet;
    90     private boolean simple;
    91     private final Set<String> excludes = new HashSet<>();
    92     private final Set<File> folders = new HashSet<>();
    93     private File classgraphFile;
    94     private File methodgraphFile;
    95     private File packagegraphFile;
    96     private Listener listener;
    97     private boolean omitAbstract;
    98     private boolean useJavaStrings;
    99     private boolean reverse;
   100 
   101     private Callgraph() {
   102     }
   103 
   104     /**
   105      * Configure a Callgraph to build.
   106      *
   107      * @return A call graph to configure
   108      */
   109     public static Callgraph configure() {
   110         return new Callgraph();
   111     }
   112 
   113     public static void main(String[] args) throws IOException {
   114         CallgraphControl arguments = null;
   115         try {
   116             arguments = new Arguments(args);
   117         } catch (InvalidArgumentsException ex) {
   118             // this will be a help message describing usage and the invalid
   119             // arguments
   120             System.err.println(ex.getMessage());
   121             System.exit(1);
   122         }
   123         assert arguments != null;
   124         invoke(arguments, arguments.isVerbose() ? new LoggingListener() : null);
   125     }
   126 
   127     /**
   128      * Run javac and produce output.
   129      *
   130      * @param arguments The parsed arguments
   131      * @return The list of all methods found, sorted by qname
   132      * @throws IOException If i/o fails
   133      */
   134     static List<SourceElement> invoke(CallgraphControl arguments, Listener listener) throws IOException {
   135         SourcesInfo info = new SourcesInfo(arguments.isDisableEightBitStrings(), arguments.isAggressive());
   136         // Build an iterable of all Java sources (without collecting them all ahead of time)
   137         List<Iterable<File>> iterables = new LinkedList<>();
   138         for (File folder : arguments) {
   139             iterables.add(JavaFilesIterator.iterable(folder));
   140         }
   141         // The thing that will run javac
   142         JavacRunner runner = new JavacRunner(info, MergeIterator.toIterable(iterables), listener);
   143         AtomicReference<File> lastFile = new AtomicReference<>();
   144         Consumer<File> monitor = lastFile::set;
   145         // run javac
   146         Set<SourceElement> allElements = runner.go(monitor, lastFile);
   147 
   148         List<SourceElement> all = new ArrayList<>(allElements);
   149         // Sort, so textual output is more human-friendly
   150         Collections.sort(all);
   151         // Now write files and print output
   152         if (!all.isEmpty()) {
   153             PrintStream outStream = createPrintStreamIfNotNull(arguments.methodGraphFile());
   154             PrintStream packageStream = createPrintStreamIfNotNull(arguments.packageGraphFile());
   155             PrintStream classStream = createPrintStreamIfNotNull(arguments.classGraphFile());
   156             // duplicate avoidance
   157             Set<CharSequence> emittedPackageLines = new HashSet<>();
   158             Set<CharSequence> emittedClassLines = new HashSet<>();
   159             List<Object> clazz = new ArrayList<>(5);
   160             CharSequence lastClass = null;
   161 
   162             List<Object> pkg = new ArrayList<>(5);
   163             CharSequence lastPackage = null;
   164             
   165             try {
   166                 // Iterate every method
   167                 outer:
   168                 for (SourceElement sce : all) {
   169                     if (arguments.isExcluded(sce.qname().toString())) { // Ignore matches
   170                         continue;
   171                     }
   172                     List<SourceElement> outbounds = new ArrayList<>(arguments.isReverse() ? sce.getInboundReferences() : sce.getOutboundReferences());
   173                     Collections.sort(outbounds); // also sort connections
   174                     // Iterate the current method's connections
   175                     List<Object> mth = new ArrayList<>(5);
   176                     CharSequence currClazz = arguments.isShortNames() ? sce.typeName() : info.strings.concat(sce.packageName(), info.strings.DOT, sce.typeName());
   177                     if (!currClazz.equals(lastClass)) {
   178                         if (classStream != null) {
   179                             writeLine(clazz, info, emittedClassLines, classStream);
   180                         }
   181                         clazz.clear();
   182                         lastClass = currClazz;
   183                     }
   184                     if (clazz.isEmpty()) {
   185                         clazz.add(currClazz);
   186                         if (arguments.isExtendedProperties()) {
   187                             clazz.add(sce.isAbstract());
   188                         }
   189                     }
   190                     CharSequence currPkg = sce.packageName();
   191                     if (pkg.isEmpty()) {
   192                         pkg.add(currPkg);
   193                     }
   194                     if (!currPkg.equals(lastPackage)) {
   195                         if (packageStream != null) {
   196                             writeLine(pkg, info, emittedPackageLines, packageStream);
   197                         }
   198                         lastPackage = currPkg;
   199                         pkg.clear();
   200                     }
   201                     for (SourceElement outbound : outbounds) {
   202                         if (arguments.isExcluded(outbound.qname().toString())) { // Ignore matches
   203                             continue;
   204                         }
   205                         // If we are ignoring abstract methods, do that - has no effect on classes
   206                         if (arguments.isOmitAbstract() && (outbound.isAbstract() | sce.isAbstract())) {
   207                             continue;
   208                         }
   209                         // If the argument is set, ignore cases where it's a class' method
   210                         // referencing another method on that class
   211                         if (!arguments.isSelfReferences() && sce.typeName().equals(outbound.typeName())) {
   212                             continue;
   213                         }
   214                         if (outStream != null || !arguments.isQuiet()) {
   215                             if (arguments.isShortNames()) {
   216                                 mth.add(outbound.shortName());
   217                             } else {
   218                                 mth.add(outbound.qname());
   219                             }
   220                             if (arguments.isExtendedProperties()) {
   221                                 mth.add(outbound.isAbstract());
   222                             }
   223                         }
   224                         // Build the package graph output if necessary
   225                         if (packageStream != null) {
   226                             if (!outbound.packageName().equals(currPkg) || arguments.isSelfReferences()) {
   227                                 pkg.add(outbound.packageName());
   228                             }
   229                         }
   230                         // Build the class graph output if necessary
   231                         if (classStream != null) {
   232                             CharSequence type1 = sce.typeName();
   233                             CharSequence type2 = outbound.typeName();
   234                             if (!arguments.isShortNames()) {
   235 //                                type1 = info.strings.concat(sce.packageName(), info.strings.DOT, type1);
   236                                 type2 = info.strings.concat(outbound.packageName(), info.strings.DOT, type2);
   237                             }
   238                             if (!type1.equals(type2) && !clazz.contains(type2)) {
   239                                 clazz.add(type2);
   240                                 if (arguments.isExtendedProperties()) {
   241                                     clazz.add(outbound.isAbstract());
   242                                 }
   243                             }
   244                         }
   245                     }
   246                     if (!arguments.isQuiet() || outStream != null) {
   247                         if (!mth.isEmpty()) {
   248                             CharSequence nm = arguments.isShortNames() ? sce.shortName() : sce.qname();
   249                             List<Object> l = mth;
   250                             if (arguments.isExtendedProperties()) {
   251                                 mth.add(0, sce.isAbstract());
   252                             }
   253                             l.add(0, nm);
   254                             CharSequence line = info.strings.concatQuoted(l);
   255                             if (!arguments.isQuiet()) {
   256                                 System.out.println(line);
   257                             }
   258                             if (outStream != null) {
   259                                 outStream.println(line);
   260                             }
   261                         }
   262                     }
   263                 }
   264                 if (classStream != null && !clazz.isEmpty()) {
   265                     writeLine(clazz, info, emittedClassLines, classStream);
   266                 }
   267                 if (packageStream != null && !pkg.isEmpty()) {
   268                     writeLine(pkg, info, emittedPackageLines, packageStream);
   269                 }
   270             } finally {
   271                 for (PrintStream ps : new PrintStream[]{outStream, packageStream, classStream}) {
   272                     if (ps != null) {
   273                         ps.close();
   274                     }
   275                 }
   276             }
   277         }
   278         return all;
   279     }
   280     private static void writeLine(List<Object> clazz, SourcesInfo info, Set<CharSequence> emittedClassLines, PrintStream classStream) {
   281         if (!clazz.isEmpty()) {
   282             CharSequence cs = info.strings.concatQuoted(clazz);
   283             if (!emittedClassLines.contains(cs)) {
   284                 classStream.println(cs);
   285                 emittedClassLines.add(cs);
   286             }
   287         }
   288     }
   289 
   290     private static PrintStream createPrintStreamIfNotNull(File outputFile) throws IOException {
   291         PrintStream outStream = null;
   292         if (outputFile != null) {
   293             if (!outputFile.exists()) {
   294                 if (!outputFile.createNewFile()) {
   295                     throw new IllegalStateException("Could not create " + outputFile);
   296                 }
   297             }
   298             outStream = new PrintStream(outputFile);
   299         }
   300         return outStream;
   301     }
   302 
   303     public List<String> toCommandLineArguments() {
   304         List<String> args = new ArrayList<>();
   305         addBoolean(CMD_NOSELF, noself, args);
   306         addBoolean(CMD_MAVEN, maven, args);
   307         addBoolean(CMD_QUIET, quiet, args);
   308         addBoolean(CMD_SIMPLE, simple, args);
   309         addBoolean(CMD_REVERSE, reverse, args);
   310         addBoolean(CMD_DISABLE_EIGHT_BIT_STRINGS, useJavaStrings, args);
   311         addBoolean(CMD_OMIT_ABSTRACT, omitAbstract, args);
   312         if (!excludes.isEmpty()) {
   313             StringBuilder concat = new StringBuilder();
   314             for (Iterator<String> it = excludes.iterator(); it.hasNext();) {
   315                 String next = it.next();
   316                 concat.append(next);
   317                 if (it.hasNext()) {
   318                     concat.append(",");
   319                 }
   320             }
   321             args.add("--" + CMD_EXCLUDE);
   322             args.add(concat.toString());
   323         }
   324         addFile(CMD_CLASSGRAPH, classgraphFile, args);
   325         addFile(CMD_METHODGRAPH, methodgraphFile, args);
   326         addFile(CMD_PACKAGEGRAPH, packagegraphFile, args);
   327         for (File fld : folders) {
   328             args.add(fld.getAbsolutePath());
   329         }
   330         return args;
   331     }
   332 
   333     CallgraphControl build() throws IOException {
   334         List<String> args = toCommandLineArguments();
   335         // We use regular string processing so we get the argument validation
   336         String[] argList = args.toArray(new String[args.size()]);
   337         Arguments arguments = new Arguments(false, argList);
   338         return arguments;
   339     }
   340 
   341     /**
   342      * Run this callgraph, writing output and returning the set of all methods
   343      * found, sorted in qname order. Call SourceElement.getInboundReferences()
   344      * and SourceElement.getOutboundReferences() to explore the graph.
   345      *
   346      * @return Sorted set of source elements
   347      * @throws IOException If i/o fails
   348      */
   349     public List<SourceElement> run() throws IOException {
   350         return Callgraph.invoke(build(), listener);
   351     }
   352 
   353     private void addFile(String command, File file, List<String> args) {
   354         if (file != null) {
   355             args.add("--" + command);
   356             args.add(file.getAbsolutePath());
   357         }
   358     }
   359 
   360     private void addBoolean(String command, boolean val, List<String> args) {
   361         if (val) {
   362             args.add("--" + command);
   363         }
   364     }
   365 
   366     public Callgraph setListener(Listener listener) {
   367         this.listener = listener;
   368         return this;
   369     }
   370 
   371     public Callgraph packageGraphOutput(File file) {
   372         packagegraphFile = file;
   373         return this;
   374     }
   375 
   376     public Callgraph methodGraphOutput(File file) {
   377         methodgraphFile = file;
   378         return this;
   379     }
   380 
   381     public Callgraph classGraphOutput(File file) {
   382         classgraphFile = file;
   383         return this;
   384     }
   385 
   386     public Callgraph addSourceParent(File folder) {
   387         folders.add(folder);
   388         return this;
   389     }
   390 
   391     public Callgraph excludePrefix(String prefix) {
   392         excludes.add(prefix);
   393         return this;
   394     }
   395 
   396     public Callgraph useSimpleClassNames() {
   397         simple = true;
   398         return this;
   399     }
   400 
   401     public Callgraph quiet() {
   402         quiet = true;
   403         return this;
   404     }
   405 
   406     public Callgraph ignoreSelfReferences() {
   407         noself = true;
   408         return this;
   409     }
   410 
   411     public Callgraph scanFoldersForMavenProjects() {
   412         maven = true;
   413         return this;
   414     }
   415 
   416     public Callgraph reverse() {
   417         reverse = true;
   418         return this;
   419     }
   420 
   421     public Callgraph useJavaStrings() {
   422         useJavaStrings = true;
   423         return this;
   424     }
   425 
   426     public Callgraph omitAbstract() {
   427         omitAbstract = true;
   428         return this;
   429     }
   430 
   431     private static final class LoggingListener implements Listener {
   432 
   433         @Override
   434         public void onStart() {
   435             //do nothing
   436         }
   437 
   438         @Override
   439         public void onFinish() {
   440             System.out.println("Done.");
   441         }
   442 
   443         @Override
   444         public void onStartActivity(String activity, int steps) {
   445             if (steps > 0) {
   446                 System.out.println(activity + " (" + steps + " steps)");
   447             }
   448         }
   449 
   450         @Override
   451         public void onStep(String step) {
   452             System.out.println("\t" + step);
   453         }
   454     }
   455 }