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