[
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)