001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2024 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.construct.vm;
015
016import java.util.ArrayDeque;
017import java.util.Deque;
018import java.util.Iterator;
019import java.util.List;
020
021import javax.lang.model.element.Element;
022import javax.lang.model.element.ElementKind;
023import javax.lang.model.element.ExecutableElement;
024import javax.lang.model.element.PackageElement;
025import javax.lang.model.element.TypeElement;
026import javax.lang.model.element.TypeParameterElement;
027import javax.lang.model.element.VariableElement;
028
029import javax.lang.model.type.ArrayType;
030import javax.lang.model.type.DeclaredType;
031import javax.lang.model.type.TypeKind;
032import javax.lang.model.type.TypeMirror;
033import javax.lang.model.type.TypeVariable;
034import javax.lang.model.type.WildcardType;
035
036import org.microbean.construct.Domain;
037
038/**
039 * A utility class that provides <dfn>signatures</dfn> for {@link TypeMirror}s and {@link Element}s.
040 *
041 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
042 *
043 * @spec https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-4.html#jvms-4.7.9.1 Java Virtual Machine
044 * Specification, section 4.7.9.1
045 */
046@SuppressWarnings("try")
047public final class Signatures {
048
049  private Signatures() {
050    super();
051  }
052
053  /**
054   * Returns a <dfn>signature</dfn> for the supplied {@link Element}.
055   *
056   * @param e the {@link Element} for which a signature should be returned; must not be {@code null}
057   *
058   * @param d a {@link Domain} from which the {@link Element} is presumed to have originated; must not be {@code null}
059   *
060   * @exception NullPointerException if either argument is {@code null}
061   *
062   * @exception IllegalArgumentException if {@code e} has an {@link ElementKind} that is either {@link
063   * ElementKind#ANNOTATION_TYPE}, {@link ElementKind#BINDING_VARIABLE}, {@link ElementKind#EXCEPTION_PARAMETER}, {@link
064   * ElementKind#LOCAL_VARIABLE}, {@link ElementKind#MODULE}, {@link ElementKind#OTHER}, {@link ElementKind#PACKAGE},
065   * {@link ElementKind#RESOURCE_VARIABLE}, or {@link ElementKind#TYPE_PARAMETER}
066   *
067   * @return a non-{@code null} signature
068   *
069   * @see ElementKind
070   *
071   * @spec https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-4.html#jvms-4.7.9.1 Java Virtual Machine
072   * Specification, section 4.7.9.1
073   */
074  public static final String signature(final Element e, final Domain d) {
075    try (var lock = d.lock()) {
076      return switch (e.getKind()) {
077      case
078        CLASS,
079        ENUM,
080        INTERFACE,
081        RECORD -> classSignature((TypeElement)e, d);
082      case
083        CONSTRUCTOR,
084        INSTANCE_INIT,
085        METHOD,
086        STATIC_INIT -> methodSignature((ExecutableElement)e, d);
087      case
088        ENUM_CONSTANT,
089        FIELD,
090        PARAMETER,
091        RECORD_COMPONENT -> fieldSignature(e, d);
092      case
093        ANNOTATION_TYPE,
094        BINDING_VARIABLE,
095        EXCEPTION_PARAMETER,
096        LOCAL_VARIABLE,
097        MODULE,
098        OTHER,
099        PACKAGE,
100        RESOURCE_VARIABLE,
101        TYPE_PARAMETER ->
102        throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind());
103      };
104    }
105  }
106
107  private static final String classSignature(final TypeElement e, final Domain d) {
108    // Precondition: under domain lock
109    return switch (e.getKind()) {
110    case CLASS, ENUM, INTERFACE, RECORD -> {
111      if (!d.generic(e) && ((DeclaredType)e.getSuperclass()).getTypeArguments().isEmpty()) {
112        boolean signatureRequired = false;
113        for (final TypeMirror iface : e.getInterfaces()) {
114          if (!((DeclaredType)iface).getTypeArguments().isEmpty()) {
115            signatureRequired = true;
116            break;
117          }
118        }
119        if (!signatureRequired) {
120          yield null;
121        }
122      }
123      final StringBuilder sb = new StringBuilder();
124      classSignature(e, sb, d);
125      yield sb.toString();
126    }
127    default -> throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind());
128    };
129  }
130
131  private static final void classSignature(final TypeElement e, final StringBuilder sb, final Domain d) {
132    // Precondition: under domain lock
133    switch (e.getKind()) {
134    case CLASS, ENUM, INTERFACE, RECORD -> { // note: no ANNOTATION_TYPE on purpose
135      typeParameters(e.getTypeParameters(), sb, d);
136      final List<? extends TypeMirror> directSupertypes = d.directSupertypes(e.asType());
137      if (directSupertypes.isEmpty()) {
138        assert e.getQualifiedName().contentEquals("java.lang.Object") : "DeclaredType with no supertypes: " + e.asType();
139      } else {
140        final DeclaredType firstSupertype = (DeclaredType)directSupertypes.get(0);
141        assert firstSupertype.getKind() == TypeKind.DECLARED;
142        // "For an interface type with no direct super-interfaces, a type mirror representing java.lang.Object is
143        // returned." Therefore in all situations, given a non-empty list of direct supertypes, the first element will
144        // always be a non-interface class.
145        assert !((TypeElement)firstSupertype.asElement()).getKind().isInterface() : "Contract violation";
146        superclassSignature(firstSupertype, sb, d);
147        superinterfaceSignatures(directSupertypes.subList(1, directSupertypes.size()), sb, d);
148      }
149      break;
150    }
151    default -> throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind());
152    }
153  }
154
155  @SuppressWarnings("fallthrough")
156  private static final String methodSignature(final ExecutableElement e, final Domain d) {
157    // Precondition: under domain lock
158    if (e.getKind().isExecutable()) {
159      boolean throwsClauseRequired = false;
160      for (final TypeMirror exceptionType : e.getThrownTypes()) {
161        if (exceptionType.getKind() == TypeKind.TYPEVAR) {
162          throwsClauseRequired = true;
163          break;
164        }
165      }
166      if (!throwsClauseRequired && !d.generic(e)) {
167        final TypeMirror returnType = e.getReturnType();
168        switch (returnType.getKind()) {
169        case TYPEVAR:
170          break;
171        case DECLARED:
172          if (!((DeclaredType)returnType).getTypeArguments().isEmpty()) {
173            break;
174          }
175          // fall through
176        default:
177          boolean signatureRequired = false;
178          for (final VariableElement p : e.getParameters()) {
179            final TypeMirror parameterType = p.asType();
180            switch (parameterType.getKind()) {
181            case DECLARED:
182              signatureRequired = !((DeclaredType)parameterType).getTypeArguments().isEmpty();
183              break;
184            case TYPEVAR:
185              signatureRequired = true;
186              break;
187            default:
188              break;
189            }
190          }
191          if (!signatureRequired) {
192            return null;
193          }
194        }
195      }
196      final StringBuilder sb = new StringBuilder();
197      methodSignature(e, sb, throwsClauseRequired, d);
198      return sb.toString();
199    } else {
200      throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind());
201    }
202  }
203
204  private static final void methodSignature(final ExecutableElement e,
205                                            final StringBuilder sb,
206                                            final boolean throwsClauseRequired,
207                                            final Domain d) {
208    // Precondition: under domain lock
209    if (!e.getKind().isExecutable()) {
210      throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind());
211    }
212    typeParameters(e.getTypeParameters(), sb, d);
213    sb.append('(');
214    parameterSignatures(e.getParameters(), sb, d);
215    sb.append(')');
216    final TypeMirror returnType = e.getReturnType();
217    if (returnType.getKind() == TypeKind.VOID) {
218      sb.append('V');
219    } else {
220      typeSignature(returnType, sb, d);
221    }
222    if (throwsClauseRequired) {
223      throwsSignatures(e.getThrownTypes(), sb, d);
224    }
225  }
226
227  @SuppressWarnings("fallthrough")
228  private static final String fieldSignature(final Element e, final Domain d) {
229    // Precondition: under domain lock
230    return switch (e.getKind()) {
231    case ENUM_CONSTANT, FIELD, PARAMETER, RECORD_COMPONENT -> {
232      final TypeMirror t = e.asType();
233      switch (t.getKind()) {
234      case DECLARED:
235        if (((DeclaredType)t).getTypeArguments().isEmpty()) {
236          // TODO: is this sufficient? Or do we, for example, have to examine the type's supertypes to see if *they*
237          // "use" a parameterized type? Maybe we have to look at the enclosing type too? But if so, why only here, and
238          // why not the same sort of thing for the return type of a method (see above)?
239          yield null;
240        }
241        // fall through
242      case TYPEVAR:
243        final StringBuilder sb = new StringBuilder();
244        fieldSignature(e, sb, d);
245        yield sb.toString();
246      default:
247        // TODO: is this sufficient? Or do we, for example, have to examine the type's supertypes to see if *they*
248        // "use" a parameterized type? Maybe we have to look at the enclosing type too? But if so, why only here, and
249        // why not the same sort of thing for the return type of a method (see above)?
250        yield null;
251      }
252    }
253    default -> throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind());
254    };
255  }
256
257  private static final void fieldSignature(final Element e, final StringBuilder sb, final Domain d) {
258    // Precondition: under domain lock
259    switch (e.getKind()) {
260    case ENUM_CONSTANT, FIELD, PARAMETER, RECORD_COMPONENT -> typeSignature(e.asType(), sb, d);
261    default -> throw new IllegalArgumentException("e: " + e);
262    }
263  }
264
265  private static final void parameterSignatures(final List<? extends VariableElement> ps,
266                                                final StringBuilder sb,
267                                                final Domain d) {
268    // Precondition: under domain lock
269    for (final VariableElement p : ps) {
270      if (p.getKind() != ElementKind.PARAMETER) {
271        throw new IllegalArgumentException("ps: " + ps);
272      }
273      typeSignature(p.asType(), sb, d);
274    }
275  }
276
277  private static final void throwsSignatures(final List<? extends TypeMirror> ts, final StringBuilder sb, final Domain d) {
278    // Precondition: under domain lock
279    for (final TypeMirror t : ts) {
280      sb.append(switch (t.getKind()) {
281        case DECLARED, TYPEVAR -> '^';
282        default -> throw new IllegalArgumentException("ts: " + ts);
283        });
284      typeSignature(t, sb, d);
285    }
286  }
287
288  private static final void typeParameters(final List<? extends TypeParameterElement> tps,
289                                           final StringBuilder sb,
290                                           final Domain d) {
291    if (!tps.isEmpty()) {
292      sb.append('<');
293      // Precondition: under domain lock
294      for (final TypeParameterElement tp : tps) {
295        switch (tp.getKind()) {
296        case TYPE_PARAMETER -> typeParameter(tp, sb, d);
297        default -> throw new IllegalArgumentException("tps: " + tps);
298        }
299      }
300      sb.append('>');
301    }
302  }
303
304  private static final void typeParameter(final TypeParameterElement e, final StringBuilder sb, final Domain d) {
305    // Precondition: under domain lock
306    if (e.getKind() != ElementKind.TYPE_PARAMETER) {
307      throw new IllegalArgumentException("e: " + e);
308    }
309    final List<? extends TypeMirror> bounds = e.getBounds();
310    sb.append(e.getSimpleName()).append(':');
311    if (bounds.isEmpty()) {
312      sb.append("java.lang.Object");
313    } else {
314      classBound(bounds.get(0), sb, d);
315    }
316    interfaceBounds(bounds.subList(1, bounds.size()), sb, d);
317  }
318
319  private static final void classBound(final TypeMirror t, final StringBuilder sb, final Domain d) {
320    // Precondition: under domain lock
321    if (t.getKind() != TypeKind.DECLARED) {
322      throw new IllegalArgumentException("t: " + t);
323    }
324    typeSignature(t, sb, d);
325  }
326
327  private static final void interfaceBounds(final List<? extends TypeMirror> ts, final StringBuilder sb, final Domain d) {
328    // Precondition: under domain lock
329    for (final TypeMirror t : ts) {
330      interfaceBound(t, sb, d);
331    }
332  }
333
334  @SuppressWarnings("fallthrough")
335  private static final void interfaceBound(final TypeMirror t, final StringBuilder sb, final Domain d) {
336    // Precondition: under domain lock
337    switch (t.getKind()) {
338    case DECLARED:
339      if (((DeclaredType)t).asElement().getKind().isInterface()) {
340        sb.append(':');
341        typeSignature(t, sb, d);
342        return;
343      }
344      // fall through
345    default:
346      throw new IllegalArgumentException("t: " + t);
347    }
348  }
349
350  private static final void superclassSignature(final TypeMirror t, final StringBuilder sb, final Domain d) {
351    classTypeSignature(t, sb, d);
352  }
353
354  private static final void superinterfaceSignatures(final List<? extends TypeMirror> ts,
355                                                     final StringBuilder sb,
356                                                     final Domain d) {
357    // Precondition: under domain lock
358    for (final TypeMirror t : ts) {
359      superinterfaceSignature(t, sb, d);
360    }
361  }
362
363  @SuppressWarnings("fallthrough")
364  private static final void superinterfaceSignature(final TypeMirror t, final StringBuilder sb, final Domain d) {
365    // Precondition: under domain lock
366    switch (t.getKind()) {
367    case DECLARED:
368      if (((DeclaredType)t).asElement().getKind().isInterface()) {
369        classTypeSignature(t, sb, d);
370        return;
371      }
372      // fall through
373    default:
374      throw new IllegalArgumentException("t: " + t);
375    }
376  }
377
378  /**
379   * Returns a <dfn>signature</dfn> for the supplied {@link TypeMirror}.
380   *
381   * @param t the {@link TypeMirror} for which a signature should be returned; must not be {@code null}
382   *
383   * @param d a {@link Domain} from which the {@link TypeMirror} is presumed to have originated; must not be {@code
384   * null}
385   *
386   * @exception NullPointerException if either argument is {@code null}
387   *
388   * @exception IllegalArgumentException if {@code t} has an {@link TypeKind} that is either {@link TypeKind#ERROR},
389   * {@link TypeKind#EXECUTABLE}, {@link TypeKind#INTERSECTION}, {@link TypeKind#MODULE}, {@link TypeKind#NONE}, {@link
390   * TypeKind#NULL}, {@link TypeKind#OTHER}, {@link TypeKind#PACKAGE}, {@link TypeKind#UNION}, {@link TypeKind#VOID}, or
391   * {@link TypeKind#WILDCARD}
392   *
393   * @return a non-{@code null} signature
394   *
395   * @see TypeKind
396   *
397   * @spec https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-4.html#jvms-4.7.9.1 Java Virtual Machine
398   * Specification, section 4.7.9.1
399   */
400  public static final String signature(final TypeMirror t, final Domain d) {
401    final StringBuilder sb = new StringBuilder();
402    try (var lock = d.lock()) {
403      typeSignature(t, sb, d);
404    }
405    return sb.toString();
406  }
407
408  private static final void typeSignature(final TypeMirror t, final StringBuilder sb, final Domain d) {
409    // Precondition: under domain lock
410    switch (t.getKind()) {
411    case ARRAY    -> typeSignature(((ArrayType)t).getComponentType(), sb.append('['), d); // recursive
412    case BOOLEAN  -> sb.append('Z');
413    case BYTE     -> sb.append('B');
414    case CHAR     -> sb.append('C');
415    case DECLARED -> classTypeSignature((DeclaredType)t, sb, d);
416    case DOUBLE   -> sb.append('D');
417    case FLOAT    -> sb.append('F');
418    case INT      -> sb.append('I');
419    case LONG     -> sb.append('J');
420    case SHORT    -> sb.append('S');
421    case TYPEVAR  -> sb.append('T').append(((TypeVariable)t).asElement().getSimpleName()).append(';');
422    default       -> throw new IllegalArgumentException("t: " + t);
423    }
424  }
425
426  private static final void classTypeSignature(final TypeMirror t, final StringBuilder sb, final Domain d) {
427    // Precondition: under domain lock
428    switch (t.getKind()) {
429    case NONE:
430      return;
431    case DECLARED:
432      break;
433    default:
434      throw new IllegalArgumentException("t: " + t);
435    }
436    final DeclaredType dt = (DeclaredType)t;
437
438    // Build a deque of elements from the package to the (possibly inner or nested) class.
439    final Deque<Element> dq = new ArrayDeque<>();
440    Element e = dt.asElement();
441    while (e != null && e.getKind() != ElementKind.MODULE) {
442      dq.push(e);
443      e = e.getEnclosingElement();
444    }
445
446    sb.append('L');
447
448    final Iterator<Element> i = dq.iterator();
449    while (i.hasNext()) {
450      e = i.next();
451      switch (e.getKind()) {
452      case PACKAGE:
453        // java.lang becomes java/lang
454        sb.append(((PackageElement)e).getQualifiedName().toString().replace('.', '/'));
455        assert i.hasNext();
456        sb.append('/');
457        break;
458      case ANNOTATION_TYPE:
459      case CLASS:
460      case ENUM:
461      case INTERFACE:
462      case RECORD:
463        // Outer.Inner remains Outer.Inner (i.e. not Outer$Inner or Outer/Inner)
464        sb.append(e.getSimpleName());
465        if (i.hasNext()) {
466          sb.append('.');
467        }
468        break;
469      default:
470        // note that a method could fall in here; we just skip it
471        break;
472      }
473      i.remove();
474    }
475    assert dq.isEmpty();
476
477    // Now for the type arguments
478    final List<? extends TypeMirror> typeArguments = dt.getTypeArguments();
479    if (!typeArguments.isEmpty()) {
480      sb.append('<');
481      for (final TypeMirror ta : typeArguments) {
482        switch (ta.getKind()) {
483        case WILDCARD:
484          final WildcardType w = (WildcardType)ta;
485          final TypeMirror superBound = w.getSuperBound();
486          if (superBound == null) {
487            final TypeMirror extendsBound = w.getExtendsBound();
488            if (extendsBound == null) {
489              sb.append('*'); // I guess?
490            } else {
491              sb.append('+');
492              typeSignature(extendsBound, sb, d);
493            }
494          } else {
495            sb.append('-');
496            typeSignature(superBound, sb, d);
497          }
498          break;
499        default:
500          typeSignature(ta, sb, d);
501          break;
502        }
503      }
504      sb.append('>');
505    }
506
507    sb.append(';');
508  }
509
510}