This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new fa8587a251 GROOVY-12132: groovyc: print collected warnings on
successful compilation (make -w effective)
fa8587a251 is described below
commit fa8587a251b7961327b542020b093b95cbb67dc6
Author: Paul King <[email protected]>
AuthorDate: Sun Jul 5 16:04:29 2026 +1000
GROOVY-12132: groovyc: print collected warnings on successful compilation
(make -w effective)
---
.../java/org/codehaus/groovy/ast/ModuleNode.java | 3 +-
.../org/codehaus/groovy/classgen/Verifier.java | 7 +-
.../org/codehaus/groovy/control/SourceUnit.java | 18 ++++-
.../codehaus/groovy/tools/FileSystemCompiler.java | 57 ++++++++++++++-
.../TupleConstructorASTTransformation.java | 3 +-
src/spec/doc/tools-groovyc.adoc | 35 ++++++++++
.../groovy/tools/FileSystemCompilerTest.java | 80 ++++++++++++++++++++++
7 files changed, 196 insertions(+), 7 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
index e26bc889cb..261a527ed5 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
@@ -29,6 +29,7 @@ import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.transform.BaseScriptASTTransformation;
@@ -786,7 +787,7 @@ public class ModuleNode extends ASTNode {
// Warn about unreachable main methods
for (int i = 1; i < validMains.size(); i++) {
MethodNode unreachable = validMains.get(i);
- getContext().addWarning("Method '" + unreachable.getText()
+ getContext().addWarning(WarningMessage.LIKELY_ERRORS, "Method '" +
unreachable.getText()
+ "' is not reachable from the Groovy runner"
+ " because a higher-priority main method '"
+ result.getText() + "' exists", unreachable);
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index 2f4d7548ba..5efbf0ab00 100644
--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ -63,6 +63,7 @@ import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.codehaus.groovy.classgen.asm.MopWriter;
import
org.codehaus.groovy.classgen.asm.OptimizingStatementWriter.ClassNodeSkip;
import org.codehaus.groovy.classgen.asm.WriterController;
+import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.reflection.ClassInfo;
import org.codehaus.groovy.syntax.RuntimeParserException;
import org.codehaus.groovy.syntax.Token;
@@ -466,7 +467,7 @@ public class Verifier implements GroovyClassVisitor,
Opcodes {
one += via.apply(i);
two += via.apply(j);
if (Traits.isTrait(in)) { //
GROOVY-11508
-
cn.getModule().getContext().addWarning("The trait " +
in.getNameWithoutPackage() +
+
cn.getModule().getContext().addWarning(WarningMessage.LIKELY_ERRORS, "The trait
" + in.getNameWithoutPackage() +
" is implemented more than
once with different arguments: " + one + " and " + two, cn);
} else {
throw new
RuntimeParserException("The interface " + in.getNameWithoutPackage() +
@@ -989,7 +990,7 @@ public class Verifier implements GroovyClassVisitor,
Opcodes {
message = message.substring(message.indexOf(' ') + 1); // strip return
type
message = String.format("Property %s cannot override final method %s
of class %s", pn.getName(), message, mn.getDeclaringClass().getName());
- classNode.getModule().getContext().addWarning(message, pn);
+
classNode.getModule().getContext().addWarning(WarningMessage.LIKELY_ERRORS,
message, pn);
}
private boolean methodNeedsReplacement(final MethodNode mn, final
Consumer<MethodNode> cannotReplace) {
@@ -1233,7 +1234,7 @@ public class Verifier implements GroovyClassVisitor,
Opcodes {
} else {
String warning = "Default argument(s) specify duplicate
constructor: " +
MethodNodeUtils.methodDescriptor(old,true).replace("<init>",type.getNameWithoutPackage());
- type.getModule().getContext().addWarning(warning,
method.getLineNumber() > 0 ? method : type);
+
type.getModule().getContext().addWarning(WarningMessage.LIKELY_ERRORS, warning,
method.getLineNumber() > 0 ? method : type);
}
});
}
diff --git a/src/main/java/org/codehaus/groovy/control/SourceUnit.java
b/src/main/java/org/codehaus/groovy/control/SourceUnit.java
index 2ba126583d..fc78c9ad21 100644
--- a/src/main/java/org/codehaus/groovy/control/SourceUnit.java
+++ b/src/main/java/org/codehaus/groovy/control/SourceUnit.java
@@ -337,11 +337,27 @@ public class SourceUnit extends ProcessingUnit {
}
/**
+ * Adds a warning with {@link WarningMessage#POSSIBLE_ERRORS} importance.
+ *
* @since 4.0.7
*/
public void addWarning(final String text, final ASTNode node) {
+ addWarning(WarningMessage.POSSIBLE_ERRORS, text, node);
+ }
+
+ /**
+ * Adds a warning at the given importance level. The warning is only
retained
+ * if the importance is relevant for the configured warning level.
+ *
+ * @param importance one of the {@link WarningMessage} level constants
+ * @param text the warning text
+ * @param node the AST node the warning relates to (for line/column info)
+ *
+ * @since 6.0.0
+ */
+ public void addWarning(final int importance, final String text, final
ASTNode node) {
Token token = new Token(0, "", node.getLineNumber(),
node.getColumnNumber()); // ASTNode to CSTNode
- getErrorCollector().addWarning(new
WarningMessage(WarningMessage.POSSIBLE_ERRORS, text, token, this));
+ getErrorCollector().addWarning(importance, text, token, this);
}
/**
diff --git a/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java
b/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java
index 7b9ebefd90..a924fc6cc1 100644
--- a/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java
+++ b/src/main/java/org/codehaus/groovy/tools/FileSystemCompiler.java
@@ -22,6 +22,8 @@ import groovy.lang.GroovySystem;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.ConfigurationException;
+import org.codehaus.groovy.control.ErrorCollector;
+import org.codehaus.groovy.control.Janitor;
import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.runtime.ArrayGroovyMethods;
import org.codehaus.groovy.runtime.DefaultGroovyStaticMethods;
@@ -39,6 +41,7 @@ import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
+import java.io.Writer;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
@@ -196,7 +199,7 @@ public class FileSystemCompiler {
fileNameErrors = fileNameErrors && !validateFiles(filenames);
if (!fileNameErrors) {
- doCompilation(configuration, null, filenames, lookupUnnamedFiles);
+ doCompilation(configuration, null, filenames, lookupUnnamedFiles,
new PrintWriter(System.err, true));
}
}
@@ -274,6 +277,31 @@ public class FileSystemCompiler {
* @throws Exception if compilation fails
*/
public static void doCompilation(CompilerConfiguration configuration,
CompilationUnit unit, String[] filenames, boolean lookupUnnamedFiles) throws
Exception {
+ doCompilation(configuration, unit, filenames, lookupUnnamedFiles,
null);
+ }
+
+ /**
+ * Compiles the supplied source files, optionally displaying any warnings
collected
+ * during a successful compilation.
+ * <p>
+ * Warning display is opt-in via {@code warningWriter} rather than
hard-wired to
+ * {@code System.err}, so that this shared entry point does not impose
console output
+ * on embedders (e.g. the in-process Ant {@code <groovyc>} task) that
route diagnostics
+ * their own way or read the {@link ErrorCollector} directly. The {@code
groovyc}
+ * command line supplies a {@code System.err} writer; other callers pass
{@code null}.
+ *
+ * @param configuration the compiler configuration to use
+ * @param unit an existing compilation unit to reuse, or {@code null}
+ * @param filenames the source files to compile
+ * @param lookupUnnamedFiles whether to search for unnamed Groovy sources
+ * beside the listed files
+ * @param warningWriter where to write warnings collected on a successful
compile,
+ * or {@code null} to leave warning handling to the caller
+ * @throws Exception if compilation fails
+ *
+ * @since 6.0.0
+ */
+ public static void doCompilation(CompilerConfiguration configuration,
CompilationUnit unit, String[] filenames, boolean lookupUnnamedFiles, Writer
warningWriter) throws Exception {
File tmpDir = null;
// if there are any joint compilation options set stubDir if not set
try {
@@ -298,6 +326,9 @@ public class FileSystemCompiler {
.setResourceLoader(filename -> null);
}
compiler.compile(filenames);
+ if (warningWriter != null) {
+ displayWarnings(compiler.unit, warningWriter);
+ }
} finally {
try {
if (tmpDir != null) deleteRecursive(tmpDir);
@@ -307,6 +338,30 @@ public class FileSystemCompiler {
}
}
+ /**
+ * Writes any warnings collected during a successful compilation to the
given writer, in
+ * the same format the diagnostics of a failed compilation use. Without
this, warnings that
+ * the compiler collected (already filtered by the configured warning
level) were silently
+ * discarded whenever compilation succeeded — only embedders reading the
+ * {@link ErrorCollector} ever saw them. The writer is flushed but not
closed; the caller
+ * retains ownership of it.
+ *
+ * @param unit the compilation unit that completed successfully
+ * @param out where to write the warnings
+ */
+ static void displayWarnings(CompilationUnit unit, Writer out) {
+ ErrorCollector collector = unit.getErrorCollector();
+ if (!collector.hasWarnings()) return;
+ Janitor janitor = new Janitor();
+ PrintWriter writer = (out instanceof PrintWriter) ? (PrintWriter) out
: new PrintWriter(out);
+ try {
+ collector.write(writer, janitor);
+ } finally {
+ writer.flush();
+ janitor.cleanup();
+ }
+ }
+
private static String[] generateFileNamesFromOptions(List<String>
filenames) {
if (filenames == null) {
return new String[0];
diff --git
a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
index ae333a41db..4109f1f498 100644
---
a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++
b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -51,6 +51,7 @@ import org.codehaus.groovy.classgen.VariableScopeVisitor;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.WarningMessage;
import java.util.ArrayList;
import java.util.Comparator;
@@ -339,7 +340,7 @@ public class TupleConstructorASTTransformation extends
AbstractASTTransformation
"%s specifies duplicate constructor: %s(%s)",
xform.getAnnotationName(), cNode.getNameWithoutPackage(),
params.stream().map(Parameter::getType).map(ClassNodeUtils::formatTypeName).collect(joining(",")));
- sourceUnit.addWarning(warning, anno.getLineNumber() > 0 ? anno
: cNode);
+ sourceUnit.addWarning(WarningMessage.LIKELY_ERRORS, warning,
anno.getLineNumber() > 0 ? anno : cNode);
}
} else {
// add main tuple constructor; if any parameters have default
values, then Verifier will generate the variants
diff --git a/src/spec/doc/tools-groovyc.adoc b/src/spec/doc/tools-groovyc.adoc
index 43ec83453f..a6f30e56f7 100644
--- a/src/spec/doc/tools-groovyc.adoc
+++ b/src/spec/doc/tools-groovyc.adoc
@@ -48,6 +48,7 @@ a number of command line switches:
| -d | | Specify where to place generated class files. | groovyc -d target
Person.groovy
| -v | --version | Displays the compiler version | groovyc -v
| -e | --exception | Displays the stack trace in case of compilation error |
groovyc -e script.groovy
+| -w | --warningLevel | Sets how many warnings are reported after a successful
compile: `0` (none), `1` (likely errors, the default), `2` (possible errors) or
`3` (all). See <<section-groovyc-warnings,Compiler warnings>>. | groovyc -w 2
MyClass.groovy
| -j | --jointCompilation* | Enables joint compilation | groovyc -j A.groovy
B.java
| -b | --basescript | Base class name for scripts (must derive from Script)|
| | --configscript | Advanced compiler configuration script | groovyc
--configscript config/config.groovy src/Person.groovy
@@ -61,6 +62,40 @@ a number of command line switches:
*Notes:*
* for a full description of joint compilation, see
<<section-jointcompilation,the joint compilation section>>.
+[[section-groovyc-warnings]]
+== Compiler warnings
+
+While compiling, Groovy may report _warnings_: diagnostics that don't stop
compilation
+but flag code that is suspicious or unlikely to behave as intended — for
example, an
+unreachable `main` method or a property that can't override a `final`
accessor. On a
+successful compile, `groovyc` prints any collected warnings to `stderr`, using
the same
+format (message, source line and caret) as compilation errors, followed by a
summary count.
+
+Every warning carries an _importance_, and the `-w` (`--warningLevel`) option
controls how
+many are reported. Only warnings whose importance is at or below the
configured level are
+kept, so a lower level is quieter:
+
+[cols="<,<,<",options="header"]
+|=======================================================================
+| Level | Name | Reports
+| `0` | none | No warnings at all.
+| `1` | likely errors | *(default)* Diagnostics that very probably indicate a
mistake — for example an unreachable `main` method, a property that can't
override a `final` accessor, or a duplicate constructor generated from default
argument values or `@TupleConstructor`/`@MapConstructor`.
+| `2` | possible errors | The above, plus diagnostics that often have benign
causes — for example a field hidden by a dynamic property under `@TypeChecked`,
or problems found while scanning global AST transformation descriptors.
+| `3` | paranoia | The above, plus any remaining lower-priority warnings.
+|=======================================================================
+
+For example, to also see the level 2 warnings:
+
+----------------------
+groovyc -w 2 MyClass.groovy
+----------------------
+
+NOTE: The warning _level_ can also be set programmatically via
+`CompilerConfiguration#setWarningLevel`, or for the `groovy` command through
the
+`groovy.warnings` system property. Displaying the collected warnings on a
successful
+compile is done by the `groovyc` command line; other embeddings (for instance,
code
+driving a `CompilationUnit` directly) can instead read them from the unit's
`ErrorCollector`.
+
== Ant task
See the <<{groovyc-ant-task}#groovyc-ant-task-using,groovyc Ant task>>
documentation.
diff --git
a/src/test/groovy/org/codehaus/groovy/tools/FileSystemCompilerTest.java
b/src/test/groovy/org/codehaus/groovy/tools/FileSystemCompilerTest.java
index 23d82c6e75..2074aad647 100644
--- a/src/test/groovy/org/codehaus/groovy/tools/FileSystemCompilerTest.java
+++ b/src/test/groovy/org/codehaus/groovy/tools/FileSystemCompilerTest.java
@@ -18,13 +18,25 @@
*/
package org.codehaus.groovy.tools;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.classgen.GeneratorContext;
+import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.customizers.CompilationCustomizer;
+import org.codehaus.groovy.control.messages.WarningMessage;
+import org.codehaus.groovy.syntax.Token;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -94,6 +106,74 @@ public class FileSystemCompilerTest {
FileSystemCompiler.commandLineCompile(new String[]
{"src/test/groovy/groovy/LittleClosureTest.groovy", "-d", dir.getPath()});
}
+ // warnings collected during a SUCCESSFUL compile must reach the
command-line user via the
+ // injectable writer; the failure path already surfaces them alongside the
errors
+ @Test
+ public void testWarningsAreDisplayedOnSuccessfulCompile(@TempDir Path dir)
throws Exception {
+ StringWriter warnings = new StringWriter();
+ compile(dir, "WarnDemo", "class WarnDemo { }",
warningEmittingConfiguration(), warnings);
+ String output = warnings.toString();
+ assertTrue(output.contains("spike warning marker"), "expected the
collected warning but got: " + output);
+ assertTrue(output.contains("1 warning"), "expected a warning summary
but got: " + output);
+ }
+
+ @Test
+ public void testCleanCompileWritesNothing(@TempDir Path dir) throws
Exception {
+ StringWriter warnings = new StringWriter();
+ compile(dir, "CleanDemo", "class CleanDemo { }", null, warnings);
+ assertTrue(warnings.toString().isEmpty(), "clean compile should
produce no warning output but got: " + warnings);
+ }
+
+ // a null writer (the default for embedders such as the in-process Ant
task) must leave
+ // warning handling to the caller — the shared doCompilation entry point
stays quiet
+ @Test
+ public void testNullWriterProducesNoWarningOutput(@TempDir Path dir)
throws Exception {
+ // proves the suppression is due to the null writer, not the absence
of a warning:
+ // the same configuration DOES emit when a writer is supplied
+ StringWriter probe = new StringWriter();
+ compile(dir, "QuietProbe", "class QuietProbe { }",
warningEmittingConfiguration(), probe);
+ assertTrue(probe.toString().contains("spike warning marker"), "sanity:
warning should exist with a writer");
+
+ // must not throw and (having no sink) produces nothing observable
+ compile(dir, "QuietDemo", "class QuietDemo { }",
warningEmittingConfiguration(), null);
+ }
+
+ // integration of the display fix with the level-1 demotion: a genuinely
warnable program
+ // (a property that cannot override a final accessor, GROOVY-8659) now
surfaces its warning
+ // at the DEFAULT warning level, where previously it was a suppressed
level-2 warning
+ @Test
+ public void testDemotedWarningVisibleAtDefaultLevel(@TempDir Path dir)
throws Exception {
+ StringWriter warnings = new StringWriter();
+ compile(dir, "OverrideDemo",
+ "abstract class A { final String getFoo() { 'A' } }\n"
+ + "class OverrideDemo extends A { final String foo =
'C' }\n",
+ null, warnings);
+ assertTrue(warnings.toString().contains("cannot override final method
getFoo"),
+ "expected the demoted (level-1) property-override warning at
default level but got: " + warnings);
+ }
+
+ private static CompilerConfiguration warningEmittingConfiguration() {
+ CompilerConfiguration configuration = new CompilerConfiguration();
+ configuration.addCompilationCustomizers(new
CompilationCustomizer(CompilePhase.CANONICALIZATION) {
+ @Override
+ public void call(SourceUnit source, GeneratorContext context,
ClassNode classNode) {
+
source.getErrorCollector().addWarning(WarningMessage.LIKELY_ERRORS, "spike
warning marker",
+ Token.newString(classNode.getName(), 1, 1), source);
+ }
+ });
+ return configuration;
+ }
+
+ private static void compile(Path dir, String className, String source,
CompilerConfiguration configuration, Writer warningWriter) throws Exception {
+ File file = dir.resolve(className + ".groovy").toFile();
+ Files.write(file.toPath(), source.getBytes(StandardCharsets.UTF_8));
+ if (configuration == null) {
+ configuration = new CompilerConfiguration();
+ }
+ configuration.setTargetDirectory(dir.toFile());
+ FileSystemCompiler.doCompilation(configuration, null, new
String[]{file.getPath()}, false, warningWriter);
+ }
+
@Test
public void testDeleteRecursiveDoesNotFollowSymlink() throws Exception {
File base =
Files.createTempDirectory("deleteRecursiveSymlink").toFile();