1 // 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html#License 3 /** 4 ******************************************************************************* 5 * Copyright (C) 2004-2012, International Business Machines Corporation and * 6 * others. All Rights Reserved. * 7 ******************************************************************************* 8 */ 9 10 package com.ibm.icu.dev.tool.docs; 11 12 import java.io.BufferedReader; 13 import java.io.File; 14 import java.io.FileInputStream; 15 import java.io.FileOutputStream; 16 import java.io.FilenameFilter; 17 import java.io.IOException; 18 import java.io.InputStream; 19 import java.io.InputStreamReader; 20 import java.io.PrintStream; 21 import java.util.ArrayList; 22 import java.util.HashMap; 23 import java.util.Iterator; 24 import java.util.Map; 25 import java.util.TreeMap; 26 27 28 /** 29 * A simple facility for adding C-like preprocessing to .java files. 30 * This only understands a subset of the C preprocessing syntax. 31 * Its used to manage files that with only small differences can be 32 * compiled for different JVMs. This changes files in place, 33 * commenting out lines based on the current flag settings. 34 */ 35 public class CodeMangler { 36 private File indir; // root of input 37 private File outdir; // root of output 38 private String suffix; // suffix to process, default '.jpp' 39 private boolean recurse; // true if recurse on directories 40 private boolean force; // true if force reprocess of files 41 private boolean clean; // true if output is to be cleaned 42 private boolean timestamp; // true if we read/write timestamp 43 private boolean nonames; // true if no names in header 44 private HashMap map; // defines 45 private ArrayList names; // files/directories to process 46 private String header; // sorted list of defines passed in 47 48 private boolean verbose; // true if we emit debug output 49 50 private static final String IGNORE_PREFIX = "//##"; 51 private static final String HEADER_PREFIX = "//##header"; 52 53 public static void main(String[] args) { 54 new CodeMangler(args).run(); 55 } 56 57 private static final String usage = "Usage:\n" + 58 " CodeMangler [flags] file... dir... @argfile... \n" + 59 "-in[dir] path - root directory of input files, otherwise use current directory\n" + 60 "-out[dir] path - root directory of output files, otherwise use input directory\n" + 61 "-s[uffix] string - suffix of inputfiles to process, otherwise use '.java' (directories only)\n" + 62 "-c[lean] - remove all control flags from code on output (does not proceed if overwriting)\n" + 63 "-r[ecurse] - if present, recursively process subdirectories\n" + 64 "-f[orce] - force reprocessing of files even if timestamp and headers match\n" + 65 "-t[imestamp] - expect/write timestamp in header\n" + 66 "-dNAME[=VALUE] - define NAME with optional value VALUE\n" + 67 " (or -d NAME[=VALUE])\n" + 68 "-n - do not put NAME/VALUE in header\n" + 69 "-help - print this usage message and exit.\n" + 70 "\n" + 71 "For file arguments, output '.java' files using the same path/name under the output directory.\n" + 72 "For directory arguments, process all files with the defined suffix in the directory.\n" + 73 " (if recursing, do the same for all files recursively under each directory)\n" + 74 "For @argfile arguments, read the specified text file (strip the '@'), and process each line of that file as \n" + 75 "an argument.\n" + 76 "\n" + 77 "Directives are one of the following:\n" + 78 " #ifdef, #ifndef, #else, #endif, #if, #elif, #define, #undef\n" + 79 "These may optionally be preceeded by whitespace or //.\n" + 80 "#if, #elif args are of the form 'key == value' or 'key != value'.\n" + 81 "Only exact character match key with value is performed.\n" + 82 "#define args are 'key [==] value', the '==' is optional.\n"; 83 84 CodeMangler(String[] args) { 85 map = new HashMap(); 86 names = new ArrayList(); 87 suffix = ".java"; 88 clean = false; 89 timestamp = false; 90 91 String inname = null; 92 String outname = null; 93 boolean processArgs = true; 94 String arg = null; 95 try { 96 for (int i = 0; i < args.length; ++i) { 97 arg = args[i]; 98 if ("--".equals(arg)) { 99 processArgs = false; 100 } else if (processArgs && arg.charAt(0) == '-') { 101 if (arg.startsWith("-in")) { 102 inname = args[++i]; 103 } else if (arg.startsWith("-out")) { 104 outname = args[++i]; 105 } else if (arg.startsWith("-d")) { 106 String id = arg.substring(2); 107 if (id.length() == 0) { 108 id = args[++i]; 109 } 110 String val = ""; 111 int ix = id.indexOf('='); 112 if (ix >= 0) { 113 val = id.substring(ix+1); 114 id = id.substring(0,ix); 115 } 116 map.put(id, val); 117 } else if (arg.startsWith("-s")) { 118 suffix = args[++i]; 119 } else if (arg.startsWith("-r")) { 120 recurse = true; 121 } else if (arg.startsWith("-f")) { 122 force = true; 123 } else if (arg.startsWith("-c")) { 124 clean = true; 125 } else if (arg.startsWith("-t")) { 126 timestamp = true; 127 } else if (arg.startsWith("-h")) { 128 System.out.print(usage); 129 break; // stop before processing arguments, so we will do nothing 130 } else if (arg.startsWith("-v")) { 131 verbose = true; 132 } else if (arg.startsWith("-n")) { 133 nonames = true; 134 } else { 135 System.err.println("Error: unrecognized argument '" + arg + "'"); 136 System.err.println(usage); 137 throw new IllegalArgumentException(arg); 138 } 139 } else { 140 if (arg.charAt(0) == '@') { 141 File argfile = new File(arg.substring(1)); 142 if (argfile.exists() && !argfile.isDirectory()) { 143 BufferedReader br = null; 144 try { 145 br = new BufferedReader(new InputStreamReader(new FileInputStream(argfile))); 146 ArrayList list = new ArrayList(); 147 for (int x = 0; x < args.length; ++x) { 148 list.add(args[x]); 149 } 150 String line; 151 while (null != (line = br.readLine())) { 152 line = line.trim(); 153 if (line.length() > 0 && line.charAt(0) != '#') { 154 if (verbose) System.out.println("adding argument: " + line); 155 list.add(line); 156 } 157 } 158 args = (String[])list.toArray(new String[list.size()]); 159 } 160 catch (IOException e) { 161 System.err.println("error reading arg file: " + e); 162 } 163 finally { 164 if (br != null) { 165 try { 166 br.close(); 167 } catch (Exception e){ 168 // ignore 169 } 170 } 171 } 172 } 173 } else { 174 names.add(arg); 175 } 176 } 177 } 178 } catch (IndexOutOfBoundsException e) { 179 String msg = "Error: argument '" + arg + "' missing value"; 180 System.err.println(msg); 181 System.err.println(usage); 182 throw new IllegalArgumentException(msg); 183 } 184 185 String username = System.getProperty("user.dir"); 186 if (inname == null) { 187 inname = username; 188 } else if (!(inname.startsWith("\\") || inname.startsWith("/"))) { 189 inname = username + File.separator + inname; 190 } 191 indir = new File(inname); 192 try { 193 indir = indir.getCanonicalFile(); 194 } 195 catch (IOException e) { 196 // continue, but most likely we'll fail later 197 } 198 if (!indir.exists()) { 199 throw new IllegalArgumentException("Input directory '" + indir.getAbsolutePath() + "' does not exist."); 200 } else if (!indir.isDirectory()) { 201 throw new IllegalArgumentException("Input path '" + indir.getAbsolutePath() + "' is not a directory."); 202 } 203 if (verbose) System.out.println("indir: " + indir.getAbsolutePath()); 204 205 if (outname == null) { 206 outname = inname; 207 } else if (!(outname.startsWith("\\") || outname.startsWith("/"))) { 208 outname = username + File.separator + outname; 209 } 210 outdir = new File(outname); 211 try { 212 outdir = outdir.getCanonicalFile(); 213 } 214 catch (IOException e) { 215 // continue, but most likely we'll fail later 216 } 217 if (!outdir.exists()) { 218 throw new IllegalArgumentException("Output directory '" + outdir.getAbsolutePath() + "' does not exist."); 219 } else if (!outdir.isDirectory()) { 220 throw new IllegalArgumentException("Output path '" + outdir.getAbsolutePath() + "' is not a directory."); 221 } 222 if (verbose) System.out.println("outdir: " + outdir.getAbsolutePath()); 223 224 if (clean && suffix.equals(".java")) { 225 try { 226 if (outdir.getCanonicalPath().equals(indir.getCanonicalPath())) { 227 throw new IllegalArgumentException("Cannot use 'clean' to overwrite .java files in same directory tree"); 228 } 229 } 230 catch (IOException e) { 231 System.err.println("possible overwrite, error: " + e.getMessage()); 232 throw new IllegalArgumentException("Cannot use 'clean' to overrwrite .java files"); 233 } 234 } 235 236 if (names.isEmpty()) { 237 names.add("."); 238 } 239 240 TreeMap sort = new TreeMap(String.CASE_INSENSITIVE_ORDER); 241 sort.putAll(map); 242 Iterator iter = sort.entrySet().iterator(); 243 StringBuffer buf = new StringBuffer(); 244 if (!nonames) { 245 while (iter.hasNext()) { 246 Map.Entry e = (Map.Entry)iter.next(); 247 if (buf.length() > 0) { 248 buf.append(", "); 249 } 250 buf.append(e.getKey()); 251 String v = (String)e.getValue(); 252 if (v != null && v.length() > 0) { 253 buf.append('='); 254 buf.append(v); 255 } 256 } 257 } 258 header = buf.toString(); 259 } 260 261 public int run() { 262 return process("", (String[])names.toArray(new String[names.size()])); 263 } 264 265 public int process(String path, String[] filenames) { 266 if (verbose) System.out.println("path: '" + path + "'"); 267 int count = 0; 268 for (int i = 0; i < filenames.length; ++i) { 269 if (verbose) System.out.println("name " + i + " of " + filenames.length + ": '" + filenames[i] + "'"); 270 String name = path + filenames[i]; 271 File fin = new File(indir, name); 272 try { 273 fin = fin.getCanonicalFile(); 274 } 275 catch (IOException e) { 276 } 277 if (!fin.exists()) { 278 System.err.println("File " + fin.getAbsolutePath() + " does not exist."); 279 continue; 280 } 281 if (fin.isFile()) { 282 if (verbose) System.out.println("processing file: '" + fin.getAbsolutePath() + "'"); 283 String oname; 284 int ix = name.lastIndexOf("."); 285 if (ix != -1) { 286 oname = name.substring(0, ix); 287 } else { 288 oname = name; 289 } 290 oname += ".java"; 291 File fout = new File(outdir, oname); 292 if (processFile(fin, fout)) { 293 ++count; 294 } 295 } else if (fin.isDirectory()) { 296 if (verbose) System.out.println("recursing on directory '" + fin.getAbsolutePath() + "'"); 297 String npath = ".".equals(name) ? path : path + fin.getName() + File.separator; 298 count += process(npath, fin.list(filter)); // recursive call 299 } 300 } 301 return count; 302 } 303 304 305 private final FilenameFilter filter = new FilenameFilter() { 306 public boolean accept(File dir, String name) { 307 File f = new File(dir, name); 308 return (f.isFile() && name.endsWith(suffix)) || (f.isDirectory() && recurse); 309 } 310 }; 311 312 public boolean processFile(File infile, File outfile) { 313 File backup = null; 314 315 class State { 316 int lc; 317 String line; 318 boolean emit = true; 319 boolean tripped; 320 private State next; 321 322 public String toString() { 323 return "line " + lc 324 + ": '" + line 325 + "' (emit: " + emit 326 + " tripped: " + tripped 327 + ")"; 328 } 329 330 void trip(boolean trip) { 331 if (!tripped & trip) { 332 tripped = true; 333 emit = next != null ? next.emit : true; 334 } else { 335 emit = false; 336 } 337 } 338 339 State push(int lc, String line, boolean trip) { 340 this.lc = lc; 341 this.line = line; 342 State ret = new State(); 343 ret.next = this; 344 ret.emit = this.emit & trip; 345 ret.tripped = trip; 346 return ret; 347 } 348 349 State pop() { 350 return next; 351 } 352 } 353 354 HashMap oldMap = null; 355 356 long outModTime = 0; 357 358 try { 359 PrintStream outstream = null; 360 InputStream instream = new FileInputStream(infile); 361 362 BufferedReader reader = new BufferedReader(new InputStreamReader(instream)); 363 int lc = 0; 364 State state = new State(); 365 String line; 366 while ((line = reader.readLine()) != null) { 367 if (lc == 0) { // check and write header for output file if needed 368 boolean hasHeader = line.startsWith(HEADER_PREFIX); 369 if (hasHeader && !force) { 370 long expectLastModified = ((infile.lastModified() + 999)/1000)*1000; 371 String headerline = HEADER_PREFIX; 372 if (header.length() > 0) { 373 headerline += " "; 374 headerline += header; 375 } 376 if (timestamp) { 377 headerline += " "; 378 headerline += String.valueOf(expectLastModified); 379 } 380 if (line.equals(headerline)) { 381 if (verbose) System.out.println("no changes necessary to " + infile.getCanonicalPath()); 382 reader.close(); 383 return false; // nothing to do 384 } 385 if (verbose) { 386 System.out.println(" old header: " + line); 387 System.out.println(" != expected: " + headerline); 388 } 389 } 390 391 // create output file directory structure 392 String outpname = outfile.getParent(); 393 if (outpname != null) { 394 File outp = new File(outpname); 395 if (!(outp.exists() || outp.mkdirs())) { 396 System.err.println("could not create directory: '" + outpname + "'"); 397 reader.close(); 398 return false; 399 } 400 } 401 402 // if we're overwriting, use a temporary file 403 if (suffix.equals(".java")) { 404 backup = outfile; 405 try { 406 outfile = File.createTempFile(outfile.getName(), null, outfile.getParentFile()); 407 } 408 catch (IOException ex) { 409 System.err.println(ex.getMessage()); 410 reader.close(); 411 return false; 412 } 413 } 414 415 outModTime = ((outfile.lastModified()+999)/1000)*1000; // round up 416 outstream = new PrintStream(new FileOutputStream(outfile)); 417 String headerline = HEADER_PREFIX; 418 if (header.length() > 0) { 419 headerline += " "; 420 headerline += header; 421 } 422 if (timestamp) { 423 headerline += " "; 424 headerline += String.valueOf(outModTime); 425 } 426 outstream.println(headerline); 427 if (verbose) System.out.println("header: " + headerline); 428 429 // discard the old header if we had one, otherwise match this line like any other 430 if (hasHeader) { 431 ++lc; // mark as having read a line so we never reexecute this block 432 continue; 433 } 434 } 435 436 String[] res = new String[3]; 437 if (patMatch(line, res)) { 438 String lead = res[0]; 439 String key = res[1]; 440 String val = res[2]; 441 442 if (verbose) System.out.println("directive: " + line 443 + " key: '" + key 444 + "' val: '" + val 445 + "' " + state); 446 if (key.equals("ifdef")) { 447 state = state.push(lc, line, map.get(val) != null); 448 } else if (key.equals("ifndef")) { 449 state = state.push(lc, line, map.get(val) == null); 450 } else if (key.equals("else")) { 451 state.trip(true); 452 } else if (key.equals("endif")) { 453 state = state.pop(); 454 } else if (key.equals("undef")) { 455 if (state.emit) { 456 if (oldMap == null) { 457 oldMap = (HashMap)map.clone(); 458 } 459 map.remove(val); 460 } 461 } else if (key.equals("define")) { 462 if (pat2Match(val, res)) { 463 String key2 = res[0]; 464 String val2 = res[2]; 465 466 if (verbose) System.out.println("val2: '" + val2 467 + "' key2: '" + key2 468 + "'"); 469 if (state.emit) { 470 if (oldMap == null) { 471 oldMap = (HashMap)map.clone(); 472 } 473 map.put(key2, val2); 474 } 475 } 476 } else { // #if, #elif 477 // only top level OR (||) operator is supported for now 478 int count = 1; 479 int index = 0; 480 while ((index = val.indexOf("||", index)) > 0) { 481 count++; 482 index++; 483 } 484 String[] expressions = new String[count]; 485 if (count == 1) { 486 expressions[0] = val; 487 } else { 488 int start = 0; 489 index = 0; 490 count = 0; 491 while (true) { 492 index = val.indexOf("||", start); 493 if (index > 0) { 494 expressions[count++] = val.substring(start, index); 495 start = index + 2; 496 } else { 497 expressions[count++] = val.substring(start); 498 break; 499 } 500 } 501 } 502 boolean eval = false; 503 for (count = 0; count < expressions.length && !eval; count++) { 504 if (pat2Match(expressions[count], res)) { 505 String key2 = res[0]; 506 String val2 = res[2]; 507 508 if (key2.equals("defined")) { 509 // defined command 510 if (verbose) System.out.println( 511 "index: '" + count 512 + "' val2: '" + val2 513 + "' key2: '" + key2 514 + "'"); 515 eval = map.containsKey(val2); 516 } else { 517 boolean neq = false; 518 if (res[1].equals("!=")) { 519 neq = true; 520 } else if (!res[1].equals("==")) { 521 System.err.println("Invalid expression: '" + val); 522 } 523 if (verbose) System.out.println( 524 "index: '" + count 525 + "' val2: '" + val2 526 + "' neq: '" + neq 527 + "' key2: '" + key2 528 + "'"); 529 eval = (val2.equals(map.get(key2)) != neq); 530 } 531 } 532 } 533 if (key.equals("if")) { 534 state = state.push(lc, line, eval); 535 } else if (key.equals("elif")) { 536 state.trip(eval); 537 } 538 } 539 if (!clean) { 540 lc++; 541 if (!lead.equals("//")) { 542 outstream.print("//"); 543 line = line.substring(lead.length()); 544 } 545 outstream.println(line); 546 } 547 continue; 548 } 549 550 lc++; 551 String found = pat3Match(line); 552 boolean hasIgnore = found != null; 553 if (state.emit == hasIgnore) { 554 if (state.emit) { 555 line = line.substring(found.length()); 556 } else { 557 line = IGNORE_PREFIX + line; 558 } 559 } else if (hasIgnore && !found.equals(IGNORE_PREFIX)) { 560 line = IGNORE_PREFIX + line.substring(found.length()); 561 } 562 if (!clean || state.emit) { 563 outstream.println(line); 564 } 565 } 566 567 state = state.pop(); 568 if (state != null) { 569 System.err.println("Error: unclosed directive(s):"); 570 do { 571 System.err.println(state); 572 } while ((state = state.pop()) != null); 573 System.err.println(" in file: " + outfile.getCanonicalPath()); 574 if (oldMap != null) { 575 map = oldMap; 576 } 577 reader.close(); 578 outstream.close(); 579 return false; 580 } 581 582 outstream.close(); 583 instream.close(); 584 585 if (backup != null) { 586 if (backup.exists()) { 587 backup.delete(); 588 } 589 outfile.renameTo(backup); 590 } 591 592 if (timestamp) { 593 outfile.setLastModified(outModTime); // synch with timestamp 594 } 595 596 if (oldMap != null) { 597 map = oldMap; 598 } 599 } 600 catch (IOException e) { 601 System.err.println(e); 602 return false; 603 } 604 return true; 605 } 606 607 608 /** 609 * Perform same operation as matching on pat. on exit 610 * leadKeyValue contains the three strings lead, key, and value. 611 * 'lead' is the portion before the #ifdef directive. 'key' is 612 * the directive. 'value' is the portion after the directive. if 613 * there is a match, return true, else return false. 614 */ 615 static boolean patMatch(String line, String[] leadKeyValue) { 616 if (line.length() == 0) { 617 return false; 618 } 619 if (!line.endsWith("\n")) { 620 line = line + '\n'; 621 } 622 int mark = 0; 623 int state = 0; 624 loop: for (int i = 0; i < line.length(); ++i) { 625 char c = line.charAt(i); 626 switch (state) { 627 case 0: // at start of line, haven't seen anything but whitespace yet 628 if (c == ' ' || c == '\t' || c == '\r') continue; 629 if (c == '/') { state = 1; continue; } 630 if (c == '#') { state = 4; continue; } 631 return false; 632 case 1: // have seen a single slash after start of line 633 if (c == '/') { state = 2; continue; } 634 return false; 635 case 2: // have seen two or more slashes 636 if (c == '/') continue; 637 if (c == ' ' || c == '\t' || c == '\r') { state = 3; continue; } 638 if (c == '#') { state = 4; continue; } 639 return false; 640 case 3: // have seen a space after two or more slashes 641 if (c == ' ' || c == '\t' || c == '\r') continue; 642 if (c == '#') { state = 4; continue; } 643 return false; 644 case 4: // have seen a '#' 645 leadKeyValue[0] = line.substring(mark, i-1); 646 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { mark = i; state = 5; continue; } 647 return false; 648 case 5: // an ascii char followed the '#' 649 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) continue; 650 if (c == ' ' || c == '\t' || c == '\n') { 651 String key = line.substring(mark, i).toLowerCase(); 652 if (key.equals("ifdef") || 653 key.equals("ifndef") || 654 key.equals("else") || 655 key.equals("endif") || 656 key.equals("undef") || 657 key.equals("define") || 658 key.equals("if") || 659 key.equals("elif")) { 660 leadKeyValue[1] = key; 661 mark = i; 662 state = 6; 663 break loop; 664 } 665 } 666 return false; 667 default: 668 throw new IllegalStateException(); 669 } 670 } 671 if (state == 6) { 672 leadKeyValue[2] = line.substring(mark, line.length()).trim(); 673 return true; 674 } 675 return false; // never reached, does the compiler know this? 676 } 677 678 /** 679 * Perform same operation as matching on pat2. on exit 680 * keyRelValue contains the three strings key, rel, and value. 681 * 'key' is the portion before the relation (or final word). 'rel' is 682 * the relation, if present, either == or !=. 'value' is the final 683 * word. if there is a match, return true, else return false. 684 */ 685 static boolean pat2Match(String line, String[] keyRelVal) { 686 687 if (line.length() == 0) { 688 return false; 689 } 690 keyRelVal[0] = keyRelVal[1] = keyRelVal[2] = ""; 691 int mark = 0; 692 int state = 0; 693 String command = null; 694 loop: for (int i = 0; i < line.length(); ++i) { 695 char c = line.charAt(i); 696 switch (state) { 697 case 0: // saw beginning or space, no rel yet 698 if (c == ' ' || c == '\t' || c == '\n') { 699 continue; 700 } 701 if ((c == '!' || c == '=')) { 702 return false; 703 } 704 state = 1; 705 continue; 706 case 1: // saw start of a word 707 if (c == ' ' || c == '\t') { 708 state = 2; 709 } 710 else if (c == '(') { 711 command = line.substring(0, i).trim(); 712 if (!command.equals("defined")) { 713 return false; 714 } 715 keyRelVal[0] = command; 716 state = 2; 717 } 718 else if (c == '!' || c == '=') { 719 state = 3; 720 } 721 continue; 722 case 2: // saw end of word, and space 723 if (c == ' ' || c == '\t') { 724 continue; 725 } 726 else if (command == null && c == '(') { 727 continue; 728 } 729 else if (c == '!' || c == '=') { 730 state = 3; 731 continue; 732 } 733 keyRelVal[0] = line.substring(0, i-1).trim(); 734 mark = i; 735 state = 4; 736 break loop; 737 case 3: // saw end of word, and '!' or '=' 738 if (c == '=') { 739 keyRelVal[0] = line.substring(0, i-1).trim(); 740 keyRelVal[1] = line.substring(i-1, i+1); 741 mark = i+1; 742 state = 4; 743 break loop; 744 } 745 return false; 746 default: 747 break; 748 } 749 } 750 switch (state) { 751 case 0: 752 return false; // found nothing 753 case 1: 754 case 2: 755 keyRelVal[0] = line.trim(); break; // found only a word 756 case 3: 757 return false; // found a word and '!' or '=" then end of line, incomplete 758 case 4: 759 keyRelVal[2] = line.substring(mark).trim(); // found a word, possible rel, and who knows what 760 if (command != null) { 761 int len = keyRelVal[2].length(); 762 if (keyRelVal[2].charAt(len - 1) != ')') { 763 // closing parenthesis is missing 764 return false; 765 } 766 keyRelVal[2] = keyRelVal[2].substring(0, len - 1).trim(); 767 } 768 break; 769 default: 770 throw new IllegalStateException(); 771 } 772 return true; 773 } 774 775 static String pat3Match(String line) { 776 int state = 0; 777 loop: for (int i = 0; i < line.length(); ++i) { 778 char c = line.charAt(i); 779 switch(state) { 780 case 0: if (c == ' ' || c == '\t') continue; 781 if (c == '/') { state = 1; continue; } 782 break loop; 783 case 1: 784 if (c == '/') { state = 2; continue; } 785 break loop; 786 case 2: 787 if (c == '#') { state = 3; continue; } 788 break loop; 789 case 3: 790 if (c == '#') return line.substring(0, i+1); 791 break loop; 792 default: 793 break loop; 794 } 795 } 796 return null; 797 } 798 } 799