Copilot commented on code in PR #2901:
URL: https://github.com/apache/groovy/pull/2901#discussion_r3964650847


##########
src/main/java/org/codehaus/groovy/transform/LogASTTransformation.java:
##########
@@ -171,8 +241,15 @@ private Expression addGuard(final MethodCallExpression 
mce) {
                     variableExpression.setAccessedVariable(logNode);
                 }
 
+                boolean simpleArguments = usesSimpleMethodArgumentsOnly(mce);
+                if (staticLocation) {
+                    Expression withLocation = 
loggingStrategy.wrapLoggingMethodCallWithLocation(
+                            receiver, methodName, mce, locationOf(mce, 
logNode.getOwner()), !simpleArguments);
+                    if (withLocation != null) return withLocation;
+                }

Review Comment:
   `locationOf(...)` has a side effect (it appends a new synthetic field) even 
if `wrapLoggingMethodCallWithLocation(...)` returns `null` (meaning: leave the 
call unchanged). That can create unused synthetic `$log$loc$*` fields for calls 
that ultimately aren’t rewritten.



##########
src/main/java/org/codehaus/groovy/transform/LogASTTransformation.java:
##########
@@ -101,9 +110,21 @@ public void visit(final ASTNode[] nodes, final SourceUnit 
sourceUnit) {
         if (!(targetClass instanceof ClassNode classNode))
             throw new GroovyBugError("Class annotation " + 
logAnnotation.getClassNode().getName() + " annotated no Class, this must not 
happen.");
 
+        final boolean staticLocation = lookupStaticLocation(logAnnotation);
+        if (staticLocation && !loggingStrategy.supportsStaticLocation()) {
+            addError("staticLocation is not supported by " + 
loggingStrategy.getClass().getName()
+                    + (loggingStrategy instanceof StaticLocationCapable
+                            ? " here: the logging API it needs is not on the 
compile classpath"
+                            : ""), logAnnotation);
+            return;
+        }

Review Comment:
   When `staticLocation` is requested but the strategy reports it can't support 
it, the error message is fairly non-actionable (and the “compile classpath” 
suffix is bolted on via `StaticLocationCapable`). Consider constructing the 
message so it clearly distinguishes “unsupported strategy” vs “missing required 
logging API on compile classpath”.



##########
subprojects/groovy-logging-test/src/test/groovy/groovy/util/logging/Log4j2Test.groovy:
##########
@@ -357,4 +358,166 @@ final class Log4j2Test {
         assert appenderForCustomCategory.getEvents().size() == 1
         assert appender.getEvents().size() == 0
     }
+
+    // GROOVY-12378 
-------------------------------------------------------------
+
+    /** line number (1-based) of the first line of {@code source} containing 
{@code needle} */
+    private static int lineOf(String source, String needle) {
+        int idx = source.readLines().findIndexOf { it.contains(needle) }
+        assert idx >= 0 : "no line contains $needle"
+        idx + 1
+    }
+
+    /** each test compiles its own class: Log4j2 loggers are cached by name, 
and a
+     *  second appender registered under an existing name is ignored */
+    private static String staticLocationSource(String className) { '''
+        @groovy.util.logging.Log4j2(staticLocation = true)
+        class CLASSNAME {
+            static int evaluations = 0
+            static String expensive() { evaluations++; 'expensive' }
+
+            def instanceMethod() {
+                log.info('plain')                       // L1 simple 
arguments: no guard
+                log.warn("interpolated ${expensive()}") // L2 guarded
+                [1].each {
+                    log.error('from closure')            // L3 inside a closure
+                }
+            }
+            static void staticMethod() {
+                log.debug('static {}', 42)               // L4 parameterised
+            }
+            def withThrowable() {
+                try { throw new IllegalStateException('boom') } catch (e) { 
log.error('failed', e) }
+            }
+            def withMarker(org.apache.logging.log4j.Marker m) {
+                log.info(m, 'marked {}', 'x')
+            }
+            def withMarkerAndThrowable(org.apache.logging.log4j.Marker m, 
Throwable t) {
+                log.warn(m, 'both {}', 'y', t)
+            }
+        }
+    '''.replace('CLASSNAME', className) }
+
+    @Test
+    void testStaticLocationSuppliesCompileTimeLocations() {
+        String source = staticLocationSource('LocatedA')
+        Class clazz = new GroovyClassLoader().parseClass(source, 
'LocatedA.groovy')
+        clazz.log.addAppender(appender)
+        clazz.log.setLevel(Level.ALL)
+        clazz.newInstance().instanceMethod()
+        clazz.staticMethod()
+
+        def events = appender.events
+        assert events*.message == ['plain', 'interpolated expensive', 'from 
closure', 'static 42']
+        assert events*.source*.className == ['LocatedA'] * 4
+        assert events*.source*.fileName == ['LocatedA.groovy'] * 4
+        assert events*.source*.methodName == ['instanceMethod', 
'instanceMethod', 'instanceMethod', 'staticMethod']
+        assert events*.source*.lineNumber == ['L1', 'L2', 'L3', 'L4'].collect 
{ lineOf(source, it) }
+
+        // one synthetic static final field per logging statement
+        def locations = clazz.declaredFields.findAll { 
it.name.startsWith('$log$loc$') }
+        assert locations.size() == 7
+        assert locations.every { isStatic(it.modifiers) && 
isFinal(it.modifiers) && it.synthetic && it.type == StackTraceElement }
+    }
+
+    @Test
+    void testStaticLocationKeepsGuardForNonSimpleArguments() {
+        Class clazz = new 
GroovyClassLoader().parseClass(staticLocationSource('LocatedB'), 
'LocatedB.groovy')
+        clazz.log.addAppender(appender)
+        clazz.log.setLevel(Level.ERROR)
+        clazz.newInstance().instanceMethod()
+
+        assert clazz.evaluations == 0 : 'a disabled level must not evaluate 
the interpolated argument'
+        assert appender.events*.message == ['from closure']
+    }
+
+    @Test
+    void testStaticLocationThrowableAndMarker() {
+        Class clazz = new 
GroovyClassLoader().parseClass(staticLocationSource('LocatedC'), 
'LocatedC.groovy')
+        clazz.log.addAppender(appender)
+        clazz.log.setLevel(Level.ALL)
+        def marker = 
org.apache.logging.log4j.MarkerManager.getMarker('GROOVY12378')
+        def instance = clazz.newInstance()
+        instance.withThrowable()
+        instance.withMarker(marker)
+        instance.withMarkerAndThrowable(marker, new 
IllegalArgumentException('bad'))
+
+        def events = appender.events
+        assert events.size() == 3
+        assert events[0].message == 'failed'
+        assert events[0].thrown instanceof IllegalStateException
+        assert events[0].source.methodName == 'withThrowable'
+        assert events[1].message == 'marked x'
+        assert events[1].marker == marker
+        assert events[1].thrown == null
+        assert events[1].source.methodName == 'withMarker'
+        assert events[2].message == 'both y'
+        assert events[2].marker == marker
+        assert events[2].thrown instanceof IllegalArgumentException
+        assert events[2].source.methodName == 'withMarkerAndThrowable'
+    }
+
+    @Test
+    void testStaticLocationWithCompileStatic() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            @groovy.transform.CompileStatic
+            @groovy.util.logging.Log4j2(staticLocation = true)
+            class LocatedStatic {
+                void run(String who) {
+                    log.info("hello $who")
+                    log.warn('plain')
+                }
+            }
+        ''', 'LocatedStatic.groovy')
+        clazz.log.addAppender(appender)
+        clazz.log.setLevel(Level.ALL)
+        clazz.newInstance().run('world')
+
+        def events = appender.events
+        assert events*.message == ['hello world', 'plain']
+        assert events*.source*.className == ['LocatedStatic', 'LocatedStatic']
+        assert events*.source*.methodName == ['run', 'run']
+        assert events*.source*.lineNumber == [6, 7]
+    }
+
+    @Test
+    void testStaticLocationOffLeavesCallsAlone() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            @groovy.util.logging.Log4j2
+            class NotLocated {
+                def run() { log.info('plain') }
+            }
+        ''', 'NotLocated.groovy')
+        assert !clazz.declaredFields.any { it.name.startsWith('$log$loc$') }
+    }
+
+    @Test
+    void testStaticLocationRejectedByStrategyWithoutSupport() {
+        // a Log-family annotation whose strategy does not support 
compile-time locations
+        def err = 
shouldFail(org.codehaus.groovy.control.MultipleCompilationErrorsException) {
+            new GroovyClassLoader().parseClass('''
+                @groovy.util.logging.Log4j2Test.NoLocationLog(staticLocation = 
true)
+                class Unsupported {
+                    def run() { log.info('plain') }
+                }
+            ''')
+        }
+        assert err.message.contains('staticLocation is not supported by')
+    }
+
+    
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE)
+    
@org.codehaus.groovy.transform.GroovyASTTransformationClass('org.codehaus.groovy.transform.LogASTTransformation')
+    static @interface NoLocationLog {
+        String value() default 'log'
+        String category() default 
org.codehaus.groovy.transform.LogASTTransformation.DEFAULT_CATEGORY_NAME
+        String visibilityId() default groovy.transform.Undefined.STRING
+        Class<? extends 
org.codehaus.groovy.transform.LogASTTransformation.LoggingStrategy> 
loggingStrategy() default NoLocationStrategy
+        boolean staticLocation() default false
+    }
+
+    static class NoLocationStrategy extends Log4j2.Log4j2LoggingStrategy {
+        NoLocationStrategy(GroovyClassLoader loader) { super(loader) }
+        @Override boolean supportsStaticLocation() { false }
+    }

