1 /*
   2  * Copyright (c) 2008, 2018, 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 
  26 package com.sun.tools.javac.util;
  27 
  28 import java.nio.file.Path;
  29 import java.util.Arrays;
  30 import java.util.Collection;
  31 import java.util.EnumSet;
  32 import java.util.HashMap;
  33 import java.util.Locale;
  34 import java.util.Map;
  35 import java.util.Set;
  36 
  37 import javax.tools.JavaFileObject;
  38 
  39 import com.sun.tools.javac.api.DiagnosticFormatter;
  40 import com.sun.tools.javac.api.DiagnosticFormatter.Configuration.DiagnosticPart;
  41 import com.sun.tools.javac.api.DiagnosticFormatter.Configuration.MultilineLimit;
  42 import com.sun.tools.javac.api.DiagnosticFormatter.PositionKind;
  43 import com.sun.tools.javac.api.Formattable;
  44 import com.sun.tools.javac.code.Lint.LintCategory;
  45 import com.sun.tools.javac.code.Printer;
  46 import com.sun.tools.javac.code.Source;
  47 import com.sun.tools.javac.code.Symbol;
  48 import com.sun.tools.javac.code.Type;
  49 import com.sun.tools.javac.code.Type.CapturedType;
  50 import com.sun.tools.javac.file.PathFileObject;
  51 import com.sun.tools.javac.jvm.Profile;
  52 import com.sun.tools.javac.jvm.Target;
  53 import com.sun.tools.javac.main.Option;
  54 import com.sun.tools.javac.tree.JCTree.*;
  55 import com.sun.tools.javac.tree.Pretty;
  56 
  57 import static com.sun.tools.javac.util.JCDiagnostic.DiagnosticType.*;
  58 
  59 /**
  60  * This abstract class provides a basic implementation of the functionalities that should be provided
  61  * by any formatter used by javac. Among the main features provided by AbstractDiagnosticFormatter are:
  62  *
  63  * <ul>
  64  *  <li> Provides a standard implementation of the visitor-like methods defined in the interface DiagnosticFormatter.
  65  *  Those implementations are specifically targeting JCDiagnostic objects.
  66  *  <li> Provides basic support for i18n and a method for executing all locale-dependent conversions
  67  *  <li> Provides the formatting logic for rendering the arguments of a JCDiagnostic object.
  68  * </ul>
  69  *
  70  * <p><b>This is NOT part of any supported API.
  71  * If you write code that depends on this, you do so at your own risk.
  72  * This code and its internal interfaces are subject to change or
  73  * deletion without notice.</b>
  74  */
  75 public abstract class AbstractDiagnosticFormatter implements DiagnosticFormatter<JCDiagnostic> {
  76 
  77     /**
  78      * JavacMessages object used by this formatter for i18n.
  79      */
  80     protected JavacMessages messages;
  81 
  82     /**
  83      * Configuration object used by this formatter
  84      */
  85     private SimpleConfiguration config;
  86 
  87     /**
  88      * Current depth level of the diagnostic being formatted
  89      * (!= 0 for subdiagnostics)
  90      */
  91     protected int depth = 0;
  92 
  93     /**
  94      * All captured types that have been encountered during diagnostic formatting.
  95      * This info is used by the FormatterPrinter in order to print friendly unique
  96      * ids for captured types
  97      */
  98     private List<Type> allCaptured = List.nil();
  99 
 100     /**
 101      * Initialize an AbstractDiagnosticFormatter by setting its JavacMessages object.
 102      * @param messages
 103      */
 104     protected AbstractDiagnosticFormatter(JavacMessages messages, SimpleConfiguration config) {
 105         this.messages = messages;
 106         this.config = config;
 107     }
 108 
 109     public String formatKind(JCDiagnostic d, Locale l) {
 110         switch (d.getType()) {
 111             case FRAGMENT: return "";
 112             case NOTE:     return localize(l, "compiler.note.note");
 113             case WARNING:  return localize(l, "compiler.warn.warning");
 114             case ERROR:    return localize(l, "compiler.err.error");
 115             default:
 116                 throw new AssertionError("Unknown diagnostic type: " + d.getType());
 117         }
 118     }
 119 
 120     @Override
 121     public String format(JCDiagnostic d, Locale locale) {
 122         allCaptured = List.nil();
 123         return formatDiagnostic(d, locale);
 124     }
 125 
 126     protected abstract String formatDiagnostic(JCDiagnostic d, Locale locale);
 127 
 128     public String formatPosition(JCDiagnostic d, PositionKind pk,Locale l) {
 129         Assert.check(d.getPosition() != Position.NOPOS);
 130         return String.valueOf(getPosition(d, pk));
 131     }
 132     //where
 133     private long getPosition(JCDiagnostic d, PositionKind pk) {
 134         switch (pk) {
 135             case START: return d.getIntStartPosition();
 136             case END: return d.getIntEndPosition();
 137             case LINE: return d.getLineNumber();
 138             case COLUMN: return d.getColumnNumber();
 139             case OFFSET: return d.getIntPosition();
 140             default:
 141                 throw new AssertionError("Unknown diagnostic position: " + pk);
 142         }
 143     }
 144 
 145     public String formatSource(JCDiagnostic d, boolean fullname, Locale l) {
 146         JavaFileObject fo = d.getSource();
 147         if (fo == null)
 148             throw new IllegalArgumentException(); // d should have source set
 149         if (fullname)
 150             return fo.getName();
 151         else if (fo instanceof PathFileObject)
 152             return ((PathFileObject) fo).getShortName();
 153         else
 154             return PathFileObject.getSimpleName(fo);
 155     }
 156 
 157     /**
 158      * Format the arguments of a given diagnostic.
 159      *
 160      * @param d diagnostic whose arguments are to be formatted
 161      * @param l locale object to be used for i18n
 162      * @return a Collection whose elements are the formatted arguments of the diagnostic
 163      */
 164     protected Collection<String> formatArguments(JCDiagnostic d, Locale l) {
 165         ListBuffer<String> buf = new ListBuffer<>();
 166         for (Object o : d.getArgs()) {
 167            buf.append(formatArgument(d, o, l));
 168         }
 169         return buf.toList();
 170     }
 171 
 172     /**
 173      * Format a single argument of a given diagnostic.
 174      *
 175      * @param d diagnostic whose argument is to be formatted
 176      * @param arg argument to be formatted
 177      * @param l locale object to be used for i18n
 178      * @return string representation of the diagnostic argument
 179      */
 180     protected String formatArgument(JCDiagnostic d, Object arg, Locale l) {
 181         if (arg instanceof JCDiagnostic) {
 182             String s = null;
 183             depth++;
 184             try {
 185                 s = formatMessage((JCDiagnostic)arg, l);
 186             }
 187             finally {
 188                 depth--;
 189             }
 190             return s;
 191         }
 192         else if (arg instanceof JCExpression) {
 193             return expr2String((JCExpression)arg);
 194         }
 195         else if (arg instanceof Iterable<?> && !(arg instanceof Path)) {
 196             return formatIterable(d, (Iterable<?>)arg, l);
 197         }
 198         else if (arg instanceof Type) {
 199             return printer.visit((Type)arg, l);
 200         }
 201         else if (arg instanceof Symbol) {
 202             return printer.visit((Symbol)arg, l);
 203         }
 204         else if (arg instanceof JavaFileObject) {
 205             return ((JavaFileObject)arg).getName();
 206         }
 207         else if (arg instanceof Profile) {
 208             return ((Profile)arg).name;
 209         }
 210         else if (arg instanceof Option) {
 211             return ((Option)arg).primaryName;
 212         }
 213         else if (arg instanceof Formattable) {
 214             return ((Formattable)arg).toString(l, messages);
 215         }
 216         else if (arg instanceof Target) {
 217             return ((Target)arg).name;
 218         }
 219         else if (arg instanceof Source) {
 220             return ((Source)arg).name;
 221         }
 222         else if (arg instanceof Tag) {
 223             return messages.getLocalizedString(l, "compiler.misc.tree.tag." +
 224                                                   StringUtils.toLowerCase(((Tag) arg).name()));
 225         }
 226         else {
 227             return String.valueOf(arg);
 228         }
 229     }
 230     //where
 231             private String expr2String(JCExpression tree) {
 232                 switch(tree.getTag()) {
 233                     case PARENS:
 234                         return expr2String(((JCParens)tree).expr);
 235                     case LAMBDA:
 236                     case REFERENCE:
 237                     case CONDEXPR:
 238                         return Pretty.toSimpleString(tree);
 239                     default:
 240                         Assert.error("unexpected tree kind " + tree.getKind());
 241                         return null;
 242                 }
 243             }
 244 
 245     /**
 246      * Format an iterable argument of a given diagnostic.
 247      *
 248      * @param d diagnostic whose argument is to be formatted
 249      * @param it iterable argument to be formatted
 250      * @param l locale object to be used for i18n
 251      * @return string representation of the diagnostic iterable argument
 252      */
 253     protected String formatIterable(JCDiagnostic d, Iterable<?> it, Locale l) {
 254         StringBuilder sbuf = new StringBuilder();
 255         String sep = "";
 256         for (Object o : it) {
 257             sbuf.append(sep);
 258             sbuf.append(formatArgument(d, o, l));
 259             sep = ",";
 260         }
 261         return sbuf.toString();
 262     }
 263 
 264     /**
 265      * Format all the subdiagnostics attached to a given diagnostic.
 266      *
 267      * @param d diagnostic whose subdiagnostics are to be formatted
 268      * @param l locale object to be used for i18n
 269      * @return list of all string representations of the subdiagnostics
 270      */
 271     protected List<String> formatSubdiagnostics(JCDiagnostic d, Locale l) {
 272         List<String> subdiagnostics = List.nil();
 273         int maxDepth = config.getMultilineLimit(MultilineLimit.DEPTH);
 274         if (maxDepth == -1 || depth < maxDepth) {
 275             depth++;
 276             try {
 277                 int maxCount = config.getMultilineLimit(MultilineLimit.LENGTH);
 278                 int count = 0;
 279                 for (JCDiagnostic d2 : d.getSubdiagnostics()) {
 280                     if (maxCount == -1 || count < maxCount) {
 281                         subdiagnostics = subdiagnostics.append(formatSubdiagnostic(d, d2, l));
 282                         count++;
 283                     }
 284                     else
 285                         break;
 286                 }
 287             }
 288             finally {
 289                 depth--;
 290             }
 291         }
 292         return subdiagnostics;
 293     }
 294 
 295     /**
 296      * Format a subdiagnostics attached to a given diagnostic.
 297      *
 298      * @param parent multiline diagnostic whose subdiagnostics is to be formatted
 299      * @param sub subdiagnostic to be formatted
 300      * @param l locale object to be used for i18n
 301      * @return string representation of the subdiagnostics
 302      */
 303     protected String formatSubdiagnostic(JCDiagnostic parent, JCDiagnostic sub, Locale l) {
 304         return formatMessage(sub, l);
 305     }
 306 
 307     /** Format the faulty source code line and point to the error.
 308      *  @param d The diagnostic for which the error line should be printed
 309      */
 310     protected String formatSourceLine(JCDiagnostic d, int nSpaces) {
 311         StringBuilder buf = new StringBuilder();
 312         DiagnosticSource source = d.getDiagnosticSource();
 313         int pos = d.getIntPosition();
 314         if (d.getIntPosition() == Position.NOPOS)
 315             throw new AssertionError();
 316         String line = (source == null ? null : source.getLine(pos));
 317         if (line == null)
 318             return "";
 319         buf.append(indent(line, nSpaces));
 320         int col = source.getColumnNumber(pos, false);
 321         if (config.isCaretEnabled()) {
 322             buf.append("\n");
 323             for (int i = 0; i < col - 1; i++)  {
 324                 buf.append((line.charAt(i) == '\t') ? "\t" : " ");
 325             }
 326             buf.append(indent("^", nSpaces));
 327         }
 328         return buf.toString();
 329     }
 330 
 331     protected String formatLintCategory(JCDiagnostic d, Locale l) {
 332         LintCategory lc = d.getLintCategory();
 333         if (lc == null)
 334             return "";
 335         return localize(l, "compiler.warn.lintOption", lc.option);
 336     }
 337 
 338     /**
 339      * Converts a String into a locale-dependent representation accordingly to a given locale.
 340      *
 341      * @param l locale object to be used for i18n
 342      * @param key locale-independent key used for looking up in a resource file
 343      * @param args localization arguments
 344      * @return a locale-dependent string
 345      */
 346     protected String localize(Locale l, String key, Object... args) {
 347         return messages.getLocalizedString(l, key, args);
 348     }
 349 
 350     public boolean displaySource(JCDiagnostic d) {
 351         return config.getVisible().contains(DiagnosticPart.SOURCE) &&
 352                 d.getType() != FRAGMENT &&
 353                 d.getIntPosition() != Position.NOPOS;
 354     }
 355 
 356     public boolean isRaw() {
 357         return false;
 358     }
 359 
 360     /**
 361      * Creates a string with a given amount of empty spaces. Useful for
 362      * indenting the text of a diagnostic message.
 363      *
 364      * @param nSpaces the amount of spaces to be added to the result string
 365      * @return the indentation string
 366      */
 367     protected String indentString(int nSpaces) {
 368         String spaces = "                        ";
 369         if (nSpaces <= spaces.length())
 370             return spaces.substring(0, nSpaces);
 371         else {
 372             StringBuilder buf = new StringBuilder();
 373             for (int i = 0 ; i < nSpaces ; i++)
 374                 buf.append(" ");
 375             return buf.toString();
 376         }
 377     }
 378 
 379     /**
 380      * Indent a string by prepending a given amount of empty spaces to each line
 381      * of the string.
 382      *
 383      * @param s the string to be indented
 384      * @param nSpaces the amount of spaces that should be prepended to each line
 385      * of the string
 386      * @return an indented string
 387      */
 388     protected String indent(String s, int nSpaces) {
 389         String indent = indentString(nSpaces);
 390         StringBuilder buf = new StringBuilder();
 391         String nl = "";
 392         for (String line : s.split("\n")) {
 393             buf.append(nl);
 394             buf.append(indent + line);
 395             nl = "\n";
 396         }
 397         return buf.toString();
 398     }
 399 
 400     public SimpleConfiguration getConfiguration() {
 401         return config;
 402     }
 403 
 404     static public class SimpleConfiguration implements Configuration {
 405 
 406         protected Map<MultilineLimit, Integer> multilineLimits;
 407         protected EnumSet<DiagnosticPart> visibleParts;
 408         protected boolean caretEnabled;
 409 
 410         public SimpleConfiguration(Set<DiagnosticPart> parts) {
 411             multilineLimits = new HashMap<>();
 412             setVisible(parts);
 413             setMultilineLimit(MultilineLimit.DEPTH, -1);
 414             setMultilineLimit(MultilineLimit.LENGTH, -1);
 415             setCaretEnabled(true);
 416         }
 417 
 418         @SuppressWarnings("fallthrough")
 419         public SimpleConfiguration(Options options, Set<DiagnosticPart> parts) {
 420             this(parts);
 421             String showSource = null;
 422             if ((showSource = options.get("diags.showSource")) != null) {
 423                 if (showSource.equals("true"))
 424                     setVisiblePart(DiagnosticPart.SOURCE, true);
 425                 else if (showSource.equals("false"))
 426                     setVisiblePart(DiagnosticPart.SOURCE, false);
 427             }
 428             String diagOpts = options.get("diags.formatterOptions");
 429             if (diagOpts != null) {//override -XDshowSource
 430                 Collection<String> args = Arrays.asList(diagOpts.split(","));
 431                 if (args.contains("short")) {
 432                     setVisiblePart(DiagnosticPart.DETAILS, false);
 433                     setVisiblePart(DiagnosticPart.SUBDIAGNOSTICS, false);
 434                 }
 435                 if (args.contains("source"))
 436                     setVisiblePart(DiagnosticPart.SOURCE, true);
 437                 if (args.contains("-source"))
 438                     setVisiblePart(DiagnosticPart.SOURCE, false);
 439             }
 440             String multiPolicy = null;
 441             if ((multiPolicy = options.get("diags.multilinePolicy")) != null) {
 442                 if (multiPolicy.equals("disabled"))
 443                     setVisiblePart(DiagnosticPart.SUBDIAGNOSTICS, false);
 444                 else if (multiPolicy.startsWith("limit:")) {
 445                     String limitString = multiPolicy.substring("limit:".length());
 446                     String[] limits = limitString.split(":");
 447                     try {
 448                         switch (limits.length) {
 449                             case 2: {
 450                                 if (!limits[1].equals("*"))
 451                                     setMultilineLimit(MultilineLimit.DEPTH, Integer.parseInt(limits[1]));
 452                             }
 453                             case 1: {
 454                                 if (!limits[0].equals("*"))
 455                                     setMultilineLimit(MultilineLimit.LENGTH, Integer.parseInt(limits[0]));
 456                             }
 457                         }
 458                     }
 459                     catch(NumberFormatException ex) {
 460                         setMultilineLimit(MultilineLimit.DEPTH, -1);
 461                         setMultilineLimit(MultilineLimit.LENGTH, -1);
 462                     }
 463                 }
 464             }
 465             String showCaret = null;
 466             if (((showCaret = options.get("diags.showCaret")) != null) &&
 467                 showCaret.equals("false"))
 468                     setCaretEnabled(false);
 469             else
 470                 setCaretEnabled(true);
 471         }
 472 
 473         public int getMultilineLimit(MultilineLimit limit) {
 474             return multilineLimits.get(limit);
 475         }
 476 
 477         public EnumSet<DiagnosticPart> getVisible() {
 478             return EnumSet.copyOf(visibleParts);
 479         }
 480 
 481         public void setMultilineLimit(MultilineLimit limit, int value) {
 482             multilineLimits.put(limit, value < -1 ? -1 : value);
 483         }
 484 
 485 
 486         public void setVisible(Set<DiagnosticPart> diagParts) {
 487             visibleParts = EnumSet.copyOf(diagParts);
 488         }
 489 
 490         public void setVisiblePart(DiagnosticPart diagParts, boolean enabled) {
 491             if (enabled)
 492                 visibleParts.add(diagParts);
 493             else
 494                 visibleParts.remove(diagParts);
 495         }
 496 
 497         /**
 498          * Shows a '^' sign under the source line displayed by the formatter
 499          * (if applicable).
 500          *
 501          * @param caretEnabled if true enables caret
 502          */
 503         public void setCaretEnabled(boolean caretEnabled) {
 504             this.caretEnabled = caretEnabled;
 505         }
 506 
 507         /**
 508          * Tells whether the caret display is active or not.
 509          *
 510          * @return true if the caret is enabled
 511          */
 512         public boolean isCaretEnabled() {
 513             return caretEnabled;
 514         }
 515     }
 516 
 517     public Printer getPrinter() {
 518         return printer;
 519     }
 520 
 521     public void setPrinter(Printer printer) {
 522         this.printer = printer;
 523     }
 524 
 525     /**
 526      * An enhanced printer for formatting types/symbols used by
 527      * AbstractDiagnosticFormatter. Provides alternate numbering of captured
 528      * types (they are numbered starting from 1 on each new diagnostic, instead
 529      * of relying on the underlying hashcode() method which generates unstable
 530      * output). Also detects cycles in wildcard messages (e.g. if the wildcard
 531      * type referred by a given captured type C contains C itself) which might
 532      * lead to infinite loops.
 533      */
 534     protected Printer printer = new Printer() {
 535 
 536         @Override
 537         protected String localize(Locale locale, String key, Object... args) {
 538             return AbstractDiagnosticFormatter.this.localize(locale, key, args);
 539         }
 540         @Override
 541         protected String capturedVarId(CapturedType t, Locale locale) {
 542             return "" + (allCaptured.indexOf(t) + 1);
 543         }
 544         @Override
 545         public String visitCapturedType(CapturedType t, Locale locale) {
 546             if (!allCaptured.contains(t)) {
 547                 allCaptured = allCaptured.append(t);
 548             }
 549             return super.visitCapturedType(t, locale);
 550         }
 551     };
 552 }