|
1 | 1 | package com.semmle.js.extractor;
|
2 | 2 |
|
3 | 3 | import java.io.File;
|
| 4 | +import java.io.FileNotFoundException; |
4 | 5 | import java.io.IOException;
|
5 | 6 | import java.io.Reader;
|
6 | 7 | import java.lang.ProcessBuilder.Redirect;
|
7 | 8 | import java.net.URI;
|
8 | 9 | import java.net.URISyntaxException;
|
9 | 10 | import java.nio.charset.StandardCharsets;
|
| 11 | +import java.nio.file.DirectoryNotEmptyException; |
10 | 12 | import java.nio.file.FileVisitResult;
|
11 | 13 | import java.nio.file.FileVisitor;
|
12 | 14 | import java.nio.file.Files;
|
13 | 15 | import java.nio.file.InvalidPathException;
|
| 16 | +import java.nio.file.NoSuchFileException; |
14 | 17 | import java.nio.file.Path;
|
15 | 18 | import java.nio.file.Paths;
|
16 | 19 | import java.nio.file.SimpleFileVisitor;
|
17 | 20 | import java.nio.file.attribute.BasicFileAttributes;
|
18 | 21 | import java.util.ArrayList;
|
19 | 22 | import java.util.Arrays;
|
| 23 | +import java.util.Collections; |
20 | 24 | import java.util.Comparator;
|
21 | 25 | import java.util.LinkedHashMap;
|
22 | 26 | import java.util.LinkedHashSet;
|
|
27 | 31 | import java.util.concurrent.ExecutorService;
|
28 | 32 | import java.util.concurrent.Executors;
|
29 | 33 | import java.util.concurrent.TimeUnit;
|
| 34 | +import java.util.concurrent.atomic.AtomicInteger; |
30 | 35 | import java.util.function.Predicate;
|
31 | 36 | import java.util.stream.Collectors;
|
32 | 37 | import java.util.stream.Stream;
|
|
41 | 46 | import com.semmle.js.extractor.trapcache.DefaultTrapCache;
|
42 | 47 | import com.semmle.js.extractor.trapcache.DummyTrapCache;
|
43 | 48 | import com.semmle.js.extractor.trapcache.ITrapCache;
|
| 49 | +import com.semmle.js.parser.ParseError; |
44 | 50 | import com.semmle.js.parser.ParsedProject;
|
45 | 51 | import com.semmle.ts.extractor.TypeExtractor;
|
46 | 52 | import com.semmle.ts.extractor.TypeScriptParser;
|
| 53 | +import com.semmle.ts.extractor.TypeScriptWrapperOOMError; |
47 | 54 | import com.semmle.ts.extractor.TypeTable;
|
48 | 55 | import com.semmle.util.data.StringUtil;
|
| 56 | +import com.semmle.util.diagnostics.DiagnosticLevel; |
| 57 | +import com.semmle.util.diagnostics.DiagnosticWriter; |
| 58 | +import com.semmle.util.diagnostics.DiagnosticLocation; |
49 | 59 | import com.semmle.util.exception.CatastrophicError;
|
50 | 60 | import com.semmle.util.exception.Exceptions;
|
51 | 61 | import com.semmle.util.exception.ResourceError;
|
@@ -444,33 +454,141 @@ protected boolean hasSeenCode() {
|
444 | 454 |
|
445 | 455 | /** Perform extraction. */
|
446 | 456 | public int run() throws IOException {
|
447 |
| - startThreadPool(); |
448 |
| - try { |
449 |
| - CompletableFuture<?> sourceFuture = extractSource(); |
450 |
| - sourceFuture.join(); // wait for source extraction to complete |
451 |
| - if (hasSeenCode()) { // don't bother with the externs if no code was seen |
452 |
| - extractExterns(); |
| 457 | + startThreadPool(); |
| 458 | + try { |
| 459 | + CompletableFuture<?> sourceFuture = extractSource(); |
| 460 | + sourceFuture.join(); // wait for source extraction to complete |
| 461 | + if (hasSeenCode()) { // don't bother with the externs if no code was seen |
| 462 | + extractExterns(); |
| 463 | + } |
| 464 | + extractXml(); |
| 465 | + } catch (OutOfMemoryError oom) { |
| 466 | + System.err.println("Out of memory while extracting the project."); |
| 467 | + return 137; // the CodeQL CLI will interpret this as an out-of-memory error |
| 468 | + // purpusely not doing anything else (printing stack, etc.), as the JVM |
| 469 | + // basically guarantees nothing after an OOM |
| 470 | + } catch (TypeScriptWrapperOOMError oom) { |
| 471 | + System.err.println("Out of memory while extracting the project."); |
| 472 | + System.err.println(oom.getMessage()); |
| 473 | + oom.printStackTrace(System.err); |
| 474 | + return 137; |
| 475 | + } catch (RuntimeException | IOException e) { |
| 476 | + writeDiagnostics("Internal error: " + e, JSDiagnosticKind.INTERNAL_ERROR); |
| 477 | + e.printStackTrace(System.err); |
| 478 | + return 1; |
| 479 | + } finally { |
| 480 | + shutdownThreadPool(); |
| 481 | + diagnosticsToClose.forEach(DiagnosticWriter::close); |
453 | 482 | }
|
454 |
| - extractXml(); |
455 |
| - } finally { |
456 |
| - shutdownThreadPool(); |
| 483 | + |
| 484 | + if (!hasSeenCode()) { |
| 485 | + if (seenFiles) { |
| 486 | + warn("Only found JavaScript or TypeScript files that were empty or contained syntax errors."); |
| 487 | + } else { |
| 488 | + warn("No JavaScript or TypeScript code found."); |
| 489 | + } |
| 490 | + // ensuring that the finalize steps detects that no code was seen. |
| 491 | + Path srcFolder = Paths.get(EnvironmentVariables.getWipDatabase(), "src"); |
| 492 | + try { |
| 493 | + // Non-recursive delete because "src/" should be empty. |
| 494 | + FileUtil8.delete(srcFolder); |
| 495 | + } catch (NoSuchFileException e) { |
| 496 | + Exceptions.ignore(e, "the directory did not exist"); |
| 497 | + } catch (DirectoryNotEmptyException e) { |
| 498 | + Exceptions.ignore(e, "just leave the directory if it is not empty"); |
| 499 | + } |
| 500 | + return 0; |
| 501 | + } |
| 502 | + return 0; |
| 503 | + } |
| 504 | + |
| 505 | + /** |
| 506 | + * A kind of error that can happen during extraction of JavaScript or TypeScript |
| 507 | + * code. |
| 508 | + * For use with the {@link #writeDiagnostics(String, JSDiagnosticKind)} method. |
| 509 | + */ |
| 510 | + public static enum JSDiagnosticKind { |
| 511 | + PARSE_ERROR("parse-error", "Parse error", DiagnosticLevel.Warning), |
| 512 | + INTERNAL_ERROR("internal-error", "Internal error", DiagnosticLevel.Debug); |
| 513 | + |
| 514 | + private final String id; |
| 515 | + private final String name; |
| 516 | + private final DiagnosticLevel level; |
| 517 | + |
| 518 | + private JSDiagnosticKind(String id, String name, DiagnosticLevel level) { |
| 519 | + this.id = id; |
| 520 | + this.name = name; |
| 521 | + this.level = level; |
| 522 | + } |
| 523 | + |
| 524 | + public String getId() { |
| 525 | + return id; |
| 526 | + } |
| 527 | + |
| 528 | + public String getName() { |
| 529 | + return name; |
| 530 | + } |
| 531 | + |
| 532 | + public DiagnosticLevel getLevel() { |
| 533 | + return level; |
457 | 534 | }
|
458 |
| - if (!hasSeenCode()) { |
459 |
| - if (seenFiles) { |
460 |
| - warn("Only found JavaScript or TypeScript files that were empty or contained syntax errors."); |
| 535 | + } |
| 536 | + |
| 537 | + private AtomicInteger diagnosticCount = new AtomicInteger(0); |
| 538 | + private List<DiagnosticWriter> diagnosticsToClose = Collections.synchronizedList(new ArrayList<>()); |
| 539 | + private ThreadLocal<DiagnosticWriter> diagnostics = new ThreadLocal<DiagnosticWriter>(){ |
| 540 | + @Override protected DiagnosticWriter initialValue() { |
| 541 | + DiagnosticWriter result = initDiagnosticsWriter(diagnosticCount.incrementAndGet()); |
| 542 | + diagnosticsToClose.add(result); |
| 543 | + return result; |
| 544 | + } |
| 545 | + }; |
| 546 | + |
| 547 | + /** |
| 548 | + * Persist a diagnostic message to a file in the diagnostics directory. |
| 549 | + * See {@link JSDiagnosticKind} for the kinds of errors that can be reported, |
| 550 | + * and see |
| 551 | + * {@link DiagnosticWriter} for more details. |
| 552 | + */ |
| 553 | + public void writeDiagnostics(String message, JSDiagnosticKind error) throws IOException { |
| 554 | + writeDiagnostics(message, error, null); |
| 555 | + } |
| 556 | + |
| 557 | + |
| 558 | + /** |
| 559 | + * Persist a diagnostic message with a location to a file in the diagnostics directory. |
| 560 | + * See {@link JSDiagnosticKind} for the kinds of errors that can be reported, |
| 561 | + * and see |
| 562 | + * {@link DiagnosticWriter} for more details. |
| 563 | + */ |
| 564 | + public void writeDiagnostics(String message, JSDiagnosticKind error, DiagnosticLocation location) throws IOException { |
| 565 | + if (diagnostics.get() == null) { |
| 566 | + warn("No diagnostics directory, so not writing diagnostic: " + message); |
| 567 | + return; |
| 568 | + } |
| 569 | + |
| 570 | + // DiagnosticLevel level, String extractorName, String sourceId, String sourceName, String markdown |
| 571 | + diagnostics.get().writeMarkdown(error.getLevel(), "javascript", "javascript/" + error.getId(), error.getName(), |
| 572 | + message, location); |
| 573 | + } |
| 574 | + |
| 575 | + private DiagnosticWriter initDiagnosticsWriter(int count) { |
| 576 | + String diagnosticsDir = System.getenv("CODEQL_EXTRACTOR_JAVASCRIPT_DIAGNOSTIC_DIR"); |
| 577 | + |
| 578 | + if (diagnosticsDir != null) { |
| 579 | + File diagnosticsDirFile = new File(diagnosticsDir); |
| 580 | + if (!diagnosticsDirFile.isDirectory()) { |
| 581 | + warn("Diagnostics directory " + diagnosticsDir + " does not exist"); |
461 | 582 | } else {
|
462 |
| - warn("No JavaScript or TypeScript code found."); |
463 |
| - } |
464 |
| - // ensuring that the finalize steps detects that no code was seen. |
465 |
| - Path srcFolder = Paths.get(EnvironmentVariables.getWipDatabase(), "src"); |
466 |
| - // check that the srcFolder is empty |
467 |
| - if (Files.list(srcFolder).count() == 0) { |
468 |
| - // Non-recursive delete because "src/" should be empty. |
469 |
| - FileUtil8.delete(srcFolder); |
| 583 | + File diagnosticsFile = new File(diagnosticsDirFile, "autobuilder-" + count + ".jsonl"); |
| 584 | + try { |
| 585 | + return new DiagnosticWriter(diagnosticsFile); |
| 586 | + } catch (FileNotFoundException e) { |
| 587 | + warn("Failed to open diagnostics file " + diagnosticsFile); |
| 588 | + } |
470 | 589 | }
|
471 |
| - return 0; |
472 | 590 | }
|
473 |
| - return 0; |
| 591 | + return null; |
474 | 592 | }
|
475 | 593 |
|
476 | 594 | private void startThreadPool() {
|
@@ -1113,13 +1231,38 @@ private void doExtract(FileExtractor extractor, Path file, ExtractorState state)
|
1113 | 1231 |
|
1114 | 1232 | try {
|
1115 | 1233 | long start = logBeginProcess("Extracting " + file);
|
1116 |
| - Integer loc = extractor.extract(f, state); |
1117 |
| - if (!extractor.getConfig().isExterns() && (loc == null || loc != 0)) seenCode = true; |
| 1234 | + ParseResultInfo loc = extractor.extract(f, state); |
| 1235 | + if (!extractor.getConfig().isExterns() && (loc == null || loc.getLinesOfCode() != 0)) seenCode = true; |
1118 | 1236 | if (!extractor.getConfig().isExterns()) seenFiles = true;
|
| 1237 | + for (ParseError err : loc.getParseErrors()) { |
| 1238 | + String msg = "A parse error occurred: " + StringUtil.escapeMarkdown(err.getMessage()) |
| 1239 | + + ". Check the syntax of the file. If the file is invalid, correct the error or [exclude](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning) the file from analysis."; |
| 1240 | + // file, relative to the source root |
| 1241 | + String relativeFilePath = null; |
| 1242 | + if (file.startsWith(LGTM_SRC)) { |
| 1243 | + relativeFilePath = file.subpath(LGTM_SRC.getNameCount(), file.getNameCount()).toString(); |
| 1244 | + } |
| 1245 | + DiagnosticLocation diagLoc = DiagnosticLocation.builder() |
| 1246 | + .setFile(relativeFilePath) |
| 1247 | + .setStartLine(err.getPosition().getLine()) |
| 1248 | + .setStartColumn(err.getPosition().getColumn()) |
| 1249 | + .setEndLine(err.getPosition().getLine()) |
| 1250 | + .setEndColumn(err.getPosition().getColumn()) |
| 1251 | + .build(); |
| 1252 | + writeDiagnostics(msg, JSDiagnosticKind.PARSE_ERROR, diagLoc); |
| 1253 | + } |
1119 | 1254 | logEndProcess(start, "Done extracting " + file);
|
| 1255 | + } catch (OutOfMemoryError oom) { |
| 1256 | + System.err.println("Out of memory while extracting a file."); |
| 1257 | + System.exit(137); // caught by the CodeQL CLI |
1120 | 1258 | } catch (Throwable t) {
|
1121 | 1259 | System.err.println("Exception while extracting " + file + ".");
|
1122 | 1260 | t.printStackTrace(System.err);
|
| 1261 | + try { |
| 1262 | + writeDiagnostics("Internal error: " + t, JSDiagnosticKind.INTERNAL_ERROR); |
| 1263 | + } catch (IOException ignored) { |
| 1264 | + Exceptions.ignore(ignored, "we are already crashing"); |
| 1265 | + } |
1123 | 1266 | System.exit(1);
|
1124 | 1267 | }
|
1125 | 1268 | }
|
|
0 commit comments