a6d7c9b2241cee615cdfa19cf53f4f622dc144c6
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInspection / InspectionApplication.java
1 /*
2  * Copyright 2000-2016 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.codeInspection;
17
18 import com.intellij.analysis.AnalysisScope;
19 import com.intellij.codeInsight.daemon.HighlightDisplayKey;
20 import com.intellij.codeInspection.ex.*;
21 import com.intellij.conversion.ConversionListener;
22 import com.intellij.conversion.ConversionService;
23 import com.intellij.ide.impl.PatchProjectUtil;
24 import com.intellij.ide.impl.ProjectUtil;
25 import com.intellij.openapi.application.ApplicationInfo;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.application.ex.ApplicationEx;
28 import com.intellij.openapi.application.ex.ApplicationInfoEx;
29 import com.intellij.openapi.application.ex.ApplicationManagerEx;
30 import com.intellij.openapi.diagnostic.Logger;
31 import com.intellij.openapi.progress.ProgressManager;
32 import com.intellij.openapi.progress.util.ProgressIndicatorBase;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.util.Comparing;
35 import com.intellij.openapi.util.io.FileUtil;
36 import com.intellij.openapi.vfs.LocalFileSystem;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.openapi.vfs.VirtualFileManager;
39 import com.intellij.profile.Profile;
40 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
41 import com.intellij.psi.PsiDirectory;
42 import com.intellij.psi.PsiManager;
43 import com.intellij.psi.search.GlobalSearchScopesCore;
44 import com.intellij.psi.search.scope.packageSet.NamedScope;
45 import com.intellij.psi.search.scope.packageSet.NamedScopesHolder;
46 import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
47 import org.jdom.JDOMException;
48 import org.jetbrains.annotations.NonNls;
49 import org.jetbrains.annotations.Nullable;
50
51 import java.io.File;
52 import java.io.FileWriter;
53 import java.io.IOException;
54 import java.util.*;
55
56 /**
57  * @author max
58  */
59 @SuppressWarnings({"UseOfSystemOutOrSystemErr"})
60 public class InspectionApplication {
61   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.InspectionApplication");
62
63   public InspectionToolCmdlineOptionHelpProvider myHelpProvider = null;
64   public String myProjectPath = null;
65   public String myOutPath = null;
66   public String mySourceDirectory = null;
67   public String myStubProfile = null;
68   public String myProfileName = null;
69   public String myProfilePath = null;
70   public boolean myRunWithEditorSettings = false;
71   public boolean myRunGlobalToolsOnly = false;
72   private Project myProject;
73   private int myVerboseLevel = 0;
74   public String myOutputFormat = null;
75
76   public boolean myErrorCodeRequired = true;
77
78   @NonNls public static final String DESCRIPTIONS = ".descriptions";
79   @NonNls public static final String PROFILE = "profile";
80   @NonNls public static final String INSPECTIONS_NODE = "inspections";
81   @NonNls public static final String XML_EXTENSION = ".xml";
82
83   public void startup() {
84     if (myProjectPath == null) {
85       logError("Project to inspect is not defined");
86       printHelp();
87     }
88
89     if (myProfileName == null && myProfilePath == null && myStubProfile == null) {
90       logError("Profile to inspect with is not defined");
91       printHelp();
92     }
93
94     final ApplicationEx application = ApplicationManagerEx.getApplicationEx();
95     application.runReadAction(() -> {
96       try {
97         final ApplicationInfoEx appInfo = (ApplicationInfoEx)ApplicationInfo.getInstance();
98         logMessage(1, InspectionsBundle.message("inspection.application.starting.up",
99                                                 appInfo.getFullApplicationName() + " (build " + appInfo.getBuild().asString() + ")"));
100         application.doNotSave();
101         logMessageLn(1, InspectionsBundle.message("inspection.done"));
102
103         this.run();
104       }
105       catch (Exception e) {
106         LOG.error(e);
107       }
108       finally {
109         if (myErrorCodeRequired) application.exit(true, true);
110       }
111     });
112   }
113
114   private void printHelp() {
115     assert myHelpProvider != null;
116
117     myHelpProvider.printHelpAndExit();
118   }
119
120   private void run() {
121
122     File tmpDir = null;
123     try {
124       myProjectPath = myProjectPath.replace(File.separatorChar, '/');
125       VirtualFile vfsProject = LocalFileSystem.getInstance().findFileByPath(myProjectPath);
126       if (vfsProject == null) {
127         logError(InspectionsBundle.message("inspection.application.file.cannot.be.found", myProjectPath));
128         printHelp();
129       }
130
131       logMessage(1, InspectionsBundle.message("inspection.application.opening.project"));
132       final ConversionService conversionService = ConversionService.getInstance();
133       if (conversionService.convertSilently(myProjectPath, createConversionListener()).openingIsCanceled()) {
134         gracefulExit();
135         return;
136       }
137       myProject = ProjectUtil.openOrImport(myProjectPath, null, false);
138
139       if (myProject == null) {
140         logError("Unable to open project");
141         gracefulExit();
142         return;
143       }
144
145       ApplicationManager.getApplication().runWriteAction(() -> VirtualFileManager.getInstance().refreshWithoutFileWatcher(false));
146
147       PatchProjectUtil.patchProject(myProject);
148
149       logMessageLn(1, InspectionsBundle.message("inspection.done"));
150       logMessage(1, InspectionsBundle.message("inspection.application.initializing.project"));
151
152       Profile inspectionProfile = loadInspectionProfile();
153       if (inspectionProfile == null) return;
154
155       final InspectionManagerEx im = (InspectionManagerEx)InspectionManager.getInstance(myProject);
156
157       final GlobalInspectionContextImpl inspectionContext = im.createNewGlobalContext(true);
158       inspectionContext.setExternalProfile((InspectionProfile)inspectionProfile);
159       im.setProfile(inspectionProfile.getName());
160
161       final AnalysisScope scope;
162       if (mySourceDirectory == null) {
163         final String scopeName = System.getProperty("idea.analyze.scope");
164         final NamedScope namedScope = scopeName != null ? NamedScopesHolder.getScope(myProject, scopeName) : null;
165         scope = namedScope != null ? new AnalysisScope(GlobalSearchScopesCore.filterScope(myProject, namedScope), myProject)
166                                    : new AnalysisScope(myProject);
167       }
168       else {
169         mySourceDirectory = mySourceDirectory.replace(File.separatorChar, '/');
170
171         VirtualFile vfsDir = LocalFileSystem.getInstance().findFileByPath(mySourceDirectory);
172         if (vfsDir == null) {
173           logError(InspectionsBundle.message("inspection.application.directory.cannot.be.found", mySourceDirectory));
174           printHelp();
175         }
176
177         PsiDirectory psiDirectory = PsiManager.getInstance(myProject).findDirectory(vfsDir);
178         scope = new AnalysisScope(psiDirectory);
179       }
180
181       logMessageLn(1, InspectionsBundle.message("inspection.done"));
182
183       if (!myRunWithEditorSettings) {
184         logMessageLn(1, InspectionsBundle.message("inspection.application.chosen.profile.log message", inspectionProfile.getName()));
185       }
186
187       InspectionsReportConverter reportConverter = getReportConverter(myOutputFormat);
188       if (reportConverter == null && myOutputFormat != null && myOutputFormat.endsWith(".xsl")) {
189         // xslt converter
190         reportConverter = new XSLTReportConverter(myOutputFormat);
191       }
192
193       final String resultsDataPath;
194       if ((reportConverter == null || !reportConverter.useTmpDirForRawData()) // use default xml converter(if null( or don't store default xml report in tmp dir
195           && myOutPath != null) {  // and don't use STDOUT stream
196         resultsDataPath = myOutPath;
197       }
198       else {
199         try {
200           tmpDir = FileUtil.createTempDirectory("inspections", "data");
201           resultsDataPath = tmpDir.getPath();
202         }
203         catch (IOException e) {
204           LOG.error(e);
205           System.err.println("Cannot create tmp directory.");
206           System.exit(1);
207           return;
208         }
209       }
210
211       final List<File> inspectionsResults = new ArrayList<File>();
212       ProgressManager.getInstance().runProcess(() -> {
213         if (!GlobalInspectionContextUtil.canRunInspections(myProject, false)) {
214           gracefulExit();
215           return;
216         }
217         inspectionContext.launchInspectionsOffline(scope, resultsDataPath, myRunGlobalToolsOnly, inspectionsResults);
218         logMessageLn(1, "\n" + InspectionsBundle.message("inspection.capitalized.done") + "\n");
219         if (!myErrorCodeRequired) {
220           closeProject();
221         }
222       }, new ProgressIndicatorBase() {
223         private String lastPrefix = "";
224         private int myLastPercent = -1;
225
226         @Override
227         public void setText(String text) {
228           if (myVerboseLevel == 0) return;
229
230           if (myVerboseLevel == 1) {
231             String prefix = getPrefix(text);
232             if (prefix == null) return;
233             if (prefix.equals(lastPrefix)) {
234               logMessage(1, ".");
235               return;
236             }
237             lastPrefix = prefix;
238             logMessageLn(1, "");
239             logMessageLn(1, prefix);
240             return;
241           }
242
243           if (myVerboseLevel == 3) {
244             if (!isIndeterminate() && getFraction() > 0) {
245               final int percent = (int)(getFraction() * 100);
246               if (myLastPercent == percent) return;
247               myLastPercent = percent;
248               String msg = InspectionsBundle.message("inspection.display.name") + " " + percent + "%";
249               logMessageLn(2, msg);
250             }
251             return;
252           }
253
254           logMessageLn(2, text);
255         }
256       });
257       final String descriptionsFile = resultsDataPath + File.separatorChar + DESCRIPTIONS + XML_EXTENSION;
258       describeInspections(descriptionsFile,
259                           myRunWithEditorSettings ? null : inspectionProfile.getName(),
260                           (InspectionProfile)inspectionProfile);
261       inspectionsResults.add(new File(descriptionsFile));
262       // convert report
263       if (reportConverter != null) {
264         try {
265           reportConverter.convert(resultsDataPath, myOutPath, inspectionContext.getTools(), inspectionsResults);
266         }
267         catch (InspectionsReportConverter.ConversionException e) {
268           logError("\n" + e.getMessage());
269           printHelp();
270         }
271       }
272     }
273     catch (IOException e) {
274       LOG.error(e);
275       logError(e.getMessage());
276       printHelp();
277     }
278     catch (Throwable e) {
279       LOG.error(e);
280       logError(e.getMessage());
281       gracefulExit();
282     }
283     finally {
284       // delete tmp dir
285       if (tmpDir != null) {
286         FileUtil.delete(tmpDir);
287       }
288     }
289   }
290
291   private void gracefulExit() {
292     if (myErrorCodeRequired) {
293       System.exit(1);
294     }
295     else {
296       closeProject();
297       throw new RuntimeException("Failed to proceed");
298     }
299   }
300
301   private void closeProject() {
302     if (myProject != null && !myProject.isDisposed()) {
303       ProjectUtil.closeAndDispose(myProject);
304       myProject = null;
305     }
306   }
307
308   @Nullable
309   private Profile loadInspectionProfile() throws IOException, JDOMException {
310     Profile inspectionProfile = null;
311
312     //fetch profile by name from project file (project profiles can be disabled)
313     if (myProfileName != null) {
314       inspectionProfile = loadProfileByName(myProfileName);
315       if (inspectionProfile == null) {
316         logError("Profile with configured name (" + myProfileName + ") was not found (neither in project nor in config directory)");
317         gracefulExit();
318         return null;
319       }
320       return inspectionProfile;
321     }
322
323     if (myProfilePath != null) {
324       inspectionProfile = loadProfileByPath(myProfilePath);
325       if (inspectionProfile == null) {
326         logError("Failed to load profile from \'" + myProfilePath + "\'");
327         gracefulExit();
328         return null;
329       }
330       return inspectionProfile;
331     }
332
333     if (myStubProfile != null) {
334       if (!myRunWithEditorSettings) {
335         inspectionProfile = loadProfileByName(myStubProfile);
336         if (inspectionProfile != null) return inspectionProfile;
337
338         inspectionProfile = loadProfileByPath(myStubProfile);
339         if (inspectionProfile != null) return inspectionProfile;
340       }
341
342       inspectionProfile = InspectionProjectProfileManager.getInstance(myProject).getCurrentProfile();
343       logError("Using default project profile");
344     }
345     return inspectionProfile;
346   }
347
348   @Nullable
349   private Profile loadProfileByPath(final String profilePath) throws IOException, JDOMException {
350     Profile inspectionProfile = ApplicationInspectionProfileManagerImpl.getInstanceImpl().loadProfile(profilePath);
351     if (inspectionProfile != null) {
352       logMessageLn(1, "Loaded profile \'" + inspectionProfile.getName() + "\' from file \'" + profilePath + "\'");
353     }
354     return inspectionProfile;
355   }
356
357   @Nullable
358   private Profile loadProfileByName(final String profileName) {
359     Profile inspectionProfile = InspectionProjectProfileManager.getInstance(myProject).getProfile(profileName, false);
360     if (inspectionProfile != null) {
361       logMessageLn(1, "Loaded shared project profile \'" + profileName + "\'");
362     }
363     else {
364       //check if ide profile is used for project
365       final Collection<Profile> profiles = InspectionProjectProfileManager.getInstance(myProject).getProfiles();
366       for (Profile profile : profiles) {
367         if (Comparing.strEqual(profile.getName(), profileName)) {
368           inspectionProfile = profile;
369           logMessageLn(1, "Loaded local profile \'" + profileName + "\'");
370           break;
371         }
372       }
373     }
374
375     return inspectionProfile;
376   }
377
378
379   @Nullable
380   private static InspectionsReportConverter getReportConverter(@Nullable final String outputFormat) {
381     for (InspectionsReportConverter converter : InspectionsReportConverter.EP_NAME.getExtensions()) {
382       if (converter.getFormatName().equals(outputFormat)) {
383         return converter;
384       }
385     }
386     return null;
387   }
388
389   private ConversionListener createConversionListener() {
390     return new ConversionListener() {
391       @Override
392       public void conversionNeeded() {
393         logMessageLn(1, InspectionsBundle.message("inspection.application.project.has.older.format.and.will.be.converted"));
394       }
395
396       @Override
397       public void successfullyConverted(final File backupDir) {
398         logMessageLn(1, InspectionsBundle.message(
399           "inspection.application.project.was.succesfully.converted.old.project.files.were.saved.to.0",
400                                                   backupDir.getAbsolutePath()));
401       }
402
403       @Override
404       public void error(final String message) {
405         logError(InspectionsBundle.message("inspection.application.cannot.convert.project.0", message));
406       }
407
408       @Override
409       public void cannotWriteToFiles(final List<File> readonlyFiles) {
410         StringBuilder files = new StringBuilder();
411         for (File file : readonlyFiles) {
412           files.append(file.getAbsolutePath()).append("; ");
413         }
414         logError(InspectionsBundle.message("inspection.application.cannot.convert.the.project.the.following.files.are.read.only.0", files.toString()));
415       }
416     };
417   }
418
419   @Nullable
420   private static String getPrefix(final String text) {
421     //noinspection HardCodedStringLiteral
422     int idx = text.indexOf(" in ");
423     if (idx == -1) {
424       //noinspection HardCodedStringLiteral
425       idx = text.indexOf(" of ");
426     }
427
428     return idx == -1 ? null : text.substring(0, idx);
429   }
430
431   public void setVerboseLevel(int verboseLevel) {
432     myVerboseLevel = verboseLevel;
433   }
434
435   private void logMessage(int minVerboseLevel, String message) {
436     if (myVerboseLevel >= minVerboseLevel) {
437       System.out.print(message);
438     }
439   }
440
441   private static void logError(String message) {
442     System.err.println(message);
443   }
444
445   private void logMessageLn(int minVerboseLevel, String message) {
446     if (myVerboseLevel >= minVerboseLevel) {
447       System.out.println(message);
448     }
449   }
450
451   private static void describeInspections(@NonNls String myOutputPath, final String name, final InspectionProfile profile) throws IOException {
452     final InspectionToolWrapper[] toolWrappers = profile.getInspectionTools(null);
453     final Map<String, Set<InspectionToolWrapper>> map = new HashMap<String, Set<InspectionToolWrapper>>();
454     for (InspectionToolWrapper toolWrapper : toolWrappers) {
455       final String groupName = toolWrapper.getGroupDisplayName();
456       Set<InspectionToolWrapper> groupInspections = map.get(groupName);
457       if (groupInspections == null) {
458         groupInspections = new HashSet<InspectionToolWrapper>();
459         map.put(groupName, groupInspections);
460       }
461       groupInspections.add(toolWrapper);
462     }
463
464     FileWriter fw = new FileWriter(myOutputPath);
465     try {
466       @NonNls final PrettyPrintWriter xmlWriter = new PrettyPrintWriter(fw);
467       xmlWriter.startNode(INSPECTIONS_NODE);
468       if (name != null) {
469         xmlWriter.addAttribute(PROFILE, name);
470       }
471       for (String groupName : map.keySet()) {
472         xmlWriter.startNode("group");
473         xmlWriter.addAttribute("name", groupName);
474         final Set<InspectionToolWrapper> entries = map.get(groupName);
475         for (InspectionToolWrapper toolWrapper : entries) {
476           xmlWriter.startNode("inspection");
477           final String shortName = toolWrapper.getShortName();
478           xmlWriter.addAttribute("shortName", shortName);
479           xmlWriter.addAttribute("displayName", toolWrapper.getDisplayName());
480           final boolean toolEnabled = profile.isToolEnabled(HighlightDisplayKey.find(shortName));
481           xmlWriter.addAttribute("enabled", Boolean.toString(toolEnabled));
482           final String description = toolWrapper.loadDescription();
483           if (description != null) {
484             xmlWriter.setValue(description);
485           }
486           else {
487             LOG.error(shortName + " descriptionUrl==" + toolWrapper);
488           }
489           xmlWriter.endNode();
490         }
491         xmlWriter.endNode();
492       }
493       xmlWriter.endNode();
494     }
495     finally {
496       fw.close();
497     }
498   }
499 }