jdaugherty commented on code in PR #431: URL: https://github.com/apache/grails-intellij-plugin/pull/431#discussion_r4175895637
########## plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDslMemberContributor.java: ########## @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.grails.intellij.plugin.spring; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiType; +import com.intellij.psi.ResolveState; +import com.intellij.psi.scope.ElementClassHint; +import com.intellij.psi.scope.PsiScopeProcessor; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter; +import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightField; +import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder; +import org.jetbrains.plugins.groovy.lang.resolve.NonCodeMembersContributor; +import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil; +import org.jetbrains.plugins.groovy.util.dynamicMembers.DynamicMemberUtils; + +/** + * Resolution, completion and type inference for the Grails 8 beans DSL (see {@link GrailsBeansDsl}): + * <ul> + * <li>directly inside a {@code beans} closure or one of its groups, the {@code bean}/{@code field}/{@code method}/ + * {@code group} declarations, whose results carry the qualifiers each of them chains with;</li> + * <li>inside a {@code bean(...)} or {@code method(...)} body, the members declared by the {@code field(...)} and + * {@code method(...)} declarations beside it, which the compiled factory methods share.</li> + * </ul> + */ +public final class GrailsBeansDslMemberContributor extends NonCodeMembersContributor { + + public static final String MEMBER_ORIGIN_INFO = "via Grails beans DSL"; + + /** + * The declarations and their qualifier chains. Every qualifier that takes arguments takes {@code Object...}, so + * named attributes, types, String class names and the trailing body closure are all accepted. + */ + static final String DSL_SOURCE = """ + class GrailsBeansDsl { + /** Declares a bean named after the decapitalized simple name of {@code type}, built by its no-argument constructor. */ + BeanDeclaration bean(Class type) {} + /** Declares a bean named after the decapitalized simple name of {@code type}. The closure's typed parameters are injected; the closure body builds the bean, or, left empty, the constructor taking the parameters is called. */ + BeanDeclaration bean(Class type, Closure body) {} + /** Declares a bean named {@code name}, built by the no-argument constructor of {@code type}. */ + BeanDeclaration bean(String name, Class type) {} + /** Declares a bean named {@code name}. The closure's typed parameters are injected; the closure body builds the bean, or, left empty, the constructor taking the parameters is called. */ + BeanDeclaration bean(String name, Class type, Closure body) {} + /** Declares a bean of type {@code type} built as {@code new implementation()}. */ + BeanDeclaration bean(Class type, Class implementation) {} + /** Declares a bean of type {@code type} built by the {@code implementation} constructor taking the closure's typed parameters. */ + BeanDeclaration bean(Class type, Class implementation, Closure parameters) {} + /** Declares a bean named {@code name} of type {@code type} built as {@code new implementation()}. */ + BeanDeclaration bean(String name, Class type, Class implementation) {} + /** Declares a bean named {@code name} of type {@code type} built by the {@code implementation} constructor taking the closure's typed parameters. */ + BeanDeclaration bean(String name, Class type, Class implementation, Closure parameters) {} + + /** Declares a private field, shared by the bean and helper methods, named after the decapitalized simple name of {@code type}. */ + FieldDeclaration field(Class type) {} + /** Declares a private field, shared by the bean and helper methods. */ + FieldDeclaration field(String name, Class type) {} + + /** Declares a private helper method, named after the decapitalized simple name of {@code type}, returning {@code type}. */ + MethodDeclaration method(Class type) {} + /** Declares a private helper method, named after the decapitalized simple name of {@code type}, whose parameters and body are the closure's. */ + MethodDeclaration method(Class type, Closure body) {} + /** Declares a private helper method returning {@code type}. */ + MethodDeclaration method(String name, Class type) {} + /** Declares a private helper method returning {@code type}, whose parameters and body are the closure's. */ + MethodDeclaration method(String name, Class type, Closure body) {} + + /** Declares a nested configuration class holding the declarations in its body, conditioned as a whole. */ + GroupDeclaration group() {} + /** Declares a nested configuration class holding the declarations in its body. */ + GroupDeclaration group(Closure body) {} + /** Declares a nested configuration class named after {@code name}, conditioned as a whole. */ + GroupDeclaration group(String name) {} + /** Declares a nested configuration class named after {@code name}, holding the declarations in its body. */ + GroupDeclaration group(String name, Closure body) {} + + static class BeanDeclaration { + /** {@code @ConditionalOnMissingBean}: types positionally, the annotation's attributes by name. With no arguments the bean's own type is used. */ + BeanDeclaration conditionalOnMissingBean(Object... typesAndAttributes) {} + /** {@code @ConditionalOnMissingBean} on this bean's own name; the annotation's other attributes by name. */ + BeanDeclaration conditionalOnMissingBeanName(Object... attributes) {} + /** {@code @ConditionalOnBean}: types positionally, the annotation's attributes by name. */ + BeanDeclaration conditionalOnBean(Object... typesAndAttributes) {} + /** {@code @ConditionalOnProperty}: property names positionally, the annotation's attributes ({@code havingValue}, {@code matchIfMissing}, ...) by name. */ + BeanDeclaration conditionalOnProperty(Object... namesAndAttributes) {} + /** {@code @ConditionalOnExpression} with a SpEL expression; single-quote it so Groovy leaves its placeholders alone. */ + BeanDeclaration conditionalOnExpression(String expression) {} + /** {@code @ConditionalOnExpression} with a SpEL expression; single-quote it so Groovy leaves its placeholders alone. */ + BeanDeclaration conditionalOnExpression(String expression, Closure body) {} + /** {@code @ConditionalOnClass}: types and String class names positionally, the annotation's attributes by name. */ + BeanDeclaration conditionalOnClass(Object... typesNamesAndAttributes) {} + /** Registers the bean only in the given Grails environments. */ + BeanDeclaration conditionalOnGrailsEnv(Object... environments) {} + /** Additional names Spring resolves to the same bean. */ + BeanDeclaration aliases(Object... names) {} + /** {@code @Primary} */ + BeanDeclaration primary() {} + /** {@code @Primary} */ + BeanDeclaration primary(Closure body) {} + /** {@code @Lazy} */ + BeanDeclaration lazy() {} + /** {@code @Lazy} */ + BeanDeclaration lazy(Closure body) {} + /** {@code @Scope}: the scope name positionally, the annotation's attributes ({@code proxyMode}, ...) by name. */ + BeanDeclaration scope(Object... nameAndAttributes) {} + /** Makes the factory method static, as {@code BeanFactoryPostProcessor} and {@code BeanPostProcessor} beans require. */ + BeanDeclaration staticMethod() {} + /** Makes the factory method static, as {@code BeanFactoryPostProcessor} and {@code BeanPostProcessor} beans require. */ + BeanDeclaration staticMethod(Closure body) {} + /** Type arguments for the declared type, where the construction does not settle them. */ + BeanDeclaration typeArguments(Object... types) {} + /** Attaches any annotation to the factory method, merging into one a qualifier already attached. */ + BeanDeclaration annotate(Class annotationType) {} + /** Attaches any annotation to the factory method, merging into one a qualifier already attached. */ + BeanDeclaration annotate(Class annotationType, Closure body) {} + /** Attaches any annotation to the factory method, with its attributes by name. */ + BeanDeclaration annotate(Map attributes, Class annotationType) {} + /** Attaches any annotation to the factory method, with its attributes by name. */ + BeanDeclaration annotate(Map attributes, Class annotationType, Closure body) {} + } + + static class FieldDeclaration { + /** {@code @Value("${key}")}, or the expression verbatim when it already holds a {@code ${...}} placeholder or {@code #{...}} SpEL expression. */ + FieldDeclaration value(String keyOrExpression) {} + /** {@code @Value("${key:defaultValue}")} */ + FieldDeclaration value(String key, Object defaultValue) {} + /** Type arguments for the declared type. */ + FieldDeclaration typeArguments(Class... types) {} + /** Attaches any annotation to the field. */ + FieldDeclaration annotate(Class annotationType) {} + /** Attaches any annotation to the field, with its attributes by name. */ + FieldDeclaration annotate(Map attributes, Class annotationType) {} + } + + static class MethodDeclaration { + /** Type arguments for the declared return type. */ + MethodDeclaration typeArguments(Object... types) {} + /** Attaches any annotation to the method. */ + MethodDeclaration annotate(Class annotationType) {} + /** Attaches any annotation to the method. */ + MethodDeclaration annotate(Class annotationType, Closure body) {} + /** Attaches any annotation to the method, with its attributes by name. */ + MethodDeclaration annotate(Map attributes, Class annotationType) {} + /** Attaches any annotation to the method, with its attributes by name. */ + MethodDeclaration annotate(Map attributes, Class annotationType, Closure body) {} + } + + static class GroupDeclaration { + /** {@code @ConditionalOnMissingBean} on the group: types positionally, the annotation's attributes by name. */ + GroupDeclaration conditionalOnMissingBean(Object... typesAndAttributes) {} + /** {@code @ConditionalOnBean} on the group: types positionally, the annotation's attributes by name. */ + GroupDeclaration conditionalOnBean(Object... typesAndAttributes) {} + /** {@code @ConditionalOnProperty} on the group: property names positionally, the annotation's attributes by name. */ + GroupDeclaration conditionalOnProperty(Object... namesAndAttributes) {} + /** {@code @ConditionalOnExpression} on the group. */ + GroupDeclaration conditionalOnExpression(String expression) {} + /** {@code @ConditionalOnExpression} on the group. */ + GroupDeclaration conditionalOnExpression(String expression, Closure body) {} + /** {@code @ConditionalOnClass} on the group: types and String class names positionally, the annotation's attributes by name. */ + GroupDeclaration conditionalOnClass(Object... typesNamesAndAttributes) {} + /** Registers the group's beans only in the given Grails environments. */ + GroupDeclaration conditionalOnGrailsEnv(Object... environments) {} + /** Attaches any annotation to the group's configuration class. */ + GroupDeclaration annotate(Class annotationType) {} + /** Attaches any annotation to the group's configuration class. */ + GroupDeclaration annotate(Class annotationType, Closure body) {} + /** Attaches any annotation to the group's configuration class, with its attributes by name. */ + GroupDeclaration annotate(Map attributes, Class annotationType) {} + /** Attaches any annotation to the group's configuration class, with its attributes by name. */ + GroupDeclaration annotate(Map attributes, Class annotationType, Closure body) {} + } + } + """; + + @Override + public void processDynamicElements(@NotNull PsiType qualifierType, + @Nullable PsiClass aClass, + @NotNull PsiScopeProcessor processor, + @NotNull PsiElement place, + @NotNull ResolveState state) { + if (!(place instanceof GrReferenceExpression ref) || ref.isQualified()) return; + + // Cheap rejection of everything not written inside a property named "beans" + GrField field = PsiTreeUtil.getParentOfType(place, GrField.class); + if (field == null || !GrailsBeansDsl.BEANS_PROPERTY.equals(field.getName())) return; + + GrClosableBlock closure = PsiTreeUtil.getParentOfType(place, GrClosableBlock.class); + if (closure == null || !PsiTreeUtil.isAncestor(field, closure, true)) return; + + if (GrailsBeansDsl.isDeclarationContainer(closure)) { Review Comment: Fixed in a9fd382: - `isBeansClosure` is cached per closure. - Each `Declaration` splits its name and type arguments once and memoizes its name and type. The declarations are now invalidated by `MODIFICATION_COUNT`, since those values depend on other files. - The light fields and methods are built once per container and filtered by the name hint. ########## plugin/src/main/java/org/apache/grails/intellij/plugin/spring/GrailsBeansDsl.java: ########## @@ -0,0 +1,458 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.grails.intellij.plugin.spring; + +import com.intellij.openapi.module.Module; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiType; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.searches.AnnotatedElementsSearch; +import com.intellij.psi.search.searches.ClassInheritorsSearch; +import com.intellij.psi.util.CachedValueProvider.Result; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrBinaryExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * The Grails 8 compile-time beans DSL: a {@code beans} closure property whose top-level statements are + * {@code bean(...)}, {@code field(...)}, {@code method(...)} and {@code group(...)} declarations, compiled by + * {@code grails.compiler.beans.GrailsBeans} into real {@code @Bean} factory methods. + * <p> + * The property is compiled on a class annotated with {@code @GrailsBeans}, and implicitly on a plugin descriptor + * ({@code grails.plugins.Plugin}), on the application class ({@code grails.boot.config.GrailsAutoConfiguration}) + * and on a unit test ({@code org.grails.testing.GrailsUnitTest}). Nothing is recognised unless the + * {@code GrailsBeans} annotation is on the class path, so pre-8 projects are unaffected. + * + * @see GrailsBeansDslMemberContributor + */ +public final class GrailsBeansDsl { + + public static final String GRAILS_BEANS_ANNOTATION = "grails.compiler.beans.GrailsBeans"; + public static final String BEANS_PROPERTY = "beans"; + + private static final String PLUGIN_CLASS = "grails.plugins.Plugin"; + private static final String GRAILS_AUTO_CONFIGURATION = "grails.boot.config.GrailsAutoConfiguration"; + private static final String GRAILS_UNIT_TEST = "org.grails.testing.GrailsUnitTest"; + + private static final String TYPE_ARGUMENTS_CALL = "typeArguments"; + private static final int MAX_CONSTANT_DEPTH = 16; + + public enum Kind { + BEAN("bean"), FIELD("field"), METHOD("method"), GROUP("group"); + + private final String myCallName; + + Kind(String callName) { + myCallName = callName; + } + + public String getCallName() { + return myCallName; + } + + static @Nullable Kind byCallName(@Nullable String callName) { + for (Kind kind : values()) { + if (kind.myCallName.equals(callName)) return kind; + } + return null; + } + } + + private GrailsBeansDsl() { + } + + /** + * Whether the beans DSL is available to code in the given context, i.e. the project builds against Grails 8+. + */ + public static boolean isAvailable(@NotNull PsiElement context) { + return JavaPsiFacade.getInstance(context.getProject()).findClass(GRAILS_BEANS_ANNOTATION, context.getResolveScope()) != null; + } + + /** + * Whether the {@code beans} property of the given class is compiled as the beans DSL. + */ + public static boolean isBeansHost(@Nullable PsiClass aClass) { + if (aClass == null) return false; + return aClass.hasAnnotation(GRAILS_BEANS_ANNOTATION) + || InheritanceUtil.isInheritor(aClass, PLUGIN_CLASS) + || InheritanceUtil.isInheritor(aClass, GRAILS_AUTO_CONFIGURATION) + || InheritanceUtil.isInheritor(aClass, GRAILS_UNIT_TEST); + } + + /** + * The {@code beans} closure declared on the given class, if it is compiled as the beans DSL. + */ + public static @Nullable GrClosableBlock getBeansClosure(@Nullable PsiClass aClass) { + if (!(aClass instanceof GrTypeDefinition)) return null; + PsiField field = ((GrTypeDefinition)aClass).findCodeFieldByName(BEANS_PROPERTY, false); + if (!(field instanceof GrField) || !(((GrField)field).getInitializerGroovy() instanceof GrClosableBlock closure)) return null; + return isBeansClosure(closure) ? closure : null; + } + + /** + * Whether the closure is the initializer of a {@code beans} property compiled as the beans DSL. + */ + public static boolean isBeansClosure(@NotNull GrClosableBlock closure) { + if (!(closure.getParent() instanceof GrField field) || !BEANS_PROPERTY.equals(field.getName())) return false; + return isBeansHost(field.getContainingClass()) && isAvailable(closure); + } + + /** + * Whether DSL declarations are written directly inside the closure: the {@code beans} closure itself, or the + * body of one of its top-level {@code group(...)} declarations. + */ + public static boolean isDeclarationContainer(@NotNull GrClosableBlock closure) { + if (isBeansClosure(closure)) return true; + Declaration declaration = getDeclarationOfBody(closure); + return declaration != null && declaration.getKind() == Kind.GROUP; + } + + /** + * The declaration the closure is the body of, when it is the body of a {@code bean(...)}, {@code method(...)} + * or {@code group(...)} declared directly in a {@code beans} closure or in one of its groups. + */ + public static @Nullable Declaration getDeclarationOfBody(@NotNull GrClosableBlock closure) { + PsiElement parent = closure.getParent(); + if (parent instanceof GrArgumentList) parent = parent.getParent(); + if (!(parent instanceof GrMethodCall call)) return null; + + GrMethodCall statement = getChainStatement(call); + if (!(statement.getParent() instanceof GrClosableBlock container)) return null; + + for (Declaration declaration : getDeclarations(container)) { + if (declaration.getStatement() == statement) { + if (declaration.getKind() == Kind.FIELD) return null; + // Groups do not nest + boolean declared = declaration.getKind() == Kind.GROUP ? isBeansClosure(container) : isDeclarationContainer(container); + return declared ? declaration : null; + } + } + return null; + } + + /** + * The DSL declarations written directly inside the closure. The closure is not checked to be a declaration + * container: see {@link #isDeclarationContainer(GrClosableBlock)}. + */ + public static @NotNull List<Declaration> getDeclarations(@NotNull GrClosableBlock container) { + return CachedValuesManager.getCachedValue(container, () -> Result.create(computeDeclarations(container), container)); + } + + private static @NotNull List<Declaration> computeDeclarations(@NotNull GrClosableBlock container) { + List<Declaration> result = new ArrayList<>(); + for (PsiElement statement : container.getStatements()) { + if (!(statement instanceof GrMethodCall call)) continue; + GrMethodCall root = getChainRoot(call); + if (root == null) continue; + Kind kind = Kind.byCallName(getUnqualifiedCallName(root)); + if (kind != null) { + result.add(new Declaration(kind, root, call)); + } + } + return Collections.unmodifiableList(result); + } + + /** + * The beans the class declares through the DSL, including those inside its groups. + */ + public static @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> getBeanDescriptors(@NotNull PsiClass aClass) { + GrClosableBlock beansClosure = getBeansClosure(aClass); + if (beansClosure == null) return Collections.emptyList(); + + List<GrailsResourceBeanExtractor.BeanDescriptor> result = new ArrayList<>(); + collectBeanDescriptors(beansClosure, result); + return result; + } + + private static void collectBeanDescriptors(@NotNull GrClosableBlock container, @NotNull List<GrailsResourceBeanExtractor.BeanDescriptor> result) { + for (Declaration declaration : getDeclarations(container)) { + if (declaration.getKind() == Kind.GROUP) { + GrClosableBlock body = declaration.getBody(); + if (body != null) collectBeanDescriptors(body, result); + continue; + } + if (declaration.getKind() != Kind.BEAN) continue; + + String name = declaration.getName(); + if (name == null || !(declaration.getTypeExpression() instanceof GrReferenceExpression typeReference)) continue; + + GrailsResourceBeanExtractor.BeanDescriptor descriptor = new GrailsResourceBeanExtractor.BeanDescriptor(name); + descriptor.getReferences().add(typeReference); + result.add(descriptor); + } + } + + /** + * The classes in the module (and the modules it depends on) whose {@code beans} property is compiled as the DSL. + */ + public static @NotNull Collection<PsiClass> findBeansHosts(@NotNull Module module) { + JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject()); + GlobalSearchScope librariesScope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false); + PsiClass annotation = facade.findClass(GRAILS_BEANS_ANNOTATION, librariesScope); + if (annotation == null) return Collections.emptyList(); + + GlobalSearchScope sourceScope = GlobalSearchScope.moduleWithDependenciesScope(module); + Set<PsiClass> result = new LinkedHashSet<>(AnnotatedElementsSearch.searchPsiClasses(annotation, sourceScope).findAll()); + for (String superClassName : new String[]{GRAILS_AUTO_CONFIGURATION, PLUGIN_CLASS}) { + PsiClass superClass = facade.findClass(superClassName, librariesScope); + if (superClass != null) { + result.addAll(ClassInheritorsSearch.search(superClass, sourceScope, true).findAll()); + } + } + return result; + } + + /** + * The outermost call of a qualifier chain such as {@code bean(Foo).primary().lazy()}, given any call in it. + */ + private static @NotNull GrMethodCall getChainStatement(@NotNull GrMethodCall call) { + GrMethodCall result = call; + while (result.getParent() instanceof GrReferenceExpression ref + && ref.getQualifierExpression() == result + && ref.getParent() instanceof GrMethodCall outer + && outer.getInvokedExpression() == ref) { + result = outer; + } + return result; + } + + /** + * The unqualified call at the root of a qualifier chain such as {@code bean(Foo).primary().lazy()}. + */ + private static @Nullable GrMethodCall getChainRoot(@NotNull GrMethodCall call) { + GrMethodCall result = call; + while (true) { + if (!(result.getInvokedExpression() instanceof GrReferenceExpression ref)) return null; + GrExpression qualifier = ref.getQualifierExpression(); + if (qualifier == null) return result; + if (!(qualifier instanceof GrMethodCall qualifierCall)) return null; + result = qualifierCall; + } + } + + private static @Nullable String getUnqualifiedCallName(@NotNull GrMethodCall call) { + return call.getInvokedExpression() instanceof GrReferenceExpression ref && !ref.isQualified() ? ref.getReferenceName() : null; + } + + private static boolean isTypeArgument(@NotNull GrExpression expression) { + if (!(expression instanceof GrReferenceExpression ref)) return false; + PsiElement resolved = ref.resolve(); + if (resolved != null) return resolved instanceof PsiClass; + String name = ref.getReferenceName(); + return name != null && !name.isEmpty() && Character.isUpperCase(name.charAt(0)) && !name.equals(name.toUpperCase()); + } + + /** + * Folds a compile-time String constant the way the transform does for a declared name: a literal, a reference to + * a {@code static final} field initialised to one, or a concatenation of those. + */ + static @Nullable String evaluateStringConstant(@Nullable GrExpression expression, int depth) { + if (expression == null || depth > MAX_CONSTANT_DEPTH) return null; + + if (expression instanceof GrParenthesizedExpression parenthesized) { + return evaluateStringConstant(parenthesized.getOperand(), depth + 1); + } + if (expression instanceof GrLiteral literal) { + return literal.getValue() instanceof String value ? value : null; + } + if (expression instanceof GrBinaryExpression binary && binary.getOperationTokenType() == GroovyTokenTypes.mPLUS) { + String left = evaluateStringConstant(binary.getLeftOperand(), depth + 1); + String right = left == null ? null : evaluateStringConstant(binary.getRightOperand(), depth + 1); + return right == null ? null : left + right; + } + if (!(expression instanceof GrReferenceExpression ref)) return null; + + PsiElement resolved = ref.resolve(); + // A Groovy property is reached through its getter from outside its class + if (resolved instanceof GrAccessorMethod accessor) resolved = accessor.getProperty(); + if (!(resolved instanceof PsiField field) + || !field.hasModifierProperty(PsiModifier.STATIC) || !field.hasModifierProperty(PsiModifier.FINAL)) { + return null; + } + if (field instanceof GrField grField) { + return evaluateStringConstant(grField.getInitializerGroovy(), depth + 1); + } + return field.computeConstantValue() instanceof String value ? value : null; + } + + /** + * The bean name Grails derives from a type, following {@code java.beans.Introspector#decapitalize}. + */ + static @NotNull String decapitalize(@NotNull String name) { Review Comment: Done in a9fd382: both now use `StringUtil.decapitalize` and `PsiUtil.getUnqualifiedMethodName`. ########## plugin/src/test/java/org/apache/grails/intellij/plugin/reference/spring/GrailsBeansDslTest.java: ########## @@ -0,0 +1,408 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.grails.intellij.plugin.reference.spring; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.containers.ContainerUtil; +import org.apache.grails.intellij.lib.testFramework.GrailsTestCase; +import org.apache.grails.intellij.plugin.spring.GrailsBeansDsl; +import org.apache.grails.intellij.plugin.spring.GrailsResourceBeanExtractor; +import org.jetbrains.plugins.groovy.codeInspection.assignment.GroovyAssignabilityCheckInspection; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The Grails 8 compile-time beans DSL ({@code grails.compiler.beans.GrailsBeans}): resolution of the declarations + * and their qualifier chains, of the shared {@code field(...)}/{@code method(...)} members inside bean bodies, and + * extraction of the declared beans. The DSL support is independent of the Spring Support plugin. + */ +public class GrailsBeansDslTest extends GrailsTestCase { + + private static final String DSL_BODY = """ + field('suffix', String).value('app.greeting-suffix', '!') + field(Formatter) + field('names', List).typeArguments(String) + method('buildGreeting', String) { String name -> + "Hello, ${name}${suffix}" + } + + bean(MyService) + bean(GREETER, Greeter) { MyService myService -> + new Greeter(buildGreeting('World').toUpperCase() + formatter.format(suffix) + names.first().trim()) + } + bean('special', Greeter).primary().lazy().scope('prototype', proxyMode: 'x').conditionalOnMissingBean(name: 'other') { + new Greeter(suffix.trim()) + } + bean('provider', Provider, DefaultProvider).conditionalOnProperty('app.enabled', havingValue: 'true').aliases('legacyProvider') + bean(URLHolder).staticMethod().annotate(Deprecated).annotate(SuppressWarnings, value: 'x').conditionalOnGrailsEnv('development') + group('optional').conditionalOnClass(name: 'com.example.Missing') { + field('prefix', String).value('app.prefix') + bean('optionalGreeter', Greeter) { + new Greeter(prefix.trim()) + } + } + """; + + @Override + protected void setUp() throws Exception { + super.setUp(); + myFixture.addClass("package grails.compiler.beans; public @interface GrailsBeans {}"); Review Comment: Added both in a9fd382. `testDslInertWithoutGrailsBeans` deletes the annotation, then checks that `bean(...)` stays unresolved and no hosts are found. `testPluginDescriptorRecognisedByName` covers an abstract descriptor, one without the suffix, and one that doesn't extend `Plugin`. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
