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]
