jdaugherty commented on code in PR #431:
URL: 
https://github.com/apache/grails-intellij-plugin/pull/431#discussion_r4175895206


##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)

Review Comment:
   Fixed in a9fd382. `isPluginDescriptor` now applies the 
`isGrailsPluginDescriptorClass` rule: the name ends with `GrailsPlugin` and the 
class isn't abstract. It's used both for editor support (`isImplicitHost`) and 
in `findBeansHosts`, which now searches by the suffix with `AllClassesSearch` 
the way the discoverer does. `testPluginDescriptorRecognisedByName` covers an 
abstract descriptor, one without the suffix, and one that doesn't extend 
`Plugin`.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);

Review Comment:
   Fixed in a9fd382. On the implicit hosts, the closure is only claimed when a 
top-level statement has a `bean`/`field`/`method`/`group` call along its chain 
(the same walk as `isBeansDslStatement`), or when it has no statements. 
`@GrailsBeans` classes skip the check. One addition over the compiler: a bare 
name on its own line counts as a declaration still being typed, so completion 
keeps working in an empty closure. Covered by 
`testUnrelatedBeansClosureOnImplicitHost`.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDslMemberContributor.java:
##########
@@ -0,0 +1,278 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.ResolveState;
+import com.intellij.psi.scope.ElementClassHint;
+import com.intellij.psi.scope.PsiScopeProcessor;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
+import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightField;
+import 
org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder;
+import org.jetbrains.plugins.groovy.lang.resolve.NonCodeMembersContributor;
+import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil;
+import org.jetbrains.plugins.groovy.util.dynamicMembers.DynamicMemberUtils;
+
+/**
+ * Resolution, completion and type inference for the Grails 8 beans DSL (see 
{@link GrailsBeansDsl}):
+ * <ul>
+ *   <li>directly inside a {@code beans} closure or one of its groups, the 
{@code bean}/{@code field}/{@code method}/
+ *   {@code group} declarations, whose results carry the qualifiers each of 
them chains with;</li>
+ *   <li>inside a {@code bean(...)} or {@code method(...)} body, the members 
declared by the {@code field(...)} and
+ *   {@code method(...)} declarations beside it, which the compiled factory 
methods share.</li>
+ * </ul>
+ */
+public final class GrailsBeansDslMemberContributor extends 
NonCodeMembersContributor {
+
+  public static final String MEMBER_ORIGIN_INFO = "via Grails beans DSL";
+
+  /**
+   * The declarations and their qualifier chains. Every qualifier that takes 
arguments takes {@code Object...}, so
+   * named attributes, types, String class names and the trailing body closure 
are all accepted.
+   */
+  static final String DSL_SOURCE = """
+    class GrailsBeansDsl {
+      /** Declares a bean named after the decapitalized simple name of {@code 
type}, built by its no-argument constructor. */
+      BeanDeclaration bean(Class type) {}
+      /** Declares a bean named after the decapitalized simple name of {@code 
type}. The closure's typed parameters are injected; the closure body builds the 
bean, or, left empty, the constructor taking the parameters is called. */
+      BeanDeclaration bean(Class type, Closure body) {}
+      /** Declares a bean named {@code name}, built by the no-argument 
constructor of {@code type}. */
+      BeanDeclaration bean(String name, Class type) {}
+      /** Declares a bean named {@code name}. The closure's typed parameters 
are injected; the closure body builds the bean, or, left empty, the constructor 
taking the parameters is called. */
+      BeanDeclaration bean(String name, Class type, Closure body) {}
+      /** Declares a bean of type {@code type} built as {@code new 
implementation()}. */
+      BeanDeclaration bean(Class type, Class implementation) {}
+      /** Declares a bean of type {@code type} built by the {@code 
implementation} constructor taking the closure's typed parameters. */
+      BeanDeclaration bean(Class type, Class implementation, Closure 
parameters) {}
+      /** Declares a bean named {@code name} of type {@code type} built as 
{@code new implementation()}. */
+      BeanDeclaration bean(String name, Class type, Class implementation) {}
+      /** Declares a bean named {@code name} of type {@code type} built by the 
{@code implementation} constructor taking the closure's typed parameters. */
+      BeanDeclaration bean(String name, Class type, Class implementation, 
Closure parameters) {}
+
+      /** Declares a private field, shared by the bean and helper methods, 
named after the decapitalized simple name of {@code type}. */
+      FieldDeclaration field(Class type) {}
+      /** Declares a private field, shared by the bean and helper methods. */
+      FieldDeclaration field(String name, Class type) {}
+
+      /** Declares a private helper method, named after the decapitalized 
simple name of {@code type}, returning {@code type}. */
+      MethodDeclaration method(Class type) {}
+      /** Declares a private helper method, named after the decapitalized 
simple name of {@code type}, whose parameters and body are the closure's. */
+      MethodDeclaration method(Class type, Closure body) {}
+      /** Declares a private helper method returning {@code type}. */
+      MethodDeclaration method(String name, Class type) {}
+      /** Declares a private helper method returning {@code type}, whose 
parameters and body are the closure's. */
+      MethodDeclaration method(String name, Class type, Closure body) {}
+
+      /** Declares a nested configuration class holding the declarations in 
its body, conditioned as a whole. */
+      GroupDeclaration group() {}

Review Comment:
   Fixed in a9fd382. The nameless `group()` and `group(Closure)` signatures are 
gone. Group bodies get a version of the DSL without `group`, so a nested 
`group(...)` doesn't resolve. One gap remains: `group()` with no arguments 
still matches `group(String)`, because Groovy lets a call with no arguments 
reach a one-parameter method with `null`. `group { ... }` is reported as not 
applicable. Tests: `testGroupRequiresName`, `testGroupsDoNotNest`, 
`testGroupBodyCompletion`.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);
+  }
+
+  /**
+   * Whether DSL declarations are written directly inside the closure: the 
{@code beans} closure itself, or the
+   * body of one of its top-level {@code group(...)} declarations.
+   */
+  public static boolean isDeclarationContainer(@NotNull GrClosableBlock 
closure) {
+    if (isBeansClosure(closure)) return true;
+    Declaration declaration = getDeclarationOfBody(closure);
+    return declaration != null && declaration.getKind() == Kind.GROUP;
+  }
+
+  /**
+   * The declaration the closure is the body of, when it is the body of a 
{@code bean(...)}, {@code method(...)}
+   * or {@code group(...)} declared directly in a {@code beans} closure or in 
one of its groups.
+   */
+  public static @Nullable Declaration getDeclarationOfBody(@NotNull 
GrClosableBlock closure) {
+    PsiElement parent = closure.getParent();
+    if (parent instanceof GrArgumentList) parent = parent.getParent();
+    if (!(parent instanceof GrMethodCall call)) return null;
+
+    GrMethodCall statement = getChainStatement(call);
+    if (!(statement.getParent() instanceof GrClosableBlock container)) return 
null;
+
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getStatement() == statement) {
+        if (declaration.getKind() == Kind.FIELD) return null;
+        // Groups do not nest
+        boolean declared = declaration.getKind() == Kind.GROUP ? 
isBeansClosure(container) : isDeclarationContainer(container);
+        return declared ? declaration : null;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * The DSL declarations written directly inside the closure. The closure is 
not checked to be a declaration
+   * container: see {@link #isDeclarationContainer(GrClosableBlock)}.
+   */
+  public static @NotNull List<Declaration> getDeclarations(@NotNull 
GrClosableBlock container) {
+    return CachedValuesManager.getCachedValue(container, () -> 
Result.create(computeDeclarations(container), container));
+  }
+
+  private static @NotNull List<Declaration> computeDeclarations(@NotNull 
GrClosableBlock container) {
+    List<Declaration> result = new ArrayList<>();
+    for (PsiElement statement : container.getStatements()) {
+      if (!(statement instanceof GrMethodCall call)) continue;
+      GrMethodCall root = getChainRoot(call);
+      if (root == null) continue;
+      Kind kind = Kind.byCallName(getUnqualifiedCallName(root));
+      if (kind != null) {
+        result.add(new Declaration(kind, root, call));
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  /**
+   * The beans the class declares through the DSL, including those inside its 
groups.
+   */
+  public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> 
getBeanDescriptors(@NotNull PsiClass aClass) {
+    GrClosableBlock beansClosure = getBeansClosure(aClass);
+    if (beansClosure == null) return Collections.emptyList();
+
+    List<GrailsResourceBeanExtractor.BeanDescriptor> result = new 
ArrayList<>();
+    collectBeanDescriptors(beansClosure, result);
+    return result;
+  }
+
+  private static void collectBeanDescriptors(@NotNull GrClosableBlock 
container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) {
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getKind() == Kind.GROUP) {
+        GrClosableBlock body = declaration.getBody();
+        if (body != null) collectBeanDescriptors(body, result);
+        continue;
+      }
+      if (declaration.getKind() != Kind.BEAN) continue;
+
+      String name = declaration.getName();

Review Comment:
   Fixed in a9fd382. `getBeanDescriptors` is now a `CachedValue` on the `beans` 
closure. It's invalidated by `MODIFICATION_COUNT`, since names fold constants 
and resolve types from other files. The computation runs under 
`RecursionManager.doPreventingRecursion` keyed on the closure, so name folding 
is guarded too. `testBeanDescriptors` asserts the same list comes back on a 
second call.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);
+  }
+
+  /**
+   * Whether DSL declarations are written directly inside the closure: the 
{@code beans} closure itself, or the
+   * body of one of its top-level {@code group(...)} declarations.
+   */
+  public static boolean isDeclarationContainer(@NotNull GrClosableBlock 
closure) {
+    if (isBeansClosure(closure)) return true;
+    Declaration declaration = getDeclarationOfBody(closure);
+    return declaration != null && declaration.getKind() == Kind.GROUP;
+  }
+
+  /**
+   * The declaration the closure is the body of, when it is the body of a 
{@code bean(...)}, {@code method(...)}
+   * or {@code group(...)} declared directly in a {@code beans} closure or in 
one of its groups.
+   */
+  public static @Nullable Declaration getDeclarationOfBody(@NotNull 
GrClosableBlock closure) {
+    PsiElement parent = closure.getParent();
+    if (parent instanceof GrArgumentList) parent = parent.getParent();
+    if (!(parent instanceof GrMethodCall call)) return null;
+
+    GrMethodCall statement = getChainStatement(call);
+    if (!(statement.getParent() instanceof GrClosableBlock container)) return 
null;
+
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getStatement() == statement) {
+        if (declaration.getKind() == Kind.FIELD) return null;
+        // Groups do not nest
+        boolean declared = declaration.getKind() == Kind.GROUP ? 
isBeansClosure(container) : isDeclarationContainer(container);
+        return declared ? declaration : null;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * The DSL declarations written directly inside the closure. The closure is 
not checked to be a declaration
+   * container: see {@link #isDeclarationContainer(GrClosableBlock)}.
+   */
+  public static @NotNull List<Declaration> getDeclarations(@NotNull 
GrClosableBlock container) {
+    return CachedValuesManager.getCachedValue(container, () -> 
Result.create(computeDeclarations(container), container));
+  }
+
+  private static @NotNull List<Declaration> computeDeclarations(@NotNull 
GrClosableBlock container) {
+    List<Declaration> result = new ArrayList<>();
+    for (PsiElement statement : container.getStatements()) {
+      if (!(statement instanceof GrMethodCall call)) continue;
+      GrMethodCall root = getChainRoot(call);
+      if (root == null) continue;
+      Kind kind = Kind.byCallName(getUnqualifiedCallName(root));
+      if (kind != null) {
+        result.add(new Declaration(kind, root, call));
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  /**
+   * The beans the class declares through the DSL, including those inside its 
groups.
+   */
+  public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> 
getBeanDescriptors(@NotNull PsiClass aClass) {
+    GrClosableBlock beansClosure = getBeansClosure(aClass);
+    if (beansClosure == null) return Collections.emptyList();
+
+    List<GrailsResourceBeanExtractor.BeanDescriptor> result = new 
ArrayList<>();
+    collectBeanDescriptors(beansClosure, result);
+    return result;
+  }
+
+  private static void collectBeanDescriptors(@NotNull GrClosableBlock 
container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) {
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getKind() == Kind.GROUP) {
+        GrClosableBlock body = declaration.getBody();
+        if (body != null) collectBeanDescriptors(body, result);
+        continue;
+      }
+      if (declaration.getKind() != Kind.BEAN) continue;
+
+      String name = declaration.getName();
+      if (name == null || !(declaration.getTypeExpression() instanceof 
GrReferenceExpression typeReference)) continue;
+
+      GrailsResourceBeanExtractor.BeanDescriptor descriptor = new 
GrailsResourceBeanExtractor.BeanDescriptor(name);
+      descriptor.getReferences().add(typeReference);
+      result.add(descriptor);
+    }
+  }
+
+  /**
+   * The classes in the module (and the modules it depends on) whose {@code 
beans} property is compiled as the DSL.
+   */
+  public static @NotNull Collection<PsiClass> findBeansHosts(@NotNull Module 
module) {
+    JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject());
+    GlobalSearchScope librariesScope = 
GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
+    PsiClass annotation = facade.findClass(GRAILS_BEANS_ANNOTATION, 
librariesScope);
+    if (annotation == null) return Collections.emptyList();
+
+    GlobalSearchScope sourceScope = 
GlobalSearchScope.moduleWithDependenciesScope(module);
+    Set<PsiClass> result = new 
LinkedHashSet<>(AnnotatedElementsSearch.searchPsiClasses(annotation, 
sourceScope).findAll());
+    for (String superClassName : new String[]{GRAILS_AUTO_CONFIGURATION, 
PLUGIN_CLASS}) {
+      PsiClass superClass = facade.findClass(superClassName, librariesScope);
+      if (superClass != null) {
+        result.addAll(ClassInheritorsSearch.search(superClass, sourceScope, 
true).findAll());
+      }
+    }
+    return result;
+  }
+
+  /**
+   * The outermost call of a qualifier chain such as {@code 
bean(Foo).primary().lazy()}, given any call in it.
+   */
+  private static @NotNull GrMethodCall getChainStatement(@NotNull GrMethodCall 
call) {
+    GrMethodCall result = call;
+    while (result.getParent() instanceof GrReferenceExpression ref
+           && ref.getQualifierExpression() == result
+           && ref.getParent() instanceof GrMethodCall outer
+           && outer.getInvokedExpression() == ref) {
+      result = outer;
+    }
+    return result;
+  }
+
+  /**
+   * The unqualified call at the root of a qualifier chain such as {@code 
bean(Foo).primary().lazy()}.
+   */
+  private static @Nullable GrMethodCall getChainRoot(@NotNull GrMethodCall 
call) {
+    GrMethodCall result = call;
+    while (true) {
+      if (!(result.getInvokedExpression() instanceof GrReferenceExpression 
ref)) return null;
+      GrExpression qualifier = ref.getQualifierExpression();
+      if (qualifier == null) return result;
+      if (!(qualifier instanceof GrMethodCall qualifierCall)) return null;
+      result = qualifierCall;
+    }
+  }
+
+  private static @Nullable String getUnqualifiedCallName(@NotNull GrMethodCall 
call) {
+    return call.getInvokedExpression() instanceof GrReferenceExpression ref && 
!ref.isQualified() ? ref.getReferenceName() : null;
+  }
+
+  private static boolean isTypeArgument(@NotNull GrExpression expression) {
+    if (!(expression instanceof GrReferenceExpression ref)) return false;
+    PsiElement resolved = ref.resolve();

Review Comment:
   Fixed in a9fd382. A `Foo.class` argument is unwrapped to `Foo` wherever a 
type is read: the declared type, `.typeArguments(...)` and the bean 
descriptors. Covered by `testClassLiteralTypes`.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);
+  }
+
+  /**
+   * Whether DSL declarations are written directly inside the closure: the 
{@code beans} closure itself, or the
+   * body of one of its top-level {@code group(...)} declarations.
+   */
+  public static boolean isDeclarationContainer(@NotNull GrClosableBlock 
closure) {
+    if (isBeansClosure(closure)) return true;
+    Declaration declaration = getDeclarationOfBody(closure);
+    return declaration != null && declaration.getKind() == Kind.GROUP;
+  }
+
+  /**
+   * The declaration the closure is the body of, when it is the body of a 
{@code bean(...)}, {@code method(...)}
+   * or {@code group(...)} declared directly in a {@code beans} closure or in 
one of its groups.
+   */
+  public static @Nullable Declaration getDeclarationOfBody(@NotNull 
GrClosableBlock closure) {
+    PsiElement parent = closure.getParent();
+    if (parent instanceof GrArgumentList) parent = parent.getParent();
+    if (!(parent instanceof GrMethodCall call)) return null;
+
+    GrMethodCall statement = getChainStatement(call);
+    if (!(statement.getParent() instanceof GrClosableBlock container)) return 
null;
+
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getStatement() == statement) {
+        if (declaration.getKind() == Kind.FIELD) return null;
+        // Groups do not nest
+        boolean declared = declaration.getKind() == Kind.GROUP ? 
isBeansClosure(container) : isDeclarationContainer(container);
+        return declared ? declaration : null;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * The DSL declarations written directly inside the closure. The closure is 
not checked to be a declaration
+   * container: see {@link #isDeclarationContainer(GrClosableBlock)}.
+   */
+  public static @NotNull List<Declaration> getDeclarations(@NotNull 
GrClosableBlock container) {
+    return CachedValuesManager.getCachedValue(container, () -> 
Result.create(computeDeclarations(container), container));
+  }
+
+  private static @NotNull List<Declaration> computeDeclarations(@NotNull 
GrClosableBlock container) {
+    List<Declaration> result = new ArrayList<>();
+    for (PsiElement statement : container.getStatements()) {
+      if (!(statement instanceof GrMethodCall call)) continue;
+      GrMethodCall root = getChainRoot(call);
+      if (root == null) continue;
+      Kind kind = Kind.byCallName(getUnqualifiedCallName(root));
+      if (kind != null) {
+        result.add(new Declaration(kind, root, call));
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  /**
+   * The beans the class declares through the DSL, including those inside its 
groups.
+   */
+  public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> 
getBeanDescriptors(@NotNull PsiClass aClass) {
+    GrClosableBlock beansClosure = getBeansClosure(aClass);
+    if (beansClosure == null) return Collections.emptyList();
+
+    List<GrailsResourceBeanExtractor.BeanDescriptor> result = new 
ArrayList<>();
+    collectBeanDescriptors(beansClosure, result);
+    return result;
+  }
+
+  private static void collectBeanDescriptors(@NotNull GrClosableBlock 
container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) {
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getKind() == Kind.GROUP) {
+        GrClosableBlock body = declaration.getBody();
+        if (body != null) collectBeanDescriptors(body, result);
+        continue;
+      }
+      if (declaration.getKind() != Kind.BEAN) continue;
+
+      String name = declaration.getName();
+      if (name == null || !(declaration.getTypeExpression() instanceof 
GrReferenceExpression typeReference)) continue;
+
+      GrailsResourceBeanExtractor.BeanDescriptor descriptor = new 
GrailsResourceBeanExtractor.BeanDescriptor(name);
+      descriptor.getReferences().add(typeReference);
+      result.add(descriptor);
+    }
+  }
+
+  /**
+   * The classes in the module (and the modules it depends on) whose {@code 
beans} property is compiled as the DSL.
+   */
+  public static @NotNull Collection<PsiClass> findBeansHosts(@NotNull Module 
module) {
+    JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject());
+    GlobalSearchScope librariesScope = 
GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
+    PsiClass annotation = facade.findClass(GRAILS_BEANS_ANNOTATION, 
librariesScope);
+    if (annotation == null) return Collections.emptyList();
+
+    GlobalSearchScope sourceScope = 
GlobalSearchScope.moduleWithDependenciesScope(module);
+    Set<PsiClass> result = new 
LinkedHashSet<>(AnnotatedElementsSearch.searchPsiClasses(annotation, 
sourceScope).findAll());
+    for (String superClassName : new String[]{GRAILS_AUTO_CONFIGURATION, 
PLUGIN_CLASS}) {
+      PsiClass superClass = facade.findClass(superClassName, librariesScope);
+      if (superClass != null) {
+        result.addAll(ClassInheritorsSearch.search(superClass, sourceScope, 
true).findAll());
+      }
+    }
+    return result;
+  }
+
+  /**
+   * The outermost call of a qualifier chain such as {@code 
bean(Foo).primary().lazy()}, given any call in it.
+   */
+  private static @NotNull GrMethodCall getChainStatement(@NotNull GrMethodCall 
call) {
+    GrMethodCall result = call;
+    while (result.getParent() instanceof GrReferenceExpression ref
+           && ref.getQualifierExpression() == result
+           && ref.getParent() instanceof GrMethodCall outer
+           && outer.getInvokedExpression() == ref) {
+      result = outer;
+    }
+    return result;
+  }
+
+  /**
+   * The unqualified call at the root of a qualifier chain such as {@code 
bean(Foo).primary().lazy()}.
+   */
+  private static @Nullable GrMethodCall getChainRoot(@NotNull GrMethodCall 
call) {
+    GrMethodCall result = call;
+    while (true) {
+      if (!(result.getInvokedExpression() instanceof GrReferenceExpression 
ref)) return null;
+      GrExpression qualifier = ref.getQualifierExpression();
+      if (qualifier == null) return result;
+      if (!(qualifier instanceof GrMethodCall qualifierCall)) return null;
+      result = qualifierCall;
+    }
+  }
+
+  private static @Nullable String getUnqualifiedCallName(@NotNull GrMethodCall 
call) {
+    return call.getInvokedExpression() instanceof GrReferenceExpression ref && 
!ref.isQualified() ? ref.getReferenceName() : null;
+  }
+
+  private static boolean isTypeArgument(@NotNull GrExpression expression) {
+    if (!(expression instanceof GrReferenceExpression ref)) return false;
+    PsiElement resolved = ref.resolve();
+    if (resolved != null) return resolved instanceof PsiClass;
+    String name = ref.getReferenceName();
+    return name != null && !name.isEmpty() && 
Character.isUpperCase(name.charAt(0)) && !name.equals(name.toUpperCase());
+  }
+
+  /**
+   * Folds a compile-time String constant the way the transform does for a 
declared name: a literal, a reference to
+   * a {@code static final} field initialised to one, or a concatenation of 
those.
+   */
+  static @Nullable String evaluateStringConstant(@Nullable GrExpression 
expression, int depth) {
+    if (expression == null || depth > MAX_CONSTANT_DEPTH) return null;
+
+    if (expression instanceof GrParenthesizedExpression parenthesized) {
+      return evaluateStringConstant(parenthesized.getOperand(), depth + 1);
+    }
+    if (expression instanceof GrLiteral literal) {
+      return literal.getValue() instanceof String value ? value : null;
+    }
+    if (expression instanceof GrBinaryExpression binary && 
binary.getOperationTokenType() == GroovyTokenTypes.mPLUS) {
+      String left = evaluateStringConstant(binary.getLeftOperand(), depth + 1);
+      String right = left == null ? null : 
evaluateStringConstant(binary.getRightOperand(), depth + 1);
+      return right == null ? null : left + right;
+    }
+    if (!(expression instanceof GrReferenceExpression ref)) return null;
+
+    PsiElement resolved = ref.resolve();
+    // A Groovy property is reached through its getter from outside its class
+    if (resolved instanceof GrAccessorMethod accessor) resolved = 
accessor.getProperty();
+    if (!(resolved instanceof PsiField field)
+        || !field.hasModifierProperty(PsiModifier.STATIC) || 
!field.hasModifierProperty(PsiModifier.FINAL)) {
+      return null;
+    }
+    if (field instanceof GrField grField) {
+      return evaluateStringConstant(grField.getInitializerGroovy(), depth + 1);
+    }
+    return field.computeConstantValue() instanceof String value ? value : null;
+  }
+
+  /**
+   * The bean name Grails derives from a type, following {@code 
java.beans.Introspector#decapitalize}.
+   */
+  static @NotNull String decapitalize(@NotNull String name) {
+    if (name.isEmpty() || (name.length() > 1 && 
Character.isUpperCase(name.charAt(0)) && 
Character.isUpperCase(name.charAt(1)))) {
+      return name;
+    }
+    return Character.toLowerCase(name.charAt(0)) + name.substring(1);
+  }
+
+  /**
+   * One top-level {@code bean}/{@code field}/{@code method}/{@code group} 
statement, with its chained qualifiers.
+   */
+  public static final class Declaration {
+    private final Kind myKind;
+    private final GrMethodCall myRoot;
+    private final GrMethodCall myStatement;
+
+    private Declaration(@NotNull Kind kind, @NotNull GrMethodCall root, 
@NotNull GrMethodCall statement) {
+      myKind = kind;
+      myRoot = root;
+      myStatement = statement;
+    }
+
+    public @NotNull Kind getKind() {
+      return myKind;
+    }
+
+    /**
+     * The whole statement, i.e. the last call of the qualifier chain.
+     */
+    public @NotNull GrMethodCall getStatement() {
+      return myStatement;
+    }
+
+    public @Nullable GrExpression getNameExpression() {
+      GrExpression[] arguments = myRoot.getExpressionArguments();
+      if (myKind == Kind.GROUP) return arguments.length > 0 ? arguments[0] : 
null;
+      return arguments.length > 0 && !isTypeArgument(arguments[0]) ? 
arguments[0] : null;
+    }
+
+    public @Nullable GrExpression getTypeExpression() {
+      if (myKind == Kind.GROUP) return null;
+      GrExpression[] arguments = myRoot.getExpressionArguments();
+      int index = getNameExpression() == null ? 0 : 1;
+      return arguments.length > index ? arguments[index] : null;
+    }
+
+    /**
+     * The element to navigate to for the declared member: its name when 
given, else its type.
+     */
+    public @NotNull PsiElement getNavigationElement() {
+      GrExpression name = getNameExpression();
+      if (name != null) return name;
+      GrExpression type = getTypeExpression();
+      return type != null ? type : myRoot;
+    }
+
+    /**
+     * The declared name: a String constant when given, else the decapitalized 
simple name of the declared type.
+     */
+    public @Nullable String getName() {
+      GrExpression nameExpression = getNameExpression();
+      if (nameExpression != null) {
+        return evaluateStringConstant(nameExpression, 0);
+      }
+      if (myKind != Kind.GROUP && getTypeExpression() instanceof 
GrReferenceExpression typeReference) {
+        String typeName = typeReference.getReferenceName();

Review Comment:
   Fixed in a9fd382. The derived name comes from the resolved class's 
`getName()`. The reference text is only a fallback when the reference doesn't 
resolve. Covered by `testDerivedNameFollowsClassNotImportAlias`.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDslMemberContributor.java:
##########
@@ -0,0 +1,278 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.ResolveState;
+import com.intellij.psi.scope.ElementClassHint;
+import com.intellij.psi.scope.PsiScopeProcessor;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
+import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightField;
+import 
org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder;
+import org.jetbrains.plugins.groovy.lang.resolve.NonCodeMembersContributor;
+import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil;
+import org.jetbrains.plugins.groovy.util.dynamicMembers.DynamicMemberUtils;
+
+/**
+ * Resolution, completion and type inference for the Grails 8 beans DSL (see 
{@link GrailsBeansDsl}):
+ * <ul>
+ *   <li>directly inside a {@code beans} closure or one of its groups, the 
{@code bean}/{@code field}/{@code method}/
+ *   {@code group} declarations, whose results carry the qualifiers each of 
them chains with;</li>
+ *   <li>inside a {@code bean(...)} or {@code method(...)} body, the members 
declared by the {@code field(...)} and
+ *   {@code method(...)} declarations beside it, which the compiled factory 
methods share.</li>
+ * </ul>
+ */
+public final class GrailsBeansDslMemberContributor extends 
NonCodeMembersContributor {
+
+  public static final String MEMBER_ORIGIN_INFO = "via Grails beans DSL";
+
+  /**
+   * The declarations and their qualifier chains. Every qualifier that takes 
arguments takes {@code Object...}, so
+   * named attributes, types, String class names and the trailing body closure 
are all accepted.
+   */
+  static final String DSL_SOURCE = """
+    class GrailsBeansDsl {
+      /** Declares a bean named after the decapitalized simple name of {@code 
type}, built by its no-argument constructor. */
+      BeanDeclaration bean(Class type) {}
+      /** Declares a bean named after the decapitalized simple name of {@code 
type}. The closure's typed parameters are injected; the closure body builds the 
bean, or, left empty, the constructor taking the parameters is called. */
+      BeanDeclaration bean(Class type, Closure body) {}
+      /** Declares a bean named {@code name}, built by the no-argument 
constructor of {@code type}. */
+      BeanDeclaration bean(String name, Class type) {}
+      /** Declares a bean named {@code name}. The closure's typed parameters 
are injected; the closure body builds the bean, or, left empty, the constructor 
taking the parameters is called. */
+      BeanDeclaration bean(String name, Class type, Closure body) {}
+      /** Declares a bean of type {@code type} built as {@code new 
implementation()}. */
+      BeanDeclaration bean(Class type, Class implementation) {}
+      /** Declares a bean of type {@code type} built by the {@code 
implementation} constructor taking the closure's typed parameters. */
+      BeanDeclaration bean(Class type, Class implementation, Closure 
parameters) {}
+      /** Declares a bean named {@code name} of type {@code type} built as 
{@code new implementation()}. */
+      BeanDeclaration bean(String name, Class type, Class implementation) {}
+      /** Declares a bean named {@code name} of type {@code type} built by the 
{@code implementation} constructor taking the closure's typed parameters. */
+      BeanDeclaration bean(String name, Class type, Class implementation, 
Closure parameters) {}
+
+      /** Declares a private field, shared by the bean and helper methods, 
named after the decapitalized simple name of {@code type}. */
+      FieldDeclaration field(Class type) {}
+      /** Declares a private field, shared by the bean and helper methods. */
+      FieldDeclaration field(String name, Class type) {}
+
+      /** Declares a private helper method, named after the decapitalized 
simple name of {@code type}, returning {@code type}. */
+      MethodDeclaration method(Class type) {}
+      /** Declares a private helper method, named after the decapitalized 
simple name of {@code type}, whose parameters and body are the closure's. */
+      MethodDeclaration method(Class type, Closure body) {}
+      /** Declares a private helper method returning {@code type}. */
+      MethodDeclaration method(String name, Class type) {}
+      /** Declares a private helper method returning {@code type}, whose 
parameters and body are the closure's. */
+      MethodDeclaration method(String name, Class type, Closure body) {}
+
+      /** Declares a nested configuration class holding the declarations in 
its body, conditioned as a whole. */
+      GroupDeclaration group() {}
+      /** Declares a nested configuration class holding the declarations in 
its body. */
+      GroupDeclaration group(Closure body) {}
+      /** Declares a nested configuration class named after {@code name}, 
conditioned as a whole. */
+      GroupDeclaration group(String name) {}
+      /** Declares a nested configuration class named after {@code name}, 
holding the declarations in its body. */
+      GroupDeclaration group(String name, Closure body) {}
+
+      static class BeanDeclaration {
+        /** {@code @ConditionalOnMissingBean}: types positionally, the 
annotation's attributes by name. With no arguments the bean's own type is used. 
*/
+        BeanDeclaration conditionalOnMissingBean(Object... typesAndAttributes) 
{}
+        /** {@code @ConditionalOnMissingBean} on this bean's own name; the 
annotation's other attributes by name. */
+        BeanDeclaration conditionalOnMissingBeanName(Object... attributes) {}
+        /** {@code @ConditionalOnBean}: types positionally, the annotation's 
attributes by name. */
+        BeanDeclaration conditionalOnBean(Object... typesAndAttributes) {}
+        /** {@code @ConditionalOnProperty}: property names positionally, the 
annotation's attributes ({@code havingValue}, {@code matchIfMissing}, ...) by 
name. */
+        BeanDeclaration conditionalOnProperty(Object... namesAndAttributes) {}
+        /** {@code @ConditionalOnExpression} with a SpEL expression; 
single-quote it so Groovy leaves its placeholders alone. */
+        BeanDeclaration conditionalOnExpression(String expression) {}
+        /** {@code @ConditionalOnExpression} with a SpEL expression; 
single-quote it so Groovy leaves its placeholders alone. */
+        BeanDeclaration conditionalOnExpression(String expression, Closure 
body) {}
+        /** {@code @ConditionalOnClass}: types and String class names 
positionally, the annotation's attributes by name. */
+        BeanDeclaration conditionalOnClass(Object... typesNamesAndAttributes) 
{}
+        /** Registers the bean only in the given Grails environments. */
+        BeanDeclaration conditionalOnGrailsEnv(Object... environments) {}
+        /** Additional names Spring resolves to the same bean. */
+        BeanDeclaration aliases(Object... names) {}
+        /** {@code @Primary} */
+        BeanDeclaration primary() {}
+        /** {@code @Primary} */
+        BeanDeclaration primary(Closure body) {}
+        /** {@code @Lazy} */
+        BeanDeclaration lazy() {}
+        /** {@code @Lazy} */
+        BeanDeclaration lazy(Closure body) {}
+        /** {@code @Scope}: the scope name positionally, the annotation's 
attributes ({@code proxyMode}, ...) by name. */
+        BeanDeclaration scope(Object... nameAndAttributes) {}
+        /** Makes the factory method static, as {@code 
BeanFactoryPostProcessor} and {@code BeanPostProcessor} beans require. */
+        BeanDeclaration staticMethod() {}
+        /** Makes the factory method static, as {@code 
BeanFactoryPostProcessor} and {@code BeanPostProcessor} beans require. */
+        BeanDeclaration staticMethod(Closure body) {}
+        /** Type arguments for the declared type, where the construction does 
not settle them. */
+        BeanDeclaration typeArguments(Object... types) {}
+        /** Attaches any annotation to the factory method, merging into one a 
qualifier already attached. */
+        BeanDeclaration annotate(Class annotationType) {}
+        /** Attaches any annotation to the factory method, merging into one a 
qualifier already attached. */
+        BeanDeclaration annotate(Class annotationType, Closure body) {}
+        /** Attaches any annotation to the factory method, with its attributes 
by name. */
+        BeanDeclaration annotate(Map attributes, Class annotationType) {}
+        /** Attaches any annotation to the factory method, with its attributes 
by name. */
+        BeanDeclaration annotate(Map attributes, Class annotationType, Closure 
body) {}
+      }
+
+      static class FieldDeclaration {
+        /** {@code @Value("${key}")}, or the expression verbatim when it 
already holds a {@code ${...}} placeholder or {@code #{...}} SpEL expression. */
+        FieldDeclaration value(String keyOrExpression) {}
+        /** {@code @Value("${key:defaultValue}")} */
+        FieldDeclaration value(String key, Object defaultValue) {}
+        /** Type arguments for the declared type. */
+        FieldDeclaration typeArguments(Class... types) {}
+        /** Attaches any annotation to the field. */
+        FieldDeclaration annotate(Class annotationType) {}
+        /** Attaches any annotation to the field, with its attributes by name. 
*/
+        FieldDeclaration annotate(Map attributes, Class annotationType) {}
+      }
+
+      static class MethodDeclaration {
+        /** Type arguments for the declared return type. */
+        MethodDeclaration typeArguments(Object... types) {}
+        /** Attaches any annotation to the method. */
+        MethodDeclaration annotate(Class annotationType) {}
+        /** Attaches any annotation to the method. */
+        MethodDeclaration annotate(Class annotationType, Closure body) {}
+        /** Attaches any annotation to the method, with its attributes by 
name. */
+        MethodDeclaration annotate(Map attributes, Class annotationType) {}
+        /** Attaches any annotation to the method, with its attributes by 
name. */
+        MethodDeclaration annotate(Map attributes, Class annotationType, 
Closure body) {}
+      }
+
+      static class GroupDeclaration {
+        /** {@code @ConditionalOnMissingBean} on the group: types 
positionally, the annotation's attributes by name. */
+        GroupDeclaration conditionalOnMissingBean(Object... 
typesAndAttributes) {}
+        /** {@code @ConditionalOnBean} on the group: types positionally, the 
annotation's attributes by name. */
+        GroupDeclaration conditionalOnBean(Object... typesAndAttributes) {}
+        /** {@code @ConditionalOnProperty} on the group: property names 
positionally, the annotation's attributes by name. */
+        GroupDeclaration conditionalOnProperty(Object... namesAndAttributes) {}
+        /** {@code @ConditionalOnExpression} on the group. */
+        GroupDeclaration conditionalOnExpression(String expression) {}
+        /** {@code @ConditionalOnExpression} on the group. */
+        GroupDeclaration conditionalOnExpression(String expression, Closure 
body) {}
+        /** {@code @ConditionalOnClass} on the group: types and String class 
names positionally, the annotation's attributes by name. */
+        GroupDeclaration conditionalOnClass(Object... typesNamesAndAttributes) 
{}
+        /** Registers the group's beans only in the given Grails environments. 
*/
+        GroupDeclaration conditionalOnGrailsEnv(Object... environments) {}
+        /** Attaches any annotation to the group's configuration class. */
+        GroupDeclaration annotate(Class annotationType) {}
+        /** Attaches any annotation to the group's configuration class. */
+        GroupDeclaration annotate(Class annotationType, Closure body) {}
+        /** Attaches any annotation to the group's configuration class, with 
its attributes by name. */
+        GroupDeclaration annotate(Map attributes, Class annotationType) {}
+        /** Attaches any annotation to the group's configuration class, with 
its attributes by name. */
+        GroupDeclaration annotate(Map attributes, Class annotationType, 
Closure body) {}
+      }
+    }
+    """;
+
+  @Override
+  public void processDynamicElements(@NotNull PsiType qualifierType,
+                                     @Nullable PsiClass aClass,
+                                     @NotNull PsiScopeProcessor processor,
+                                     @NotNull PsiElement place,
+                                     @NotNull ResolveState state) {
+    if (!(place instanceof GrReferenceExpression ref) || ref.isQualified()) 
return;
+
+    // Cheap rejection of everything not written inside a property named 
"beans"
+    GrField field = PsiTreeUtil.getParentOfType(place, GrField.class);
+    if (field == null || 
!GrailsBeansDsl.BEANS_PROPERTY.equals(field.getName())) return;
+
+    GrClosableBlock closure = PsiTreeUtil.getParentOfType(place, 
GrClosableBlock.class);
+    if (closure == null || !PsiTreeUtil.isAncestor(field, closure, true)) 
return;
+
+    if (GrailsBeansDsl.isDeclarationContainer(closure)) {
+      DynamicMemberUtils.process(processor, false, place, DSL_SOURCE);
+      return;
+    }
+
+    for (GrClosableBlock c = closure; c != null; c = 
PsiTreeUtil.getParentOfType(c, GrClosableBlock.class, true)) {
+      GrailsBeansDsl.Declaration declaration = 
GrailsBeansDsl.getDeclarationOfBody(c);
+      if (declaration == null) continue;
+
+      if (declaration.getKind() == GrailsBeansDsl.Kind.GROUP) return;
+      if (declaration.getStatement().getParent() instanceof GrClosableBlock 
container) {
+        processSharedMembers(container, processor, state);
+      }
+      return;
+    }
+  }
+
+  private static void processSharedMembers(@NotNull GrClosableBlock container,
+                                           @NotNull PsiScopeProcessor 
processor,
+                                           @NotNull ResolveState state) {
+    ElementClassHint classHint = processor.getHint(ElementClassHint.KEY);
+    boolean processFields = ResolveUtil.shouldProcessProperties(classHint);
+    boolean processMethods = ResolveUtil.shouldProcessMethods(classHint);
+    String nameHint = ResolveUtil.getNameHint(processor);
+
+    for (GrailsBeansDsl.Declaration declaration : 
GrailsBeansDsl.getDeclarations(container)) {
+      GrailsBeansDsl.Kind kind = declaration.getKind();
+      if (kind == GrailsBeansDsl.Kind.FIELD ? !processFields : kind != 
GrailsBeansDsl.Kind.METHOD || !processMethods) continue;
+
+      String name = declaration.getName();
+      if (name == null || (nameHint != null && !nameHint.equals(name))) 
continue;
+
+      PsiElement member = kind == GrailsBeansDsl.Kind.FIELD ? 
createField(declaration, name) : createMethod(declaration, name);
+      if (!processor.execute(member, state)) return;
+    }
+  }
+
+  private static @NotNull PsiElement createField(@NotNull 
GrailsBeansDsl.Declaration declaration, @NotNull String name) {
+    PsiElement navigationElement = declaration.getNavigationElement();
+    PsiType type = declaration.getType();
+    if (type == null) type = 
PsiType.getJavaLangObject(navigationElement.getManager(), 
navigationElement.getResolveScope());
+
+    GrLightField field = new GrLightField(declaration.getHostClass(), name, 
type, navigationElement);
+    field.setOriginInfo(MEMBER_ORIGIN_INFO);
+    return field;
+  }
+
+  private static @NotNull PsiElement createMethod(@NotNull 
GrailsBeansDsl.Declaration declaration, @NotNull String name) {
+    PsiElement navigationElement = declaration.getNavigationElement();
+    GrLightMethodBuilder method = new 
GrLightMethodBuilder(navigationElement.getManager(), name);
+    method.setNavigationElement(navigationElement);
+    method.setContainingClass(declaration.getHostClass());
+    method.setOriginInfo(MEMBER_ORIGIN_INFO);
+
+    PsiType returnType = declaration.getType();
+    if (returnType != null) method.setReturnType(returnType);
+
+    GrClosableBlock body = declaration.getBody();
+    if (body != null) {
+      for (GrParameter parameter : body.getParameters()) {
+        method.addParameter(parameter.getName(), parameter.getType());

Review Comment:
   Fixed in a9fd382. `parameter.isOptional()` is now passed through. 
`testSharedMethodDefaultParameterValues` runs the assignability inspection over 
both `greet('World')` and `greet('World', '?')`.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);
+  }
+
+  /**
+   * Whether DSL declarations are written directly inside the closure: the 
{@code beans} closure itself, or the
+   * body of one of its top-level {@code group(...)} declarations.
+   */
+  public static boolean isDeclarationContainer(@NotNull GrClosableBlock 
closure) {
+    if (isBeansClosure(closure)) return true;
+    Declaration declaration = getDeclarationOfBody(closure);
+    return declaration != null && declaration.getKind() == Kind.GROUP;
+  }
+
+  /**
+   * The declaration the closure is the body of, when it is the body of a 
{@code bean(...)}, {@code method(...)}
+   * or {@code group(...)} declared directly in a {@code beans} closure or in 
one of its groups.
+   */
+  public static @Nullable Declaration getDeclarationOfBody(@NotNull 
GrClosableBlock closure) {
+    PsiElement parent = closure.getParent();
+    if (parent instanceof GrArgumentList) parent = parent.getParent();
+    if (!(parent instanceof GrMethodCall call)) return null;
+
+    GrMethodCall statement = getChainStatement(call);
+    if (!(statement.getParent() instanceof GrClosableBlock container)) return 
null;
+
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getStatement() == statement) {
+        if (declaration.getKind() == Kind.FIELD) return null;
+        // Groups do not nest
+        boolean declared = declaration.getKind() == Kind.GROUP ? 
isBeansClosure(container) : isDeclarationContainer(container);
+        return declared ? declaration : null;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * The DSL declarations written directly inside the closure. The closure is 
not checked to be a declaration
+   * container: see {@link #isDeclarationContainer(GrClosableBlock)}.
+   */
+  public static @NotNull List<Declaration> getDeclarations(@NotNull 
GrClosableBlock container) {
+    return CachedValuesManager.getCachedValue(container, () -> 
Result.create(computeDeclarations(container), container));
+  }
+
+  private static @NotNull List<Declaration> computeDeclarations(@NotNull 
GrClosableBlock container) {
+    List<Declaration> result = new ArrayList<>();
+    for (PsiElement statement : container.getStatements()) {
+      if (!(statement instanceof GrMethodCall call)) continue;
+      GrMethodCall root = getChainRoot(call);
+      if (root == null) continue;
+      Kind kind = Kind.byCallName(getUnqualifiedCallName(root));
+      if (kind != null) {
+        result.add(new Declaration(kind, root, call));
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  /**
+   * The beans the class declares through the DSL, including those inside its 
groups.
+   */
+  public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> 
getBeanDescriptors(@NotNull PsiClass aClass) {
+    GrClosableBlock beansClosure = getBeansClosure(aClass);
+    if (beansClosure == null) return Collections.emptyList();
+
+    List<GrailsResourceBeanExtractor.BeanDescriptor> result = new 
ArrayList<>();
+    collectBeanDescriptors(beansClosure, result);
+    return result;
+  }
+
+  private static void collectBeanDescriptors(@NotNull GrClosableBlock 
container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) {
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getKind() == Kind.GROUP) {
+        GrClosableBlock body = declaration.getBody();
+        if (body != null) collectBeanDescriptors(body, result);
+        continue;
+      }
+      if (declaration.getKind() != Kind.BEAN) continue;
+
+      String name = declaration.getName();
+      if (name == null || !(declaration.getTypeExpression() instanceof 
GrReferenceExpression typeReference)) continue;
+
+      GrailsResourceBeanExtractor.BeanDescriptor descriptor = new 
GrailsResourceBeanExtractor.BeanDescriptor(name);
+      descriptor.getReferences().add(typeReference);
+      result.add(descriptor);
+    }
+  }
+
+  /**
+   * The classes in the module (and the modules it depends on) whose {@code 
beans} property is compiled as the DSL.
+   */
+  public static @NotNull Collection<PsiClass> findBeansHosts(@NotNull Module 
module) {
+    JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject());
+    GlobalSearchScope librariesScope = 
GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
+    PsiClass annotation = facade.findClass(GRAILS_BEANS_ANNOTATION, 
librariesScope);
+    if (annotation == null) return Collections.emptyList();
+
+    GlobalSearchScope sourceScope = 
GlobalSearchScope.moduleWithDependenciesScope(module);

Review Comment:
   It wasn't intentional. Fixed in a9fd382: hosts are now searched in 
`moduleWithDependenciesAndLibrariesScope(module, false)`, intersected with the 
project scope. `testTestSourcesAreNotHosts` puts a `@GrailsBeans` class and an 
application class under a test root and checks they're excluded.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);
+  }
+
+  /**
+   * Whether DSL declarations are written directly inside the closure: the 
{@code beans} closure itself, or the
+   * body of one of its top-level {@code group(...)} declarations.
+   */
+  public static boolean isDeclarationContainer(@NotNull GrClosableBlock 
closure) {
+    if (isBeansClosure(closure)) return true;
+    Declaration declaration = getDeclarationOfBody(closure);
+    return declaration != null && declaration.getKind() == Kind.GROUP;
+  }
+
+  /**
+   * The declaration the closure is the body of, when it is the body of a 
{@code bean(...)}, {@code method(...)}
+   * or {@code group(...)} declared directly in a {@code beans} closure or in 
one of its groups.
+   */
+  public static @Nullable Declaration getDeclarationOfBody(@NotNull 
GrClosableBlock closure) {
+    PsiElement parent = closure.getParent();
+    if (parent instanceof GrArgumentList) parent = parent.getParent();
+    if (!(parent instanceof GrMethodCall call)) return null;
+
+    GrMethodCall statement = getChainStatement(call);
+    if (!(statement.getParent() instanceof GrClosableBlock container)) return 
null;
+
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getStatement() == statement) {
+        if (declaration.getKind() == Kind.FIELD) return null;
+        // Groups do not nest
+        boolean declared = declaration.getKind() == Kind.GROUP ? 
isBeansClosure(container) : isDeclarationContainer(container);
+        return declared ? declaration : null;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * The DSL declarations written directly inside the closure. The closure is 
not checked to be a declaration
+   * container: see {@link #isDeclarationContainer(GrClosableBlock)}.
+   */
+  public static @NotNull List<Declaration> getDeclarations(@NotNull 
GrClosableBlock container) {
+    return CachedValuesManager.getCachedValue(container, () -> 
Result.create(computeDeclarations(container), container));
+  }
+
+  private static @NotNull List<Declaration> computeDeclarations(@NotNull 
GrClosableBlock container) {
+    List<Declaration> result = new ArrayList<>();
+    for (PsiElement statement : container.getStatements()) {
+      if (!(statement instanceof GrMethodCall call)) continue;
+      GrMethodCall root = getChainRoot(call);
+      if (root == null) continue;
+      Kind kind = Kind.byCallName(getUnqualifiedCallName(root));
+      if (kind != null) {
+        result.add(new Declaration(kind, root, call));
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  /**
+   * The beans the class declares through the DSL, including those inside its 
groups.
+   */
+  public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> 
getBeanDescriptors(@NotNull PsiClass aClass) {
+    GrClosableBlock beansClosure = getBeansClosure(aClass);
+    if (beansClosure == null) return Collections.emptyList();
+
+    List<GrailsResourceBeanExtractor.BeanDescriptor> result = new 
ArrayList<>();
+    collectBeanDescriptors(beansClosure, result);
+    return result;
+  }
+
+  private static void collectBeanDescriptors(@NotNull GrClosableBlock 
container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) {
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getKind() == Kind.GROUP) {
+        GrClosableBlock body = declaration.getBody();
+        if (body != null) collectBeanDescriptors(body, result);
+        continue;
+      }
+      if (declaration.getKind() != Kind.BEAN) continue;
+
+      String name = declaration.getName();
+      if (name == null || !(declaration.getTypeExpression() instanceof 
GrReferenceExpression typeReference)) continue;
+
+      GrailsResourceBeanExtractor.BeanDescriptor descriptor = new 
GrailsResourceBeanExtractor.BeanDescriptor(name);

Review Comment:
   Fixed in a9fd382, but not with one descriptor per alias. The Spring model 
treats two `CustomSpringComponent`s of the same class as the same bean 
(`JamCommonModelElement.equals` compares the class and the PSI anchor), so the 
extra component for the alias was dropped. Instead, `BeanDescriptor` carries 
the aliases and `GrailsCustomSpringComponent` returns them from `getAliases()`. 
`testBeansDslBeanInjection` now injects `def salutation` through an alias.



##########
plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java:
##########
@@ -0,0 +1,458 @@
+/*
+ * 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
+ *
+ *   https://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.apache.grails.intellij.plugin.spring;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.searches.AnnotatedElementsSearch;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.InheritanceUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
+import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose 
top-level statements are
+ * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code 
group(...)} declarations, compiled by
+ * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory 
methods.
+ * <p>
+ * The property is compiled on a class annotated with {@code @GrailsBeans}, 
and implicitly on a plugin descriptor
+ * ({@code grails.plugins.Plugin}), on the application class ({@code 
grails.boot.config.GrailsAutoConfiguration})
+ * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is 
recognised unless the
+ * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are 
unaffected.
+ *
+ * @see GrailsBeansDslMemberContributor
+ */
+public final class GrailsBeansDsl {
+
+  public static final String GRAILS_BEANS_ANNOTATION = 
"grails.compiler.beans.GrailsBeans";
+  public static final String BEANS_PROPERTY = "beans";
+
+  private static final String PLUGIN_CLASS = "grails.plugins.Plugin";
+  private static final String GRAILS_AUTO_CONFIGURATION = 
"grails.boot.config.GrailsAutoConfiguration";
+  private static final String GRAILS_UNIT_TEST = 
"org.grails.testing.GrailsUnitTest";
+
+  private static final String TYPE_ARGUMENTS_CALL = "typeArguments";
+  private static final int MAX_CONSTANT_DEPTH = 16;
+
+  public enum Kind {
+    BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group");
+
+    private final String myCallName;
+
+    Kind(String callName) {
+      myCallName = callName;
+    }
+
+    public String getCallName() {
+      return myCallName;
+    }
+
+    static @Nullable Kind byCallName(@Nullable String callName) {
+      for (Kind kind : values()) {
+        if (kind.myCallName.equals(callName)) return kind;
+      }
+      return null;
+    }
+  }
+
+  private GrailsBeansDsl() {
+  }
+
+  /**
+   * Whether the beans DSL is available to code in the given context, i.e. the 
project builds against Grails 8+.
+   */
+  public static boolean isAvailable(@NotNull PsiElement context) {
+    return 
JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION,
 context.getResolveScope()) != null;
+  }
+
+  /**
+   * Whether the {@code beans} property of the given class is compiled as the 
beans DSL.
+   */
+  public static boolean isBeansHost(@Nullable PsiClass aClass) {
+    if (aClass == null) return false;
+    return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION)
+           || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION)
+           || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST);
+  }
+
+  /**
+   * The {@code beans} closure declared on the given class, if it is compiled 
as the beans DSL.
+   */
+  public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass 
aClass) {
+    if (!(aClass instanceof GrTypeDefinition)) return null;
+    PsiField field = 
((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false);
+    if (!(field instanceof GrField) || 
!(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) 
return null;
+    return isBeansClosure(closure) ? closure : null;
+  }
+
+  /**
+   * Whether the closure is the initializer of a {@code beans} property 
compiled as the beans DSL.
+   */
+  public static boolean isBeansClosure(@NotNull GrClosableBlock closure) {
+    if (!(closure.getParent() instanceof GrField field) || 
!BEANS_PROPERTY.equals(field.getName())) return false;
+    return isBeansHost(field.getContainingClass()) && isAvailable(closure);
+  }
+
+  /**
+   * Whether DSL declarations are written directly inside the closure: the 
{@code beans} closure itself, or the
+   * body of one of its top-level {@code group(...)} declarations.
+   */
+  public static boolean isDeclarationContainer(@NotNull GrClosableBlock 
closure) {
+    if (isBeansClosure(closure)) return true;
+    Declaration declaration = getDeclarationOfBody(closure);
+    return declaration != null && declaration.getKind() == Kind.GROUP;
+  }
+
+  /**
+   * The declaration the closure is the body of, when it is the body of a 
{@code bean(...)}, {@code method(...)}
+   * or {@code group(...)} declared directly in a {@code beans} closure or in 
one of its groups.
+   */
+  public static @Nullable Declaration getDeclarationOfBody(@NotNull 
GrClosableBlock closure) {
+    PsiElement parent = closure.getParent();
+    if (parent instanceof GrArgumentList) parent = parent.getParent();
+    if (!(parent instanceof GrMethodCall call)) return null;
+
+    GrMethodCall statement = getChainStatement(call);
+    if (!(statement.getParent() instanceof GrClosableBlock container)) return 
null;
+
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getStatement() == statement) {
+        if (declaration.getKind() == Kind.FIELD) return null;
+        // Groups do not nest
+        boolean declared = declaration.getKind() == Kind.GROUP ? 
isBeansClosure(container) : isDeclarationContainer(container);
+        return declared ? declaration : null;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * The DSL declarations written directly inside the closure. The closure is 
not checked to be a declaration
+   * container: see {@link #isDeclarationContainer(GrClosableBlock)}.
+   */
+  public static @NotNull List<Declaration> getDeclarations(@NotNull 
GrClosableBlock container) {
+    return CachedValuesManager.getCachedValue(container, () -> 
Result.create(computeDeclarations(container), container));
+  }
+
+  private static @NotNull List<Declaration> computeDeclarations(@NotNull 
GrClosableBlock container) {
+    List<Declaration> result = new ArrayList<>();
+    for (PsiElement statement : container.getStatements()) {
+      if (!(statement instanceof GrMethodCall call)) continue;
+      GrMethodCall root = getChainRoot(call);
+      if (root == null) continue;
+      Kind kind = Kind.byCallName(getUnqualifiedCallName(root));
+      if (kind != null) {
+        result.add(new Declaration(kind, root, call));
+      }
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  /**
+   * The beans the class declares through the DSL, including those inside its 
groups.
+   */
+  public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> 
getBeanDescriptors(@NotNull PsiClass aClass) {
+    GrClosableBlock beansClosure = getBeansClosure(aClass);
+    if (beansClosure == null) return Collections.emptyList();
+
+    List<GrailsResourceBeanExtractor.BeanDescriptor> result = new 
ArrayList<>();
+    collectBeanDescriptors(beansClosure, result);
+    return result;
+  }
+
+  private static void collectBeanDescriptors(@NotNull GrClosableBlock 
container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) {
+    for (Declaration declaration : getDeclarations(container)) {
+      if (declaration.getKind() == Kind.GROUP) {
+        GrClosableBlock body = declaration.getBody();
+        if (body != null) collectBeanDescriptors(body, result);
+        continue;
+      }
+      if (declaration.getKind() != Kind.BEAN) continue;
+
+      String name = declaration.getName();
+      if (name == null || !(declaration.getTypeExpression() instanceof 
GrReferenceExpression typeReference)) continue;
+
+      GrailsResourceBeanExtractor.BeanDescriptor descriptor = new 
GrailsResourceBeanExtractor.BeanDescriptor(name);
+      descriptor.getReferences().add(typeReference);
+      result.add(descriptor);
+    }
+  }
+
+  /**
+   * The classes in the module (and the modules it depends on) whose {@code 
beans} property is compiled as the DSL.
+   */
+  public static @NotNull Collection<PsiClass> findBeansHosts(@NotNull Module 
module) {
+    JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject());
+    GlobalSearchScope librariesScope = 
GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
+    PsiClass annotation = facade.findClass(GRAILS_BEANS_ANNOTATION, 
librariesScope);
+    if (annotation == null) return Collections.emptyList();
+
+    GlobalSearchScope sourceScope = 
GlobalSearchScope.moduleWithDependenciesScope(module);
+    Set<PsiClass> result = new 
LinkedHashSet<>(AnnotatedElementsSearch.searchPsiClasses(annotation, 
sourceScope).findAll());
+    for (String superClassName : new String[]{GRAILS_AUTO_CONFIGURATION, 
PLUGIN_CLASS}) {
+      PsiClass superClass = facade.findClass(superClassName, librariesScope);
+      if (superClass != null) {
+        result.addAll(ClassInheritorsSearch.search(superClass, sourceScope, 
true).findAll());
+      }
+    }
+    return result;
+  }
+
+  /**
+   * The outermost call of a qualifier chain such as {@code 
bean(Foo).primary().lazy()}, given any call in it.
+   */
+  private static @NotNull GrMethodCall getChainStatement(@NotNull GrMethodCall 
call) {
+    GrMethodCall result = call;
+    while (result.getParent() instanceof GrReferenceExpression ref
+           && ref.getQualifierExpression() == result
+           && ref.getParent() instanceof GrMethodCall outer
+           && outer.getInvokedExpression() == ref) {
+      result = outer;
+    }
+    return result;
+  }
+
+  /**
+   * The unqualified call at the root of a qualifier chain such as {@code 
bean(Foo).primary().lazy()}.
+   */
+  private static @Nullable GrMethodCall getChainRoot(@NotNull GrMethodCall 
call) {
+    GrMethodCall result = call;
+    while (true) {
+      if (!(result.getInvokedExpression() instanceof GrReferenceExpression 
ref)) return null;
+      GrExpression qualifier = ref.getQualifierExpression();
+      if (qualifier == null) return result;
+      if (!(qualifier instanceof GrMethodCall qualifierCall)) return null;
+      result = qualifierCall;
+    }
+  }
+
+  private static @Nullable String getUnqualifiedCallName(@NotNull GrMethodCall 
call) {
+    return call.getInvokedExpression() instanceof GrReferenceExpression ref && 
!ref.isQualified() ? ref.getReferenceName() : null;
+  }
+
+  private static boolean isTypeArgument(@NotNull GrExpression expression) {
+    if (!(expression instanceof GrReferenceExpression ref)) return false;
+    PsiElement resolved = ref.resolve();
+    if (resolved != null) return resolved instanceof PsiClass;
+    String name = ref.getReferenceName();
+    return name != null && !name.isEmpty() && 
Character.isUpperCase(name.charAt(0)) && !name.equals(name.toUpperCase());
+  }
+
+  /**
+   * Folds a compile-time String constant the way the transform does for a 
declared name: a literal, a reference to
+   * a {@code static final} field initialised to one, or a concatenation of 
those.
+   */
+  static @Nullable String evaluateStringConstant(@Nullable GrExpression 
expression, int depth) {
+    if (expression == null || depth > MAX_CONSTANT_DEPTH) return null;
+
+    if (expression instanceof GrParenthesizedExpression parenthesized) {
+      return evaluateStringConstant(parenthesized.getOperand(), depth + 1);
+    }
+    if (expression instanceof GrLiteral literal) {
+      return literal.getValue() instanceof String value ? value : null;
+    }
+    if (expression instanceof GrBinaryExpression binary && 
binary.getOperationTokenType() == GroovyTokenTypes.mPLUS) {
+      String left = evaluateStringConstant(binary.getLeftOperand(), depth + 1);
+      String right = left == null ? null : 
evaluateStringConstant(binary.getRightOperand(), depth + 1);
+      return right == null ? null : left + right;
+    }
+    if (!(expression instanceof GrReferenceExpression ref)) return null;
+
+    PsiElement resolved = ref.resolve();
+    // A Groovy property is reached through its getter from outside its class
+    if (resolved instanceof GrAccessorMethod accessor) resolved = 
accessor.getProperty();
+    if (!(resolved instanceof PsiField field)
+        || !field.hasModifierProperty(PsiModifier.STATIC) || 
!field.hasModifierProperty(PsiModifier.FINAL)) {
+      return null;
+    }
+    if (field instanceof GrField grField) {
+      return evaluateStringConstant(grField.getInitializerGroovy(), depth + 1);
+    }
+    return field.computeConstantValue() instanceof String value ? value : null;
+  }
+
+  /**
+   * The bean name Grails derives from a type, following {@code 
java.beans.Introspector#decapitalize}.
+   */
+  static @NotNull String decapitalize(@NotNull String name) {
+    if (name.isEmpty() || (name.length() > 1 && 
Character.isUpperCase(name.charAt(0)) && 
Character.isUpperCase(name.charAt(1)))) {
+      return name;
+    }
+    return Character.toLowerCase(name.charAt(0)) + name.substring(1);
+  }
+
+  /**
+   * One top-level {@code bean}/{@code field}/{@code method}/{@code group} 
statement, with its chained qualifiers.
+   */
+  public static final class Declaration {
+    private final Kind myKind;
+    private final GrMethodCall myRoot;
+    private final GrMethodCall myStatement;
+
+    private Declaration(@NotNull Kind kind, @NotNull GrMethodCall root, 
@NotNull GrMethodCall statement) {
+      myKind = kind;
+      myRoot = root;
+      myStatement = statement;
+    }
+
+    public @NotNull Kind getKind() {
+      return myKind;
+    }
+
+    /**
+     * The whole statement, i.e. the last call of the qualifier chain.
+     */
+    public @NotNull GrMethodCall getStatement() {
+      return myStatement;
+    }
+
+    public @Nullable GrExpression getNameExpression() {
+      GrExpression[] arguments = myRoot.getExpressionArguments();
+      if (myKind == Kind.GROUP) return arguments.length > 0 ? arguments[0] : 
null;
+      return arguments.length > 0 && !isTypeArgument(arguments[0]) ? 
arguments[0] : null;
+    }
+
+    public @Nullable GrExpression getTypeExpression() {
+      if (myKind == Kind.GROUP) return null;
+      GrExpression[] arguments = myRoot.getExpressionArguments();
+      int index = getNameExpression() == null ? 0 : 1;
+      return arguments.length > index ? arguments[index] : null;
+    }
+
+    /**
+     * The element to navigate to for the declared member: its name when 
given, else its type.
+     */
+    public @NotNull PsiElement getNavigationElement() {
+      GrExpression name = getNameExpression();
+      if (name != null) return name;
+      GrExpression type = getTypeExpression();
+      return type != null ? type : myRoot;
+    }
+
+    /**
+     * The declared name: a String constant when given, else the decapitalized 
simple name of the declared type.
+     */
+    public @Nullable String getName() {
+      GrExpression nameExpression = getNameExpression();
+      if (nameExpression != null) {
+        return evaluateStringConstant(nameExpression, 0);
+      }
+      if (myKind != Kind.GROUP && getTypeExpression() instanceof 
GrReferenceExpression typeReference) {
+        String typeName = typeReference.getReferenceName();
+        return typeName == null ? null : decapitalize(typeName);
+      }
+      return null;
+    }
+
+    /**
+     * The declared type, carrying any {@code .typeArguments(...)} chained 
onto the declaration.
+     */
+    public @Nullable PsiType getType() {
+      if (!(getTypeExpression() instanceof GrReferenceExpression typeReference)
+          || !(typeReference.resolve() instanceof PsiClass typeClass)) {
+        return null;
+      }
+
+      JavaPsiFacade facade = JavaPsiFacade.getInstance(typeClass.getProject());
+      List<PsiType> typeArguments = getTypeArguments();
+      if (typeArguments.size() == typeClass.getTypeParameters().length && 
!typeArguments.isEmpty()) {
+        return facade.getElementFactory().createType(typeClass, 
typeArguments.toArray(PsiType.EMPTY_ARRAY));
+      }
+      return facade.getElementFactory().createType(typeClass);
+    }
+
+    private @NotNull List<PsiType> getTypeArguments() {
+      for (GrMethodCall call = myStatement; call != myRoot; ) {
+        if (!(call.getInvokedExpression() instanceof GrReferenceExpression 
ref)) break;
+        if (TYPE_ARGUMENTS_CALL.equals(ref.getReferenceName())) {
+          List<PsiType> result = new ArrayList<>();
+          for (GrExpression argument : call.getExpressionArguments()) {
+            if (!(argument instanceof GrReferenceExpression argumentReference)
+                || !(argumentReference.resolve() instanceof PsiClass 
argumentClass)) {
+              return Collections.emptyList();
+            }
+            PsiClassType argumentType = 
JavaPsiFacade.getElementFactory(argumentClass.getProject()).createType(argumentClass);
+            result.add(argumentType);
+          }
+          return result;
+        }
+        if (!(ref.getQualifierExpression() instanceof GrMethodCall qualifier)) 
break;
+        call = qualifier;
+      }
+      return Collections.emptyList();
+    }
+
+    /**
+     * The closure the declaration's body is written in, wherever in the 
qualifier chain it is passed.
+     */
+    public @Nullable GrClosableBlock getBody() {
+      for (GrMethodCall call = myStatement; ; ) {

Review Comment:
   Fixed in a9fd382. `getBody()` only takes the trailing closure of the 
outermost call, and `getDeclarationOfBody` only accepts that closure. In 
`testBodyIsTrailingClosureOfOutermostCall`, closures written before 
`.annotate(...)` or `.primary()` get no shared members.



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