1 /* 2 * Copyright (c) 2009, 2014, 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 package com.sun.tools.javac.util; 26 27 import java.nio.file.Path; 28 import java.util.EnumMap; 29 import java.util.EnumSet; 30 import java.util.HashMap; 31 import java.util.LinkedHashMap; 32 import java.util.Locale; 33 import java.util.Map; 34 35 import com.sun.tools.javac.code.Printer; 36 import com.sun.tools.javac.code.Symbol; 37 import com.sun.tools.javac.code.Symbol.*; 38 import com.sun.tools.javac.code.Symtab; 39 import com.sun.tools.javac.code.Type; 40 import com.sun.tools.javac.code.Type.*; 41 import com.sun.tools.javac.code.Types; 42 43 import static com.sun.tools.javac.code.Flags.*; 44 import static com.sun.tools.javac.code.TypeTag.*; 45 import static com.sun.tools.javac.code.Kinds.*; 46 import static com.sun.tools.javac.code.Kinds.Kind.*; 47 import static com.sun.tools.javac.util.LayoutCharacters.*; 48 import static com.sun.tools.javac.util.RichDiagnosticFormatter.RichConfiguration.*; 49 50 /** 51 * A rich diagnostic formatter is a formatter that provides better integration 52 * with javac's type system. A diagostic is first preprocessed in order to keep 53 * track of each types/symbols in it; after these informations are collected, 54 * the diagnostic is rendered using a standard formatter, whose type/symbol printer 55 * has been replaced by a more refined version provided by this rich formatter. 56 * The rich formatter currently enables three different features: (i) simple class 57 * names - that is class names are displayed used a non qualified name (thus 58 * omitting package info) whenever possible - (ii) where clause list - a list of 59 * additional subdiagnostics that provide specific info about type-variables, 60 * captured types, intersection types that occur in the diagnostic that is to be 61 * formatted and (iii) type-variable disambiguation - when the diagnostic refers 62 * to two different type-variables with the same name, their representation is 63 * disambiguated by appending an index to the type variable name. 64 * 65 * <p><b>This is NOT part of any supported API. 66 * If you write code that depends on this, you do so at your own risk. 67 * This code and its internal interfaces are subject to change or 68 * deletion without notice.</b> 69 */ 70 public class RichDiagnosticFormatter extends 71 ForwardingDiagnosticFormatter<JCDiagnostic, AbstractDiagnosticFormatter> { 72 73 final Symtab syms; 74 final Types types; 75 final JCDiagnostic.Factory diags; 76 final JavacMessages messages; 77 78 /* name simplifier used by this formatter */ 79 protected ClassNameSimplifier nameSimplifier; 80 81 /* type/symbol printer used by this formatter */ 82 private RichPrinter printer; 83 84 /* map for keeping track of a where clause associated to a given type */ 85 Map<WhereClauseKind, Map<Type, JCDiagnostic>> whereClauses; 86 87 /** Get the DiagnosticFormatter instance for this context. */ 88 public static RichDiagnosticFormatter instance(Context context) { 89 RichDiagnosticFormatter instance = context.get(RichDiagnosticFormatter.class); 90 if (instance == null) 91 instance = new RichDiagnosticFormatter(context); 92 return instance; 93 } 94 95 protected RichDiagnosticFormatter(Context context) { 96 super((AbstractDiagnosticFormatter)Log.instance(context).getDiagnosticFormatter()); 97 setRichPrinter(new RichPrinter()); 98 this.syms = Symtab.instance(context); 99 this.diags = JCDiagnostic.Factory.instance(context); 100 this.types = Types.instance(context); 101 this.messages = JavacMessages.instance(context); 102 whereClauses = new EnumMap<>(WhereClauseKind.class); 103 configuration = new RichConfiguration(Options.instance(context), formatter); 104 for (WhereClauseKind kind : WhereClauseKind.values()) 105 whereClauses.put(kind, new LinkedHashMap<Type, JCDiagnostic>()); 106 } 107 108 @Override 109 public String format(JCDiagnostic diag, Locale l) { 110 StringBuilder sb = new StringBuilder(); 111 nameSimplifier = new ClassNameSimplifier(); 112 for (WhereClauseKind kind : WhereClauseKind.values()) 113 whereClauses.get(kind).clear(); 114 preprocessDiagnostic(diag); 115 sb.append(formatter.format(diag, l)); 116 if (getConfiguration().isEnabled(RichFormatterFeature.WHERE_CLAUSES)) { 117 List<JCDiagnostic> clauses = getWhereClauses(); 118 String indent = formatter.isRaw() ? "" : 119 formatter.indentString(DetailsInc); 120 for (JCDiagnostic d : clauses) { 121 String whereClause = formatter.format(d, l); 122 if (whereClause.length() > 0) { 123 sb.append('\n' + indent + whereClause); 124 } 125 } 126 } 127 return sb.toString(); 128 } 129 130 @Override 131 public String formatMessage(JCDiagnostic diag, Locale l) { 132 nameSimplifier = new ClassNameSimplifier(); 133 preprocessDiagnostic(diag); 134 return super.formatMessage(diag, l); 135 } 136 137 /** 138 * Sets the type/symbol printer used by this formatter. 139 * @param printer the rich printer to be set 140 */ 141 protected void setRichPrinter(RichPrinter printer) { 142 this.printer = printer; 143 formatter.setPrinter(printer); 144 } 145 146 /** 147 * Returns the type/symbol printer used by this formatter. 148 * @return type/symbol rich printer 149 */ 150 protected RichPrinter getRichPrinter() { 151 return printer; 152 } 153 154 /** 155 * Preprocess a given diagnostic by looking both into its arguments and into 156 * its subdiagnostics (if any). This preprocessing is responsible for 157 * generating info corresponding to features like where clauses, name 158 * simplification, etc. 159 * 160 * @param diag the diagnostic to be preprocessed 161 */ 162 protected void preprocessDiagnostic(JCDiagnostic diag) { 163 for (Object o : diag.getArgs()) { 164 if (o != null) { 165 preprocessArgument(o); 166 } 167 } 168 if (diag.isMultiline()) { 169 for (JCDiagnostic d : diag.getSubdiagnostics()) 170 preprocessDiagnostic(d); 171 } 172 } 173 174 /** 175 * Preprocess a diagnostic argument. A type/symbol argument is 176 * preprocessed by specialized type/symbol preprocessors. 177 * 178 * @param arg the argument to be translated 179 */ 180 protected void preprocessArgument(Object arg) { 181 if (arg instanceof Type) { 182 preprocessType((Type)arg); 183 } 184 else if (arg instanceof Symbol) { 185 preprocessSymbol((Symbol)arg); 186 } 187 else if (arg instanceof JCDiagnostic) { 188 preprocessDiagnostic((JCDiagnostic)arg); 189 } 190 else if (arg instanceof Iterable<?> && !(arg instanceof Path)) { 191 for (Object o : (Iterable<?>)arg) { 192 preprocessArgument(o); 193 } 194 } 195 } 196 197 /** 198 * Build a list of multiline diagnostics containing detailed info about 199 * type-variables, captured types, and intersection types 200 * 201 * @return where clause list 202 */ 203 protected List<JCDiagnostic> getWhereClauses() { 204 List<JCDiagnostic> clauses = List.nil(); 205 for (WhereClauseKind kind : WhereClauseKind.values()) { 206 List<JCDiagnostic> lines = List.nil(); 207 for (Map.Entry<Type, JCDiagnostic> entry : whereClauses.get(kind).entrySet()) { 208 lines = lines.prepend(entry.getValue()); 209 } 210 if (!lines.isEmpty()) { 211 String key = kind.key(); 212 if (lines.size() > 1) 213 key += ".1"; 214 JCDiagnostic d = diags.fragment(key, whereClauses.get(kind).keySet()); 215 d = new JCDiagnostic.MultilineDiagnostic(d, lines.reverse()); 216 clauses = clauses.prepend(d); 217 } 218 } 219 return clauses.reverse(); 220 } 221 222 private int indexOf(Type type, WhereClauseKind kind) { 223 int index = 1; 224 for (Type t : whereClauses.get(kind).keySet()) { 225 if (t.tsym == type.tsym) { 226 return index; 227 } 228 if (kind != WhereClauseKind.TYPEVAR || 229 t.toString().equals(type.toString())) { 230 index++; 231 } 232 } 233 return -1; 234 } 235 236 private boolean unique(TypeVar typevar) { 237 int found = 0; 238 for (Type t : whereClauses.get(WhereClauseKind.TYPEVAR).keySet()) { 239 if (t.toString().equals(typevar.toString())) { 240 found++; 241 } 242 } 243 if (found < 1) 244 throw new AssertionError("Missing type variable in where clause " + typevar); 245 return found == 1; 246 } 247 //where 248 /** 249 * This enum defines all posssible kinds of where clauses that can be 250 * attached by a rich diagnostic formatter to a given diagnostic 251 */ 252 enum WhereClauseKind { 253 254 /** where clause regarding a type variable */ 255 TYPEVAR("where.description.typevar"), 256 /** where clause regarding a captured type */ 257 CAPTURED("where.description.captured"), 258 /** where clause regarding an intersection type */ 259 INTERSECTION("where.description.intersection"); 260 261 /** resource key for this where clause kind */ 262 private final String key; 263 264 WhereClauseKind(String key) { 265 this.key = key; 266 } 267 268 String key() { 269 return key; 270 } 271 } 272 273 // <editor-fold defaultstate="collapsed" desc="name simplifier"> 274 /** 275 * A name simplifier keeps track of class names usages in order to determine 276 * whether a class name can be compacted or not. Short names are not used 277 * if a conflict is detected, e.g. when two classes with the same simple 278 * name belong to different packages - in this case the formatter reverts 279 * to fullnames as compact names might lead to a confusing diagnostic. 280 */ 281 protected class ClassNameSimplifier { 282 283 /* table for keeping track of all short name usages */ 284 Map<Name, List<Symbol>> nameClashes = new HashMap<>(); 285 286 /** 287 * Add a name usage to the simplifier's internal cache 288 */ 289 protected void addUsage(Symbol sym) { 290 Name n = sym.getSimpleName(); 291 List<Symbol> conflicts = nameClashes.get(n); 292 if (conflicts == null) { 293 conflicts = List.nil(); 294 } 295 if (!conflicts.contains(sym)) 296 nameClashes.put(n, conflicts.append(sym)); 297 } 298 299 public String simplify(Symbol s) { 300 String name = s.getQualifiedName().toString(); 301 if (!s.type.isCompound() && !s.type.isPrimitive()) { 302 List<Symbol> conflicts = nameClashes.get(s.getSimpleName()); 303 if (conflicts == null || 304 (conflicts.size() == 1 && 305 conflicts.contains(s))) { 306 List<Name> l = List.nil(); 307 Symbol s2 = s; 308 while (s2.type.hasTag(CLASS) && 309 s2.type.getEnclosingType().hasTag(CLASS) && 310 s2.owner.kind == TYP) { 311 l = l.prepend(s2.getSimpleName()); 312 s2 = s2.owner; 313 } 314 l = l.prepend(s2.getSimpleName()); 315 StringBuilder buf = new StringBuilder(); 316 String sep = ""; 317 for (Name n2 : l) { 318 buf.append(sep); 319 buf.append(n2); 320 sep = "."; 321 } 322 name = buf.toString(); 323 } 324 } 325 return name; 326 } 327 } 328 // </editor-fold> 329 330 // <editor-fold defaultstate="collapsed" desc="rich printer"> 331 /** 332 * Enhanced type/symbol printer that provides support for features like simple names 333 * and type variable disambiguation. This enriched printer exploits the info 334 * discovered during type/symbol preprocessing. This printer is set on the delegate 335 * formatter so that rich type/symbol info can be properly rendered. 336 */ 337 protected class RichPrinter extends Printer { 338 339 @Override 340 public String localize(Locale locale, String key, Object... args) { 341 return formatter.localize(locale, key, args); 342 } 343 344 @Override 345 public String capturedVarId(CapturedType t, Locale locale) { 346 return indexOf(t, WhereClauseKind.CAPTURED) + ""; 347 } 348 349 @Override 350 public String visitType(Type t, Locale locale) { 351 String s = super.visitType(t, locale); 352 if (t == syms.botType) 353 s = localize(locale, "compiler.misc.type.null"); 354 return s; 355 } 356 357 @Override 358 public String visitCapturedType(CapturedType t, Locale locale) { 359 if (getConfiguration().isEnabled(RichFormatterFeature.WHERE_CLAUSES)) { 360 return localize(locale, 361 "compiler.misc.captured.type", 362 indexOf(t, WhereClauseKind.CAPTURED)); 363 } 364 else 365 return super.visitCapturedType(t, locale); 366 } 367 368 @Override 369 public String visitClassType(ClassType t, Locale locale) { 370 if (t.isCompound() && 371 getConfiguration().isEnabled(RichFormatterFeature.WHERE_CLAUSES)) { 372 return localize(locale, 373 "compiler.misc.intersection.type", 374 indexOf(t, WhereClauseKind.INTERSECTION)); 375 } 376 else 377 return super.visitClassType(t, locale); 378 } 379 380 @Override 381 protected String className(ClassType t, boolean longform, Locale locale) { 382 Symbol sym = t.tsym; 383 if (sym.name.length() == 0 || 384 !getConfiguration().isEnabled(RichFormatterFeature.SIMPLE_NAMES)) { 385 return super.className(t, longform, locale); 386 } 387 else if (longform) 388 return nameSimplifier.simplify(sym).toString(); 389 else 390 return sym.name.toString(); 391 } 392 393 @Override 394 public String visitTypeVar(TypeVar t, Locale locale) { 395 if (unique(t) || 396 !getConfiguration().isEnabled(RichFormatterFeature.UNIQUE_TYPEVAR_NAMES)) { 397 return t.toString(); 398 } 399 else { 400 return localize(locale, 401 "compiler.misc.type.var", 402 t.toString(), indexOf(t, WhereClauseKind.TYPEVAR)); 403 } 404 } 405 406 @Override 407 public String visitClassSymbol(ClassSymbol s, Locale locale) { 408 if (s.type.isCompound()) { 409 return visit(s.type, locale); 410 } 411 String name = nameSimplifier.simplify(s); 412 if (name.length() == 0 || 413 !getConfiguration().isEnabled(RichFormatterFeature.SIMPLE_NAMES)) { 414 return super.visitClassSymbol(s, locale); 415 } 416 else { 417 return name; 418 } 419 } 420 421 @Override 422 public String visitMethodSymbol(MethodSymbol s, Locale locale) { 423 String ownerName = visit(s.owner, locale); 424 if (s.isStaticOrInstanceInit()) { 425 return ownerName; 426 } else { 427 String ms = (s.name == s.name.table.names.init) 428 ? ownerName 429 : s.name.toString(); 430 if (s.type != null) { 431 if (s.type.hasTag(FORALL)) { 432 ms = "<" + visitTypes(s.type.getTypeArguments(), locale) + ">" + ms; 433 } 434 ms += "(" + printMethodArgs( 435 s.type.getParameterTypes(), 436 (s.flags() & VARARGS) != 0, 437 locale) + ")"; 438 } 439 return ms; 440 } 441 } 442 } 443 // </editor-fold> 444 445 // <editor-fold defaultstate="collapsed" desc="type scanner"> 446 /** 447 * Preprocess a given type looking for (i) additional info (where clauses) to be 448 * added to the main diagnostic (ii) names to be compacted. 449 */ 450 protected void preprocessType(Type t) { 451 typePreprocessor.visit(t); 452 } 453 //where 454 protected Types.UnaryVisitor<Void> typePreprocessor = 455 new Types.UnaryVisitor<Void>() { 456 457 public Void visit(List<Type> ts) { 458 for (Type t : ts) 459 visit(t); 460 return null; 461 } 462 463 @Override 464 public Void visitForAll(ForAll t, Void ignored) { 465 visit(t.tvars); 466 visit(t.qtype); 467 return null; 468 } 469 470 @Override 471 public Void visitMethodType(MethodType t, Void ignored) { 472 visit(t.argtypes); 473 visit(t.restype); 474 return null; 475 } 476 477 @Override 478 public Void visitErrorType(ErrorType t, Void ignored) { 479 Type ot = t.getOriginalType(); 480 if (ot != null) 481 visit(ot); 482 return null; 483 } 484 485 @Override 486 public Void visitArrayType(ArrayType t, Void ignored) { 487 visit(t.elemtype); 488 return null; 489 } 490 491 @Override 492 public Void visitWildcardType(WildcardType t, Void ignored) { 493 visit(t.type); 494 return null; 495 } 496 497 public Void visitType(Type t, Void ignored) { 498 return null; 499 } 500 501 @Override 502 public Void visitCapturedType(CapturedType t, Void ignored) { 503 if (indexOf(t, WhereClauseKind.CAPTURED) == -1) { 504 String suffix = t.lower == syms.botType ? ".1" : ""; 505 JCDiagnostic d = diags.fragment("where.captured"+ suffix, t, t.bound, t.lower, t.wildcard); 506 whereClauses.get(WhereClauseKind.CAPTURED).put(t, d); 507 visit(t.wildcard); 508 visit(t.lower); 509 visit(t.bound); 510 } 511 return null; 512 } 513 514 @Override 515 public Void visitClassType(ClassType t, Void ignored) { 516 if (t.isCompound()) { 517 if (indexOf(t, WhereClauseKind.INTERSECTION) == -1) { 518 Type supertype = types.supertype(t); 519 List<Type> interfaces = types.interfaces(t); 520 JCDiagnostic d = diags.fragment("where.intersection", t, interfaces.prepend(supertype)); 521 whereClauses.get(WhereClauseKind.INTERSECTION).put(t, d); 522 visit(supertype); 523 visit(interfaces); 524 } 525 } else if (t.tsym.name.isEmpty()) { 526 //anon class 527 ClassType norm = (ClassType) t.tsym.type; 528 if (norm != null) { 529 if (norm.interfaces_field != null && norm.interfaces_field.nonEmpty()) { 530 visit(norm.interfaces_field.head); 531 } else { 532 visit(norm.supertype_field); 533 } 534 } 535 } 536 nameSimplifier.addUsage(t.tsym); 537 visit(t.getTypeArguments()); 538 if (t.getEnclosingType() != Type.noType) 539 visit(t.getEnclosingType()); 540 return null; 541 } 542 543 @Override 544 public Void visitTypeVar(TypeVar t, Void ignored) { 545 if (indexOf(t, WhereClauseKind.TYPEVAR) == -1) { 546 //access the bound type and skip error types 547 Type bound = t.bound; 548 while ((bound instanceof ErrorType)) 549 bound = ((ErrorType)bound).getOriginalType(); 550 //retrieve the bound list - if the type variable 551 //has not been attributed the bound is not set 552 List<Type> bounds = (bound != null) && 553 (bound.hasTag(CLASS) || bound.hasTag(TYPEVAR)) ? 554 types.getBounds(t) : 555 List.<Type>nil(); 556 557 nameSimplifier.addUsage(t.tsym); 558 559 boolean boundErroneous = bounds.head == null || 560 bounds.head.hasTag(NONE) || 561 bounds.head.hasTag(ERROR); 562 563 if ((t.tsym.flags() & SYNTHETIC) == 0) { 564 //this is a true typevar 565 JCDiagnostic d = diags.fragment("where.typevar" + 566 (boundErroneous ? ".1" : ""), t, bounds, 567 kindName(t.tsym.location()), t.tsym.location()); 568 whereClauses.get(WhereClauseKind.TYPEVAR).put(t, d); 569 symbolPreprocessor.visit(t.tsym.location(), null); 570 visit(bounds); 571 } else { 572 Assert.check(!boundErroneous); 573 //this is a fresh (synthetic) tvar 574 JCDiagnostic d = diags.fragment("where.fresh.typevar", t, bounds); 575 whereClauses.get(WhereClauseKind.TYPEVAR).put(t, d); 576 visit(bounds); 577 } 578 579 } 580 return null; 581 } 582 }; 583 // </editor-fold> 584 585 // <editor-fold defaultstate="collapsed" desc="symbol scanner"> 586 /** 587 * Preprocess a given symbol looking for (i) additional info (where clauses) to be 588 * added to the main diagnostic (ii) names to be compacted 589 */ 590 protected void preprocessSymbol(Symbol s) { 591 symbolPreprocessor.visit(s, null); 592 } 593 //where 594 protected Types.DefaultSymbolVisitor<Void, Void> symbolPreprocessor = 595 new Types.DefaultSymbolVisitor<Void, Void>() { 596 597 @Override 598 public Void visitClassSymbol(ClassSymbol s, Void ignored) { 599 if (s.type.isCompound()) { 600 typePreprocessor.visit(s.type); 601 } else { 602 nameSimplifier.addUsage(s); 603 } 604 return null; 605 } 606 607 @Override 608 public Void visitSymbol(Symbol s, Void ignored) { 609 return null; 610 } 611 612 @Override 613 public Void visitMethodSymbol(MethodSymbol s, Void ignored) { 614 visit(s.owner, null); 615 if (s.type != null) 616 typePreprocessor.visit(s.type); 617 return null; 618 } 619 }; 620 // </editor-fold> 621 622 @Override 623 public RichConfiguration getConfiguration() { 624 //the following cast is always safe - see init 625 return (RichConfiguration)configuration; 626 } 627 628 /** 629 * Configuration object provided by the rich formatter. 630 */ 631 public static class RichConfiguration extends ForwardingDiagnosticFormatter.ForwardingConfiguration { 632 633 /** set of enabled rich formatter's features */ 634 protected java.util.EnumSet<RichFormatterFeature> features; 635 636 @SuppressWarnings("fallthrough") 637 public RichConfiguration(Options options, AbstractDiagnosticFormatter formatter) { 638 super(formatter.getConfiguration()); 639 features = formatter.isRaw() ? EnumSet.noneOf(RichFormatterFeature.class) : 640 EnumSet.of(RichFormatterFeature.SIMPLE_NAMES, 641 RichFormatterFeature.WHERE_CLAUSES, 642 RichFormatterFeature.UNIQUE_TYPEVAR_NAMES); 643 String diagOpts = options.get("diags"); 644 if (diagOpts != null) { 645 for (String args: diagOpts.split(",")) { 646 if (args.equals("-where")) { 647 features.remove(RichFormatterFeature.WHERE_CLAUSES); 648 } 649 else if (args.equals("where")) { 650 features.add(RichFormatterFeature.WHERE_CLAUSES); 651 } 652 if (args.equals("-simpleNames")) { 653 features.remove(RichFormatterFeature.SIMPLE_NAMES); 654 } 655 else if (args.equals("simpleNames")) { 656 features.add(RichFormatterFeature.SIMPLE_NAMES); 657 } 658 if (args.equals("-disambiguateTvars")) { 659 features.remove(RichFormatterFeature.UNIQUE_TYPEVAR_NAMES); 660 } 661 else if (args.equals("disambiguateTvars")) { 662 features.add(RichFormatterFeature.UNIQUE_TYPEVAR_NAMES); 663 } 664 } 665 } 666 } 667 668 /** 669 * Returns a list of all the features supported by the rich formatter. 670 * @return list of supported features 671 */ 672 public RichFormatterFeature[] getAvailableFeatures() { 673 return RichFormatterFeature.values(); 674 } 675 676 /** 677 * Enable a specific feature on this rich formatter. 678 * @param feature feature to be enabled 679 */ 680 public void enable(RichFormatterFeature feature) { 681 features.add(feature); 682 } 683 684 /** 685 * Disable a specific feature on this rich formatter. 686 * @param feature feature to be disabled 687 */ 688 public void disable(RichFormatterFeature feature) { 689 features.remove(feature); 690 } 691 692 /** 693 * Is a given feature enabled on this formatter? 694 * @param feature feature to be tested 695 */ 696 public boolean isEnabled(RichFormatterFeature feature) { 697 return features.contains(feature); 698 } 699 700 /** 701 * The advanced formatting features provided by the rich formatter 702 */ 703 public enum RichFormatterFeature { 704 /** a list of additional info regarding a given type/symbol */ 705 WHERE_CLAUSES, 706 /** full class names simplification (where possible) */ 707 SIMPLE_NAMES, 708 /** type-variable names disambiguation */ 709 UNIQUE_TYPEVAR_NAMES 710 } 711 } 712 }