RichDiagnosticFormatter.java revision 2674:e284f560acf6
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.nio.file.Path;
28import java.util.EnumMap;
29import java.util.EnumSet;
30import java.util.HashMap;
31import java.util.LinkedHashMap;
32import java.util.Locale;
33import java.util.Map;
34
35import com.sun.tools.javac.code.Printer;
36import com.sun.tools.javac.code.Symbol;
37import com.sun.tools.javac.code.Symbol.*;
38import com.sun.tools.javac.code.Symtab;
39import com.sun.tools.javac.code.Type;
40import com.sun.tools.javac.code.Type.*;
41import com.sun.tools.javac.code.Types;
42
43import static com.sun.tools.javac.code.Flags.*;
44import static com.sun.tools.javac.code.TypeTag.*;
45import static com.sun.tools.javac.code.Kinds.*;
46import static com.sun.tools.javac.code.Kinds.Kind.*;
47import static com.sun.tools.javac.util.LayoutCharacters.*;
48import 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 */
70public 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}
713