/*
 * Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.javac.util;

import java.nio.file.Path;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

import com.sun.tools.javac.code.Printer;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.*;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.*;
import com.sun.tools.javac.code.Types;

import static com.sun.tools.javac.code.Flags.*;
import static com.sun.tools.javac.code.TypeTag.*;
import static com.sun.tools.javac.code.Kinds.*;
import static com.sun.tools.javac.code.Kinds.Kind.*;
import static com.sun.tools.javac.util.LayoutCharacters.*;
import static com.sun.tools.javac.util.RichDiagnosticFormatter.RichConfiguration.*;

A rich diagnostic formatter is a formatter that provides better integration with javac's type system. A diagostic is first preprocessed in order to keep track of each types/symbols in it; after these informations are collected, the diagnostic is rendered using a standard formatter, whose type/symbol printer has been replaced by a more refined version provided by this rich formatter. The rich formatter currently enables three different features: (i) simple class names - that is class names are displayed used a non qualified name (thus omitting package info) whenever possible - (ii) where clause list - a list of additional subdiagnostics that provide specific info about type-variables, captured types, intersection types that occur in the diagnostic that is to be formatted and (iii) type-variable disambiguation - when the diagnostic refers to two different type-variables with the same name, their representation is disambiguated by appending an index to the type variable name.

This is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice.

/** * A rich diagnostic formatter is a formatter that provides better integration * with javac's type system. A diagostic is first preprocessed in order to keep * track of each types/symbols in it; after these informations are collected, * the diagnostic is rendered using a standard formatter, whose type/symbol printer * has been replaced by a more refined version provided by this rich formatter. * The rich formatter currently enables three different features: (i) simple class * names - that is class names are displayed used a non qualified name (thus * omitting package info) whenever possible - (ii) where clause list - a list of * additional subdiagnostics that provide specific info about type-variables, * captured types, intersection types that occur in the diagnostic that is to be * formatted and (iii) type-variable disambiguation - when the diagnostic refers * to two different type-variables with the same name, their representation is * disambiguated by appending an index to the type variable name. * * <p><b>This is NOT part of any supported API. * If you write code that depends on this, you do so at your own risk. * This code and its internal interfaces are subject to change or * deletion without notice.</b> */
public class RichDiagnosticFormatter extends ForwardingDiagnosticFormatter<JCDiagnostic, AbstractDiagnosticFormatter> { final Symtab syms; final Types types; final JCDiagnostic.Factory diags; final JavacMessages messages; /* name simplifier used by this formatter */ protected ClassNameSimplifier nameSimplifier; /* type/symbol printer used by this formatter */ private RichPrinter printer; /* map for keeping track of a where clause associated to a given type */ Map<WhereClauseKind, Map<Type, JCDiagnostic>> whereClauses;
Get the DiagnosticFormatter instance for this context.
/** Get the DiagnosticFormatter instance for this context. */
public static RichDiagnosticFormatter instance(Context context) { RichDiagnosticFormatter instance = context.get(RichDiagnosticFormatter.class); if (instance == null) instance = new RichDiagnosticFormatter(context); return instance; } protected RichDiagnosticFormatter(Context context) { super((AbstractDiagnosticFormatter)Log.instance(context).getDiagnosticFormatter()); setRichPrinter(new RichPrinter()); this.syms = Symtab.instance(context); this.diags = JCDiagnostic.Factory.instance(context); this.types = Types.instance(context); this.messages = JavacMessages.instance(context); whereClauses = new EnumMap<>(WhereClauseKind.class); configuration = new RichConfiguration(Options.instance(context), formatter); for (WhereClauseKind kind : WhereClauseKind.values()) whereClauses.put(kind, new LinkedHashMap<Type, JCDiagnostic>()); } @Override public String format(JCDiagnostic diag, Locale l) { StringBuilder sb = new StringBuilder(); nameSimplifier = new ClassNameSimplifier(); for (WhereClauseKind kind : WhereClauseKind.values()) whereClauses.get(kind).clear(); preprocessDiagnostic(diag); sb.append(formatter.format(diag, l)); if (getConfiguration().isEnabled(RichFormatterFeature.WHERE_CLAUSES)) { List<JCDiagnostic> clauses = getWhereClauses(); String indent = formatter.isRaw() ? "" : formatter.indentString(DetailsInc); for (JCDiagnostic d : clauses) { String whereClause = formatter.format(d, l); if (whereClause.length() > 0) { sb.append('\n' + indent + whereClause); } } } return sb.toString(); } @Override public String formatMessage(JCDiagnostic diag, Locale l) { nameSimplifier = new ClassNameSimplifier(); preprocessDiagnostic(diag); return super.formatMessage(diag, l); }
Sets the type/symbol printer used by this formatter.
Params:
  • printer – the rich printer to be set
/** * Sets the type/symbol printer used by this formatter. * @param printer the rich printer to be set */
protected void setRichPrinter(RichPrinter printer) { this.printer = printer; formatter.setPrinter(printer); }
Returns the type/symbol printer used by this formatter.
Returns:type/symbol rich printer
/** * Returns the type/symbol printer used by this formatter. * @return type/symbol rich printer */
protected RichPrinter getRichPrinter() { return printer; }
Preprocess a given diagnostic by looking both into its arguments and into its subdiagnostics (if any). This preprocessing is responsible for generating info corresponding to features like where clauses, name simplification, etc.
Params:
  • diag – the diagnostic to be preprocessed
/** * Preprocess a given diagnostic by looking both into its arguments and into * its subdiagnostics (if any). This preprocessing is responsible for * generating info corresponding to features like where clauses, name * simplification, etc. * * @param diag the diagnostic to be preprocessed */
protected void preprocessDiagnostic(JCDiagnostic diag) { for (Object o : diag.getArgs()) { if (o != null) { preprocessArgument(o); } } if (diag.isMultiline()) { for (JCDiagnostic d : diag.getSubdiagnostics()) preprocessDiagnostic(d); } }
Preprocess a diagnostic argument. A type/symbol argument is preprocessed by specialized type/symbol preprocessors.
Params:
  • arg – the argument to be translated
/** * Preprocess a diagnostic argument. A type/symbol argument is * preprocessed by specialized type/symbol preprocessors. * * @param arg the argument to be translated */
protected void preprocessArgument(Object arg) { if (arg instanceof Type) { preprocessType((Type)arg); } else if (arg instanceof Symbol) { preprocessSymbol((Symbol)arg); } else if (arg instanceof JCDiagnostic) { preprocessDiagnostic((JCDiagnostic)arg); } else if (arg instanceof Iterable<?> && !(arg instanceof Path)) { for (Object o : (Iterable<?>)arg) { preprocessArgument(o); } } }
Build a list of multiline diagnostics containing detailed info about type-variables, captured types, and intersection types
Returns:where clause list
/** * Build a list of multiline diagnostics containing detailed info about * type-variables, captured types, and intersection types * * @return where clause list */
protected List<JCDiagnostic> getWhereClauses() { List<JCDiagnostic> clauses = List.nil(); for (WhereClauseKind kind : WhereClauseKind.values()) { List<JCDiagnostic> lines = List.nil(); for (Map.Entry<Type, JCDiagnostic> entry : whereClauses.get(kind).entrySet()) { lines = lines.prepend(entry.getValue()); } if (!lines.isEmpty()) { String key = kind.key(); if (lines.size() > 1) key += ".1"; JCDiagnostic d = diags.fragment(key, whereClauses.get(kind).keySet()); d = new JCDiagnostic.MultilineDiagnostic(d, lines.reverse()); clauses = clauses.prepend(d); } } return clauses.reverse(); } private int indexOf(Type type, WhereClauseKind kind) { int index = 1; for (Type t : whereClauses.get(kind).keySet()) { if (t.tsym == type.tsym) { return index; } if (kind != WhereClauseKind.TYPEVAR || t.toString().equals(type.toString())) { index++; } } return -1; } private boolean unique(TypeVar typevar) { typevar = (TypeVar) typevar.stripMetadata(); int found = 0; for (Type t : whereClauses.get(WhereClauseKind.TYPEVAR).keySet()) { if (t.stripMetadata().toString().equals(typevar.toString())) { found++; } } if (found < 1) throw new AssertionError("Missing type variable in where clause: " + typevar); return found == 1; } //where
This enum defines all posssible kinds of where clauses that can be attached by a rich diagnostic formatter to a given diagnostic
/** * This enum defines all posssible kinds of where clauses that can be * attached by a rich diagnostic formatter to a given diagnostic */
enum WhereClauseKind {
where clause regarding a type variable
/** where clause regarding a type variable */
TYPEVAR("where.description.typevar"),
where clause regarding a captured type
/** where clause regarding a captured type */
CAPTURED("where.description.captured"),
where clause regarding an intersection type
/** where clause regarding an intersection type */
INTERSECTION("where.description.intersection");
resource key for this where clause kind
/** resource key for this where clause kind */
private final String key; WhereClauseKind(String key) { this.key = key; } String key() { return key; } } // <editor-fold defaultstate="collapsed" desc="name simplifier">
A name simplifier keeps track of class names usages in order to determine whether a class name can be compacted or not. Short names are not used if a conflict is detected, e.g. when two classes with the same simple name belong to different packages - in this case the formatter reverts to fullnames as compact names might lead to a confusing diagnostic.
/** * A name simplifier keeps track of class names usages in order to determine * whether a class name can be compacted or not. Short names are not used * if a conflict is detected, e.g. when two classes with the same simple * name belong to different packages - in this case the formatter reverts * to fullnames as compact names might lead to a confusing diagnostic. */
protected class ClassNameSimplifier { /* table for keeping track of all short name usages */ Map<Name, List<Symbol>> nameClashes = new HashMap<>();
Add a name usage to the simplifier's internal cache
/** * Add a name usage to the simplifier's internal cache */
protected void addUsage(Symbol sym) { Name n = sym.getSimpleName(); List<Symbol> conflicts = nameClashes.get(n); if (conflicts == null) { conflicts = List.nil(); } if (!conflicts.contains(sym)) nameClashes.put(n, conflicts.append(sym)); } public String simplify(Symbol s) { String name = s.getQualifiedName().toString(); if (!s.type.isCompound() && !s.type.isPrimitive()) { List<Symbol> conflicts = nameClashes.get(s.getSimpleName()); if (conflicts == null || (conflicts.size() == 1 && conflicts.contains(s))) { List<Name> l = List.nil(); Symbol s2 = s; while (s2.type.hasTag(CLASS) && s2.type.getEnclosingType().hasTag(CLASS) && s2.owner.kind == TYP) { l = l.prepend(s2.getSimpleName()); s2 = s2.owner; } l = l.prepend(s2.getSimpleName()); StringBuilder buf = new StringBuilder(); String sep = ""; for (Name n2 : l) { buf.append(sep); buf.append(n2); sep = "."; } name = buf.toString(); } } return name; } } // </editor-fold> // <editor-fold defaultstate="collapsed" desc="rich printer">
Enhanced type/symbol printer that provides support for features like simple names and type variable disambiguation. This enriched printer exploits the info discovered during type/symbol preprocessing. This printer is set on the delegate formatter so that rich type/symbol info can be properly rendered.
/** * Enhanced type/symbol printer that provides support for features like simple names * and type variable disambiguation. This enriched printer exploits the info * discovered during type/symbol preprocessing. This printer is set on the delegate * formatter so that rich type/symbol info can be properly rendered. */
protected class RichPrinter extends Printer { @Override public String localize(Locale locale, String key, Object... args) { return formatter.localize(locale, key, args); } @Override public String capturedVarId(CapturedType t, Locale locale) { return indexOf(t, WhereClauseKind.CAPTURED) + ""; } @Override public String visitType(Type t, Locale locale) { String s = super.visitType(t, locale); if (t == syms.botType) s = localize(locale, "compiler.misc.type.null"); return s; } @Override public String visitCapturedType(CapturedType t, Locale locale) { if (getConfiguration().isEnabled(RichFormatterFeature.WHERE_CLAUSES)) { return localize(locale, "compiler.misc.captured.type", indexOf(t, WhereClauseKind.CAPTURED)); } else return super.visitCapturedType(t, locale); } @Override public String visitClassType(ClassType t, Locale locale) { if (t.isCompound() && getConfiguration().isEnabled(RichFormatterFeature.WHERE_CLAUSES)) { return localize(locale, "compiler.misc.intersection.type", indexOf(t, WhereClauseKind.INTERSECTION)); } else return super.visitClassType(t, locale); } @Override protected String className(ClassType t, boolean longform, Locale locale) { Symbol sym = t.tsym; if (sym.name.length() == 0 || !getConfiguration().isEnabled(RichFormatterFeature.SIMPLE_NAMES)) { return super.className(t, longform, locale); } else if (longform) return nameSimplifier.simplify(sym).toString(); else return sym.name.toString(); } @Override public String visitTypeVar(TypeVar t, Locale locale) { if (unique(t) || !getConfiguration().isEnabled(RichFormatterFeature.UNIQUE_TYPEVAR_NAMES)) { return t.toString(); } else { return localize(locale, "compiler.misc.type.var", t.toString(), indexOf(t, WhereClauseKind.TYPEVAR)); } } @Override public String visitClassSymbol(ClassSymbol s, Locale locale) { if (s.type.isCompound()) { return visit(s.type, locale); } String name = nameSimplifier.simplify(s); if (name.length() == 0 || !getConfiguration().isEnabled(RichFormatterFeature.SIMPLE_NAMES)) { return super.visitClassSymbol(s, locale); } else { return name; } } @Override public String visitMethodSymbol(MethodSymbol s, Locale locale) { String ownerName = visit(s.owner, locale); if (s.isStaticOrInstanceInit()) { return ownerName; } else { String ms = (s.name == s.name.table.names.init) ? ownerName : s.name.toString(); if (s.type != null) { if (s.type.hasTag(FORALL)) { ms = "<" + visitTypes(s.type.getTypeArguments(), locale) + ">" + ms; } ms += "(" + printMethodArgs( s.type.getParameterTypes(), (s.flags() & VARARGS) != 0, locale) + ")"; } return ms; } } } // </editor-fold> // <editor-fold defaultstate="collapsed" desc="type scanner">
Preprocess a given type looking for (i) additional info (where clauses) to be added to the main diagnostic (ii) names to be compacted.
/** * Preprocess a given type looking for (i) additional info (where clauses) to be * added to the main diagnostic (ii) names to be compacted. */
protected void preprocessType(Type t) { typePreprocessor.visit(t); } //where protected Types.UnaryVisitor<Void> typePreprocessor = new Types.UnaryVisitor<Void>() { public Void visit(List<Type> ts) { for (Type t : ts) visit(t); return null; } @Override public Void visitForAll(ForAll t, Void ignored) { visit(t.tvars); visit(t.qtype); return null; } @Override public Void visitMethodType(MethodType t, Void ignored) { visit(t.argtypes); visit(t.restype); return null; } @Override public Void visitErrorType(ErrorType t, Void ignored) { Type ot = t.getOriginalType(); if (ot != null) visit(ot); return null; } @Override public Void visitArrayType(ArrayType t, Void ignored) { visit(t.elemtype); return null; } @Override public Void visitWildcardType(WildcardType t, Void ignored) { visit(t.type); return null; } public Void visitType(Type t, Void ignored) { return null; } @Override public Void visitCapturedType(CapturedType t, Void ignored) { if (indexOf(t, WhereClauseKind.CAPTURED) == -1) { String suffix = t.lower == syms.botType ? ".1" : ""; JCDiagnostic d = diags.fragment("where.captured"+ suffix, t, t.bound, t.lower, t.wildcard); whereClauses.get(WhereClauseKind.CAPTURED).put(t, d); visit(t.wildcard); visit(t.lower); visit(t.bound); } return null; } @Override public Void visitClassType(ClassType t, Void ignored) { if (t.isCompound()) { if (indexOf(t, WhereClauseKind.INTERSECTION) == -1) { Type supertype = types.supertype(t); List<Type> interfaces = types.interfaces(t); JCDiagnostic d = diags.fragment("where.intersection", t, interfaces.prepend(supertype)); whereClauses.get(WhereClauseKind.INTERSECTION).put(t, d); visit(supertype); visit(interfaces); } } else if (t.tsym.name.isEmpty()) { //anon class ClassType norm = (ClassType) t.tsym.type; if (norm != null) { if (norm.interfaces_field != null && norm.interfaces_field.nonEmpty()) { visit(norm.interfaces_field.head); } else { visit(norm.supertype_field); } } } nameSimplifier.addUsage(t.tsym); visit(t.getTypeArguments()); if (t.getEnclosingType() != Type.noType) visit(t.getEnclosingType()); return null; } @Override public Void visitTypeVar(TypeVar t, Void ignored) { t = (TypeVar)t.stripMetadataIfNeeded(); if (indexOf(t, WhereClauseKind.TYPEVAR) == -1) { //access the bound type and skip error types Type bound = t.bound; while ((bound instanceof ErrorType)) bound = ((ErrorType)bound).getOriginalType(); //retrieve the bound list - if the type variable //has not been attributed the bound is not set List<Type> bounds = (bound != null) && (bound.hasTag(CLASS) || bound.hasTag(TYPEVAR)) ? types.getBounds(t) : List.nil(); nameSimplifier.addUsage(t.tsym); boolean boundErroneous = bounds.head == null || bounds.head.hasTag(NONE) || bounds.head.hasTag(ERROR); if ((t.tsym.flags() & SYNTHETIC) == 0) { //this is a true typevar JCDiagnostic d = diags.fragment("where.typevar" + (boundErroneous ? ".1" : ""), t, bounds, kindName(t.tsym.location()), t.tsym.location()); whereClauses.get(WhereClauseKind.TYPEVAR).put(t, d); symbolPreprocessor.visit(t.tsym.location(), null); visit(bounds); } else { Assert.check(!boundErroneous); //this is a fresh (synthetic) tvar JCDiagnostic d = diags.fragment("where.fresh.typevar", t, bounds); whereClauses.get(WhereClauseKind.TYPEVAR).put(t, d); visit(bounds); } } return null; } }; // </editor-fold> // <editor-fold defaultstate="collapsed" desc="symbol scanner">
Preprocess a given symbol looking for (i) additional info (where clauses) to be added to the main diagnostic (ii) names to be compacted
/** * Preprocess a given symbol looking for (i) additional info (where clauses) to be * added to the main diagnostic (ii) names to be compacted */
protected void preprocessSymbol(Symbol s) { symbolPreprocessor.visit(s, null); } //where protected Types.DefaultSymbolVisitor<Void, Void> symbolPreprocessor = new Types.DefaultSymbolVisitor<Void, Void>() { @Override public Void visitClassSymbol(ClassSymbol s, Void ignored) { if (s.type.isCompound()) { typePreprocessor.visit(s.type); } else { nameSimplifier.addUsage(s); } return null; } @Override public Void visitSymbol(Symbol s, Void ignored) { return null; } @Override public Void visitMethodSymbol(MethodSymbol s, Void ignored) { visit(s.owner, null); if (s.type != null) typePreprocessor.visit(s.type); return null; } }; // </editor-fold> @Override public RichConfiguration getConfiguration() { //the following cast is always safe - see init return (RichConfiguration)configuration; }
Configuration object provided by the rich formatter.
/** * Configuration object provided by the rich formatter. */
public static class RichConfiguration extends ForwardingDiagnosticFormatter.ForwardingConfiguration {
set of enabled rich formatter's features
/** set of enabled rich formatter's features */
protected java.util.EnumSet<RichFormatterFeature> features; @SuppressWarnings("fallthrough") public RichConfiguration(Options options, AbstractDiagnosticFormatter formatter) { super(formatter.getConfiguration()); features = formatter.isRaw() ? EnumSet.noneOf(RichFormatterFeature.class) : EnumSet.of(RichFormatterFeature.SIMPLE_NAMES, RichFormatterFeature.WHERE_CLAUSES, RichFormatterFeature.UNIQUE_TYPEVAR_NAMES); String diagOpts = options.get("diags.formatterOptions"); if (diagOpts != null) { for (String args: diagOpts.split(",")) { if (args.equals("-where")) { features.remove(RichFormatterFeature.WHERE_CLAUSES); } else if (args.equals("where")) { features.add(RichFormatterFeature.WHERE_CLAUSES); } if (args.equals("-simpleNames")) { features.remove(RichFormatterFeature.SIMPLE_NAMES); } else if (args.equals("simpleNames")) { features.add(RichFormatterFeature.SIMPLE_NAMES); } if (args.equals("-disambiguateTvars")) { features.remove(RichFormatterFeature.UNIQUE_TYPEVAR_NAMES); } else if (args.equals("disambiguateTvars")) { features.add(RichFormatterFeature.UNIQUE_TYPEVAR_NAMES); } } } }
Returns a list of all the features supported by the rich formatter.
Returns:list of supported features
/** * Returns a list of all the features supported by the rich formatter. * @return list of supported features */
public RichFormatterFeature[] getAvailableFeatures() { return RichFormatterFeature.values(); }
Enable a specific feature on this rich formatter.
Params:
  • feature – feature to be enabled
/** * Enable a specific feature on this rich formatter. * @param feature feature to be enabled */
public void enable(RichFormatterFeature feature) { features.add(feature); }
Disable a specific feature on this rich formatter.
Params:
  • feature – feature to be disabled
/** * Disable a specific feature on this rich formatter. * @param feature feature to be disabled */
public void disable(RichFormatterFeature feature) { features.remove(feature); }
Is a given feature enabled on this formatter?
Params:
  • feature – feature to be tested
/** * Is a given feature enabled on this formatter? * @param feature feature to be tested */
public boolean isEnabled(RichFormatterFeature feature) { return features.contains(feature); }
The advanced formatting features provided by the rich formatter
/** * The advanced formatting features provided by the rich formatter */
public enum RichFormatterFeature {
a list of additional info regarding a given type/symbol
/** a list of additional info regarding a given type/symbol */
WHERE_CLAUSES,
full class names simplification (where possible)
/** full class names simplification (where possible) */
SIMPLE_NAMES,
type-variable names disambiguation
/** type-variable names disambiguation */
UNIQUE_TYPEVAR_NAMES } } }