001package Torello.JavaDoc;
002
003import Torello.Java.Additional.Ret3;
004
005import static Torello.Java.C.*;
006
007import Torello.JDUInternal.Messager.Messager;
008import Torello.JDUInternal.Messager.Where.JDUUserAPI;
009import Torello.JDUInternal.Messager.Where.Where_Am_I;
010
011import java.util.*;
012import com.sun.source.util.*;
013import com.sun.source.tree.*;
014import com.sun.source.doctree.*;
015
016import javax.tools.JavaCompiler;
017import javax.tools.ToolProvider;
018import javax.tools.StandardJavaFileManager;
019import javax.tools.JavaFileObject;
020
021/**
022 * This is a helpful interface to retrieve and extract the utilities provided by the Java-Compiler
023 * interface provided by the JDK.  This class utilizes the Tree-Classes defined in the Java Package
024 * {@code com.sun.source.tree.*} (and also {@code com.sun.source.util.*}.  These AST (Tree's) work
025 * and function as an alternative to the {@code JavaParser} AST Generator.
026 */
027public class TreeUtils
028{
029    private static final Where_Am_I WHERE_AM_I = JDUUserAPI.TreeUtils;
030
031
032    // ********************************************************************************************
033    // ********************************************************************************************
034    // Static-Instances & Static-Constants
035    // ********************************************************************************************
036    // ********************************************************************************************
037
038
039    // javac options we pass to the compiler.  We enable preview so that all preview features can
040    // be parsed.
041
042    private static final List<String> OPTIONS = 
043        List.of("--enable-preview", "--release=" + getJavaMajorVersion());
044
045    // major version of JDK such as 16, 17, 18 etc.
046    private static int getJavaMajorVersion() { return Runtime.version().feature(); }
047
048    // Standard JDK Stuff - no fancy library imports here
049    // javax.tools.JavaCompiler, javax.tools.ToolProvider
050
051    private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
052
053    // javax.tools.StandardJavaFileManager
054    private static final StandardJavaFileManager fileManager =
055        compiler.getStandardFileManager(null, null, null);
056
057
058    // ********************************************************************************************
059    // ********************************************************************************************
060    // Public Fields
061    // ********************************************************************************************
062    // ********************************************************************************************
063
064
065    /** <EMBED CLASS='external-html' DATA-FILE-ID=TU_SOURCE_POS> */
066    public final SourcePositions sourcePositions;
067
068    /**
069     * Java's Sun/Oracle Abstract Syntax Tree engine provides AST's for Java-Doc Comments, in
070     * addition to trees for the actual source-code.  This reference here is merely an instance of
071     * the utility class containing tools for traversing Java-Doc Comment Syntax Trees.
072     * 
073     * <BR /><BR />More information about these trees is available in the documentation for the
074     * Java package {@code com.sun.source.util.DocTrees}.
075     */
076    public final DocTrees docTrees;
077
078    /** <EMBED CLASS='external-html' DATA-FILE-ID=TU_DOC_SOURCE_POS> */
079    public final DocSourcePositions docSourcePositions;
080
081    /**
082     * This instance is generated by the Java-Compiler when it is asked to perform an AST (Abstract
083     * Syntax Tree) parse of a Java Source-Code File (a {@code '.java'} File).  A
084     * {@code CompilationUnitTree} is essentially the root node (the 'top' of the tree) of a
085     * Source-Code AST.
086     * 
087     * <BR /><BR />Please review the Sun/Oracle Documentation Page for a fuller explanation of this
088     * class' features.
089     */
090    public final CompilationUnitTree compilationUnitTree;
091
092    /** <EMBED CLASS='external-html' DATA-FILE-ID=TU_LINE_MAP> */
093    public final LineMap lineMap;
094
095    /**
096     * This is nothing more than a copy of the constructor-parameter by the same name.  This shall
097     * contain the file-name for the source-code file that has been parsed to create an instance
098     * of this class, {@code TreeUtils}.
099     */
100    public final String srcFileName;
101
102    /**
103     * This is also a copy of a constructor-parameter, reserved for any needed use by a reference
104     * in this class.  It holds a {@code '.java'} source-code file, loaded into memory and saved as
105     * a {@code java.lang.String}.
106     */
107    public final String srcFileAsStr;
108
109
110    // ********************************************************************************************
111    // ********************************************************************************************
112    // Constructor
113    // ********************************************************************************************
114    // ********************************************************************************************
115
116
117    /**
118     * Constructs an instance of this class.  Accepts a Java Source-Code File-Name, and that file
119     * alredy fully-loaded into memory - as a {@code java.lang.String}.  This constructs invokes
120     * the internal instance of the Java Compiler, and then fills in all of the public fields for
121     * this class.
122     * 
123     * @param srcFileName The name of a Java Source-Code File
124     * 
125     * @param srcFileAsStr The textual-content of that Source-File, loaded into a
126     * {@code java.lang.String}
127     * 
128     * @see #srcFileName
129     * @see #srcFileAsStr
130     */
131    public TreeUtils(String srcFileName, String srcFileAsStr)
132    {
133        this.srcFileName    = srcFileName;
134        this.srcFileAsStr   = srcFileAsStr;
135
136
137        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
138        // javax.tools.JavaFileObject
139        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
140
141        Iterable<? extends JavaFileObject> fileObjects =
142            fileManager.getJavaFileObjects(srcFileName);
143
144        Iterator<? extends JavaFileObject> iterFileObjects = fileObjects.iterator();
145
146        if (! iterFileObjects.hasNext()) Messager.assertFailOracleParser(
147            "The JavaFileManager instance seems to have returned an empty JavaFileObject-List",
148            null,
149            WHERE_AM_I
150        );
151
152        // The Java-Compiler actually expects the Iterator, not the single-instance.  We can throw
153        // the return object of the iterator away, it isn't used at all.  This is just more error
154        // checking since some of this is quite new.
155
156        iterFileObjects.next();
157
158        if (iterFileObjects.hasNext()) Messager.assertFailOracleParser(
159            "The JavaFileManager instance has returned a JavaFileObject-List with more than one " +
160            "element",
161            null,
162            WHERE_AM_I
163        );
164
165
166        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
167        // Running the Compiler to get a "JavacTask"
168        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
169
170        // com.sun.source.util.JavacTask
171        JavacTask task = (JavacTask) compiler.getTask
172            (null, null, null, OPTIONS, null, fileObjects);
173
174
175        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
176        // The Helper Utilities: SourcePositions, DocTrees, DocSourcePositions
177        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
178
179        // com.sun.source.util.SourcePositions
180        this.sourcePositions = Trees.instance(task).getSourcePositions();
181
182        // com.sun.source.util.DocTrees
183        this.docTrees = DocTrees.instance(task);
184
185        // com.sun.source.util.DocSourcePositions
186        this.docSourcePositions = docTrees.getSourcePositions();
187
188
189        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
190        // com.sun.source.tree.CompilationUnitTree
191        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
192
193        Iterable<? extends CompilationUnitTree> cutList = null;
194
195        try 
196            { cutList = task.parse(); }
197
198        catch (Exception e)
199        {
200            Messager.assertFailOracleParser(
201                e,
202                "Exception thrown while parsing Source-Code File:\n" +
203                "    [" + BYELLOW + srcFileName + RESET + ']',
204                null,
205                WHERE_AM_I
206            );
207        }
208
209        Iterator<? extends CompilationUnitTree> iter = cutList.iterator();
210
211        if (! iter.hasNext()) Messager.assertFailOracleParser(
212            "The JavacTask instance seems to have returned an empty CompilationUnitTree List",
213            null,
214            WHERE_AM_I
215        );
216
217        // com.sun.source.tree.CompilationUnitTree
218        this.compilationUnitTree = iter.next();
219
220        if (iter.hasNext()) Messager.assertFailOracleParser(
221            "The JavacTask instance has returned a CompilationUnitTree List with more than one " +
222            "element",
223            null,
224            WHERE_AM_I
225        );
226
227
228        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
229        // Last, but not least: com.sun.source.tree.LineMap
230        // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
231
232        this.lineMap = this.compilationUnitTree.getLineMap();
233    }
234
235
236    // ********************************************************************************************
237    // ********************************************************************************************
238    // Methods
239    // ********************************************************************************************
240    // ********************************************************************************************
241
242
243    /**
244     * Returns the starting position within the text of the relevant Java Source-Code File for the
245     * sub-tree specified by parameter {@code 'tree'}.
246     * 
247     * @param tree This may be any AST branch or leaf, but it must be sub-node that falls within
248     * the root-node {@link #compilationUnitTree}.
249     * 
250     * @see #sourcePositions
251     * @see #compilationUnitTree
252     */
253    public int startPos(Tree tree)
254    { return (int) this.sourcePositions.getStartPosition(this.compilationUnitTree, tree); }
255
256    /**
257     * Returns the ending position within the text of the relevant Java Source-Code File for the
258     * sub-tree specified by parameter {@code 'tree'}.
259     * 
260     * @param tree This may be any AST branch or leaf, but it must be sub-node that falls within
261     * the root-node {@link #compilationUnitTree}.
262     * 
263     * @see #sourcePositions
264     * @see #compilationUnitTree
265     */
266    public int endPos(Tree tree)
267    { return (int) this.sourcePositions.getEndPosition(this.compilationUnitTree, tree); }
268
269    /**
270     * Returns the line-number within the text of the relevant Java Source-Code File for the
271     * <B>first line of code</B> of the element specified by parameter {@code 'tree'}.
272     * 
273     * @param tree This may be any AST branch or leaf, but it must be sub-node that falls within
274     * the root-node {@link #compilationUnitTree}.
275     * 
276     * @see #lineMap
277     * @see #sourcePositions
278     * @see #compilationUnitTree
279     */
280    public int startLineNum(Tree tree)
281    {
282        return (int) this.lineMap.getLineNumber
283            (this.sourcePositions.getStartPosition(this.compilationUnitTree, tree));
284    }
285
286    /**
287     * Returns the line-number within the text of the relevant Java Source-Code File for the
288     * <B>last line of code</B> of the element specified by parameter {@code 'tree'}.
289     * 
290     * @param tree This may be any AST branch or leaf, but it must be sub-node that falls within
291     * the root-node {@link #compilationUnitTree}.
292     * 
293     * @see #lineMap
294     * @see #sourcePositions
295     * @see #compilationUnitTree
296     */
297    public int endLineNum(Tree tree)
298    {
299        return (int) this.lineMap.getLineNumber
300            (this.sourcePositions.getEndPosition(this.compilationUnitTree, tree));
301    }
302
303    /**
304     * Performs a standard Java {@code 'substring'} operation on the Source-Code File, which has
305     * been saved as a {@code String} into internal &amp; public field {@link #srcFileAsStr}.
306     * 
307     * @param sPos The first character-postion within the text-file
308     * @param ePos The last character-postion within the text-file
309     * @return The extracted sub-string.
310     */
311    public String substring(int sPos, int ePos)
312    { return this.srcFileAsStr.substring(sPos, ePos); }
313
314    /**
315     * Extracts the <I>exact text / character-data</I> from the original {@code '.java'} Source
316     * Code File for any node in the AST 'Parse-Tree' that was produced from that Source-File.
317     * 
318     * @param tree Any node in the AST that was produced from the Source-File saved inside 
319     * {@code 'this'} instance of {@code TreeUtils}
320     * 
321     * @return The original Java-Source, as a {@code java.lang.String}
322     */
323    public String substring(Tree tree)
324    { return this.srcFileAsStr.substring(startPos(tree), endPos(tree)); }
325
326    /**
327     * An instance of {@code TreePath} is a class which stores node / branch references for a node
328     * in an AST extending back to the top-level node in the Compilation-Unit.
329     * 
330     * <BR /><BR />The Parse-Trees produced by the <CODE>com.sun.source.tree</CODE> classes do not
331     * provide very much in the way of parent or ancestor node pointers.  This makes finding where
332     * a particular branch in an Abstract Syntax Tree a little more difficult.  An instance of
333     * {@code 'TreePath'} simplifies the process.
334     * 
335     * @param tree This should be a node that exists within the AST that is associated / stored
336     * inside {@code 'this'} reference of {@code TreeUtils}.
337     * 
338     * @return A {@code TreePath} instance that contains the straight-line path to the root node
339     * in the Abstract Syntax Tree.
340     */
341    public TreePath getTreePath(Tree tree)
342    { return TreePath.getPath(this.compilationUnitTree, tree); }
343
344    /**
345     * Retrieve  Java-Doc Comment information (if present / if provided) for any of the entities /
346     * members of a CIET / Type.
347     * 
348     * @param tree The {@link Entity} for which a Java-Doc Comment is sought.
349     * 
350     * @return An instance of {@link Ret3} as follows:
351     * <BR /><BR /><UL CLASS=JDUL>
352     * 
353     * <LI> {@code Ret3.a: Integer}
354     *      <BR />The <B STYLE='color: red'>starting</B> character-position of the Java-Doc Comment
355     *      in the original Source-Code File.
356     *      <BR /><BR /></LI>
357     * 
358     * <LI> {@code Ret3.b: Integer}
359     *      <BR />The <B STYLE='color: red'>ending</B> character-position of the Java-Doc Comment
360     *      in the original Source-Code File.
361     *      <BR /><BR /></LI>
362     * 
363     * <LI> {@code Ret3.b: DocCommentTree}
364     *      <BR />The Parse-Tree for the Java-Doc Comment.
365     *      <BR /><BR /></LI>
366     * </UL>
367     * 
368     * <BR /><B STYLE='color: red;'>NOTE:</B> If the {@link Entity} / Member of the {@code 'tree'}
369     * that was passed as input to this method does not have a Java-Doc Comment written, then this
370     * method will return null instead.
371     * 
372     * @see #docTrees
373     * @see #compilationUnitTree
374     * @see #docSourcePositions
375     * @see #srcFileAsStr
376     */
377    public Ret3<Integer, Integer, DocCommentTree> getJavaDocInfoPos(Tree tree)
378    {
379        TreePath treePath = TreePath.getPath(this.compilationUnitTree, tree);
380
381        DocCommentTree dct = (treePath != null)
382            ? this.docTrees.getDocCommentTree(treePath)
383            : null;
384
385        if (dct == null) return new Ret3<>(-1, -1, null);
386
387        int sPos = (int) this.docSourcePositions.getStartPosition
388            (this.compilationUnitTree, dct, dct);
389
390        int ePos = (int) this.docSourcePositions.getEndPosition
391            (this.compilationUnitTree, dct, dct);
392
393        // This is a case where there is an "empty comment".  I'm not sure at the moment if there
394        // is a "more appropriate" way to be handling this.  However, while writing code, an empty
395        // comment did, indeed, generate a **NON-NULL** 'dct' - but sPos and ePos were both -1
396        // 
397        // For now, I guess, we are going to pretend that a PERFECTLy-EMPTY JavaDoc-Comment (such
398        // as /**    */), is identical to have NO-COMMENT at all.
399
400        if ((sPos == -1) && (ePos == -1)) return new Ret3<>(-1, -1, null);
401
402        while (sPos >= 2)
403            if (this.srcFileAsStr.charAt(sPos--) == '*')
404                if (this.srcFileAsStr.regionMatches(sPos - 1, "/**", 0, 3))
405                { sPos--; break; }
406
407        /*
408        System.out.println(
409            "this.srcFileName: " + this.srcFileName + '\n' +
410            "sPos: " + sPos + ", ePos: " + ePos + '\n' +
411            "this.srcFileAsStr.substring(sPos, sPos+50): " +
412            this.srcFileAsStr.substring(sPos, sPos+50)
413        );
414
415        try { System.out.println("str: " + this.srcFileAsStr.substring(sPos, ePos)); }
416        catch (Exception e) { System.out.println("e.getMessage(): " + e.getMessage()); }
417        */
418
419        // Note that today is the "BIG DAY", I am trying to run my build on OCI rather than GCP.  
420        // It is April 29th, 2024.  I have just run into an issue (which never happened on whatever
421        // version of `javadoc` is running on GCP).  Here, the first package to have shown the 
422        // issue was in Apache.CLI.DefaultParser - which has an internal-static inner-class simply
423        // named "Builder".  For "Builder" the ePos is returning '-1'
424        // 
425        // Thinking about how "TreeUtils" works is not something I've been doing lately, but I 
426        // just reviewed all of the sordid details of "com.sun.source.util.DocSourcePositions".
427        // 
428        // These Methods:
429        // getStartPosition(CompilationUnitTree file, DocCommentTree comment, DocTree tree)
430        // getEndPosition(CompilationUnitTree file, DocCommentTree comment, DocTree tree)
431        // 
432        // Look for the Position in the source file's "comment" for the specific item inside that
433        // comment, specifically item "tree".  For static-inner classes, this new version of 
434        // javadoc that is running on OCI (rather than GCP) is returning "-1".
435        // 
436        // Fortunately, for me, there is an answer, but I think this solution could fail if the
437        // user included a "*/" Sub-String somwhere inside their comments.
438        // 
439        // THIS HACK IS TO JUST SET ePos to sPos if "getEndPosition" failed and returned -1
440        // 
441        // NOTE: I **THINK** - but I'm not 100% that I should switch this "hacky" like line to 
442        // one that iterates through the elements in the List<DocTree> returned by this method
443        // from class DocCommentTree.  I used to have a method that did just that, but I deleted it
444        // last year because iterating the character-data itself was much more efficient.  However,
445        // apparently for static-inner classes, newer versions of javadoc are just failing.
446        // 
447        // default List<? extends DocTree> getFullBody()
448        // Returns the entire body of a documentation comment, appearing before any block tags,
449        // including the first sentence.
450
451        if (ePos < 0) ePos = sPos;  // "The Hack"
452
453        // And then this stuff below is the original code written in 2023...
454        int MAX = this.srcFileAsStr.length() - 2;
455        while (ePos < MAX)
456            if (this.srcFileAsStr.charAt(ePos++) == '*')
457                if (this.srcFileAsStr.regionMatches(ePos - 1, "*/", 0, 2))
458                { ePos++; break; }
459
460        return new Ret3<>(sPos, ePos, dct);
461    }
462
463    /**
464     * Retrieve Java-Doc Comment information (if present / if provided) for any of the entities /
465     * members of a CIET / Type.
466     * 
467     * @param tree The {@link Entity} for which a Java-Doc Comment is sought.
468     * 
469     * @return An instance of {@link Ret3} as follows:
470     * <BR /><BR /><UL CLASS=JDUL>
471     * 
472     * <LI> {@code Ret3.a: Integer}
473     *      <BR />The <B STYLE='color: red'>starting</B> line-number of the Java-Doc Comment in the
474     *      original Source-Code File.
475     *      <BR /><BR /></LI>
476     * 
477     * <LI> {@code Ret3.b: Integer}
478     *      <BR />The <B STYLE='color: red'>ending</B> line-number of the Java-Doc Comment in the
479     *      original Source-Code File.
480     *      <BR /><BR /></LI>
481     * 
482     * <LI> {@code Ret3.b: DocCommentTree}
483     *      <BR />The Parse-Tree for the Java-Doc Comment.
484     *      <BR /><BR /></LI>
485     * </UL>
486     * 
487     * <BR /><B STYLE='color: red;'>NOTE:</B> If the {@link Entity} / Member of the {@code 'tree'}
488     * that was passed as input to this method does not have a Java-Doc Comment written, then this
489     * method will return null instead.
490     * 
491     * @see #docTrees
492     * @see #compilationUnitTree
493     * @see #srcFileAsStr
494     * @see #docSourcePositions
495     * @see #lineMap
496     */
497    public Ret3<Integer, Integer, DocCommentTree> getJavaDocInfoLine(Tree tree)
498    {
499        TreePath treePath = TreePath.getPath(this.compilationUnitTree, tree);
500
501        DocCommentTree dct = (treePath != null)
502            ? this.docTrees.getDocCommentTree(treePath)
503            : null;
504
505        if (dct == null) return new Ret3<>(-1, -1, null);
506
507        int sPos = (int) this.docSourcePositions.getStartPosition
508            (this.compilationUnitTree, dct, dct);
509
510        int ePos = (int) this.docSourcePositions.getEndPosition
511            (this.compilationUnitTree, dct, dct);
512
513        while (sPos >= 2)
514            if (this.srcFileAsStr.charAt(sPos--) == '*')
515                if (this.srcFileAsStr.regionMatches(sPos - 1, "/**", 0, 3))
516                { sPos--; break; }
517
518        int MAX = this.srcFileAsStr.length() - 2;
519        while (ePos < MAX)
520            if (this.srcFileAsStr.charAt(ePos++) == '*')
521                if (this.srcFileAsStr.regionMatches(ePos - 1, "*/", 0, 2))
522                { ePos++; break; }
523
524        return new Ret3<>(
525            (int) this.lineMap.getLineNumber(sPos),
526            (int) this.lineMap.getLineNumber(ePos),
527            dct
528        );
529    }
530
531    /**
532     * Retrieves just the Java-Doc Comment Parse-Tree, and does not include Line-Number or
533     * Character-Position information.
534     * 
535     * @param tree The {@link Entity} / Member of the Type for which the Java-Doc Comment is being
536     * sought.
537     * 
538     * @return The Java-Doc Comment Parse-Tree
539     * 
540     * @see #compilationUnitTree
541     * @see #docTrees
542     */
543    public DocCommentTree getJavaDocCommentTree(Tree tree)
544    {
545        TreePath treePath = TreePath.getPath(this.compilationUnitTree, tree);
546
547        if (treePath == null) return null;
548
549        return this.docTrees.getDocCommentTree(treePath);
550    }
551}