package org.checkerframework.framework.test.diagnostics; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import org.checkerframework.javacutil.Pair; /** A set of utilities and factory methods useful for working with TestDiagnostics */ public class TestDiagnosticUtils { // this regex represents how the diagnostics appear in Java source files public static final String DIAGNOSTIC_IN_JAVA_REGEX = "\\s*(error|fixable-error|warning|other):\\s*(\\(?.*\\)?)\\s*"; public static final Pattern DIAGNOSTIC_IN_JAVA_PATTERN = Pattern.compile(DIAGNOSTIC_IN_JAVA_REGEX); public static final String DIAGNOSTIC_WARNING_IN_JAVA_REGEX = "\\s*warning:\\s*(.*\\s*.*)\\s*"; public static final Pattern DIAGNOSTIC_WARNING_IN_JAVA_PATTERN = Pattern.compile(DIAGNOSTIC_WARNING_IN_JAVA_REGEX); // this regex represents how the diagnostics appear in javax tools diagnostics from the compiler public static final String DIAGNOSTIC_REGEX = ":(\\d+):" + DIAGNOSTIC_IN_JAVA_REGEX; public static final Pattern DIAGNOSTIC_PATTERN = Pattern.compile(DIAGNOSTIC_REGEX); public static final String DIAGNOSTIC_WARNING_REGEX = ":(\\d+):" + DIAGNOSTIC_WARNING_IN_JAVA_REGEX; public static final Pattern DIAGNOSTIC_WARNING_PATTERN = Pattern.compile(DIAGNOSTIC_WARNING_REGEX); // represents how the diagnostics appearn in diagnostic files (.out) public static final String DIAGNOSTIC_FILE_REGEX = ".+\\.java" + DIAGNOSTIC_REGEX; public static final Pattern DIAGNOSTIC_FILE_PATTERN = Pattern.compile(DIAGNOSTIC_FILE_REGEX); public static final String DIAGNOSTIC_FILE_WARNING_REGEX = ".+\\.java" + DIAGNOSTIC_WARNING_REGEX; public static final Pattern DIAGNOSTIC_FILE_WARNING_PATTERN = Pattern.compile(DIAGNOSTIC_FILE_WARNING_REGEX); /** * Instantiate the diagnostic based on a string that would appear in diagnostic files (i.e. * files that only contain line after line of expected diagnostics) * * @param stringFromDiagnosticFile a single diagnostic string to parse */ public static TestDiagnostic fromDiagnosticFileString(String stringFromDiagnosticFile) { return fromPatternMatching( DIAGNOSTIC_FILE_PATTERN, DIAGNOSTIC_WARNING_IN_JAVA_PATTERN, "", null, stringFromDiagnosticFile); } /** * Instantiate the diagnostic from a string that would appear in a Java file, e.g.: "error: * (message)" * * @param lineNumber the lineNumber of the line immediately below the diagnostic comment in the * Java file * @param stringFromjavaFile the string containing the diagnostic */ public static TestDiagnostic fromJavaFileComment( String filename, long lineNumber, String stringFromjavaFile) { return fromPatternMatching( DIAGNOSTIC_IN_JAVA_PATTERN, DIAGNOSTIC_WARNING_IN_JAVA_PATTERN, filename, lineNumber, stringFromjavaFile); } /** * Instantiate a diagnostic using a diagnostic from the Java Compiler. The resulting diagnostic * is never fixable and always has parentheses */ public static TestDiagnostic fromJavaxToolsDiagnostic( String diagnosticString, boolean noMsgText) { // It would be nice not to parse this from the diagnostic string // however, the interface provides no way to know when an [unchecked] or similar // message is added to the reported error. That is, when doing diagnostic.toString // the message may contain an [unchecked] even though getMessage does not report one // Since we want to match the error messages reported by javac exactly, we must parse Pair<String, String> trimmed = formatJavaxToolString(diagnosticString, noMsgText); return fromPatternMatching( DIAGNOSTIC_PATTERN, DIAGNOSTIC_WARNING_PATTERN, trimmed.second, null, trimmed.first); } static Pair<Boolean, String> dropParentheses(final String str) { if (str.charAt(0) == '(' && str.charAt(str.length() - 1) == ')') { return Pair.of(true, str.substring(1, str.length() - 1)); } return Pair.of(false, str); } protected static TestDiagnostic fromPatternMatching( Pattern diagnosticPattern, Pattern warningPattern, String filename, Long lineNumber, String diagnosticString) { final DiagnosticKind kind; final String message; final boolean isFixable; final boolean noParentheses; long lineNo = -1; int groupOffset = 1; if (lineNumber != null) { lineNo = lineNumber; groupOffset = 0; } Matcher diagnosticMatcher = diagnosticPattern.matcher(diagnosticString); if (diagnosticMatcher.matches()) { Pair<DiagnosticKind, Boolean> categoryToFixable = parseCategoryString(diagnosticMatcher.group(1 + groupOffset)); kind = categoryToFixable.first; isFixable = categoryToFixable.second; Pair<Boolean, String> dropQuotesToString = dropParentheses(diagnosticMatcher.group(2 + groupOffset).trim()); message = dropQuotesToString.second; noParentheses = !dropQuotesToString.first; if (lineNumber == null) { lineNo = Long.parseLong(diagnosticMatcher.group(1)); } } else { Matcher warningMatcher = warningPattern.matcher(diagnosticString); if (warningMatcher.matches()) { kind = DiagnosticKind.Warning; isFixable = false; message = warningMatcher.group(1 + groupOffset); noParentheses = true; if (lineNumber == null) { lineNo = Long.parseLong(diagnosticMatcher.group(1)); } } else if (diagnosticString.startsWith("warning:")) { kind = DiagnosticKind.Warning; isFixable = false; message = diagnosticString.substring("warning:".length()).trim(); noParentheses = true; if (lineNumber != null) { lineNo = lineNumber; } else { lineNo = 0; } } else { kind = DiagnosticKind.Other; isFixable = false; message = diagnosticString; noParentheses = true; // this should only happen if we are parsing a Java Diagnostic from the compiler // that we did do not handle if (lineNumber == null) { lineNo = -1; } } } return new TestDiagnostic(filename, lineNo, kind, message, isFixable, noParentheses); } public static Pair<String, String> formatJavaxToolString(String original, boolean noMsgText) { String trimmed = original; String filename = ""; if (noMsgText) { // Only keep the first line of the error or warning, unless it is a thrown exception // "unexpected Throwable" or it is an Checker Error (contains "Compilation unit"). if (!trimmed.contains("unexpected Throwable") && !trimmed.contains("Compilation unit")) { if (trimmed.contains("\n")) { trimmed = trimmed.substring(0, trimmed.indexOf('\n')); } if (trimmed.contains(".java:")) { int start = trimmed.lastIndexOf(File.separator); filename = trimmed.substring(start + 1, trimmed.indexOf(".java:") + 5).trim(); trimmed = trimmed.substring(trimmed.indexOf(".java:") + 5).trim(); } } } return Pair.of(trimmed, filename); } /** * Given a category string that may be prepended with "fixable-", return the category enum that * corresponds with the category and whether or not it is a isFixable error */ private static Pair<DiagnosticKind, Boolean> parseCategoryString(String category) { final String fixable = "fixable-"; final boolean isFixable = category.startsWith(fixable); if (isFixable) { category = category.substring(fixable.length()); } DiagnosticKind categoryEnum = DiagnosticKind.fromParseString(category); return Pair.of(categoryEnum, isFixable); } /** Convert a line in a JavaSource file to a (possibly empty) TestDiagnosticLine */ public static TestDiagnosticLine fromJavaSourceLine( String filename, String originalLine, long lineNumber) { final String trimmedLine = originalLine.trim(); long errorLine = lineNumber + 1; if (trimmedLine.startsWith("//::")) { String restOfLine = trimmedLine.substring(4); // drop the //:: String[] diagnosticStrs = restOfLine.split("::"); List<TestDiagnostic> diagnostics = new ArrayList<>(diagnosticStrs.length); for (String diagnostic : diagnosticStrs) { diagnostics.add(fromJavaFileComment(filename, errorLine, diagnostic)); } return new TestDiagnosticLine( filename, errorLine, originalLine, Collections.unmodifiableList(diagnostics)); } else if (trimmedLine.startsWith("//warning:")) { // This special diagnostic does not expect a line number nor a file name String diagnosticString = trimmedLine.substring(2); TestDiagnostic diagnostic = fromJavaFileComment("", 0, diagnosticString); return new TestDiagnosticLine( "", 0, originalLine, Collections.singletonList(diagnostic)); } else { return new TestDiagnosticLine(filename, errorLine, originalLine, EMPTY); } } /** Convert a line in a DiagnosticFile to a TestDiagnosticLine */ public static TestDiagnosticLine fromDiagnosticFileLine(String diagnosticLine) { final String trimmedLine = diagnosticLine.trim(); if (trimmedLine.startsWith("#") || trimmedLine.isEmpty()) { return new TestDiagnosticLine("", -1, diagnosticLine, EMPTY); } TestDiagnostic diagnostic = fromDiagnosticFileString(diagnosticLine); return new TestDiagnosticLine( "", diagnostic.getLineNumber(), diagnosticLine, Arrays.asList(diagnostic)); } public static Set<TestDiagnostic> fromJavaxDiagnosticList( List<Diagnostic<? extends JavaFileObject>> javaxDiagnostics, boolean noMsgText) { Set<TestDiagnostic> diagnostics = new LinkedHashSet<>(javaxDiagnostics.size()); for (Diagnostic<? extends JavaFileObject> diagnostic : javaxDiagnostics) { // See TestDiagnosticUtils as to why we use diagnostic.toString rather // than convert from the diagnostic itself final String diagnosticString = diagnostic.toString(); // suppress Xlint warnings if (diagnosticString.contains("uses unchecked or unsafe operations.") || diagnosticString.contains("Recompile with -Xlint:unchecked for details.") || diagnosticString.endsWith(" declares unsafe vararg methods.") || diagnosticString.contains("Recompile with -Xlint:varargs for details.")) continue; diagnostics.add( TestDiagnosticUtils.fromJavaxToolsDiagnostic(diagnosticString, noMsgText)); } return diagnostics; } /** * Converts the given diagnostics to strings (as they would appear in a source file * individually) */ public static List<String> diagnosticsToString(List<TestDiagnostic> diagnostics) { final List<String> strings = new ArrayList<String>(diagnostics.size()); for (TestDiagnostic diagnostic : diagnostics) { strings.add(diagnostic.toString()); } return strings; } private static final List<TestDiagnostic> EMPTY = Collections.unmodifiableList(new ArrayList<TestDiagnostic>()); public static void removeDiagnosticsOfKind( DiagnosticKind kind, List<TestDiagnostic> expectedDiagnostics) { for (int i = 0; i < expectedDiagnostics.size(); /*no-increment*/ ) { if (expectedDiagnostics.get(i).getKind() == kind) { expectedDiagnostics.remove(i); } else { ++i; } } } }