001/*
002 * ============================================================================
003 * Copyright © 2015 Square, Inc.
004 * Copyright for the modifications © 2018-2024 by Thomas Thrien.
005 * ============================================================================
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019
020package org.tquadrat.foundation.javacomposer.internal;
021
022import static java.lang.String.join;
023import static java.util.Collections.unmodifiableMap;
024import static java.util.Locale.ROOT;
025import static java.util.stream.Collectors.toCollection;
026import static org.apiguardian.api.API.Status.INTERNAL;
027import static org.tquadrat.foundation.javacomposer.SuppressableWarnings.JAVADOC;
028import static org.tquadrat.foundation.javacomposer.SuppressableWarnings.createSuppressWarningsAnnotation;
029import static org.tquadrat.foundation.javacomposer.internal.Util.NULL_REFERENCE;
030import static org.tquadrat.foundation.javacomposer.internal.Util.stringLiteralWithDoubleQuotes;
031import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
032import static org.tquadrat.foundation.lang.CommonConstants.NULL_STRING;
033import static org.tquadrat.foundation.lang.Objects.checkState;
034import static org.tquadrat.foundation.lang.Objects.isNull;
035import static org.tquadrat.foundation.lang.Objects.nonNull;
036import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
037import static org.tquadrat.foundation.lang.Objects.requireValidIntegerArgument;
038import static org.tquadrat.foundation.lang.Objects.requireValidNonNullArgument;
039import static org.tquadrat.foundation.util.StringUtils.isEmpty;
040import static org.tquadrat.foundation.util.StringUtils.isNotEmpty;
041
042import javax.lang.model.SourceVersion;
043import javax.lang.model.element.Modifier;
044import java.io.IOException;
045import java.io.UncheckedIOException;
046import java.util.ArrayList;
047import java.util.Collection;
048import java.util.EnumSet;
049import java.util.LinkedHashMap;
050import java.util.LinkedHashSet;
051import java.util.List;
052import java.util.Map;
053import java.util.Optional;
054import java.util.Set;
055
056import org.apiguardian.api.API;
057import org.tquadrat.foundation.annotation.ClassVersion;
058import org.tquadrat.foundation.exception.UnsupportedEnumError;
059import org.tquadrat.foundation.javacomposer.JavaComposer;
060import org.tquadrat.foundation.javacomposer.Layout;
061import org.tquadrat.foundation.lang.Objects;
062
063/**
064 *  Converts a
065 *  {@link org.tquadrat.foundation.javacomposer.JavaFile JavaFile}
066 *  to a string suitable to both human- and javac-consumption. This honours
067 *  imports, indentation, and deferred variable names.
068 *
069 *  @author Square,Inc.
070 *  @modified Thomas Thrien - thomas.thrien@tquadrat.org
071 *  @version $Id: CodeWriter.java 1105 2024-02-28 12:58:46Z tquadrat $
072 *  @since 0.0.5
073 *
074 *  @UMLGraph.link
075 */
076@SuppressWarnings( {"ClassWithTooManyFields", "ClassWithTooManyMethods", "OverlyComplexClass"} )
077@ClassVersion( sourceVersion = "$Id: CodeWriter.java 1105 2024-02-28 12:58:46Z tquadrat $" )
078@API( status = INTERNAL, since = "0.0.5" )
079public final class CodeWriter
080{
081        /*---------------*\
082    ====** Inner Classes **====================================================
083        \*---------------*/
084    /**
085     *  The comment types.
086     *
087     *  @extauthor  Thomas Thrien - thomas.thrien@tquadrat.org
088     *  @version $Id: CodeWriter.java 1105 2024-02-28 12:58:46Z tquadrat $
089     *  @since 0.2.0
090     *
091     *  @UMLGraph.link
092     */
093    @ClassVersion( sourceVersion = "$Id: CodeWriter.java 1105 2024-02-28 12:58:46Z tquadrat $" )
094    @API( status = INTERNAL, since = "0.0.5" )
095    private static enum CommentType
096    {
097            /*------------------*\
098        ====** Enum Declaration **=============================================
099            \*------------------*/
100        /**
101         *  No comment at all.
102         */
103        NO_COMMENT,
104
105        /**
106         *  A Javadoc comment.
107         */
108        JAVADOC_COMMENT,
109
110        /**
111         *  A block comment.
112         */
113        BLOCK_COMMENT,
114
115        /**
116         *  A line comment.
117         */
118        LINE_COMMENT
119    }
120    //  enum CommentType
121
122        /*-----------*\
123    ====** Constants **========================================================
124        \*-----------*/
125    /**
126     *  Sentinel value that indicates that no user-provided package has been
127     *  set.
128     */
129    @SuppressWarnings( "StringOperationCanBeSimplified" )
130    private static final String NO_PACKAGE = new String();
131
132        /*------------*\
133    ====** Attributes **=======================================================
134        \*------------*/
135    /**
136     *  The reference to the factory.
137     */
138    @SuppressWarnings( "UseOfConcreteClass" )
139    private final JavaComposer m_Composer;
140
141    /**
142     *  Flag that indicates that we are currently writing a normal comment.
143     */
144    private CommentType m_CurrentlyEmittingComment = CommentType.NO_COMMENT;
145
146    /**
147     *  The types that can be imported.
148     */
149    private final Map<String,ClassNameImpl> m_ImportableTypes = new LinkedHashMap<>();
150
151    /**
152     *  The imported types.
153     */
154    private final Map<String,ClassNameImpl> m_ImportedTypes;
155
156    /**
157     *  The indentation.
158     */
159    private final String m_Indent;
160
161    /**
162     *  The indentation level.
163     */
164    private int m_IndentLevel;
165
166    /**
167     *  The layout for the output.
168     */
169    private final Layout m_Layout;
170
171    /**
172     *  The output target.
173     */
174    @SuppressWarnings( "UseOfConcreteClass" )
175    private final LineWrapper m_LineWrapper;
176
177    /**
178     *  The current package name.
179     */
180    private String m_PackageName = NO_PACKAGE;
181
182    /**
183     *  The referenced names.
184     */
185    private final Collection<String> m_ReferencedNames = new LinkedHashSet<>();
186
187    /**
188     *  When a statement will be emitted, this is the line of the statement
189     *  currently being written. The first line of a statement is indented
190     *  normally and subsequent wrapped lines are double-indented. This is -1
191     *  when the currently-written line isn't part of a statement.
192     */
193    private int m_StatementLine = -1;
194
195    /**
196     *  The names of statically imported classes.
197     */
198    private final Set<String> m_StaticImportClassNames;
199
200    /**
201     *  The static imports.
202     */
203    private final Set<String> m_StaticImports;
204
205    /**
206     *  A flag that controls the trailing new line.
207     */
208    private boolean m_TrailingNewline;
209
210    /**
211     *  The types.
212     */
213    private final List<TypeSpecImpl> m_TypeSpecStack = new ArrayList<>();
214
215        /*--------------*\
216    ====** Constructors **=====================================================
217        \*--------------*/
218    /**
219     *  Creates a new {@code CodeWriter} instance.
220     *
221     *  @param  out The output target.
222     *
223     *  @deprecated Use
224     *      {@link #CodeWriter(JavaComposer, Appendable)}
225     *      instead.
226     */
227    @Deprecated( since = "0.2.0", forRemoval = true )
228    public CodeWriter( final Appendable out ) { this( new JavaComposer(), out, Set.of() ); }
229
230    /**
231     *  Creates a new {@code CodeWriter} instance.
232     *
233     *  @param  out The output target.
234     *  @param  layout  The layout for the output.
235     *  @param  staticImports   The static imports.
236     *
237     *  @deprecated Use
238     *      {@link #CodeWriter(JavaComposer, Appendable)}
239     *      instead.
240     */
241    @Deprecated( since = "0.2.0", forRemoval = true )
242    public CodeWriter( final Appendable out, final Layout layout, final Set<String> staticImports )
243    {
244        this( new JavaComposer( layout ), out, Map.of(), staticImports );
245    }   //  CodeWriter()
246
247    /**
248     *  Creates a new {@code CodeWriter} instance.
249     *
250     *  @param  out The output target.
251     *  @param  layout  The layout for the output.
252     *  @param  indent  The indentation; will be ignored.
253     *  @param  importedTypes   The imported types.
254     *  @param  staticImports   The static imports.
255     *
256     *  @deprecated Use
257     *      {@link #CodeWriter(JavaComposer, Appendable)}
258     *      instead.
259     */
260    @SuppressWarnings( "unused" )
261    @Deprecated( since = "0.2.0", forRemoval = true )
262    public CodeWriter( final Appendable out, final Layout layout, final String indent, final Map<String,ClassNameImpl> importedTypes, final Set<String> staticImports )
263    {
264        this( new JavaComposer( layout ), out, importedTypes, staticImports );
265    }   //  CodeWriter()
266
267    /**
268     *  Creates a new {@code CodeWriter} instance.
269     *
270     *  @param  composer    The reference to the factory that created this
271     *      code writer instance.
272     *  @param  out The output target.
273     */
274    @SuppressWarnings( "UseOfConcreteClass" )
275    public CodeWriter( final JavaComposer composer, final Appendable out ) { this( composer, out, Set.of() ); }
276
277    /**
278     *  Creates a new {@code CodeWriter} instance.
279     *
280     *  @param  composer    The reference to the factory that created this
281     *      code writer instance.
282     *  @param  out The output target.
283     *  @param  staticImports   The static imports.
284     */
285    @SuppressWarnings( "UseOfConcreteClass" )
286    public CodeWriter( final JavaComposer composer, final Appendable out, final Set<String> staticImports )
287    {
288        this( composer, out, Map.of(), staticImports );
289    }   //  CodeWriter()
290
291    /**
292     *  Creates a new {@code CodeWriter} instance.
293     *
294     *  @param  composer    The reference to the factory that created this
295     *      code writer instance.
296     *  @param  out The output target.
297     *  @param  importedTypes   The imported types.
298     *  @param  staticImports   The static imports.
299     */
300    @SuppressWarnings( "UseOfConcreteClass" )
301    public CodeWriter( final JavaComposer composer, final Appendable out, final Map<String,ClassNameImpl> importedTypes, final Set<String> staticImports )
302    {
303        m_Composer = requireNonNullArgument( composer, "composer" );
304        m_Layout = m_Composer.getLayout();
305        m_Indent = m_Layout.indent();
306        m_LineWrapper = new LineWrapper( requireNonNullArgument( out, "out" ), m_Indent, 100 );
307        m_ImportedTypes = requireNonNullArgument( importedTypes, "importedTypes" );
308        m_StaticImports = requireNonNullArgument( staticImports, "staticImports" );
309        m_StaticImportClassNames = m_StaticImports.stream()
310            .map( s -> s.substring( 0, s.lastIndexOf( '.' ) ) )
311            .collect( toCollection( LinkedHashSet::new ) );
312    }   //  CodeWriter()
313
314        /*---------*\
315    ====** Methods **==========================================================
316        \*---------*/
317    /**
318     *  <p>{@summary Emits the given String to the output target.}</p>
319     *  <p>Delegates to
320     *  {@link #emitAndIndent(CharSequence)}.</p>
321     *
322     *  @param  input   The String.
323     *  @return This {@code CodeWriter} instance.
324     *  @throws UncheckedIOException A problem occurred when writing to the
325     *      output target.
326     */
327    public final CodeWriter emit( final CharSequence input ) throws UncheckedIOException { return emitAndIndent( input ); }
328
329    /**
330     *  Emits a
331     *  {@link CodeBlockImpl}
332     *  instance to the output target that is created on the fly from the given
333     *  arguments.
334     *
335     *  @param  format  The format.
336     *  @param  args    The arguments.
337     *  @return This {@code CodeWriter} instance.
338     *  @throws UncheckedIOException A problem occurred when writing to the
339     *      output target.
340     */
341    public final CodeWriter emit( final String format, final Object... args ) throws UncheckedIOException
342    {
343        final var builder = new CodeBlockImpl.BuilderImpl( m_Composer );
344        builder.addWithoutDebugInfo( format, args );
345        emit( builder.build() );
346
347        //---* Done *----------------------------------------------------------
348        return this;
349    }   //  emit()
350
351    /**
352     *  Emits the given
353     *  {@link CodeBlockImpl}
354     *  instance to the output target.
355     *
356     *  @param  codeBlock   The code block.
357     *  @return This {@code CodeWriter} instance.
358     *  @throws UncheckedIOException A problem occurred when writing to the
359     *      output target.
360     */
361    @SuppressWarnings( {"AssignmentToNull", "OverlyNestedMethod", "OverlyComplexMethod", "UseOfConcreteClass"} )
362    public final CodeWriter emit( final CodeBlockImpl codeBlock ) throws UncheckedIOException
363    {
364        var argIndex = 0;
365        ClassNameImpl deferredTypeName = null; // used by "import static" logic
366        final var partIterator = requireNonNullArgument( codeBlock, "codeBlock" ).formatParts().listIterator();
367        while( partIterator.hasNext() )
368        {
369            final var part = partIterator.next();
370            //noinspection SwitchStatementWithTooManyBranches
371            switch( part )
372            {
373                case "$L" -> emitLiteral( codeBlock.args().get( argIndex++ ) );
374                case "$N" -> emitAndIndent( (CharSequence) codeBlock.args().get( argIndex++ ) );
375                case "$S" ->
376                {
377                    final var string = codeBlock.args().get( argIndex++ );
378
379                    //---* Emit null as a literal null: no quotes *------------
380                    emitAndIndent( string == NULL_REFERENCE ? "null" : stringLiteralWithDoubleQuotes( (String) string, m_Indent ) );
381                }
382
383                case "$T" ->
384                {
385                    final var typeName = (TypeNameImpl) codeBlock.args().get( argIndex++ );
386
387                    /*
388                     * Defer "typeName.emit(this)" if next format part will be
389                     * handled by the default case.
390                     */
391                    deferredTypeName = null;
392                    if( typeName instanceof final ClassNameImpl candidate && partIterator.hasNext() )
393                    {
394                        if( !codeBlock.formatParts().get( partIterator.nextIndex() ).startsWith( "$" ) )
395                        {
396                            if( m_StaticImportClassNames.contains( candidate.canonicalName() ) )
397                            {
398                                checkState( isNull( deferredTypeName ), () -> new IllegalStateException( "pending type for static import?!" ) );
399                                deferredTypeName = candidate;
400                            }
401                        }
402                    }
403                    if( isNull( deferredTypeName ) ) typeName.emit( this );
404                }
405
406                case "$$" -> emitAndIndent( "$" );
407                case "$>" -> indent();
408                case "$<" -> unindent();
409                case "$[" ->
410                {
411                    checkState( m_StatementLine == -1, () -> new IllegalStateException( "statement enter $[ followed by statement enter $[" ) );
412                    m_StatementLine = 0;
413                }
414
415                case "$]" ->
416                {
417                    checkState( m_StatementLine != -1, () -> new IllegalStateException( "statement exit $] has no matching statement enter $[" ) );
418                    if( m_StatementLine > 0 )
419                    {
420                        unindent( 2 ); // End a multi-line statement. Decrease
421                                       // the indentation level.
422                    }
423                    m_StatementLine = -1;
424                }
425
426                case "$W" ->
427                {
428                    try
429                    {
430                        m_LineWrapper.wrappingSpace( m_IndentLevel + 2 );
431                    }
432                    catch( final IOException e )
433                    {
434                        throw new UncheckedIOException( e );
435                    }
436                }
437
438                case "$Z" ->
439                {
440                    try
441                    {
442                        m_LineWrapper.zeroWidthSpace( m_IndentLevel + 2 );
443                    }
444                    catch( final IOException e )
445                    {
446                        throw new UncheckedIOException( e );
447                    }
448                }
449
450                default ->
451                {
452                    //---* Handle deferred type *------------------------------
453                    if( nonNull( deferredTypeName ) )
454                    {
455                        if( part.startsWith( "." ) )
456                        {
457                            if( emitStaticImportMember( deferredTypeName.canonicalName(), part ) )
458                            {
459                                /*
460                                 * Okay, static import hit and all was emitted,
461                                 * so clean-up and jump to next part.
462                                 */
463                                deferredTypeName = null;
464                                break;
465                            }
466                        }
467                        deferredTypeName.emit( this );
468                        deferredTypeName = null;
469                    }
470                    emitAndIndent( part );
471                }
472            }
473        }
474
475        //---* Done *----------------------------------------------------------
476        return this;
477    }   //  emit()
478
479    /**
480     *  Emits the given String to the output target with indentation as
481     *  required. It's important that all code that writes to
482     *  {@link #m_LineWrapper}
483     *  does it through here, since we emit indentation lazily in order to
484     *  avoid unnecessary trailing whitespace.
485     *
486     *  @param  input   The String.
487     *  @return This {@code CodeWriter} instance.
488     *  @throws UncheckedIOException A problem occurred when writing to the
489     *      output target.
490     */
491    @SuppressWarnings( "OverlyComplexMethod" )
492    public final CodeWriter emitAndIndent( final CharSequence input ) throws UncheckedIOException
493    {
494        if( isNotEmpty( input ) )
495        {
496            var first = true;
497            LineLoop: for( final var line : input.toString().split( "\n", -1 ) )
498            {
499                /*
500                 * Emit a newline character. Make sure blank lines in Javadoc
501                 * and comments look good.
502                 */
503                if( !first )
504                {
505                    if( (m_CurrentlyEmittingComment != CommentType.NO_COMMENT) && m_TrailingNewline )
506                    {
507                        emitIndentation();
508                        try
509                        {
510                            m_LineWrapper.append( m_CurrentlyEmittingComment == CommentType.LINE_COMMENT ? "//" : " *" );
511                        }
512                        catch( final IOException e )
513                        {
514                            throw new UncheckedIOException( e );
515                        }
516                    }
517                    try
518                    {
519                        m_LineWrapper.append( "\n" );
520                    }
521                    catch( final IOException e )
522                    {
523                        throw new UncheckedIOException( e );
524                    }
525                    m_TrailingNewline = true;
526                    if( m_StatementLine != -1 )
527                    {
528                        if( m_StatementLine == 0 )
529                        {
530                            /*
531                             * Begin multiple-line statement. Increase the
532                             * indentation level.
533                             */
534                            indent( 2 );
535                        }
536                        ++m_StatementLine;
537                    }
538                }
539
540                first = false;
541                if( line.isEmpty() )
542                {
543                    //---* Don't indent empty lines *--------------------------
544                    continue LineLoop;
545                }
546
547                //---* Emit indentation and comment prefix if necessary *------
548                if( m_TrailingNewline )
549                {
550                    emitIndentation();
551                    try
552                    {
553                        switch( m_CurrentlyEmittingComment )
554                        {
555                            case BLOCK_COMMENT, JAVADOC_COMMENT -> m_LineWrapper.append( " * " );
556                            case LINE_COMMENT -> m_LineWrapper.append( "// " );
557                            case NO_COMMENT -> m_LineWrapper.append( EMPTY_STRING );
558                            default -> throw new UnsupportedEnumError( m_CurrentlyEmittingComment );
559                        }
560                    }
561                    catch( final IOException e )
562                    {
563                        throw new UncheckedIOException( e );
564                    }
565                }
566
567                try
568                {
569                    m_LineWrapper.append( line );
570                }
571                catch( final IOException e )
572                {
573                    throw new UncheckedIOException( e );
574                }
575                m_TrailingNewline = false;
576            }
577        }   //  LineLoop:
578
579        //---* Done *----------------------------------------------------------
580        return this;
581    }   //  emitAndIndent()
582
583    /**
584     *  Emits the given annotations to the output target.
585     *
586     *  @param  annotations The annotations.
587     *  @param  inline  {@code true} if the annotations should be placed on the
588     *      same line as the annotated element, {@code false} otherwise.
589     *  @throws UncheckedIOException A problem occurred when writing to the
590     *      output target.
591     */
592    public final void emitAnnotations( final Iterable<AnnotationSpecImpl> annotations, final boolean inline ) throws UncheckedIOException
593    {
594        for( final var annotationSpec : annotations )
595        {
596            annotationSpec.emit( this, inline );
597            emit( inline ? " " : "\n" );
598        }
599    }   //  emitAnnotations()
600
601    /**
602     *  Emits the given
603     *  {@link CodeBlockImpl}
604     *  instance as a block comment to the output target.
605     *
606     *  @param  codeBlock   The code block with the comment.
607     *  @throws UncheckedIOException A problem occurred when writing to the
608     *      output target.
609     */
610    @SuppressWarnings( {"UseOfConcreteClass", "ThrowFromFinallyBlock"} )
611    public final void emitBlockComment( final CodeBlockImpl codeBlock ) throws UncheckedIOException
612    {
613        emit( "/*\n" );
614        m_TrailingNewline = true; // Force the ' *' prefix for the comment.
615        m_CurrentlyEmittingComment = CommentType.BLOCK_COMMENT;
616        try
617        {
618            emit( codeBlock );
619            emit( "\n" );
620        }
621        finally
622        {
623            m_CurrentlyEmittingComment = CommentType.NO_COMMENT;
624            emit( " */\n" );
625        }
626    }   //  emitBlockComment()
627
628    /**
629     *  Writes the indentation to the output target.
630     *
631     *  @throws UncheckedIOException A problem occurred when writing to the
632     *      output target.
633     */
634    private final void emitIndentation() throws UncheckedIOException
635    {
636        try
637        {
638            for( var i = 0; i < m_IndentLevel; ++i ) m_LineWrapper.append( m_Indent );
639        }
640        catch( final IOException e )
641        {
642            throw new UncheckedIOException( e );
643        }
644    }   //  emitIndentation()
645
646    /**
647     *  Emits the given
648     *  {@link CodeBlockImpl}
649     *  instance as a JavaDoc comment to the output target.
650     *
651     *  @param  codeBlock   The code block with the JavaDoc comment.
652     *  @throws UncheckedIOException A problem occurred when writing to the
653     *      output target.
654     */
655    @SuppressWarnings( "UseOfConcreteClass" )
656    public final void emitJavadoc( final CodeBlockImpl codeBlock ) throws UncheckedIOException
657    {
658        if( codeBlock.isEmpty() )
659        {
660            LayoutSwitch: switch( m_Layout )
661            {
662                case LAYOUT_DEFAULT:
663                case LAYOUT_JAVAPOET:
664                case LAYOUT_JAVAPOET_WITH_TAB: break;
665
666                case LAYOUT_FOUNDATION:
667                {
668                    emitAnnotations( List.of( (AnnotationSpecImpl) createSuppressWarningsAnnotation( m_Composer, JAVADOC ) ), false );
669                    break;
670                }
671
672                default: throw new UnsupportedEnumError( m_Layout );
673            }   //  LayoutSwitch:
674        }
675        else
676        {
677            emit( "/**\n" );
678            m_CurrentlyEmittingComment = CommentType.JAVADOC_COMMENT;
679            try
680            {
681                emit( codeBlock );
682            }
683            finally
684            {
685                m_CurrentlyEmittingComment = CommentType.NO_COMMENT;
686            }
687            emit( " */\n" );
688        }
689    }   //  emitJavadoc()
690
691    /**
692     *  Emits the given
693     *  {@link CodeBlockImpl}
694     *  instance as a line comment to the output target.
695     *
696     *  @param  codeBlock   The code block with the comment.
697     *  @throws UncheckedIOException A problem occurred when writing to the
698     *      output target.
699     */
700    @SuppressWarnings( "UseOfConcreteClass" )
701    public final void emitLineComment( final CodeBlockImpl codeBlock ) throws UncheckedIOException
702    {
703        m_TrailingNewline = true; // Force the '//' prefix for the comment.
704        m_CurrentlyEmittingComment = CommentType.LINE_COMMENT;
705        try
706        {
707            emit( codeBlock );
708            emit( "\n" );
709        }
710        finally
711        {
712            m_CurrentlyEmittingComment = CommentType.NO_COMMENT;
713        }
714    }   //  emitLineComment()
715
716    /**
717     *  Emits the given argument literally to the output target.
718     *
719     *  @param  o   The object to emit.
720     *  @throws UncheckedIOException A problem occurred when writing to the
721     *      output target.
722     */
723    @SuppressWarnings( {"IfStatementWithTooManyBranches", "ChainOfInstanceofChecks"} )
724    private final void emitLiteral( final Object o ) throws UncheckedIOException
725    {
726        if( o instanceof final TypeSpecImpl typeSpec )
727        {
728            typeSpec.emit( this, null, Set.of() );
729        }
730        else if( o instanceof final AnnotationSpecImpl annotationSpec )
731        {
732            annotationSpec.emit( this, true );
733        }
734        else if( o instanceof final CodeBlockImpl codeBlock )
735        {
736            emit( codeBlock );
737        }
738        else if( o == NULL_REFERENCE )
739        {
740            emitAndIndent( NULL_STRING );
741        }
742        else
743        {
744            emitAndIndent( String.valueOf( o ) );
745        }
746    }   //  emitLiteral()
747
748    /**
749     *  Emits {@code modifiers} to the output target in the standard order.
750     *  Modifiers in {@code implicitModifiers} will not be emitted.
751     *
752     *  @param  modifiers   The modifiers to emit.
753     *  @param  implicitModifiers   The modifiers to omit.
754     *  @throws UncheckedIOException A problem occurred when writing to the
755     *      output target.
756     */
757    public final void emitModifiers( final Collection<Modifier> modifiers, final Collection<Modifier> implicitModifiers ) throws UncheckedIOException
758    {
759        if( !modifiers.isEmpty() )
760        {
761            for( final var modifier : EnumSet.copyOf( modifiers ) )
762            {
763                if( !implicitModifiers.contains( modifier ) )
764                {
765                    emitAndIndent( modifier.name().toLowerCase( ROOT ) );
766                    emitAndIndent( " " );
767                }
768            }
769        }
770    }   //  emitModifiers()
771
772    /**
773     *  Emits {@code modifiers} to the output target in the standard order.
774     *
775     *  @param  modifiers   The modifiers to emit.
776     *  @throws UncheckedIOException A problem occurred when writing to the
777     *      output target.
778     */
779    public final void emitModifiers( final Collection<Modifier> modifiers ) throws UncheckedIOException
780    {
781        emitModifiers( modifiers, Set.of() );
782    }   //  emitModifiers()
783
784    /**
785     *  Emits a static import entry to the output target.
786     *
787     *  @param  canonical   The canonical name of the class to import.
788     *  @param  part    The part to emit.
789     *  @return {@code true} if something was emitted, {@code false} otherwise.
790     *  @throws UncheckedIOException A problem occurred when writing to the
791     *      output target.
792     */
793    @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" )
794    private final boolean emitStaticImportMember( final String canonical, final String part ) throws UncheckedIOException
795    {
796        final var partWithoutLeadingDot = requireNonNullArgument( part, "part" ).substring( 1 );
797
798        var retValue = !partWithoutLeadingDot.isEmpty();
799        if( retValue )
800        {
801            final var first = partWithoutLeadingDot.charAt( 0 );
802            //noinspection NestedAssignment
803            if( (retValue = Character.isJavaIdentifierStart( first )) == true )
804            {
805                final var explicit = canonical + "." + extractMemberName( partWithoutLeadingDot );
806                final var wildcard = canonical + ".*";
807                //noinspection NestedAssignment
808                if( (retValue = m_StaticImports.contains( explicit ) || m_StaticImports.contains( wildcard )) == true )
809                {
810                    emitAndIndent( partWithoutLeadingDot );
811                }
812            }
813        }
814
815        //---* Done *----------------------------------------------------------
816        return retValue;
817    }   //  emitStaticImportMember()
818
819    /**
820     *  Emits type variables with their bounds. This should only be used when
821     *  declaring type variables; everywhere else bounds are omitted.
822     *
823     *  @param  typeVariables   The type variables.
824     *  @throws UncheckedIOException A problem occurred when writing to the output
825     *      target.
826     */
827    public final void emitTypeVariables( final List<TypeVariableNameImpl> typeVariables ) throws UncheckedIOException
828    {
829        if( !requireNonNullArgument( typeVariables, "typeVariables" ).isEmpty() )
830        {
831            emit( "<" );
832            var firstTypeVariable = true;
833            for( final var typeVariable : typeVariables )
834            {
835                if( !firstTypeVariable ) emit( ", " );
836                emitAnnotations( typeVariable.annotations(), true );
837                emit( "$L", typeVariable.name() );
838                var firstBound = true;
839                for( final var bound : typeVariable.bounds() )
840                {
841                    emit( firstBound ? " extends $T" : " & $T", bound );
842                    firstBound = false;
843                }
844                firstTypeVariable = false;
845            }
846            emit( ">" );
847        }
848    }   //  emitTypeVariables()
849
850    /**
851     *  Emits wrapping space to the output target.
852     *
853     *  @return This {@code CodeWriter} instance.
854     *  @throws UncheckedIOException A problem occurred when writing to the
855     *      output target.
856     */
857    public final CodeWriter emitWrappingSpace() throws UncheckedIOException
858    {
859        try
860        {
861            m_LineWrapper.wrappingSpace( m_IndentLevel + 2 );
862        }
863        catch( final IOException e )
864        {
865            throw new UncheckedIOException( e );
866        }
867
868        //---* Done *----------------------------------------------------------
869        return this;
870    }   //  emitWrappingSpace()
871
872    /**
873     *  Extracts a member name from the given part.
874     *
875     *  @param  part    The part.
876     *  @return The member name, or if none could be found, the given part.
877     */
878    private static final String extractMemberName( final String part )
879    {
880        var retValue = requireValidNonNullArgument( part, "part", v -> Character.isJavaIdentifierStart( v.charAt( 0 ) ), $ -> "not an identifier: %s".formatted( part ) );
881        CheckLoop: for( var i = 1; i <= part.length(); ++i )
882        {
883            if( !SourceVersion.isIdentifier( part.substring( 0, i ) ) )
884            {
885                retValue = part.substring( 0, i - 1 );
886                break CheckLoop;
887            }
888        }   //  CheckLoop:
889
890        //---* Done *----------------------------------------------------------
891        return retValue;
892    }   //  extractMemberName()
893
894    /**
895     *  Marks the given type as importable.
896     *
897     *  @param  className   The type.
898     */
899    @SuppressWarnings( "UseOfConcreteClass" )
900    private final void importableType( final ClassNameImpl className )
901    {
902        if( !requireNonNullArgument( className, "className" ).packageName().isEmpty() )
903        {
904            final var topLevelClassName = className.topLevelClassName();
905            final var simpleName = topLevelClassName.simpleName();
906            m_ImportableTypes.putIfAbsent( simpleName, topLevelClassName );
907        }
908    }   //  importableType
909
910    /**
911     *  Returns the imported types.
912     *
913     *  @return The imported types.
914     */
915    /*
916     * Originally, the return value was a reference to the internal field that
917     * allowed the modification of the Map.
918     */
919    public final Map<String,ClassNameImpl> importedTypes() { return unmodifiableMap( m_ImportedTypes ); }
920
921    /**
922     *  Increments the indentation level.
923     *
924     *  @return This {@code CodeWriter} instance.
925     */
926    public final CodeWriter indent() { return indent( 1 ); }
927
928    /**
929     *  Increases the indentation level by the given value.
930     *
931     *  @param  levels  The increase value.
932     *  @return This {@code CodeWriter} instance.
933     */
934    public final CodeWriter indent( final int levels )
935    {
936        m_IndentLevel += levels;
937
938        //---* Done *----------------------------------------------------------
939        return this;
940    }   //  indent()
941
942    /**
943     *  Returns the layout for the output.
944     *
945     *  @return The layout.
946     */
947    public final Layout layout() { return m_Layout; }
948
949    /**
950     *  Returns the best name to identify {@code className} within the current
951     *  context. This uses the available imports and the current scope to find
952     *  the shortest name available. It does not honour names that are visible
953     *  due to inheritance.
954     *
955     *  @param  className   The name of the class.
956     *  @return The shortest possible name for the given class.
957     */
958    @SuppressWarnings( "UseOfConcreteClass" )
959    public final String lookupName( final ClassNameImpl className )
960    {
961        String retValue = null;
962        /*
963         * Find the shortest suffix of className that resolves to className.
964         * This uses both local type names (so `Entry` in 'Map' refers to
965         * 'Map.Entry'). Also uses imports.
966         */
967        var nameResolved = false;
968        for( var currentClassName = className; nonNull( currentClassName ) && isEmpty( retValue ); currentClassName = currentClassName.enclosingClassName().orElse( null ) )
969        {
970            final var resolved = resolve( currentClassName.simpleName() );
971            nameResolved = resolved.isPresent();
972
973            if( nameResolved && Objects.equals( resolved.get().canonicalName(), currentClassName.canonicalName() ) )
974            {
975                final var suffixOffset = currentClassName.simpleNames().size() - 1;
976                retValue = join( ".", className.simpleNames().subList( suffixOffset, className.simpleNames().size() ) );
977            }
978        }
979
980        if( isEmpty( retValue ) )
981        {
982            /*
983             * If the name resolved but wasn't a match, we're stuck with the
984             * fully qualified name.
985             */
986            if( nameResolved )
987            {
988                retValue = className.canonicalName();
989            }
990            else
991            //---* If the class is in the same package, we're done *-----------
992            {
993                if( Objects.equals( m_PackageName, className.packageName() ) )
994                {
995                    m_ReferencedNames.add( className.topLevelClassName().simpleName() );
996                    retValue = join( ".", className.simpleNames() );
997                }
998                else
999                {
1000                    /*
1001                     * We'll have to use the fully-qualified name. Mark the
1002                     * type as importable for a future pass.
1003                     */
1004                    if( m_CurrentlyEmittingComment != CommentType.JAVADOC_COMMENT ) importableType( className );
1005                    retValue = className.canonicalName();
1006                }
1007            }
1008        }
1009
1010        //---* Done *----------------------------------------------------------
1011        return retValue;
1012    }   //  lookupName()
1013
1014    /**
1015     *  Pops the package name.
1016     *
1017     *  @return This {@code CodeWriter} instance.
1018     */
1019    @SuppressWarnings( {"UnusedReturnValue", "StringEquality"} )
1020    public final CodeWriter popPackage()
1021    {
1022        checkState( m_PackageName != NO_PACKAGE, () -> new IllegalStateException( "package not set" ) );
1023        m_PackageName = NO_PACKAGE;
1024
1025        //---* Done *----------------------------------------------------------
1026        return this;
1027    }   //  popPackage()
1028
1029    /**
1030     *  Pops the top most type.
1031     *
1032     *  @return This {@code CodeWriter} instance.
1033     */
1034    @SuppressWarnings( "UnusedReturnValue" )
1035    public final CodeWriter popType()
1036    {
1037        m_TypeSpecStack.removeLast();
1038
1039        //---* Done *----------------------------------------------------------
1040        return this;
1041    }   //  popPackage()
1042
1043    /**
1044     *  Pushes the given package name.
1045     *
1046     *  @param  packageName The name of the package.
1047     *  @return This {@code CodeWriter} instance.
1048     */
1049    @SuppressWarnings( {"UnusedReturnValue", "StringEquality"} )
1050    public final CodeWriter pushPackage( final String packageName )
1051    {
1052        checkState( m_PackageName == NO_PACKAGE, () -> new IllegalStateException( "package already set: %s".formatted( m_PackageName ) ) );
1053        m_PackageName = requireNonNullArgument( packageName, "packageName" );
1054
1055        //---* Done *----------------------------------------------------------
1056        return this;
1057    }   //  pushPackage()
1058
1059    /**
1060     *  Pushes the give type.
1061     *
1062     *  @param  type    The type.
1063     *  @return This {@code CodeWriter} instance.
1064     */
1065    @SuppressWarnings( "UnusedReturnValue" )
1066    public final CodeWriter pushType( final TypeSpecImpl type )
1067    {
1068        m_TypeSpecStack.add( type );
1069
1070        //---* Done *----------------------------------------------------------
1071        return this;
1072    }   //  pushType()
1073
1074    /**
1075     *  Returns the class referenced by {@code simpleName}, using the current
1076     *  nesting context and imports.
1077     *
1078     *  @param  simpleName  The name of the class we search for.
1079     *  @return An instance of
1080     *      {@link Optional}
1081     *      that holds the {@code ClassName} instance for the resolved class.
1082     */
1083    // TODO(jwilson): also honour superclass members when resolving names.
1084    @SuppressWarnings( "OptionalGetWithoutIsPresent" )
1085    private final Optional<ClassNameImpl> resolve( final String simpleName )
1086    {
1087        Optional<ClassNameImpl> retValue = Optional.empty();
1088
1089        //---* Match a child of the current (potentially nested) class *-------
1090        for( var i = m_TypeSpecStack.size() - 1; (i >= 0) && retValue.isEmpty(); --i )
1091        {
1092            final var typeSpec = m_TypeSpecStack.get( i );
1093            for( final var visibleChild : typeSpec.typeSpecs() )
1094            {
1095                if( Objects.equals( visibleChild.name().get(), simpleName ) )
1096                {
1097                    retValue = Optional.of( stackClassName( i, simpleName ) );
1098                }
1099            }
1100        }
1101
1102        if( retValue.isEmpty() )
1103        {
1104            //---* Match the top-level class *---------------------------------
1105            if( (!m_TypeSpecStack.isEmpty()) && Objects.equals( m_TypeSpecStack.getFirst().name(), simpleName ) )
1106            {
1107                retValue = Optional.of( ClassNameImpl.from( m_PackageName, simpleName ) );
1108            }
1109            else
1110            {
1111                //---* Match an imported type *--------------------------------
1112                final var importedType = m_ImportedTypes.get( simpleName );
1113                retValue = Optional.ofNullable( importedType );
1114            }
1115        }
1116
1117        //---* Done *----------------------------------------------------------
1118        return retValue;
1119    }   //  resolve()
1120
1121    /**
1122     *  Returns the class named {@code simpleName} when nested in the class at
1123     *  {@code stackDepth}.
1124     *
1125     *  @param  simpleName  The class name.
1126     *  @param  stackDepth  The search depth.
1127     *  @return The found class.
1128     */
1129    @SuppressWarnings( "OptionalGetWithoutIsPresent" )
1130    private final ClassNameImpl stackClassName( final int stackDepth, final String simpleName )
1131    {
1132        /*
1133         * The type spec stack may not contain anonymous types, so no check for
1134         * the name is required.
1135         */
1136        @SuppressWarnings( "OptionalGetWithoutIsPresent" )
1137        var className = ClassNameImpl.from( m_PackageName, m_TypeSpecStack.getFirst().name().get() );
1138        for( var i = 1; i <= stackDepth; ++i )
1139        {
1140            className = className.nestedClass( m_TypeSpecStack.get( i ).name().get() );
1141        }
1142        final var retValue = className.nestedClass( simpleName );
1143
1144        //---* Done *----------------------------------------------------------
1145        return retValue;
1146    }   //  stackClassName()
1147
1148    /**
1149     *  <p>{@summary Returns the current statement line.}</p>
1150     *  <p>When a statement will be emitted, this method returns the line of
1151     *  the statement currently being written. The first line of a statement is
1152     *  indented normally and subsequent wrapped lines are double-indented.
1153     *  This is -1 when the currently-written line isn't part of a
1154     *  statement.</p>
1155     *
1156     *  @return The statement line, or -1.
1157     */
1158    public final int statementLine() { return m_StatementLine; }
1159
1160    /**
1161     *  Sets the current statement line.
1162     *
1163     *  @param  statementLine   The new value for the current statement line.
1164     *
1165     *  @see #statementLine()
1166     */
1167    public final void statementLine( final int statementLine ) { m_StatementLine = statementLine; }
1168
1169    /**
1170     *  Returns the types that should have been imported for this code. If
1171     *  there were any simple name collisions, that type's first use is
1172     *  imported.
1173     *
1174     *  @return The types that should have been imported.
1175     */
1176    public final Map<String,ClassNameImpl> suggestedImports()
1177    {
1178        final Map<String,ClassNameImpl> retValue = new LinkedHashMap<>( m_ImportableTypes );
1179        retValue.keySet().removeAll( m_ReferencedNames );
1180
1181        //---* Done *----------------------------------------------------------
1182        return retValue;
1183    }   //  suggestedImports()
1184
1185    /**
1186     *  Decrements the indentation level.
1187     *
1188     *  @return This {@code CodeWriter} instance.
1189     */
1190    public final CodeWriter unindent() { return unindent( 1 ); }
1191
1192    /**
1193     *  Decreases the indentation level by the given value.
1194     *
1195     *  @param  levels  The decrease value.
1196     *  @return This {@code CodeWriter} instance.
1197     */
1198    public final CodeWriter unindent( final int levels )
1199    {
1200        m_IndentLevel -= requireValidIntegerArgument( levels, "levels", $ -> m_IndentLevel - levels >= 0, $ -> "cannot unindent %d from %d".formatted( levels, m_IndentLevel ) );
1201
1202        //---* Done *----------------------------------------------------------
1203        return this;
1204    }   //  unindent()
1205}
1206//  class CodeWriter
1207
1208/*
1209 *  End of File
1210 */