Cherry pick the banglets and such from wip-l1summercoat, this will be the basis for...
[SquirrelJME.git] / buildSrc / src / main / java / cc / squirreljme / plugin / multivm / __SourceInfo__.java
blob210cf26d3c814717433737ed2fe07b7886c1412d
1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
3 // SquirrelJME
4 // Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
5 // ---------------------------------------------------------------------------
6 // SquirrelJME is under the GNU General Public License v3+, or later.
7 // See license.mkd for licensing and copyright information.
8 // ---------------------------------------------------------------------------
10 package cc.squirreljme.plugin.multivm;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.StreamTokenizer;
15 import java.util.Collections;
16 import java.util.Deque;
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.LinkedHashSet;
20 import java.util.LinkedList;
21 import java.util.Map;
22 import java.util.Set;
24 /**
25 * Information on a source code file.
27 * @since 2020/10/09
29 final class __SourceInfo__
31 /** The package this source is in. */
32 public final String inPackage;
34 /** The name of the current class. */
35 public final String thisClass;
37 /** The super-class of this one, if one is set. */
38 public final String superClass;
40 /** The classes this implements. */
41 public final Set<String> implementsClasses;
43 /**
44 * Initializes the source information.
46 * @param __inPackage The package this source is in.
47 * @param __thisClass The current class.
48 * @param __superClass The super class, may be {@code null}.
49 * @param __implementsClasses The classes this implements.
50 * @throws NullPointerException On null arguments.
51 * @since 2020/10/09
53 __SourceInfo__(String __inPackage, String __thisClass, String __superClass,
54 Set<String> __implementsClasses)
55 throws NullPointerException
57 if (__inPackage == null || __thisClass == null)
58 throw new NullPointerException("NARG");
60 this.inPackage = __inPackage;
61 this.thisClass = __thisClass;
62 this.superClass = __superClass;
63 this.implementsClasses = (__implementsClasses == null ||
64 __implementsClasses.isEmpty() ? Collections.emptySet() :
65 Collections.unmodifiableSet(
66 new LinkedHashSet<>(__implementsClasses)));
69 /**
70 * {@inheritDoc}
71 * @since 2022/09/05
73 @Override
74 public String toString()
76 return String.format(
77 "__SourceInfo__{inPackage='%s', thisClass='%s'," +
78 "superClass='%s', implementsClasses=%s}",
79 this.inPackage, this.thisClass, this.superClass,
80 this.implementsClasses);
83 /**
84 * Loads class information from a class file.
86 * @param __in The stream to from.
87 * @return The implementing class information.
88 * @throws IOException On read errors.
89 * @throws NullPointerException On null arguments.
90 * @since 2022/09/05
92 public static __SourceInfo__ loadClass(InputStream __in)
93 throws IOException, NullPointerException
95 if (__in == null)
96 throw new NullPointerException("NARG");
98 throw new Error("TODO");
102 * Loads information gleaned from source code in Jasmin.
104 * @param __in The file to read from.
105 * @return The information on the class.
106 * @throws IOException On read errors.
107 * @throws NullPointerException On null arguments.
108 * @since 2020/10/10
110 public static __SourceInfo__ loadJasmin(InputStream __in)
111 throws IOException, NullPointerException
113 if (__in == null)
114 throw new NullPointerException("NARG");
116 StreamTokenizer stream = new StreamTokenizer(__in);
118 // Jasmin files, do not use standard syntax so clear everything
119 stream.resetSyntax();
121 // Set these specifically
122 stream.commentChar(';');
123 stream.wordChars('.', '.');
124 stream.wordChars('/', '/');
125 stream.wordChars('$', '$');
126 stream.wordChars('a', 'z');
127 stream.wordChars('A', 'Z');
128 stream.wordChars('0', '9');
129 stream.wordChars('_', '_');
130 stream.eolIsSignificant(true);
131 stream.quoteChar('"');
132 stream.quoteChar('\'');
133 stream.whitespaceChars(0, ' ');
135 // Current state
136 String thisClass = null;
137 String superClass = null;
138 Set<String> implementsClasses = new LinkedHashSet<>();
140 // Parse tokens
141 Deque<String> queue = new LinkedList<>();
142 for (;;)
144 int type = stream.nextToken();
146 // End of file or line, will be a statement
147 if (type == StreamTokenizer.TT_EOL ||
148 type == StreamTokenizer.TT_EOF)
150 String first = queue.pollFirst();
152 // Declares current class or interface?
153 if (".class".equals(first) || ".interface".equals(first))
155 // Ignore any access specifiers before the class name
156 String second;
159 second = queue.pollFirst();
160 } while (second != null &&
161 __SourceInfo__.__isAccessSpec(second));
163 // The class name just follows these
164 if (second != null)
165 thisClass = second.replace('/', '.');
168 // Declares super class?
169 else if (".super".equals(first))
171 // The super class just follows this
172 String second = queue.pollFirst();
173 if (second != null)
174 superClass = second.replace('/', '.');
177 // Declares implementing interface?
178 else if (".implements".equals(first))
180 // The super class just follows this
181 String second = queue.pollFirst();
182 if (second != null)
183 implementsClasses.add(
184 second.replace('/', '.'));
187 // Stop on EOF, or if we hit a field or method as we know
188 // there will be nothing following this
189 if (type == StreamTokenizer.TT_EOF ||
190 ".method".equals(first) ||
191 ".field".equals(first))
192 break;
194 // Clear the queue for the next run
195 queue.clear();
198 // Push these to the queue
199 else if (type == StreamTokenizer.TT_NUMBER)
200 queue.addLast(Double.toString(stream.nval));
201 else if (type == StreamTokenizer.TT_WORD)
202 queue.addLast(stream.sval);
204 // .class public foo/bar
205 // .super foo/bar
206 // .implements foo/bar
209 // This should not happen, unless the source is malformed
210 if (thisClass == null)
211 throw new IOException("Jasmin class has no name?");
213 // Determine the package we are in, which is just the package of our
214 // binary class name
215 int ls = thisClass.lastIndexOf('.');
216 String inPackage = (ls < 0 ? "" : thisClass.substring(0, ls));
218 // Normalize to Java forms, as these all use binary names
219 return new __SourceInfo__(inPackage,
220 thisClass, superClass, implementsClasses);
224 * Loads information gleaned from source code in Java.
226 * @param __in The file to read from.
227 * @return The information on the class.
228 * @throws IOException On read errors.
229 * @throws NullPointerException On null arguments.
230 * @since 2020/10/09
232 public static __SourceInfo__ loadJava(InputStream __in)
233 throws IOException, NullPointerException
235 if (__in == null)
236 throw new NullPointerException("NARG");
238 StreamTokenizer stream = new StreamTokenizer(__in);
240 // Initialize tokenizer parameters
241 stream.slashStarComments(true);
242 stream.slashSlashComments(true);
243 stream.whitespaceChars(0, ' ');
244 stream.parseNumbers();
245 stream.wordChars('.', '.');
246 stream.wordChars('_', '_');
247 stream.wordChars('$', '$');
248 stream.ordinaryChar('<');
249 stream.ordinaryChar('>');
250 stream.ordinaryChar('@');
251 stream.ordinaryChar(';');
252 stream.ordinaryChar('{');
253 stream.ordinaryChar('}');
254 stream.ordinaryChar('?');
256 // Parsed state
257 String inPackage = null;
258 String thisClass = null;
259 String superClass = null;
260 Set<String> implementsClasses = new LinkedHashSet<>();
261 Map<String, String> imports = new HashMap<>();
262 boolean foundClassName = false;
264 // Parse tokens
265 Deque<String> queue = new LinkedList<>();
266 for (;;)
268 int type = stream.nextToken();
270 // End of line or statement, curly braces are included since they
271 // open the class file
272 if (type == ';' || type == '{' || type == StreamTokenizer.TT_EOF)
274 boolean isPackage = false;
275 boolean isImport = false;
277 // Is this a package or import statement?
278 String first = queue.pollFirst();
279 if (first != null)
280 switch (first)
282 case "package":
283 isPackage = true;
284 break;
286 case "import":
287 isImport = true;
288 break;
291 // Package or import statement
292 if (isPackage || isImport)
294 String second = __SourceInfo__.__glue(queue);
296 // Declare package
297 if (isPackage)
298 inPackage = second;
300 // Import statement that is not a static import
301 else if (!queue.contains("static"))
303 // Determine the identifier this links to
304 int ld = second.lastIndexOf('.');
305 String ident = (ld < 0 ? second :
306 second.substring(ld + 1));
308 // Declare import, used for full name getting
309 imports.put(ident, second);
313 // Potential class statement?
314 else if (type == '{' && (queue.contains("class") ||
315 queue.contains("interface")))
317 // If no package was found, then this will be the default
318 // package
319 if (inPackage == null)
320 inPackage = "";
322 // It may or may not be here on this line
323 String maybeClass = __SourceInfo__
324 .__follow(queue, "class", "interface");
325 String maybeExtend = __SourceInfo__
326 .__follow(queue, "extends");
328 // We did find the class name?
329 if (maybeClass != null)
331 // Determine the name of the current class
332 thisClass = (inPackage.isEmpty() ? maybeClass :
333 inPackage + "." + maybeClass);
335 // We found this, so we should be able to stop now
336 foundClassName = true;
339 // We did find the super class?
340 if (maybeExtend != null)
342 // Super class is from an import statement?
343 String foundImport = imports.get(maybeExtend);
344 if (foundImport != null)
345 superClass = foundImport;
347 // Has dot, so is fully qualified
348 else if (maybeExtend.indexOf('.') >= 0)
349 superClass = maybeExtend;
351 // Otherwise, assume it is in the same package as our
352 // current class
353 else
354 superClass = (inPackage.isEmpty() ? maybeExtend :
355 inPackage + "." + maybeExtend);
358 // Handle implements? There may be multiple ones
359 for (int count = 0;; count++)
361 // Try to read the next interface, if any
362 String maybeImplements = __SourceInfo__.__follow(queue,
363 (count == 0 ? "implements" : ","));
364 if (maybeImplements == null)
365 break;
367 // Implements class is from an import statement?
368 String foundImport = imports.get(maybeImplements);
369 if (foundImport != null)
370 implementsClasses.add(foundImport);
372 // Has dot, so is fully qualified
373 else if (maybeImplements.indexOf('.') >= 0)
374 implementsClasses.add(maybeImplements);
376 // Otherwise, assume it is in the same package as our
377 // current class
378 else
379 implementsClasses.add((inPackage.isEmpty() ?
380 maybeImplements :
381 inPackage + "." + maybeImplements));
385 // Stop on EOF or if we found the class we want
386 if (foundClassName || type == StreamTokenizer.TT_EOF)
387 break;
389 // Clear the queue for the next run
390 queue.clear();
393 // Push these to the queue
394 else if (type == StreamTokenizer.TT_NUMBER)
395 queue.addLast(Double.toString(stream.nval));
396 else if (type == StreamTokenizer.TT_WORD)
397 queue.addLast(stream.sval);
398 else
399 queue.addLast(Character.toString((char)type));
402 // This should not happen, unless the source is malformed
403 if (inPackage == null || thisClass == null)
404 throw new IOException("Java class has no package or name?");
406 return new __SourceInfo__(inPackage, thisClass, superClass,
407 implementsClasses);
411 * Attempts to locate the token that follows the given identifying token.
413 * For example {@code __follow(["public", "class", "foo"], "class")} will
414 * return {@code "foo"}.
416 * @param __seq The token sequence.
417 * @param __tokens The tokens to follow from.
418 * @return The following word.
419 * @throws NullPointerException On null arguments.
420 * @since 2020/10/10
422 private static String __follow(Deque<String> __seq, String... __tokens)
423 throws NullPointerException
425 if (__seq == null || __tokens == null || __tokens.length <= 0)
426 throw new NullPointerException("NARG");
428 // Try to discover a match
429 for (;;)
431 // Try to find first valid token matching the word
432 String at = __seq.pollFirst();
433 if (at == null)
434 break;
436 // Find match for our token
437 String matching = null;
438 for (String maybe : __tokens)
439 if (maybe.equals(at))
441 matching = maybe;
442 break;
445 // Did we match our token?
446 if (matching == null)
447 continue;
449 // Return the follower if it is a valid identifier
450 String follower = __seq.pollFirst();
451 if (follower != null && __SourceInfo__.__isJavaWord(follower))
453 // Skip any < and > since we do not care for generics at all
454 String skip = __seq.peekFirst();
455 if ("<".equals(skip))
457 int count = 0;
460 String token = __seq.pollFirst();
462 if ("<".equals(token))
463 count++;
464 else if (">".equals(token))
465 count--;
466 } while (count > 0);
469 return follower;
473 // No match found
474 return null;
478 * Glues the given strings together.
480 * @param __strings The strings to glue together.
481 * @return The joined together strings.
482 * @throws NullPointerException On null arguments.
483 * @since 2020/10/10
485 private static String __glue(Iterable<String> __strings)
486 throws NullPointerException
488 if (__strings == null)
489 throw new NullPointerException("NARG");
491 StringBuilder sb = new StringBuilder();
492 for (String s : __strings)
493 sb.append(s);
495 return sb.toString();
499 * Is this an access specifier? This is used by Jasmin to declare a class
500 * or method with special flag types.
502 * @param __word The word to check.
503 * @return If this is an access specifier.
504 * @throws NullPointerException On null arguments.
505 * @since 2020/10/10
507 private static boolean __isAccessSpec(String __word)
508 throws NullPointerException
510 if (__word == null)
511 throw new NullPointerException("NARG");
513 switch (__word)
515 case "public":
516 case "private":
517 case "protected":
518 case "static":
519 case "final":
520 case "synchronized":
521 case "volatile":
522 case "transient":
523 case "native":
524 case "interface":
525 case "abstract":
526 return true;
529 return false;
533 * Checks if this is a Java word or not.
535 * @param __word The word to check.
536 * @return If this is a Java word or not.
537 * @throws NullPointerException On null arguments.
538 * @since 2020/10/10
540 private static boolean __isJavaWord(String __word)
541 throws NullPointerException
543 if (__word == null)
544 throw new NullPointerException("NARG");
546 // Blank words are never words
547 int n = __word.length();
548 if (n == 0)
549 return false;
551 // Check for invalid characters
552 for (int i = 0; i < n; i++)
554 char c = __word.charAt(i);
556 // Consider dots valid for fully qualified names
557 if (c == '.')
558 continue;
560 // Otherwise, must be a valid character here
561 if ((i == 0 && !Character.isJavaIdentifierStart(c)) ||
562 (i > 0 && !Character.isJavaIdentifierPart(c)))
563 return false;
566 // Did not fail, so must be a Java word
567 return true;