Skip to content

Commit 79f2de5

Browse files
Closure Teamcopybara-github
authored andcommitted
Implement JSCompiler expected diagnostics support in runners.
PiperOrigin-RevId: 897704570
1 parent 030e0fd commit 79f2de5

File tree

4 files changed

+252
-2
lines changed

4 files changed

+252
-2
lines changed

src/com/google/javascript/jscomp/AbstractCommandLineRunner.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ public abstract class AbstractCommandLineRunner<A extends Compiler, B extends Co
143143
"When using --chunk or --module flags, the --create_source_map flag must contain "
144144
+ "%outname% in the value.");
145145

146+
static final DiagnosticType EXPECTED_DIAGNOSTIC_NOT_FOUND =
147+
DiagnosticType.error(
148+
"JSC_EXPECTED_DIAGNOSTIC_NOT_FOUND", "Expected diagnostic not found: {0}");
149+
150+
static final DiagnosticType AMBIGUOUS_EXPECTATION =
151+
DiagnosticType.error(
152+
"JSC_AMBIGUOUS_EXPECTATION",
153+
"Multiple expected diagnostics matched the error: \"{0}\". Matches: \"{1}\"");
154+
146155
static final String WAITING_FOR_INPUT_WARNING = "The compiler is waiting for input via stdin.";
147156
// Use an 8MiB buffer since the concatenated TypedAst file can be very large.
148157
private static final int GZIPPED_TYPEDAST_BUFFER_SIZE = 8 * 1024 * 1024;
@@ -1148,6 +1157,10 @@ protected int doRun() throws IOException {
11481157
Compiler.setLoggingLevel(Level.parse(config.loggingLevel));
11491158

11501159
compiler = createCompiler();
1160+
if (!config.expectedDiagnostics.isEmpty()) {
1161+
compiler.setErrorManager(
1162+
new VerifyingErrorManager(compiler.getErrorManager(), config.expectedDiagnostics));
1163+
}
11511164
B options = createOptions();
11521165
setRunOptions(options);
11531166

@@ -1271,7 +1284,7 @@ protected int doRun() throws IOException {
12711284
try {
12721285
// This is the common case - we're actually compiling something.
12731286
performCompilation(metricsRecorder);
1274-
result = compiler.getResult();
1287+
12751288
// If we're finished with compilation (i.e. we're not saving state), /and/ the compiler was
12761289
// restored from a previous state, then we need to re-initialize the set of chunks.
12771290
// TODO(lharker): figure out if this is still needed.
@@ -1287,6 +1300,7 @@ protected int doRun() throws IOException {
12871300
// exception somewhere.
12881301
compiler.generateReport();
12891302
}
1303+
result = compiler.getResult();
12901304
}
12911305

12921306
if (createCommonJsModules) {
@@ -1444,7 +1458,7 @@ private void initializeStateBeforeCompilation() {
14441458
} else {
14451459
// parsing!
14461460
compiler.parseForCompilation();
1447-
}
1461+
}
14481462
}
14491463

14501464
private void restoreState(String filename) {
@@ -2577,6 +2591,19 @@ public CommandLineConfig setParseInlineSourceMaps(boolean parseInlineSourceMaps)
25772591
return this;
25782592
}
25792593

2594+
private final List<String> expectedDiagnostics = new ArrayList<>();
2595+
2596+
/**
2597+
* Expected diagnostics in the format [(line,col)]CODE:regex. CODE is the JSCompiler
2598+
* DiagnosticType key. regex matches the description.
2599+
*/
2600+
@CanIgnoreReturnValue
2601+
public CommandLineConfig setExpectedDiagnostics(List<String> expectedDiagnostics) {
2602+
this.expectedDiagnostics.clear();
2603+
this.expectedDiagnostics.addAll(expectedDiagnostics);
2604+
return this;
2605+
}
2606+
25802607
private String variableMapInputFile = "";
25812608

25822609
/**

src/com/google/javascript/jscomp/CommandLineRunner.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,13 @@ private static class Flags {
951951
+ "renaming map produced by a previous compilation")
952952
private String propertyMapInputFile = "";
953953

954+
@Option(
955+
name = "--expected_diagnostics",
956+
usage =
957+
"Expected diagnostics in the format [(line,col)]CODE:regex. "
958+
+ "CODE is the JSCompiler DiagnosticType key. regex matches the description.")
959+
private List<String> expectedDiagnostics = new ArrayList<>();
960+
954961
@Argument private List<String> arguments = new ArrayList<>();
955962
private final CmdLineParser parser;
956963

@@ -1738,6 +1745,7 @@ private void initConfigFromFlags(String[] args, PrintStream out, PrintStream err
17381745
.setVariableMapOutputFile(flags.variableMapOutputFile)
17391746
.setCreateNameMapFiles(flags.createNameMapFiles)
17401747
.setPropertyMapOutputFile(flags.propertyMapOutputFile)
1748+
.setExpectedDiagnostics(flags.expectedDiagnostics)
17411749
.setPropertyMapInputFile(flags.propertyMapInputFile)
17421750
.setVariableMapInputFile(flags.variableMapInputFile)
17431751
.setInstrumentationMappingFile(flags.instrumentationMappingOutputFile)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2026 The Closure Compiler Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.javascript.jscomp;
18+
19+
import java.util.ArrayList;
20+
import java.util.Iterator;
21+
import java.util.List;
22+
import java.util.regex.Pattern;
23+
24+
/**
25+
* An ErrorManager that verifies expected diagnostics and reports missing or unexpected ones.
26+
*
27+
* <p>It wraps a delegate ErrorManager. Expected diagnostics (passed as regexes) are swallowed if
28+
* matched. Unexpected diagnostics are passed to the delegate. At the end of compilation (when
29+
* generateReport is called), any unmatched expectations are reported as errors to the delegate.
30+
*/
31+
public final class VerifyingErrorManager extends ThreadSafeDelegatingErrorManager {
32+
33+
private final List<Pattern> unmatchedExpectations;
34+
private final LightweightMessageFormatter formatter;
35+
36+
public VerifyingErrorManager(ErrorManager delegate, List<String> expectedDiagnostics) {
37+
super(delegate);
38+
this.formatter = LightweightMessageFormatter.withoutSource();
39+
this.unmatchedExpectations = new ArrayList<>();
40+
for (String exp : expectedDiagnostics) {
41+
this.unmatchedExpectations.add(Pattern.compile(exp));
42+
}
43+
}
44+
45+
@Override
46+
public synchronized void report(CheckLevel level, JSError error) {
47+
String formattedError = formatErrorForMatching(level, error);
48+
49+
Pattern matchedPattern = null;
50+
Iterator<Pattern> iterator = unmatchedExpectations.iterator();
51+
while (iterator.hasNext()) {
52+
Pattern p = iterator.next();
53+
if (p.matcher(formattedError).find()) {
54+
if (matchedPattern == null) {
55+
matchedPattern = p;
56+
iterator.remove();
57+
continue;
58+
}
59+
60+
if (matchedPattern.toString().equals(p.toString())) {
61+
// Allow duplicates that are identical.
62+
continue;
63+
}
64+
65+
super.report(
66+
CheckLevel.ERROR,
67+
JSError.make(
68+
AbstractCommandLineRunner.AMBIGUOUS_EXPECTATION,
69+
formattedError,
70+
matchedPattern.toString(),
71+
p.toString()));
72+
iterator.remove();
73+
}
74+
}
75+
76+
if (matchedPattern == null) {
77+
super.report(level, error);
78+
}
79+
}
80+
81+
@Override
82+
public synchronized void generateReport() {
83+
reportMissingExpectations();
84+
super.generateReport();
85+
}
86+
87+
private synchronized void reportMissingExpectations() {
88+
for (Pattern p : unmatchedExpectations) {
89+
super.report(
90+
CheckLevel.ERROR,
91+
JSError.make(AbstractCommandLineRunner.EXPECTED_DIAGNOSTIC_NOT_FOUND, p.toString()));
92+
}
93+
unmatchedExpectations.clear();
94+
}
95+
96+
private String formatErrorForMatching(CheckLevel level, JSError error) {
97+
return level == CheckLevel.ERROR
98+
? formatter.formatError(error)
99+
: formatter.formatWarning(error);
100+
}
101+
}

test/com/google/javascript/jscomp/CommandLineRunnerTest.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.common.collect.ImmutableSet;
3434
import com.google.common.collect.Iterables;
3535
import com.google.common.io.Files;
36+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3637
import com.google.gson.Gson;
3738
import com.google.gson.JsonArray;
3839
import com.google.gson.JsonObject;
@@ -3616,6 +3617,100 @@ public void testTranspileOnlyModeSyntaxError() {
36163617
test("const x = {", RhinoErrorReporter.PARSE_ERROR);
36173618
}
36183619

3620+
@Test
3621+
public void testExpectedDiagnostics_matchErrorAndWarning() {
3622+
args.add("--jscomp_error=checkTypes");
3623+
args.add("--jscomp_warning=uselessCode");
3624+
ImmutableList<JSError> actualErrors =
3625+
testExpectedDiagnostics(
3626+
"/** @type {string} */ var x = 1; var y = 1; y;",
3627+
new String[] {
3628+
"""
3629+
input0:1:30: ERROR - \\[JSC_TYPE_MISMATCH\\] initializing variable
3630+
found : number
3631+
required: string\
3632+
""",
3633+
"input0:1:44: WARNING - \\[JSC_USELESS_CODE\\] Suspicious code. This code lacks"
3634+
+ " side-effects. Is there a bug?",
3635+
});
3636+
assertThat(actualErrors).isEmpty();
3637+
}
3638+
3639+
@Test
3640+
public void testExpectedDiagnostics_partialMatch() {
3641+
args.add("--jscomp_error=checkTypes");
3642+
ImmutableList<JSError> actualErrors =
3643+
testExpectedDiagnostics(
3644+
"/** @type {string} */ var x = 1;", new String[] {"JSC_TYPE_MISMATCH"});
3645+
assertThat(actualErrors).isEmpty();
3646+
}
3647+
3648+
@Test
3649+
public void testExpectedDiagnostics_regexMatch() {
3650+
args.add("--jscomp_error=checkTypes");
3651+
ImmutableList<JSError> actualErrors =
3652+
testExpectedDiagnostics(
3653+
"/** @type {string} */ var x = 1;",
3654+
new String[] {"input0:.* ERROR - \\[JSC_TYPE_MISMATCH\\] .*"});
3655+
assertThat(actualErrors).isEmpty();
3656+
}
3657+
3658+
@Test
3659+
public void testExpectedDiagnostics_unmatchedExpectation() {
3660+
args.add("--jscomp_error=checkTypes");
3661+
ImmutableList<JSError> actualErrors =
3662+
testExpectedDiagnostics(
3663+
"/** @type {string} */ var x = 1;", new String[] {".*JSC_OTHER_ERROR.*"});
3664+
assertThat(actualErrors).hasSize(2);
3665+
assertError(actualErrors.get(0))
3666+
.hasType(AbstractCommandLineRunner.EXPECTED_DIAGNOSTIC_NOT_FOUND);
3667+
assertError(actualErrors.get(0))
3668+
.hasMessage("Expected diagnostic not found: .*JSC_OTHER_ERROR.*");
3669+
assertError(actualErrors.get(1)).hasType(TypeValidator.TYPE_MISMATCH_WARNING);
3670+
}
3671+
3672+
@Test
3673+
public void testExpectedDiagnostics_ambiguousMatch() {
3674+
args.add("--jscomp_error=checkTypes");
3675+
ImmutableList<JSError> actualErrors =
3676+
testExpectedDiagnostics(
3677+
"/** @type {string} */ var x = 1;",
3678+
new String[] {".*JSC_TYPE_MISMATCH.*", ".*initializing variable.*"});
3679+
assertThat(actualErrors).hasSize(1);
3680+
assertError(actualErrors.get(0)).hasType(AbstractCommandLineRunner.AMBIGUOUS_EXPECTATION);
3681+
assertError(actualErrors.get(0))
3682+
.hasMessage(
3683+
"Multiple expected diagnostics matched the error: \"input0:1:30: ERROR -"
3684+
+ " [JSC_TYPE_MISMATCH] initializing variable\n"
3685+
+ "found : number\n"
3686+
+ "required: string\n"
3687+
+ "\". Matches: \".*JSC_TYPE_MISMATCH.*\"");
3688+
}
3689+
3690+
@Test
3691+
public void testExpectedDiagnostics_identicalExpectations() {
3692+
args.add("--jscomp_error=checkTypes");
3693+
ImmutableList<JSError> actualErrors =
3694+
testExpectedDiagnostics(
3695+
"/** @type {string} */ var x = 1; /** @type {number} */ var y = 'a';",
3696+
new String[] {".*JSC_TYPE_MISMATCH.*", ".*JSC_TYPE_MISMATCH.*"});
3697+
assertThat(actualErrors).isEmpty();
3698+
}
3699+
3700+
@Test
3701+
public void testExpectedDiagnostics_singleRegexMultipleActuals() {
3702+
args.add("--jscomp_error=checkTypes");
3703+
3704+
ImmutableList<JSError> actualErrors =
3705+
testExpectedDiagnostics(
3706+
"/** @type {string} */ var x = 1; /** @type {number} */ var y = 'a';",
3707+
new String[] {".*JSC_TYPE_MISMATCH.*"});
3708+
assertThat(actualErrors).hasSize(1);
3709+
assertError(actualErrors.get(0)).hasType(TypeValidator.TYPE_MISMATCH_WARNING);
3710+
assertError(actualErrors.get(0))
3711+
.hasMessage("initializing variable\nfound : string\nrequired: number");
3712+
}
3713+
36193714
/* Helper functions */
36203715

36213716
private void testSame(String original) {
@@ -3714,6 +3809,7 @@ private void test(String[] original, DiagnosticType warning) {
37143809
}
37153810
}
37163811

3812+
37173813
private CommandLineRunner createCommandLineRunner(String[] original) {
37183814
for (int i = 0; i < original.length; i++) {
37193815
args.add("--js");
@@ -3884,6 +3980,24 @@ private Compiler compile(String[] original) {
38843980
return lastCompiler;
38853981
}
38863982

3983+
@CanIgnoreReturnValue
3984+
private ImmutableList<JSError> testExpectedDiagnostics(
3985+
String input, String[] expectedDiagnostics) {
3986+
for (String diag : expectedDiagnostics) {
3987+
args.add("--expected_diagnostics=" + diag);
3988+
}
3989+
compile(new String[] {input});
3990+
3991+
Compiler compiler = lastCompiler;
3992+
ImmutableList<JSError> actualDiagnostics =
3993+
ImmutableList.<JSError>builder()
3994+
.addAll(compiler.getErrors())
3995+
.addAll(compiler.getWarnings())
3996+
.build();
3997+
3998+
return actualDiagnostics;
3999+
}
4000+
38874001
private Node parse(String[] original) {
38884002
String[] argStrings = args.toArray(new String[] {});
38894003
CommandLineRunner runner = new CommandLineRunner(argStrings);

0 commit comments

Comments
 (0)