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();

Reply via email to