[ 
https://issues.apache.org/jira/browse/GROOVY-11914?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18072468#comment-18072468
 ] 

ASF GitHub Bot commented on GROOVY-11914:
-----------------------------------------

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


##########
subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/PurityChecker.groovy:
##########
@@ -0,0 +1,442 @@
+/*
+ *  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.typecheckers
+
+import org.apache.groovy.ast.tools.ImmutablePropertyUtils
+import org.apache.groovy.lang.annotation.Incubating
+import org.apache.groovy.typecheckers.CheckingVisitor
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.FieldNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.Variable
+import org.codehaus.groovy.ast.expr.BinaryExpression
+import org.codehaus.groovy.ast.expr.ClassExpression
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression
+import org.codehaus.groovy.ast.expr.DeclarationExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.PostfixExpression
+import org.codehaus.groovy.ast.expr.PrefixExpression
+import org.codehaus.groovy.ast.expr.PropertyExpression
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import static org.codehaus.groovy.syntax.Types.isAssignment
+
+/**
+ * A compile-time checker that verifies {@code @Pure} methods have no side 
effects.
+ * <p>
+ * By default, strict purity is enforced: no field mutations, no I/O, no 
logging,
+ * no non-deterministic calls. The {@code allows} option declares which effect
+ * categories are tolerated:
+ * <pre>
+ * // Strict: no side effects at all
+ * {@code @TypeChecked(extensions = 'groovy.typecheckers.PurityChecker')}
+ *
+ * // Tolerate logging and metrics
+ * {@code @TypeChecked(extensions = 'groovy.typecheckers.PurityChecker(allows: 
"LOGGING|METRICS")')}
+ * </pre>
+ * <p>
+ * Effect categories:
+ * <ul>
+ *   <li>{@code LOGGING} — calls to logging frameworks (SLF4J, JUL, etc.) and 
{@code println}</li>
+ *   <li>{@code METRICS} — calls to metrics instruments (Micrometer, 
OpenTelemetry, etc.)</li>
+ *   <li>{@code IO} — file, network, database, and console I/O</li>
+ *   <li>{@code NONDETERMINISM} — time-dependent, random, and 
environment-dependent calls</li>
+ * </ul>
+ * <p>
+ * Also recognises:
+ * <ul>
+ *   <li>{@code @SideEffectFree} (Checker Framework) — treated as {@code 
@Pure} with implicit NONDETERMINISM allowed</li>
+ *   <li>{@code @Contract(pure = true)} (JetBrains) — treated as {@code 
@Pure}</li>
+ *   <li>{@code @Memoized} — treated as effectively pure</li>
+ * </ul>
+ *
+ * @since 6.0.0
+ * @see groovy.transform.Pure
+ */
+@Incubating
+class PurityChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL 
{
+
+    private static final Set<String> PURE_ANNOS = Set.of('Pure')
+    private static final Set<String> SIDE_EFFECT_FREE_ANNOS = 
Set.of('SideEffectFree')
+    private static final Set<String> CONTRACT_ANNOS = Set.of('Contract')
+    private static final Set<String> MEMOIZED_ANNOS = Set.of('Memoized')
+
+    // Methods on mutable types known to be pure (no mutation, no effects, no 
closures)
+    private static final Set<String> KNOWN_PURE_METHODS = Set.of(
+            // Object fundamentals
+            'toString', 'hashCode', 'equals', 'compareTo', 'getClass',
+            // Collection/Map queries
+            'size', 'length', 'isEmpty', 'contains', 'containsKey', 
'containsValue',
+            'get', 'getAt', 'getOrDefault', 'indexOf', 'lastIndexOf',
+            'iterator', 'listIterator', 'spliterator', 'stream', 
'parallelStream',
+            'toArray', 'subList', 'keySet', 'values', 'entrySet',
+            'first', 'last', 'head', 'tail', 'init',
+            'asBoolean', 'is', 'isCase',
+            // Type info
+            'getMetaClass', 'respondsTo', 'hasProperty',
+    )
+
+    // Known non-deterministic static methods (class.method)
+    private static final Map<String, Set<String>> 
NONDETERMINISTIC_STATIC_METHODS = [
+            'java.lang.System'        : Set.of('nanoTime', 
'currentTimeMillis', 'getProperty', 'getenv'),
+            'java.lang.Math'          : Set.of('random'),
+            'java.util.UUID'          : Set.of('randomUUID'),
+            'java.time.Instant'       : Set.of('now'),
+            'java.time.LocalDateTime' : Set.of('now'),
+            'java.time.LocalDate'     : Set.of('now'),
+            'java.time.LocalTime'     : Set.of('now'),
+            'java.time.ZonedDateTime' : Set.of('now'),
+            'java.time.OffsetDateTime': Set.of('now'),
+            'java.time.OffsetTime'    : Set.of('now'),
+            'java.time.Year'          : Set.of('now'),
+            'java.time.YearMonth'     : Set.of('now'),
+            'java.time.MonthDay'      : Set.of('now'),
+    ]
+
+    // Non-deterministic no-arg constructors
+    private static final Set<String> NONDETERMINISTIC_CONSTRUCTORS = Set.of(
+            'java.util.Date',
+            'java.util.Random',
+    )
+
+    // Instance method that is non-deterministic
+    private static final Map<String, Set<String>> 
NONDETERMINISTIC_INSTANCE_METHODS = [
+            'java.util.concurrent.ThreadLocalRandom': Set.of('current'),
+    ]
+
+    // Logging receiver type prefixes
+    private static final List<String> LOGGING_TYPE_PREFIXES = [
+            'org.slf4j.Logger',
+            'java.util.logging.Logger',
+            'org.apache.commons.logging.Log',
+            'org.apache.log4j.Logger',
+            'org.apache.logging.log4j.Logger',
+            'java.lang.System.Logger',
+    ]
+
+    // Logging method names (on implicit this or any receiver)
+    private static final Set<String> LOGGING_METHOD_NAMES = Set.of(
+            'println', 'print', 'printf',
+    )
+
+    // Metrics receiver type prefixes
+    private static final List<String> METRICS_TYPE_PREFIXES = [
+            'io.micrometer.core.instrument',
+            'io.opentelemetry.api.metrics',
+            'com.codahale.metrics',
+            'org.eclipse.microprofile.metrics',
+    ]
+
+    // I/O type prefixes
+    private static final List<String> IO_TYPE_PREFIXES = [
+            'java.io.',
+            'java.nio.',
+            'java.net.',
+            'java.sql.',
+            'javax.sql.',
+            'groovy.io.',
+    ]
+
+    // I/O class names for constructor detection
+    private static final List<String> IO_CONSTRUCTOR_PREFIXES = [
+            'java.io.',
+            'java.nio.',
+            'java.net.',
+            'java.sql.',
+    ]
+
+    @Override
+    Object run() {
+        Set<String> baseAllows = parseAllows(options?.allows as String)
+
+        afterVisitMethod { MethodNode mn ->
+            Set<String> allows = baseAllows
+
+            if (hasPureAnno(mn) || hasMemoizedAnno(mn) || 
hasContractPureAnno(mn)) {
+                // strict purity (or whatever baseAllows says)
+            } else if (hasSideEffectFreeAnno(mn)) {
+                // @SideEffectFree implies NONDETERMINISM is allowed
+                allows = new HashSet<>(baseAllows)
+                allows.add('NONDETERMINISM')
+            } else {
+                return // no purity annotation — nothing to check
+            }
+
+            mn.code?.visit(makeVisitor(allows, mn))
+        }
+    }
+
+    private static Set<String> parseAllows(String allowsStr) {
+        if (!allowsStr) return Collections.emptySet()
+        allowsStr.split('\\|')*.trim()*.toUpperCase() as Set<String>
+    }
+
+    private static boolean hasPureAnno(MethodNode method) {
+        method.annotations?.any { it.classNode?.nameWithoutPackage in 
PURE_ANNOS } ?: false
+    }
+
+    private static boolean hasSideEffectFreeAnno(MethodNode method) {
+        method.annotations?.any { it.classNode?.nameWithoutPackage in 
SIDE_EFFECT_FREE_ANNOS } ?: false
+    }
+
+    /**
+     * Checks for {@code @Contract(pure = true)} (JetBrains annotations).
+     * Works with CLASS retention since annotation nodes are available during 
type checking.
+     */
+    private static boolean hasContractPureAnno(MethodNode method) {
+        method.annotations?.any { anno ->
+            anno.classNode?.nameWithoutPackage in CONTRACT_ANNOS &&
+                    anno.getMember('pure')?.text == 'true'
+        } ?: false
+    }
+
+    private static boolean hasMemoizedAnno(MethodNode method) {
+        method.annotations?.any { it.classNode?.nameWithoutPackage in 
MEMOIZED_ANNOS } ?: false
+    }
+
+    private CheckingVisitor makeVisitor(Set<String> allows, MethodNode 
methodNode) {
+        boolean allowLogging = 'LOGGING' in allows
+        boolean allowMetrics = 'METRICS' in allows
+        boolean allowIO = 'IO' in allows
+        boolean allowNondeterminism = 'NONDETERMINISM' in allows
+
+        new CheckingVisitor() {
+
+            @Override
+            void visitDeclarationExpression(DeclarationExpression decl) {
+                super.visitDeclarationExpression(decl)
+                // Local variable declarations are always fine
+            }
+
+            @Override
+            void visitBinaryExpression(BinaryExpression expression) {
+                super.visitBinaryExpression(expression)
+                if (isAssignment(expression.operation.type)) {
+                    checkFieldWrite(expression.leftExpression, expression)
+                }
+            }
+
+            @Override
+            void visitPostfixExpression(PostfixExpression expression) {
+                super.visitPostfixExpression(expression)
+                checkFieldWrite(expression.expression, expression)
+            }
+
+            @Override
+            void visitPrefixExpression(PrefixExpression expression) {
+                super.visitPrefixExpression(expression)
+                checkFieldWrite(expression.expression, expression)
+            }
+
+            @Override
+            void visitMethodCallExpression(MethodCallExpression call) {
+                super.visitMethodCallExpression(call)
+                checkInstanceCall(call)
+            }
+
+            @Override
+            void visitStaticMethodCallExpression(StaticMethodCallExpression 
call) {
+                super.visitStaticMethodCallExpression(call)
+                checkStaticCall(call)
+            }
+
+            @Override
+            void visitConstructorCallExpression(ConstructorCallExpression 
call) {
+                super.visitConstructorCallExpression(call)
+                checkConstructorCall(call)
+            }
+
+            // --

> Add PurityChecker type checking extension
> -----------------------------------------
>
>                 Key: GROOVY-11914
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11914
>             Project: Groovy
>          Issue Type: New Feature
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> I asked AI what features could be added to Groovy to give it vastly improved 
> reasoning capabilities of Groovy code. First up it suggested @Modifies 
> (GROOVY-11909) and the ModifiesChecker (GROOVY-11910). It said a close second 
> would be a PurityChecker type checking extension. This issue looks at that.
> The key thing to note here is that this is an opt-in extension.
> If you use it, along with the existing @Pure annotation, it allows humans and 
> AI to make assumptions about class or method behavior without reading the 
> respective body.
> The checker doesn't provide fool-proof purity checking. It just covers the 
> common cases.
> You may need to not enable the extension on code which falls outside those 
> common cases.
> It just means that the human and/or AI will need to look inside the method 
> body to reason about its purity.
> *Strict:*
> {code:groovy}
> @TypeChecked(extensions = 'groovy.typecheckers.PurityChecker')
> class MathUtils {
>     @Pure
>     int square(int x) { x * x }                             // OK
>     @Pure
>     long timestamp() { System.nanoTime() }                   // ERROR: 
> non-deterministic
>     @Pure
>     int logged(int x) { println("x=$x"); x * 2 }            // ERROR: logging
> }
> {code}
> ----
> *Allowing logging:*
> {code:groovy}
> @TypeChecked(extensions = 'groovy.typecheckers.PurityChecker(allows: 
> "LOGGING")')
> class Service {
>     @Pure
>     int compute(int x) {
>         println("computing $x")          // OK: logging allowed
>         return x * x
>     }
>     @Pure
>     long getTime() { System.nanoTime() } // ERROR: non-deterministic (not 
> allowed)
> }
> {code}
> ----
> *Allowing logging and non-determinism:*
> {code:groovy}
> @TypeChecked(extensions = 'groovy.typecheckers.PurityChecker(allows: 
> "LOGGING|NONDETERMINISM")')
> class Diagnostics {
>     @Pure
>     String snapshot(Map state) {
>         println("snapshot at ${System.nanoTime()}")   // OK: both allowed
>         return state.toString()
>     }
> }
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to