1 /*
2 * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3 * SPDX-License-Identifier: MIT
4 */
5 package com.qulice.errorprone;
6
7 import com.qulice.spi.Violation;
8 import java.util.ArrayList;
9 import java.util.Collection;
10 import java.util.List;
11 import java.util.Optional;
12 import java.util.regex.Matcher;
13 import java.util.regex.Pattern;
14
15 /**
16 * What a forked {@code javac} printed, read as violations.
17 *
18 * <p>Every diagnostic counts, not just the ones ErrorProne labels with
19 * its {@code [CheckName]} prefix: a plain {@code cannot find symbol} is
20 * the loudest signal of all, because {@code javac} stops before the
21 * analysis phase once it sees an error and ErrorProne then reports
22 * nothing at all. Lines that carry no source position, such as
23 * {@code error: warnings found and -Werror specified}, are blamed on the
24 * project itself.</p>
25 *
26 * @since 1.0
27 */
28 final class Diagnostics {
29
30 /**
31 * Standard {@code javac} diagnostic format,
32 * {@code path:line: warning|error: body}, where the body optionally
33 * opens with the {@code [Name]} prefix that ErrorProne puts on its
34 * findings and {@code javac} puts on its lint categories.
35 */
36 private static final Pattern POSITIONED = Pattern.compile(
37 "^(.+?):(\\d+): (?:warning|error): (?:\\[([A-Za-z][A-Za-z0-9_]*)] )?(.+)$"
38 );
39
40 /**
41 * A {@code javac} diagnostic that names no source position.
42 */
43 private static final Pattern GLOBAL = Pattern.compile(
44 "^(?:warning|error): (?:\\[([A-Za-z][A-Za-z0-9_]*)] )?(.+)$"
45 );
46
47 /**
48 * Name of the validator to attribute the violations to.
49 */
50 private final String validator;
51
52 /**
53 * File to blame for the diagnostics that name no source position.
54 */
55 private final String fallback;
56
57 /**
58 * Constructor.
59 *
60 * @param validator Name of the validator reporting these diagnostics
61 * @param fallback File to blame when a diagnostic names no position
62 */
63 Diagnostics(final String validator, final String fallback) {
64 this.validator = validator;
65 this.fallback = fallback;
66 }
67
68 /**
69 * Read the diagnostics out of the compiler's output.
70 *
71 * @param output Combined stdout/stderr of the forked process
72 * @return Violations, one per diagnostic line
73 */
74 Collection<Violation> violations(final List<String> output) {
75 final Collection<Violation> violations = new ArrayList<>(0);
76 for (final String line : output) {
77 final Matcher positioned = Diagnostics.POSITIONED.matcher(line);
78 final Matcher global = Diagnostics.GLOBAL.matcher(line);
79 if (positioned.matches()) {
80 violations.add(
81 this.violation(
82 positioned.group(1),
83 positioned.group(2),
84 Diagnostics.check(positioned.group(3)),
85 positioned.group(4)
86 )
87 );
88 } else if (global.matches()) {
89 violations.add(
90 this.violation(
91 this.fallback,
92 "0",
93 Diagnostics.check(global.group(1)),
94 global.group(2)
95 )
96 );
97 }
98 }
99 return violations;
100 }
101
102 private Violation violation(final String file, final String lines,
103 final String check, final String message) {
104 return new Violation.Default(
105 this.validator,
106 check,
107 file,
108 lines,
109 String.format("[%s] %s", check, message)
110 );
111 }
112
113 private static String check(final String name) {
114 return Optional.ofNullable(name).orElse("javac");
115 }
116 }