diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/BuilderAnnotationContributor.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/BuilderAnnotationContributor.java new file mode 100644 index 000000000000..aed4f3617914 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/BuilderAnnotationContributor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.lang.resolve.ast.builder; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.PsiModificationTracker; +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.typedef.GrTypeDefinition; +import org.jetbrains.plugins.groovy.lang.resolve.ast.AstTransformContributor; +import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.GrBuilderStrategySupport.Members; + +import java.util.Collection; + +public class BuilderAnnotationContributor extends AstTransformContributor { + + @Override + public void collectMethods(@NotNull GrTypeDefinition clazz, final Collection collector) { + collector.addAll(collectAll(clazz).methods); + } + + @Override + public void collectClasses(@NotNull GrTypeDefinition clazz, Collection collector) { + collector.addAll(collectAll(clazz).classes); + } + + @Override + public void collectFields(@NotNull GrTypeDefinition clazz, Collection collector) { + collector.addAll(collectAll(clazz).fields); + } + + private static Members collectAll(final GrTypeDefinition clazz) { + return CachedValuesManager.getCachedValue(clazz, new CachedValueProvider() { + @Nullable + @Override + public Result compute() { + final Members result = new Members(); + for (GrBuilderStrategySupport strategySupport : GrBuilderStrategySupport.EP.getExtensions()) { + result.addFrom(strategySupport.process(clazz)); + } + return Result.create(result, PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT); + } + }); + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/BuilderHelperLightPsiClass.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/BuilderHelperLightPsiClass.java new file mode 100644 index 000000000000..f2954a1d3476 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/BuilderHelperLightPsiClass.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.lang.resolve.ast.builder; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.impl.light.LightPsiClassBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition; + +public class BuilderHelperLightPsiClass extends LightPsiClassBuilder { + + private final @NotNull GrTypeDefinition myContainingClass; + + public BuilderHelperLightPsiClass(@NotNull GrTypeDefinition containingClass, @NotNull String name) { + super(containingClass, name); + myContainingClass = containingClass; + setScope(myContainingClass); + setOriginInfo(GrBuilderStrategySupport.ORIGIN_INFO); + getModifierList().addModifier(PsiModifier.STATIC); + } + + @NotNull + @Override + public PsiClass getContainingClass() { + return myContainingClass; + } + + @Override + public PsiFile getContainingFile() { + return myContainingClass.getContainingFile(); + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/GrBuilderStrategySupport.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/GrBuilderStrategySupport.java new file mode 100644 index 000000000000..082a6f20e2af --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/GrBuilderStrategySupport.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.lang.resolve.ast.builder; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.*; +import com.intellij.util.containers.ContainerUtil; +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.expressions.GrReferenceExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition; +import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil; +import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy.DefaultBuilderStrategySupport; + +import java.util.Collection; + +public abstract class GrBuilderStrategySupport { + + public static final ExtensionPointName EP = ExtensionPointName.create("org.intellij.groovy.builderStrategySupport"); + public static final String BUILDER_FQN = "groovy.transform.builder.Builder"; + public static final String ORIGIN_INFO = "by @Builder"; + + public static class Members { + public final Collection methods = ContainerUtil.newArrayList(); + public final Collection fields = ContainerUtil.newArrayList(); + public final Collection classes = ContainerUtil.newArrayList(); + public void addFrom(Members other) { + methods.addAll(other.methods); + fields.addAll(other.fields); + classes.addAll(other.classes); + } + } + + @NotNull + public abstract Members process(GrTypeDefinition typeDefinition); + + @Nullable + public static String getStrategy(PsiModifierListOwner annotatedMember) { + final PsiAnnotation annotation = PsiImplUtil.getAnnotation(annotatedMember, BUILDER_FQN); + if (annotation == null) return null; + + final PsiAnnotationMemberValue strategy = annotation.findDeclaredAttributeValue("builderStrategy"); + if (strategy instanceof GrReferenceExpression) { + final PsiElement element = ((GrReferenceExpression)strategy).resolve(); + return element instanceof PsiClass ? ((PsiClass)element).getQualifiedName() : null; + } + + return DefaultBuilderStrategySupport.DEFAULT_STRATEGY_FQN; + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/strategy/DefaultBuilderStrategySupport.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/strategy/DefaultBuilderStrategySupport.java new file mode 100644 index 000000000000..7ed40938d27b --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/resolve/ast/builder/strategy/DefaultBuilderStrategySupport.java @@ -0,0 +1,176 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy; + +import com.intellij.codeInsight.AnnotationUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightMethodBuilder; +import com.intellij.psi.impl.light.LightPsiClassBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod; +import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil; +import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.BuilderHelperLightPsiClass; +import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.GrBuilderStrategySupport; + +public class DefaultBuilderStrategySupport extends GrBuilderStrategySupport { + + public static final String DEFAULT_STRATEGY_FQN = "groovy.transform.builder.DefaultStrategy"; + + @NotNull + public Members process(@NotNull final GrTypeDefinition typeDefinition) { + return new DefaultBuilderStrategyHandler(typeDefinition).doProcess(); + } + + private static class DefaultBuilderStrategyHandler { + + private final @NotNull GrTypeDefinition myContainingClass; + private final @NotNull PsiElementFactory myElementFactory; + private final Members myMembers; + + private DefaultBuilderStrategyHandler(@NotNull GrTypeDefinition typeDefinition) { + myContainingClass = typeDefinition; + myElementFactory = PsiElementFactory.SERVICE.getInstance(typeDefinition.getProject()); + myMembers = new Members(); + } + + @NotNull + private PsiManager getManager() { + return myContainingClass.getManager(); + } + + @NotNull + public Members doProcess() { + processTypeDefinition(); + processMethods(); + return myMembers; + } + + private void processTypeDefinition() { + final PsiAnnotation builderAnno = PsiImplUtil.getAnnotation(myContainingClass, BUILDER_FQN); + if (builderAnno == null || !DEFAULT_STRATEGY_FQN.equals(getStrategy(myContainingClass))) return; + final PsiClass builderClass = createBuilderClass(builderAnno, myContainingClass.getCodeFields()); + final LightMethodBuilder builderMethod = createBuilderMethod(builderClass, builderAnno); + myMembers.classes.add(builderClass); + myMembers.methods.add(builderMethod); + } + + @NotNull + private LightPsiClassBuilder createBuilderClass(@NotNull final PsiAnnotation annotation, @NotNull GrVariable[] setters) { + return createBuilderClass(annotation, setters, null); + } + + @NotNull + private LightPsiClassBuilder createBuilderClass(@NotNull final PsiAnnotation annotation, + @NotNull GrVariable[] setters, + @Nullable PsiType builtType) { + final LightPsiClassBuilder builderClass = new BuilderHelperLightPsiClass( + myContainingClass, getBuilderClassName(annotation, myContainingClass) + ); + + for (GrVariable field : setters) { + builderClass.addMethod(createFieldSetter(builderClass, field, annotation)); + } + + final LightMethodBuilder buildMethod = new LightMethodBuilder(getManager(), getBuildMethodName(annotation)); + buildMethod.setContainingClass(builderClass); + buildMethod.setOriginInfo(ORIGIN_INFO); + buildMethod.setMethodReturnType(builtType == null ? myElementFactory.createType(myContainingClass) : builtType); + return builderClass.addMethod(buildMethod); + } + + @NotNull + private LightMethodBuilder createBuilderMethod(@NotNull PsiClass builderClass, @NotNull PsiAnnotation annotation) { + final LightMethodBuilder builderMethod = new LightMethodBuilder(getManager(), getBuilderMethodName(annotation)); + builderMethod.addModifier(PsiModifier.STATIC); + builderMethod.setContainingClass(myContainingClass); + builderMethod.setOriginInfo(ORIGIN_INFO); + builderMethod.setNavigationElement(annotation); + builderMethod.setMethodReturnType(myElementFactory.createType(builderClass)); + return builderMethod; + } + + private void processMethods() { + for (GrMethod method : myContainingClass.getCodeMethods()) { + processMethod(method); + } + } + + private void processMethod(@NotNull GrMethod method) { + final PsiAnnotation annotation = PsiImplUtil.getAnnotation(method, BUILDER_FQN); + if (annotation == null || !DEFAULT_STRATEGY_FQN.equals(getStrategy(method))) return; + if (method.isConstructor()) { + processConstructor(method, annotation); + } + else if (method.hasModifierProperty(PsiModifier.STATIC)) { + processFactoryMethod(method, annotation); + } + } + + private void processConstructor(@NotNull GrMethod method, PsiAnnotation annotation) { + PsiClass builderClass = createBuilderClass(annotation, method.getParameters()); + LightMethodBuilder builderMethod = createBuilderMethod(builderClass, annotation); + myMembers.methods.add(builderMethod); + myMembers.classes.add(builderClass); + } + + private void processFactoryMethod(@NotNull GrMethod method, PsiAnnotation annotation) { + PsiClass builderClass = createBuilderClass(annotation, method.getParameters(), method.getReturnType()); + LightMethodBuilder builderMethod = createBuilderMethod(builderClass, annotation); + myMembers.methods.add(builderMethod); + myMembers.classes.add(builderClass); + } + + @NotNull + private LightMethodBuilder createFieldSetter(@NotNull PsiClass builderClass, @NotNull GrVariable field, @NotNull PsiAnnotation annotation) { + final String name = field.getName(); + final LightMethodBuilder fieldSetter = new LightMethodBuilder(getManager(), getFieldMethodName(annotation, name)); + fieldSetter.addModifier(PsiModifier.PUBLIC); + fieldSetter.addParameter(name, field.getType(), false); + fieldSetter.setContainingClass(builderClass); + fieldSetter.setMethodReturnType(myElementFactory.createType(builderClass)); + fieldSetter.setNavigationElement(field); + return fieldSetter; + } + + @NotNull + private static String getBuilderMethodName(@NotNull PsiAnnotation annotation) { + final String builderMethodName = AnnotationUtil.getDeclaredStringAttributeValue(annotation, "builderMethodName"); + return builderMethodName == null ? "builder" : builderMethodName; + } + + @NotNull + private static String getBuilderClassName(@NotNull PsiAnnotation annotation, @NotNull GrTypeDefinition clazz) { + final String builderClassName = AnnotationUtil.getDeclaredStringAttributeValue(annotation, "builderClassName"); + return builderClassName == null ? String.format("%s%s", clazz.getName(), "Builder") : builderClassName; + } + + @NotNull + private static String getFieldMethodName(@NotNull PsiAnnotation annotation, @NotNull String fieldName) { + final String prefix = AnnotationUtil.getDeclaredStringAttributeValue(annotation, "prefix"); + return prefix == null ? fieldName : String.format("%s%s", prefix, StringUtil.capitalize(fieldName)); + } + + @NotNull + private static String getBuildMethodName(@NotNull PsiAnnotation annotation) { + final String buildMethodName = AnnotationUtil.getDeclaredStringAttributeValue(annotation, "buildMethodName"); + return buildMethodName == null ? "build" : buildMethodName; + } + } +} diff --git a/plugins/groovy/src/META-INF/plugin.xml b/plugins/groovy/src/META-INF/plugin.xml index 06972930aab0..f62e87a939a8 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -31,6 +31,7 @@ + @@ -112,6 +113,8 @@ + +