You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2020/05/13 11:53:28 UTC

[groovy] branch master updated: GROOVY-9534: support platform logging AST transform (closes #1237)

This is an automated email from the ASF dual-hosted git repository.

paulk 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 d9c0a3b  GROOVY-9534: support platform logging AST transform (closes #1237)
d9c0a3b is described below

commit d9c0a3bc0ff0de9b773187e635477e3f7846cceb
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri May 1 21:43:56 2020 +1000

    GROOVY-9534: support platform logging AST transform (closes #1237)
---
 build.gradle                                       |   1 +
 gradle/docs.gradle                                 |   1 +
 src/main/java/groovy/util/logging/PlatformLog.java | 113 +++++++++
 .../org/codehaus/groovy/vmplugin/v9/Java9.java     |  16 ++
 .../vmplugin/v9/PluginDefaultGroovyMethods.java    | 170 +++++++++++++
 .../services/java.lang.System$LoggerFinder         |  19 ++
 src/test/groovy/util/logging/vm9/LoggerSpy.groovy  |  54 ++++
 .../groovy/util/logging/vm9/LoggerSpyFinder.groovy |  13 +
 .../groovy/util/logging/vm9/PlatformLogTest.groovy | 280 +++++++++++++++++++++
 9 files changed, 667 insertions(+)

diff --git a/build.gradle b/build.gradle
index ffb1753..c62c564 100644
--- a/build.gradle
+++ b/build.gradle
@@ -214,6 +214,7 @@ sourceSets {
             if (!JavaVersion.current().isJava9Compatible()) {
                 exclude '**/v9/*'
                 exclude '**/vm9/*'
+                exclude '**/PlatformLog.java'
             }
         }
         groovy {
diff --git a/gradle/docs.gradle b/gradle/docs.gradle
index a73b3e9..1f32490 100644
--- a/gradle/docs.gradle
+++ b/gradle/docs.gradle
@@ -160,6 +160,7 @@ task docGDK {
                     arg(value: 'org.codehaus.groovy.runtime.SocketGroovyMethods')
                     arg(value: 'org.codehaus.groovy.runtime.StringGroovyMethods')
                     arg(value: 'org.codehaus.groovy.vmplugin.v8.PluginDefaultGroovyMethods')
+                    arg(value: 'org.codehaus.groovy.vmplugin.v9.PluginDefaultGroovyMethods')
                     // TODO don't hard-code these
                     arg(value: 'subprojects/groovy-dateutil/src/main/java/org/apache/groovy/dateutil/extensions/DateUtilExtensions.java')
                     arg(value: 'subprojects/groovy-datetime/src/main/java/org/apache/groovy/datetime/extensions/DateTimeExtensions.java')
diff --git a/src/main/java/groovy/util/logging/PlatformLog.java b/src/main/java/groovy/util/logging/PlatformLog.java
new file mode 100644
index 0000000..aec56fa
--- /dev/null
+++ b/src/main/java/groovy/util/logging/PlatformLog.java
@@ -0,0 +1,113 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util.logging;
+
+import groovy.lang.GroovyClassLoader;
+import groovy.transform.Undefined;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+import org.codehaus.groovy.transform.LogASTTransformation;
+import org.codehaus.groovy.transform.LogASTTransformation.LoggingStrategy;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+
+/**
+ * This local transform adds a logging ability to your program using
+ * java.logging. Every method call on a unbound variable named <i>log</i>
+ * will be mapped to a call to the logger. For this a <i>log</i> field will be
+ * inserted in the class. If the field already exists the usage of this transform
+ * will cause a compilation error. The method name will be used to determine
+ * what to call on the logger.
+ * <pre>
+ * import groovy.util.logging.*
+ * import static java.lang.System.Logger.Level.INFO
+ *
+ * {@code @PlatformLog}
+ * class Foo {
+ *     def method() {
+ *         log.log INFO, 'Foobar'
+ *     }
+ * }
+ *
+ * new Foo().method()
+ * </pre>
+ *
+ * @since 4.0.0
+ */
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.LogASTTransformation")
+public @interface PlatformLog {
+    String value() default "log";
+
+    String category() default LogASTTransformation.DEFAULT_CATEGORY_NAME;
+
+    /**
+     * If specified, must match the "id" attribute in a VisibilityOptions annotation to enable a custom visibility.
+     */
+    String visibilityId() default Undefined.STRING;
+
+    Class<? extends LoggingStrategy> loggingStrategy() default JavaUtilLoggingStrategy.class;
+
+    /**
+     * This class contains the logic of how to weave a Java platform logger into the host class.
+     */
+    class JavaUtilLoggingStrategy extends LogASTTransformation.AbstractLoggingStrategyV2 {
+
+        private static final ClassNode LOGGER_CLASSNODE = ClassHelper.make(System.Logger.class);
+        private static final ClassNode LOGGER_FINDER_CLASSNODE = ClassHelper.make(System.LoggerFinder.class);
+
+        protected JavaUtilLoggingStrategy(final GroovyClassLoader loader) {
+            super(loader);
+        }
+
+        @Override
+        public FieldNode addLoggerFieldToClass(ClassNode classNode, String logFieldName, String categoryName, int fieldModifiers) {
+            Expression module = callX(classX(classNode), "getModule");
+            Expression loggerFinder = callX(classX(LOGGER_FINDER_CLASSNODE), "getLoggerFinder");
+            Expression initialValue = callX(loggerFinder, "getLogger", args(constX(getCategoryName(classNode, categoryName)), module));
+            return classNode.addField(logFieldName, fieldModifiers, LOGGER_CLASSNODE, initialValue);
+        }
+
+        @Override
+        public boolean isLoggingMethod(String methodName) {
+//            return methodName.matches("error|warn|info|debug|trace");
+            return false;
+        }
+
+        @Override
+        public Expression wrapLoggingMethodCall(Expression logVariable, String methodName, Expression originalExpression) {
+            // no shortcut
+            return originalExpression;
+        }
+    }
+}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java b/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java
index e635157..ebd08f3 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java
@@ -46,6 +46,7 @@ import java.math.BigInteger;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -64,6 +65,16 @@ import java.util.stream.Collectors;
 public class Java9 extends Java8 {
     private static final Logger LOGGER = Logger.getLogger(Java9.class.getName());
 
+    private final Class<?>[] PLUGIN_DGM;
+
+    public Java9() {
+        super();
+        List<Class<?>> dgmClasses = new ArrayList<>();
+        Collections.addAll(dgmClasses, super.getPluginDefaultGroovyMethods());
+        dgmClasses.add(PluginDefaultGroovyMethods.class);
+        PLUGIN_DGM = dgmClasses.toArray(new Class<?>[0]);
+    }
+
     @Override
     public Map<String, Set<String>> getDefaultImportClasses(String[] packageNames) {
         List<String> javaPns = new ArrayList<>(4);
@@ -165,6 +176,11 @@ public class Java9 extends Java8 {
     }
 
     @Override
+    public Class<?>[] getPluginDefaultGroovyMethods() {
+        return PLUGIN_DGM;
+    }
+
+    @Override
     public int getVersion() {
         return 9;
     }
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v9/PluginDefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/vmplugin/v9/PluginDefaultGroovyMethods.java
new file mode 100644
index 0000000..b332853
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v9/PluginDefaultGroovyMethods.java
@@ -0,0 +1,170 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.vmplugin.v9;
+
+import org.codehaus.groovy.runtime.DefaultGroovyMethodsSupport;
+
+/**
+ * Defines new Groovy methods which appear on normal JDK 9
+ * classes inside the Groovy environment.
+ *
+ * @since 3.0.4
+ */
+public class PluginDefaultGroovyMethods extends DefaultGroovyMethodsSupport {
+
+    // No instances, static methods only
+    private PluginDefaultGroovyMethods() {
+    }
+
+    /**
+     * Convenience method for logging info level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void info(final System.Logger logger, final String msg) {
+        logger.log(System.Logger.Level.INFO, msg);
+    }
+
+    /**
+     * Convenience method for logging info level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void info(final System.Logger logger, final String msg, final Throwable thrown) {
+        logger.log(System.Logger.Level.INFO, msg, thrown);
+    }
+
+    /**
+     * Convenience method for logging info level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void info(final System.Logger logger, final String format, final Object... params) {
+        logger.log(System.Logger.Level.INFO, format, params);
+    }
+
+    /**
+     * Convenience method for logging trace level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void trace(final System.Logger logger, final String msg) {
+        logger.log(System.Logger.Level.TRACE, msg);
+    }
+
+    /**
+     * Convenience method for logging trace level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void trace(final System.Logger logger, final String msg, final Throwable thrown) {
+        logger.log(System.Logger.Level.TRACE, msg, thrown);
+    }
+
+    /**
+     * Convenience method for logging trace level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void trace(final System.Logger logger, final String format, final Object... params) {
+        logger.log(System.Logger.Level.TRACE, format, params);
+    }
+
+    /**
+     * Convenience method for logging warning level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void warn(final System.Logger logger, final String msg) {
+        logger.log(System.Logger.Level.WARNING, msg);
+    }
+
+    /**
+     * Convenience method for logging warning level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void warn(final System.Logger logger, final String msg, final Throwable thrown) {
+        logger.log(System.Logger.Level.WARNING, msg, thrown);
+    }
+
+    /**
+     * Convenience method for logging warning level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void warn(final System.Logger logger, final String format, final Object... params) {
+        logger.log(System.Logger.Level.WARNING, format, params);
+    }
+
+    /**
+     * Convenience method for logging error level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void error(final System.Logger logger, final String msg) {
+        logger.log(System.Logger.Level.ERROR, msg);
+    }
+
+    /**
+     * Convenience method for logging error level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void error(final System.Logger logger, final String msg, final Throwable thrown) {
+        logger.log(System.Logger.Level.ERROR, msg, thrown);
+    }
+
+    /**
+     * Convenience method for logging error level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void error(final System.Logger logger, final String format, final Object... params) {
+        logger.log(System.Logger.Level.ERROR, format, params);
+    }
+
+    /**
+     * Convenience method for logging debug level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void debug(final System.Logger logger, final String msg) {
+        logger.log(System.Logger.Level.DEBUG, msg);
+    }
+
+    /**
+     * Convenience method for logging debug level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void debug(final System.Logger logger, final String msg, final Throwable thrown) {
+        logger.log(System.Logger.Level.DEBUG, msg, thrown);
+    }
+
+    /**
+     * Convenience method for logging debug level messages with the platform logger.
+     *
+     * @param logger the Optional
+     */
+    public static void debug(final System.Logger logger, final String format, final Object... params) {
+        logger.log(System.Logger.Level.DEBUG, format, params);
+    }
+
+}
diff --git a/src/test-resources/META-INF/services/java.lang.System$LoggerFinder b/src/test-resources/META-INF/services/java.lang.System$LoggerFinder
new file mode 100644
index 0000000..86210b2
--- /dev/null
+++ b/src/test-resources/META-INF/services/java.lang.System$LoggerFinder
@@ -0,0 +1,19 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+groovy.util.logging.vm9.LoggerSpyFinder
\ No newline at end of file
diff --git a/src/test/groovy/util/logging/vm9/LoggerSpy.groovy b/src/test/groovy/util/logging/vm9/LoggerSpy.groovy
new file mode 100644
index 0000000..a9a9c5c
--- /dev/null
+++ b/src/test/groovy/util/logging/vm9/LoggerSpy.groovy
@@ -0,0 +1,54 @@
+package groovy.util.logging.vm9
+
+import groovy.transform.AutoImplement
+import groovy.transform.CompileStatic
+import groovy.transform.PackageScope
+
+@PackageScope
+@AutoImplement
+@CompileStatic
+class LoggerSpy implements System.Logger {
+    String warningParameter = null
+    String infoParameter = null
+    String traceParameter = null
+    String errorParameter = null
+    String debugParameter = null
+
+    void reset() {
+        warningParameter = null
+        infoParameter = null
+        traceParameter = null
+        errorParameter = null
+        debugParameter = null
+    }
+
+    @Override
+    void log(Level lev, String s) {
+        switch (lev) {
+            case Level.WARNING:
+                if (warningParameter) throwAssertionError("Warning already called once with parameter $warningParameter")
+                warningParameter = s
+                break
+            case Level.INFO:
+                if (infoParameter) throwAssertionError("Info already called once with parameter $infoParameter")
+                infoParameter = s
+                break
+            case Level.ERROR:
+                if (errorParameter) throwAssertionError("Error already called once with parameter $errorParameter")
+                errorParameter = s
+                break
+            case Level.DEBUG:
+                if (debugParameter) throwAssertionError("Debug already called once with parameter $debugParameter")
+                debugParameter = s
+                break
+            case Level.TRACE:
+                if (traceParameter) throwAssertionError("Trace already called once with parameter $traceParameter")
+                traceParameter = s
+                break
+        }
+    }
+
+    private static void throwAssertionError(Object detailMessage) {
+        throw new AssertionError(detailMessage)
+    }
+}
diff --git a/src/test/groovy/util/logging/vm9/LoggerSpyFinder.groovy b/src/test/groovy/util/logging/vm9/LoggerSpyFinder.groovy
new file mode 100644
index 0000000..dce6f9f
--- /dev/null
+++ b/src/test/groovy/util/logging/vm9/LoggerSpyFinder.groovy
@@ -0,0 +1,13 @@
+package groovy.util.logging.vm9
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+class LoggerSpyFinder extends System.LoggerFinder {
+    static System.Logger spy = null
+
+    @Override
+    System.Logger getLogger(String name, Module module) {
+        spy = new LoggerSpy()
+    }
+}
\ No newline at end of file
diff --git a/src/test/groovy/util/logging/vm9/PlatformLogTest.groovy b/src/test/groovy/util/logging/vm9/PlatformLogTest.groovy
new file mode 100644
index 0000000..b1b4aed
--- /dev/null
+++ b/src/test/groovy/util/logging/vm9/PlatformLogTest.groovy
@@ -0,0 +1,280 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.util.logging.vm9
+
+import groovy.test.GroovyTestCase
+import groovy.util.logging.PlatformLog
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+
+import java.lang.reflect.Field
+import java.lang.reflect.Modifier
+
+/**
+ * Test to make sure the @Log annotation is working correctly. 
+ */
+class PlatformLogTest extends GroovyTestCase {
+    void testPrivateFinalStaticLogFieldAppears() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            @groovy.util.logging.PlatformLog
+            class MyClassPrivateFinalStaticLogFieldAppears { }
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'log' &&
+                    Modifier.isPrivate(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testPrivateFinalStaticNamedLogFieldAppears() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            @groovy.util.logging.PlatformLog('logger')
+            class MyClassPrivateFinalStaticNamedLogFieldAppears { }
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'logger' &&
+                    Modifier.isPrivate(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testExplicitPrivateFinalStaticLogFieldAppears() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            import static groovy.transform.options.Visibility.*
+            @groovy.transform.VisibilityOptions(value = PRIVATE)
+            @groovy.util.logging.PlatformLog
+            class MyClassExplicitPrivateFinalStaticLogFieldAppears { }
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'log' &&
+                    Modifier.isPrivate(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testPackagePrivateFinalStaticLogFieldAppears() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            import static groovy.transform.options.Visibility.*
+            @groovy.transform.VisibilityOptions(value = PACKAGE_PRIVATE)
+            @groovy.util.logging.PlatformLog
+            class MyClassPackagePrivateFinalStaticLogFieldAppears { }
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'log' &&
+                    !Modifier.isPrivate(field.getModifiers()) &&
+                    !Modifier.isProtected(field.getModifiers()) &&
+                    !Modifier.isPublic(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testProtectedFinalStaticLogFieldAppears() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            import static groovy.transform.options.Visibility.*
+            @groovy.transform.VisibilityOptions(value = PROTECTED)
+            @groovy.util.logging.PlatformLog
+            class MyClassProtectedFinalStaticLogFieldAppears { }
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'log' &&
+                    Modifier.isProtected(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testPublicFinalStaticLogFieldAppears() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            import static groovy.transform.options.Visibility.*
+            @groovy.transform.VisibilityOptions(value = PUBLIC)
+            @groovy.util.logging.PlatformLog
+            class MyClassPublicFinalStaticLogFieldAppears { }
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'log' &&
+                    Modifier.isPublic(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testClassAlreadyHasLogField() {
+        def msg = shouldFail {
+            Class clazz = new GroovyClassLoader().parseClass('''
+                @groovy.util.logging.PlatformLog
+                class MyClassAlreadyHasLogField {
+                    String log
+                }
+            ''')
+
+            assert clazz.getConstructor().newInstance()
+        }
+        assert msg.contains('cannot have log field declared')
+    }
+
+    void testClassAlreadyHasNamedLogField() {
+        def msg = shouldFail {
+            Class clazz = new GroovyClassLoader().parseClass('''
+                @groovy.util.logging.PlatformLog('logger')
+                class MyClassAlreadyHasNamedLogField {
+                    String logger
+                }
+            ''')
+            assert clazz.getConstructor().newInstance()
+        }
+        assert msg.contains('cannot have log field declared')
+    }
+
+    @PlatformLog
+    class MyClassLogFromStaticMethods {
+        static loggingMethod() {
+            log.info ('info    called')
+        }
+    }
+
+    void testLogFromStaticMethods() {
+        MyClassLogFromStaticMethods.loggingMethod()
+        def finder = System.LoggerFinder.getLoggerFinder()
+        assert finder instanceof LoggerSpyFinder
+        def logSpy = finder.spy
+        assert logSpy.infoParameter    == 'info    called'
+    }
+
+    void testLogInfo() {
+        Class clazz = new GroovyClassLoader().parseClass('''
+            @groovy.util.logging.PlatformLog
+            class MyClassLogInfo {
+                def loggingMethod() {
+                    log.error('error   called')
+                    log.warn ('warning called')
+                    log.info ('info    called')
+                    log.debug('debug   called')
+                    log.trace('trace   called')
+                }
+            }
+        ''')
+        def s = clazz.getConstructor().newInstance()
+        s.loggingMethod()
+        def finder = System.LoggerFinder.getLoggerFinder()
+        assert finder instanceof LoggerSpyFinder
+        def logSpy = finder.spy
+        assert logSpy.warningParameter == 'warning called'
+        assert logSpy.infoParameter    == 'info    called'
+        assert logSpy.debugParameter   == 'debug   called'
+        assert logSpy.traceParameter   == 'trace   called'
+        assert logSpy.errorParameter   == 'error   called'
+        logSpy.reset()
+    }
+
+    @PlatformLog('logger')
+    static class MyClassLogInfoWithName {
+        def loggingMethod() {
+            logger.error('error   called')
+            logger.warn ('warning called')
+            logger.info ('info    called')
+            logger.debug('debug   called')
+            logger.trace('trace   called')
+        }
+    }
+
+    void testLogInfoWithName() {
+        new MyClassLogInfoWithName().loggingMethod()
+        def finder = System.LoggerFinder.getLoggerFinder()
+        assert finder instanceof LoggerSpyFinder
+        def logSpy = finder.spy
+        assert logSpy.warningParameter == 'warning called'
+        assert logSpy.infoParameter    == 'info    called'
+        assert logSpy.debugParameter   == 'debug   called'
+        assert logSpy.traceParameter   == 'trace   called'
+        assert logSpy.errorParameter   == 'error   called'
+    }
+
+    void testInheritancePrivateNoShadowingIssue() {
+        def clazz = new GroovyShell().evaluate('''
+            class MyParentTestInheritance {
+                private log
+            }
+
+            @groovy.util.logging.PlatformLog
+            class MyClassTestInheritance extends MyParentTestInheritance {
+                def loggingMethod() {
+                    log.info   (prepareLogMessage())
+                }
+                def prepareLogMessage() {
+                    'formatted log message'
+                }
+            }
+
+            return MyClassTestInheritance
+        ''')
+
+        assert clazz.declaredFields.find { Field field ->
+            field.name == 'log' &&
+                    Modifier.isPrivate(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                    Modifier.isTransient(field.getModifiers()) &&
+                    Modifier.isFinal(field.getModifiers())
+        }
+    }
+
+    void testInheritanceProtectedShadowing() {
+        def msg = shouldFail(MultipleCompilationErrorsException) {
+            new GroovyClassLoader().parseClass('''
+                class MyParentProtectedShadowing {
+                    protected log
+                }
+
+                @groovy.util.logging.PlatformLog
+                class MyClassProtectedShadowing extends MyParentProtectedShadowing { }
+            ''')
+        }
+        assert msg.contains('cannot have log field declared because the field exists in the parent class')
+    }
+
+    void testInheritancePublicShadowing() {
+        def msg = shouldFail(MultipleCompilationErrorsException) {
+            new GroovyClassLoader().parseClass('''
+                class MyParentPublicShadowing {
+                    public log
+                }
+
+                @groovy.util.logging.PlatformLog
+                class MyClassPublicShadowing extends MyParentPublicShadowing {
+                }
+            ''')
+        }
+        assert msg.contains('cannot have log field declared because the field exists in the parent class')
+    }
+}
+