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 DiagnisticFormatter.
  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 disgnostic 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 {
 223             return String.valueOf(arg);
 224         }
 225     }
 226     //where
 227             private String expr2String(JCExpression tree) {
 228                 switch(tree.getTag()) {
 229                     case PARENS:
 230                         return expr2String(((JCParens)tree).expr);
 231                     case LAMBDA:
 232                     case REFERENCE:
 233                     case CONDEXPR:
 234                         return Pretty.toSimpleString(tree);
 235                     default:
 236                         Assert.error("unexpected tree kind " + tree.getKind());
 237                         return null;
 238                 }
 239             }
 240 
 241     /**
 242      * Format an iterable argument of a given diagnostic.
 243      *
 244      * @param d diagnostic whose argument is to be formatted
 245      * @param it iterable argument to be formatted
 246      * @param l locale object to be used for i18n
 247      * @return string representation of the diagnostic iterable argument
 248      */
 249     protected String formatIterable(JCDiagnostic d, Iterable<?> it, Locale l) {
 250         StringBuilder sbuf = new StringBuilder();
 251         String sep = "";
 252         for (Object o : it) {
 253             sbuf.append(sep);
 254             sbuf.append(formatArgument(d, o, l));
 255             sep = ",";
 256         }
 257         return sbuf.toString();
 258     }
 259 
 260     /**
 261      * Format all the subdiagnostics attached to a given diagnostic.
 262      *
 263      * @param d diagnostic whose subdiagnostics are to be formatted
 264      * @param l locale object to be used for i18n
 265      * @return list of all string representations of the subdiagnostics
 266      */
 267     protected List<String> formatSubdiagnostics(JCDiagnostic d, Locale l) {
 268         List<String> subdiagnostics = List.nil();
 269         int maxDepth = config.getMultilineLimit(MultilineLimit.DEPTH);
 270         if (maxDepth == -1 || depth < maxDepth) {
 271             depth++;
 272             try {
 273                 int maxCount = config.getMultilineLimit(MultilineLimit.LENGTH);
 274                 int count = 0;
 275                 for (JCDiagnostic d2 : d.getSubdiagnostics()) {
 276                     if (maxCount == -1 || count < maxCount) {
 277                         subdiagnostics = subdiagnostics.append(formatSubdiagnostic(d, d2, l));
 278                         count++;
 279                     }
 280                     else
 281                         break;
 282                 }
 283             }
 284             finally {
 285                 depth--;
 286             }
 287         }
 288         return subdiagnostics;
 289     }
 290 
 291     /**
 292      * Format a subdiagnostics attached to a given diagnostic.
 293      *
 294      * @param parent multiline diagnostic whose subdiagnostics is to be formatted
 295      * @param sub subdiagnostic to be formatted
 296      * @param l locale object to be used for i18n
 297      * @return string representation of the subdiagnostics
 298      */
 299     protected String formatSubdiagnostic(JCDiagnostic parent, JCDiagnostic sub, Locale l) {
 300         return formatMessage(sub, l);
 301     }
 302 
 303     /** Format the faulty source code line and point to the error.
 304      *  @param d The diagnostic for which the error line should be printed
 305      */
 306     protected String formatSourceLine(JCDiagnostic d, int nSpaces) {
 307         StringBuilder buf = new StringBuilder();
 308         DiagnosticSource source = d.getDiagnosticSource();
 309         int pos = d.getIntPosition();
 310         if (d.getIntPosition() == Position.NOPOS)
 311             throw new AssertionError();
 312         String line = (source == null ? null : source.getLine(pos));
 313         if (line == null)
 314             return "";
 315         buf.append(indent(line, nSpaces));
 316         int col = source.getColumnNumber(pos, false);
 317         if (config.isCaretEnabled()) {
 318             buf.append("\n");
 319             for (int i = 0; i < col - 1; i++)  {
 320                 buf.append((line.charAt(i) == '\t') ? "\t" : " ");
 321             }
 322             buf.append(indent("^", nSpaces));
 323         }
 324         return buf.toString();
 325     }
 326 
 327     protected String formatLintCategory(JCDiagnostic d, Locale l) {
 328         LintCategory lc = d.getLintCategory();
 329         if (lc == null)
 330             return "";
 331         return localize(l, "compiler.warn.lintOption", lc.option);
 332     }
 333 
 334     /**
 335      * Converts a String into a locale-dependent representation accordingly to a given locale.
 336      *
 337      * @param l locale object to be used for i18n
 338      * @param key locale-independent key used for looking up in a resource file
 339      * @param args localization arguments
 340      * @return a locale-dependent string
 341      */
 342     protected String localize(Locale l, String key, Object... args) {
 343         return messages.getLocalizedString(l, key, args);
 344     }
 345 
 346     public boolean displaySource(JCDiagnostic d) {
 347         return config.getVisible().contains(DiagnosticPart.SOURCE) &&
 348                 d.getType() != FRAGMENT &&
 349                 d.getIntPosition() != Position.NOPOS;
 350     }
 351 
 352     public boolean isRaw() {
 353         return false;
 354     }
 355 
 356     /**
 357      * Creates a string with a given amount of empty spaces. Useful for
 358      * indenting the text of a diagnostic message.
 359      *
 360      * @param nSpaces the amount of spaces to be added to the result string
 361      * @return the indentation string
 362      */
 363     protected String indentString(int nSpaces) {
 364         String spaces = "                        ";
 365         if (nSpaces <= spaces.length())
 366             return spaces.substring(0, nSpaces);
 367         else {
 368             StringBuilder buf = new StringBuilder();
 369             for (int i = 0 ; i < nSpaces ; i++)
 370                 buf.append(" ");
 371             return buf.toString();
 372         }
 373     }
 374 
 375     /**
 376      * Indent a string by prepending a given amount of empty spaces to each line
 377      * of the string.
 378      *
 379      * @param s the string to be indented
 380      * @param nSpaces the amount of spaces that should be prepended to each line
 381      * of the string
 382      * @return an indented string
 383      */
 384     protected String indent(String s, int nSpaces) {
 385         String indent = indentString(nSpaces);
 386         StringBuilder buf = new StringBuilder();
 387         String nl = "";
 388         for (String line : s.split("\n")) {
 389             buf.append(nl);
 390             buf.append(indent + line);
 391             nl = "\n";
 392         }
 393         return buf.toString();
 394     }
 395 
 396     public SimpleConfiguration getConfiguration() {
 397         return config;
 398     }
 399 
 400     static public class SimpleConfiguration implements Configuration {
 401 
 402         protected Map<MultilineLimit, Integer> multilineLimits;
 403         protected EnumSet<DiagnosticPart> visibleParts;
 404         protected boolean caretEnabled;
 405 
 406         public SimpleConfiguration(Set<DiagnosticPart> parts) {
 407             multilineLimits = new HashMap<>();
 408             setVisible(parts);
 409             setMultilineLimit(MultilineLimit.DEPTH, -1);
 410             setMultilineLimit(MultilineLimit.LENGTH, -1);
 411             setCaretEnabled(true);
 412         }
 413 
 414         @SuppressWarnings("fallthrough")
 415         public SimpleConfiguration(Options options, Set<DiagnosticPart> parts) {
 416             this(parts);
 417             String showSource = null;
 418             if ((showSource = options.get("diags.showSource")) != null) {
 419                 if (showSource.equals("true"))
 420                     setVisiblePart(DiagnosticPart.SOURCE, true);
 421                 else if (showSource.equals("false"))
 422                     setVisiblePart(DiagnosticPart.SOURCE, false);
 423             }
 424             String diagOpts = options.get("diags.formatterOptions");
 425             if (diagOpts != null) {//override -XDshowSource
 426                 Collection<String> args = Arrays.asList(diagOpts.split(","));
 427                 if (args.contains("short")) {
 428                     setVisiblePart(DiagnosticPart.DETAILS, false);
 429                     setVisiblePart(DiagnosticPart.SUBDIAGNOSTICS, false);
 430                 }
 431                 if (args.contains("source"))
 432                     setVisiblePart(DiagnosticPart.SOURCE, true);
 433                 if (args.contains("-source"))
 434                     setVisiblePart(DiagnosticPart.SOURCE, false);
 435             }
 436             String multiPolicy = null;
 437             if ((multiPolicy = options.get("diags.multilinePolicy")) != null) {
 438                 if (multiPolicy.equals("disabled"))
 439                     setVisiblePart(DiagnosticPart.SUBDIAGNOSTICS, false);
 440                 else if (multiPolicy.startsWith("limit:")) {
 441                     String limitString = multiPolicy.substring("limit:".length());
 442                     String[] limits = limitString.split(":");
 443                     try {
 444                         switch (limits.length) {
 445                             case 2: {
 446                                 if (!limits[1].equals("*"))
 447                                     setMultilineLimit(MultilineLimit.DEPTH, Integer.parseInt(limits[1]));
 448                             }
 449                             case 1: {
 450                                 if (!limits[0].equals("*"))
 451                                     setMultilineLimit(MultilineLimit.LENGTH, Integer.parseInt(limits[0]));
 452                             }
 453                         }
 454                     }
 455                     catch(NumberFormatException ex) {
 456                         setMultilineLimit(MultilineLimit.DEPTH, -1);
 457                         setMultilineLimit(MultilineLimit.LENGTH, -1);
 458                     }
 459                 }
 460             }
 461             String showCaret = null;
 462             if (((showCaret = options.get("diags.showCaret")) != null) &&
 463                 showCaret.equals("false"))
 464                     setCaretEnabled(false);
 465             else
 466                 setCaretEnabled(true);
 467         }
 468 
 469         public int getMultilineLimit(MultilineLimit limit) {
 470             return multilineLimits.get(limit);
 471         }
 472 
 473         public EnumSet<DiagnosticPart> getVisible() {
 474             return EnumSet.copyOf(visibleParts);
 475         }
 476 
 477         public void setMultilineLimit(MultilineLimit limit, int value) {
 478             multilineLimits.put(limit, value < -1 ? -1 : value);
 479         }
 480 
 481 
 482         public void setVisible(Set<DiagnosticPart> diagParts) {
 483             visibleParts = EnumSet.copyOf(diagParts);
 484         }
 485 
 486         public void setVisiblePart(DiagnosticPart diagParts, boolean enabled) {
 487             if (enabled)
 488                 visibleParts.add(diagParts);
 489             else
 490                 visibleParts.remove(diagParts);
 491         }
 492 
 493         /**
 494          * Shows a '^' sign under the source line displayed by the formatter
 495          * (if applicable).
 496          *
 497          * @param caretEnabled if true enables caret
 498          */
 499         public void setCaretEnabled(boolean caretEnabled) {
 500             this.caretEnabled = caretEnabled;
 501         }
 502 
 503         /**
 504          * Tells whether the caret display is active or not.
 505          *
 506          * @return true if the caret is enabled
 507          */
 508         public boolean isCaretEnabled() {
 509             return caretEnabled;
 510         }
 511     }
 512 
 513     public Printer getPrinter() {
 514         return printer;
 515     }
 516 
 517     public void setPrinter(Printer printer) {
 518         this.printer = printer;
 519     }
 520 
 521     /**
 522      * An enhanced printer for formatting types/symbols used by
 523      * AbstractDiagnosticFormatter. Provides alternate numbering of captured
 524      * types (they are numbered starting from 1 on each new diagnostic, instead
 525      * of relying on the underlying hashcode() method which generates unstable
 526      * output). Also detects cycles in wildcard messages (e.g. if the wildcard
 527      * type referred by a given captured type C contains C itself) which might
 528      * lead to infinite loops.
 529      */
 530     protected Printer printer = new Printer() {
 531 
 532         @Override
 533         protected String localize(Locale locale, String key, Object... args) {
 534             return AbstractDiagnosticFormatter.this.localize(locale, key, args);
 535         }
 536         @Override
 537         protected String capturedVarId(CapturedType t, Locale locale) {
 538             return "" + (allCaptured.indexOf(t) + 1);
 539         }
 540         @Override
 541         public String visitCapturedType(CapturedType t, Locale locale) {
 542             if (!allCaptured.contains(t)) {
 543                 allCaptured = allCaptured.append(t);
 544             }
 545             return super.visitCapturedType(t, locale);
 546         }
 547     };
 548 }