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


##########
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)) {

Review Comment:
   Fixed in a9fd382:
   - `isBeansClosure` is cached per closure.
   - Each `Declaration` splits its name and type arguments once and memoizes 
its name and type. The declarations are now invalidated by 
`MODIFICATION_COUNT`, since those values depend on other files.
   - The light fields and methods are built once per container and filtered by 
the name hint.



##########
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) {

Review Comment:
   Done in a9fd382: both now use `StringUtil.decapitalize` and 
`PsiUtil.getUnqualifiedMethodName`.



##########
plugin/src/test/java/org/apache/grails/intellij/plugin/reference/spring/GrailsBeansDslTest.java:
##########
@@ -0,0 +1,408 @@
+/*
+ * 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.reference.spring;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.PsiVariable;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.containers.ContainerUtil;
+import org.apache.grails.intellij.lib.testFramework.GrailsTestCase;
+import org.apache.grails.intellij.plugin.spring.GrailsBeansDsl;
+import org.apache.grails.intellij.plugin.spring.GrailsResourceBeanExtractor;
+import 
org.jetbrains.plugins.groovy.codeInspection.assignment.GroovyAssignabilityCheckInspection;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
+import 
org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The Grails 8 compile-time beans DSL ({@code 
grails.compiler.beans.GrailsBeans}): resolution of the declarations
+ * and their qualifier chains, of the shared {@code field(...)}/{@code 
method(...)} members inside bean bodies, and
+ * extraction of the declared beans. The DSL support is independent of the 
Spring Support plugin.
+ */
+public class GrailsBeansDslTest extends GrailsTestCase {
+
+  private static final String DSL_BODY = """
+        field('suffix', String).value('app.greeting-suffix', '!')
+        field(Formatter)
+        field('names', List).typeArguments(String)
+        method('buildGreeting', String) { String name ->
+            "Hello, ${name}${suffix}"
+        }
+
+        bean(MyService)
+        bean(GREETER, Greeter) { MyService myService ->
+            new Greeter(buildGreeting('World').toUpperCase() + 
formatter.format(suffix) + names.first().trim())
+        }
+        bean('special', Greeter).primary().lazy().scope('prototype', 
proxyMode: 'x').conditionalOnMissingBean(name: 'other') {
+            new Greeter(suffix.trim())
+        }
+        bean('provider', Provider, 
DefaultProvider).conditionalOnProperty('app.enabled', havingValue: 
'true').aliases('legacyProvider')
+        
bean(URLHolder).staticMethod().annotate(Deprecated).annotate(SuppressWarnings, 
value: 'x').conditionalOnGrailsEnv('development')
+        group('optional').conditionalOnClass(name: 'com.example.Missing') {
+            field('prefix', String).value('app.prefix')
+            bean('optionalGreeter', Greeter) {
+                new Greeter(prefix.trim())
+            }
+        }
+    """;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    myFixture.addClass("package grails.compiler.beans; public @interface 
GrailsBeans {}");

Review Comment:
   Added both in a9fd382. `testDslInertWithoutGrailsBeans` deletes the 
annotation, then checks that `bean(...)` stays unresolved and no hosts are 
found. `testPluginDescriptorRecognisedByName` covers an abstract descriptor, 
one without the suffix, and one that doesn't extend `Plugin`.



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