replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / ui-designer / src / com / intellij / uiDesigner / ErrorAnalyzer.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.uiDesigner;
17
18 import com.intellij.codeHighlighting.HighlightDisplayLevel;
19 import com.intellij.codeInsight.daemon.impl.SeverityRegistrar;
20 import com.intellij.codeInspection.LocalInspectionTool;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.extensions.Extensions;
23 import com.intellij.openapi.module.Module;
24 import com.intellij.openapi.progress.ProcessCanceledException;
25 import com.intellij.openapi.progress.ProgressIndicator;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.util.Comparing;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import com.intellij.psi.*;
30 import com.intellij.uiDesigner.designSurface.GuiEditor;
31 import com.intellij.uiDesigner.inspections.FormInspectionTool;
32 import com.intellij.uiDesigner.lw.IButtonGroup;
33 import com.intellij.uiDesigner.lw.IComponent;
34 import com.intellij.uiDesigner.lw.IContainer;
35 import com.intellij.uiDesigner.lw.IRootContainer;
36 import com.intellij.uiDesigner.quickFixes.*;
37 import com.intellij.uiDesigner.radComponents.RadComponent;
38 import com.intellij.uiDesigner.radComponents.RadRootContainer;
39 import com.intellij.util.IncorrectOperationException;
40 import com.intellij.util.containers.HashSet;
41 import org.jetbrains.annotations.NonNls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44
45 import javax.swing.*;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 import java.util.Set;
50
51 /**
52  * @author Anton Katilin
53  * @author Vladimir Kondratyev
54  */
55 public final class ErrorAnalyzer {
56   private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.ErrorAnalyzer");
57
58   /**
59    * Value {@link ErrorInfo}
60    */
61   @NonNls
62   public static final String CLIENT_PROP_CLASS_TO_BIND_ERROR = "classToBindError";
63   /**
64    * Value {@link ErrorInfo}
65    */
66   @NonNls
67   public static final String CLIENT_PROP_BINDING_ERROR = "bindingError";
68
69   @NonNls public static final String CLIENT_PROP_ERROR_ARRAY = "errorArray";
70
71   private ErrorAnalyzer() {
72   }
73
74   static void analyzeErrors(@NotNull GuiEditor editor, final IRootContainer rootContainer, @Nullable final ProgressIndicator progress) {
75     analyzeErrors(editor.getModule(), editor.getFile(), editor, rootContainer, progress);
76   }
77
78   /**
79    * @param editor if null, no quick fixes are created. This is used in form to source compiler.
80    */
81   public static void analyzeErrors(@NotNull final Module module,
82                                    @NotNull final VirtualFile formFile,
83                                    @Nullable final GuiEditor editor,
84                                    @NotNull final IRootContainer rootContainer,
85                                    @Nullable final ProgressIndicator progress) {
86     if (module.isDisposed()) {
87       return;
88     }
89
90     // 1. Validate class to bind
91     final String classToBind = rootContainer.getClassToBind();
92     final PsiClass psiClass;
93     if (classToBind != null) {
94       psiClass = FormEditingUtil.findClassToBind(module, classToBind);
95       if (psiClass == null) {
96         final QuickFix[] fixes = editor != null ? new QuickFix[]{new CreateClassToBindFix(editor, classToBind)} : QuickFix.EMPTY_ARRAY;
97         final ErrorInfo errorInfo = new ErrorInfo(null, null, UIDesignerBundle.message("error.class.does.not.exist", classToBind),
98                                                   HighlightDisplayLevel.ERROR, fixes);
99         rootContainer.putClientProperty(CLIENT_PROP_CLASS_TO_BIND_ERROR, errorInfo);
100       }
101       else {
102         rootContainer.putClientProperty(CLIENT_PROP_CLASS_TO_BIND_ERROR, null);
103       }
104     }
105     else {
106       rootContainer.putClientProperty(CLIENT_PROP_CLASS_TO_BIND_ERROR, null);
107       psiClass = null;
108     }
109
110     // 2. Validate bindings to fields
111     // field name -> error message
112     final ArrayList<String> usedBindings = new ArrayList<>(); // for performance reasons
113     final Set<IButtonGroup> processedGroups = new HashSet<>();
114     FormEditingUtil.iterate(
115       rootContainer,
116       new FormEditingUtil.ComponentVisitor<IComponent>() {
117         public boolean visit(final IComponent component) {
118           if (progress != null && progress.isCanceled()) return false;
119
120           // Reset previous error (if any)
121           component.putClientProperty(CLIENT_PROP_BINDING_ERROR, null);
122
123           final String binding = component.getBinding();
124
125           // a. Check that field exists and field is not static
126           if (psiClass != null && binding != null) {
127             if (validateFieldInClass(component, binding, component.getComponentClassName(), psiClass, editor, module)) return true;
128           }
129
130           // b. Check that binding is unique
131           if (binding != null) {
132             if (usedBindings.contains(binding)) {
133               // TODO[vova] implement
134               component.putClientProperty(
135                 CLIENT_PROP_BINDING_ERROR,
136                 new ErrorInfo(
137                   component, null, UIDesignerBundle.message("error.binding.already.exists", binding),
138                   HighlightDisplayLevel.ERROR,
139                   QuickFix.EMPTY_ARRAY
140                 )
141               );
142               return true;
143             }
144
145             usedBindings.add(binding);
146           }
147
148           IButtonGroup group = FormEditingUtil.findGroupForComponent(rootContainer, component);
149           if (group != null && !processedGroups.contains(group)) {
150             processedGroups.add(group);
151             if (group.isBound()) {
152               validateFieldInClass(component, group.getName(), ButtonGroup.class.getName(), psiClass, editor, module);
153             }
154           }
155
156           return true;
157         }
158       }
159     );
160     if (progress != null) progress.checkCanceled();
161
162     // Check that there are no panels in XY with children
163     FormEditingUtil.iterate(
164       rootContainer,
165       new FormEditingUtil.ComponentVisitor<IComponent>() {
166         public boolean visit(final IComponent component) {
167           if (progress != null && progress.isCanceled()) return false;
168
169           // Clear previous error (if any)
170           component.putClientProperty(CLIENT_PROP_ERROR_ARRAY, null);
171
172           if (!(component instanceof IContainer)) {
173             return true;
174           }
175
176           final IContainer container = (IContainer)component;
177           if (container instanceof IRootContainer) {
178             final IRootContainer rootContainer = (IRootContainer)container;
179             if (rootContainer.getComponentCount() > 1) {
180               // TODO[vova] implement
181               putError(component, new ErrorInfo(
182                 component, null, UIDesignerBundle.message("error.multiple.toplevel.components"),
183                 HighlightDisplayLevel.ERROR,
184                 QuickFix.EMPTY_ARRAY
185               ));
186             }
187           }
188           else if (container.isXY() && container.getComponentCount() > 0) {
189             // TODO[vova] implement
190             putError(component, new ErrorInfo(
191               component, null, UIDesignerBundle.message("error.panel.not.laid.out"),
192               HighlightDisplayLevel.ERROR,
193               QuickFix.EMPTY_ARRAY
194             )
195             );
196           }
197           return true;
198         }
199       }
200     );
201     if (progress != null) progress.checkCanceled();
202
203     try {
204       // Run inspections for form elements
205       final PsiFile formPsiFile = PsiManager.getInstance(module.getProject()).findFile(formFile);
206       if (formPsiFile != null && rootContainer instanceof RadRootContainer) {
207         final List<FormInspectionTool> formInspectionTools = new ArrayList<>();
208         final FormInspectionTool[] registeredFormInspections = Extensions.getExtensions(FormInspectionTool.EP_NAME);
209         for (FormInspectionTool formInspectionTool : registeredFormInspections) {
210           if (formInspectionTool.isActive(formPsiFile) && !isSuppressed(rootContainer, formInspectionTool, null)) {
211             formInspectionTools.add(formInspectionTool);
212           }
213         }
214
215         if (!formInspectionTools.isEmpty() && editor != null) {
216           for (FormInspectionTool tool : formInspectionTools) {
217             tool.startCheckForm(rootContainer);
218           }
219           FormEditingUtil.iterate(
220             rootContainer,
221             (FormEditingUtil.ComponentVisitor<RadComponent>)(RadComponent component) -> {
222               if (progress != null && progress.isCanceled()) return false;
223
224               for (FormInspectionTool tool : formInspectionTools) {
225                 if (isSuppressed(rootContainer, tool, component.getId())) continue;
226                 ErrorInfo[] errorInfos = tool.checkComponent(editor, component);
227                 if (errorInfos != null) {
228                   ArrayList<ErrorInfo> errorList = getErrorInfos(component);
229                   if (errorList == null) {
230                     errorList = new ArrayList<>();
231                     component.putClientProperty(CLIENT_PROP_ERROR_ARRAY, errorList);
232                   }
233                   Collections.addAll(errorList, errorInfos);
234                 }
235               }
236               return true;
237             }
238           );
239           for (FormInspectionTool tool : formInspectionTools) {
240             tool.doneCheckForm(rootContainer);
241           }
242         }
243       }
244     }
245     catch (ProcessCanceledException e) {
246       throw e;
247     }
248     catch (Exception e) {
249       LOG.error(e);
250     }
251   }
252
253   public static boolean isSuppressed(@NotNull IRootContainer rootContainer,
254                                      @NotNull FormInspectionTool formInspectionTool, String componentId) {
255     String shortName = formInspectionTool.getShortName();
256     if (rootContainer.isInspectionSuppressed(shortName, componentId)) return true;
257     if (formInspectionTool instanceof LocalInspectionTool) {
258       String alternativeID = ((LocalInspectionTool)formInspectionTool).getAlternativeID();
259       if (!Comparing.equal(alternativeID, shortName)) {
260         return rootContainer.isInspectionSuppressed(alternativeID, componentId);
261       }
262     }
263     return false;
264   }
265
266   private static boolean validateFieldInClass(final IComponent component, final String fieldName, final String fieldClassName,
267                                               final PsiClass psiClass, final GuiEditor editor, final Module module) {
268     final PsiField[] fields = psiClass.getFields();
269     PsiField field = null;
270     for(int i = fields.length - 1; i >=0 ; i--){
271       if(fieldName.equals(fields[i].getName())){
272         field = fields[i];
273         break;
274       }
275     }
276     if(field == null){
277       final QuickFix[] fixes = editor != null
278                                ? new QuickFix[]{ new CreateFieldFix(editor, psiClass, fieldClassName, fieldName) }
279                                : QuickFix.EMPTY_ARRAY;
280       component.putClientProperty(
281        CLIENT_PROP_BINDING_ERROR,
282        new ErrorInfo(
283          component, null, UIDesignerBundle.message("error.no.field.in.class", fieldName, psiClass.getQualifiedName()),
284          HighlightDisplayLevel.ERROR,
285          fixes
286        )
287       );
288       return true;
289     }
290     else if(field.hasModifierProperty(PsiModifier.STATIC)){
291       component.putClientProperty(
292         CLIENT_PROP_BINDING_ERROR,
293         new ErrorInfo(
294           component, null, UIDesignerBundle.message("error.cant.bind.to.static", fieldName),
295           HighlightDisplayLevel.ERROR,
296           QuickFix.EMPTY_ARRAY
297         )
298       );
299       return true;
300     }
301
302     // Check that field has correct fieldType
303     try {
304       final String className = fieldClassName.replace('$', '.'); // workaround for PSI
305       final PsiType componentType = JavaPsiFacade.getInstance(module.getProject()).getElementFactory().createTypeFromText(
306         className,
307         null
308       );
309       final PsiType fieldType = field.getType();
310       if(!fieldType.isAssignableFrom(componentType)){
311         final QuickFix[] fixes = editor != null ? new QuickFix[]{
312           new ChangeFieldTypeFix(editor, field, componentType)
313         } : QuickFix.EMPTY_ARRAY;
314         component.putClientProperty(
315           CLIENT_PROP_BINDING_ERROR,
316           new ErrorInfo(
317             component, null, UIDesignerBundle.message("error.bind.incompatible.types", fieldType.getPresentableText(), className),
318             HighlightDisplayLevel.ERROR,
319             fixes
320           )
321         );
322         return true;
323       }
324     }
325     catch (IncorrectOperationException ignored) {
326     }
327
328     if (component.isCustomCreate() && FormEditingUtil.findCreateComponentsMethod(psiClass) == null) {
329       final QuickFix[] fixes = editor != null ? new QuickFix[]{
330         new GenerateCreateComponentsFix(editor, psiClass)
331       } : QuickFix.EMPTY_ARRAY;
332       component.putClientProperty(
333         CLIENT_PROP_BINDING_ERROR,
334         new ErrorInfo(
335           component, "Custom Create",
336           UIDesignerBundle.message("error.no.custom.create.method"), HighlightDisplayLevel.ERROR,
337           fixes));
338       return true;
339     }
340     return false;
341   }
342
343   private static void putError(final IComponent component, final ErrorInfo errorInfo) {
344     ArrayList<ErrorInfo> errorList = getErrorInfos(component);
345     if (errorList == null) {
346       errorList = new ArrayList<>();
347       component.putClientProperty(CLIENT_PROP_ERROR_ARRAY, errorList);
348     }
349
350     errorList.add(errorInfo);
351   }
352
353   /**
354    * @return first ErrorInfo for the specified component. If component doesn't contain
355    * any error then the method returns {@code null}.
356    */
357   @Nullable
358   public static ErrorInfo getErrorForComponent(@NotNull final IComponent component){
359     // Check bind to class errors
360     {
361       final ErrorInfo errorInfo = (ErrorInfo)component.getClientProperty(CLIENT_PROP_CLASS_TO_BIND_ERROR);
362       if(errorInfo != null){
363         return errorInfo;
364       }
365     }
366
367     // Check binding errors
368     {
369       final ErrorInfo error = (ErrorInfo)component.getClientProperty(CLIENT_PROP_BINDING_ERROR);
370       if(error != null){
371         return error;
372       }
373     }
374
375     // General error
376     {
377       final ArrayList<ErrorInfo> errorInfo = getErrorInfos(component);
378       if(errorInfo != null && errorInfo.size() > 0){
379         return errorInfo.get(0);
380       }
381     }
382
383     return null;
384   }
385
386   @NotNull public static ErrorInfo[] getAllErrorsForComponent(@NotNull IComponent component) {
387     List<ErrorInfo> result = new ArrayList<>();
388     ErrorInfo errorInfo = (ErrorInfo)component.getClientProperty(CLIENT_PROP_CLASS_TO_BIND_ERROR);
389     if (errorInfo != null) {
390       result.add(errorInfo);
391     }
392     errorInfo = (ErrorInfo)component.getClientProperty(CLIENT_PROP_BINDING_ERROR);
393     if (errorInfo != null) {
394       result.add(errorInfo);
395     }
396     final ArrayList<ErrorInfo> errorInfos = getErrorInfos(component);
397     if (errorInfos != null) {
398       result.addAll(errorInfos);
399     }
400     return result.toArray(new ErrorInfo[result.size()]);
401   }
402
403   private static ArrayList<ErrorInfo> getErrorInfos(final IComponent component) {
404     //noinspection unchecked
405     return (ArrayList<ErrorInfo>)component.getClientProperty(CLIENT_PROP_ERROR_ARRAY);
406   }
407
408   @Nullable
409   public static HighlightDisplayLevel getHighlightDisplayLevel(final Project project, @NotNull final RadComponent component) {
410     HighlightDisplayLevel displayLevel = null;
411     for(ErrorInfo errInfo: getAllErrorsForComponent(component)) {
412       if (displayLevel == null || SeverityRegistrar.getSeverityRegistrar(project).compare(errInfo.getHighlightDisplayLevel().getSeverity(), displayLevel.getSeverity()) > 0) {
413         displayLevel = errInfo.getHighlightDisplayLevel();
414       }
415     }
416     return displayLevel;
417   }
418 }