Review Comment:
   `NoLocationStrategy` extends `Log4j2LoggingStrategy`, which implements 
`StaticLocationCapable`. That makes the transformation treat 
`supportsStaticLocation()==false` as a “missing API on compile classpath” case, 
even though this test is trying to model a strategy that simply doesn’t support 
static locations. Using a delegating strategy that does not implement 
`StaticLocationCapable` keeps the scenario and error classification consistent.



##########
src/spec/doc/invokedynamic-support.adoc:
##########
@@ -181,7 +181,12 @@ context.frameworkPackages.addAll([
 ----
 
 Log4j2:: Log4j2 has no equivalent skip list; its location is always the frame 
following the logger's
-own class. Use `@CompileStatic` for the classes whose log locations matter.
+own class. Its answer is a location supplied by the caller, which Groovy's 
`@Log4j2` transform can
+provide at compile time: `@Log4j2(staticLocation = true)` rewrites each 
logging statement to
+`log.atInfo().withLocation(location).log(...)`, where `location` is a 
`StackTraceElement` for the

Review Comment:
   The example rewrite hard-codes `atInfo()`, but the transform emits 
`at<Level>()` corresponding to the original logging method 
(info/warn/error/etc). As written, this can mislead readers into thinking 
non-info calls are rewritten to `atInfo()`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to