[
https://issues.apache.org/jira/browse/GROOVY-11974?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18077202#comment-18077202
]
ASF GitHub Bot commented on GROOVY-11974:
-----------------------------------------
Copilot commented on code in PR #2502:
URL: https://github.com/apache/groovy/pull/2502#discussion_r3165511139
##########
src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java:
##########
@@ -183,6 +183,31 @@ public static boolean recordNative(final ClassNode node) {
return node.getUnresolvedSuperClass() != null &&
RECORD_CLASS_NAME.equals(node.getUnresolvedSuperClass().getName());
}
+ /**
+ * Predicts whether {@code cNode} will be compiled as a native JVM record.
+ * Mirrors the decision logic in {@link #doProcessRecordType} (presence of
+ * {@code @RecordBase}, target bytecode {@code >= 16}, and
+ * {@code @RecordOptions(mode != EMULATE)}) so callers running before the
+ * transform itself fires can act on the same outcome.
+ * <p>
+ * The joint-compilation stub generator uses this at {@code
Phases.CONVERSION}
+ * to decide whether to emit {@code record Foo(...)} syntax. Unlike
+ * {@link #recordNative(ClassNode)}, which inspects the post-transform
+ * super class, this predicate inspects only the source-level annotations.
+ *
+ * @param cNode the candidate class node
+ * @param targetBytecode the target bytecode level
+ * (see {@link
CompilerConfiguration#getTargetBytecode()})
+ */
+ @Incubating
+ public static boolean wouldBeNativeRecord(final ClassNode cNode, final
String targetBytecode) {
+ if (cNode == null || cNode.getAnnotations(MY_TYPE).isEmpty()) return
false;
Review Comment:
`wouldBeNativeRecord` calls `StringGroovyMethods.isAtLeast(targetBytecode,
...)`, which throws if `targetBytecode` is null (it constructs `new
BigDecimal(left)`). Since the method is public API and callers may not always
have a target bytecode (and this class already has messaging for the 'unable to
determine target bytecode' case), add a null/blank guard (return false) before
calling `isAtLeast`.
```suggestion
if (cNode == null || cNode.getAnnotations(MY_TYPE).isEmpty()) return
false;
if (targetBytecode == null || targetBytecode.trim().isEmpty())
return false;
```
##########
src/test/groovy/org/codehaus/groovy/tools/stubgenerator/RecordTypeJointCompilationStubTest.groovy:
##########
@@ -0,0 +1,64 @@
+/*
+ * 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.tools.stubgenerator
+
+/**
+ * GROOVY-11974: native records in joint compilation must produce a stub
+ * that javac compiles as a record, so that Java callers see the canonical
+ * constructor and the {@code componentName()} accessors.
+ */
+class RecordTypeJointCompilationStubTest extends StringSourcesStubTestCase {
+
+ @Override
+ protected void configure() {
Review Comment:
This stub test uses Java record syntax and `Class::isRecord`, which requires
running on JDK 16+. To keep the test suite green when executed on older JDKs
(where `javac` can't compile records), add a JUnit assumption (e.g.,
`assumeTrue(isAtLeastJdk('16.0'))`) early in the test (such as in `init()` or
`configure()`) so it is skipped when records aren't supported.
```suggestion
private static boolean isAtLeastJdk(final String version) {
List<Integer> current =
System.getProperty('java.specification.version')
.tokenize('.')
.collect { it as Integer }
List<Integer> required = version.tokenize('.')
.collect { it as Integer }
int max = Math.max(current.size(), required.size())
for (int i = 0; i < max; i += 1) {
int currentPart = (i < current.size() ? current[i] : 0)
int requiredPart = (i < required.size() ? required[i] : 0)
if (currentPart != requiredPart) {
return currentPart > requiredPart
}
}
true
}
@Override
protected void configure() {
org.junit.Assume.assumeTrue(isAtLeastJdk('16.0'))
```
##########
src/test/groovy/org/codehaus/groovy/tools/stubgenerator/RecordTypeJointCompilationStubTest.groovy:
##########
@@ -0,0 +1,64 @@
+/*
+ * 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.tools.stubgenerator
+
+/**
+ * GROOVY-11974: native records in joint compilation must produce a stub
+ * that javac compiles as a record, so that Java callers see the canonical
+ * constructor and the {@code componentName()} accessors.
+ */
+class RecordTypeJointCompilationStubTest extends StringSourcesStubTestCase {
+
+ @Override
+ protected void configure() {
+ super.configure()
+ config.targetBytecode = '17'
+ }
+
+ Map<String, String> provideSources() {
+ [
+ 'JavaCaller.java': '''
+ public class JavaCaller {
+ public static int sumViaRecordAccessors() {
+ Point p = new Point(3, 4);
+ return p.x() + p.y();
+ }
+ public static String describe() {
+ return new Point(1, 2).getClass().isRecord()
+ ? "record" : "class";
+ }
+ }
+ ''',
+ 'Point.groovy': '''
+ record Point(int x, int y) {}
+ '''
+ ]
+ }
+
+ void verifyStubs() {
+ def stub = stubJavaSourceFor('Point')
+ // header is rendered as record syntax, not class
+ assert stub =~ /\brecord\s+Point\s*\(\s*int\s+x\s*,\s*int\s+y\s*\)/
+ // no `class Point`, no `extends`
+ assert !(stub =~ /\bclass\s+Point\b/)
+ assert !(stub =~ /\bextends\s+java\.lang\.Object\b/)
+ // no instance-field declarations
+ assert !(stub =~ /(?m)^\s*(?:private|protected|public)?\s*int\s+x\s*;/)
Review Comment:
The 'no instance-field declarations' assertion only checks for an `int x;`
field and doesn't cover `y` (or modifiers like `final`). If the stub
accidentally emitted `private final int x;` or `int y;`, this test would still
pass. Consider broadening the regex (e.g., allow optional `final` and check
both component names) to make the test reliably detect any instance field
emission for record components.
```suggestion
// no instance-field declarations for record components
assert !(stub =~
/(?m)^\s*(?:private|protected|public)?\s*(?:final\s+)?int\s+(?:x|y)\s*;/)
```
> stub generator could emit records into stubs when in native records mode
> ------------------------------------------------------------------------
>
> Key: GROOVY-11974
> URL: https://issues.apache.org/jira/browse/GROOVY-11974
> Project: Groovy
> Issue Type: Improvement
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)