python: write action is unnecessary in some quick fixes
[idea/community.git] / python / src / com / jetbrains / python / inspections / quickfix / GenerateBinaryStubsFix.java
1 /*
2  * Copyright 2000-2014 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.jetbrains.python.inspections.quickfix;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.codeInspection.LocalQuickFix;
20 import com.intellij.codeInspection.ProblemDescriptor;
21 import com.intellij.execution.configurations.GeneralCommandLine;
22 import com.intellij.execution.process.ProcessOutput;
23 import com.intellij.openapi.application.ReadAction;
24 import com.intellij.openapi.application.Result;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.module.Module;
27 import com.intellij.openapi.module.ModuleUtilCore;
28 import com.intellij.openapi.progress.ProgressIndicator;
29 import com.intellij.openapi.progress.ProgressManager;
30 import com.intellij.openapi.progress.Task;
31 import com.intellij.openapi.progress.Task.Backgroundable;
32 import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.projectRoots.Sdk;
35 import com.intellij.openapi.vfs.LocalFileSystem;
36 import com.intellij.openapi.vfs.VirtualFile;
37 import com.intellij.psi.PsiElement;
38 import com.intellij.psi.PsiFile;
39 import com.intellij.psi.util.QualifiedName;
40 import com.intellij.util.Consumer;
41 import com.jetbrains.python.PyBundle;
42 import com.jetbrains.python.PythonHelper;
43 import com.jetbrains.python.psi.*;
44 import com.jetbrains.python.sdk.InvalidSdkException;
45 import com.jetbrains.python.sdk.PySdkUtil;
46 import com.jetbrains.python.sdk.PythonSdkType;
47 import com.jetbrains.python.sdk.flavors.IronPythonSdkFlavor;
48 import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
49 import com.jetbrains.python.sdk.skeletons.PySkeletonGenerator;
50 import com.jetbrains.python.sdk.skeletons.PySkeletonRefresher;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import java.io.File;
55 import java.util.ArrayList;
56 import java.util.Collection;
57 import java.util.Collections;
58 import java.util.List;
59
60 /**
61  * @author yole
62  */
63 public class GenerateBinaryStubsFix implements LocalQuickFix {
64   private static final Logger LOG = Logger.getInstance("#" + GenerateBinaryStubsFix.class.getName());
65
66   private final String myQualifiedName;
67   private final Sdk mySdk;
68
69   /**
70    * Generates pack of fixes available for some unresolved import statement.
71    * Be sure to call {@link #isApplicable(com.jetbrains.python.psi.PyImportStatementBase)} first to make sure this statement is supported
72    *
73    * @param importStatementBase statement to fix
74    * @return pack of fixes
75    */
76   @NotNull
77   public static Collection<GenerateBinaryStubsFix> generateFixes(@NotNull final PyImportStatementBase importStatementBase) {
78     final List<String> names = importStatementBase.getFullyQualifiedObjectNames();
79     final List<GenerateBinaryStubsFix> result = new ArrayList<>(names.size());
80     if (importStatementBase instanceof PyFromImportStatement && names.isEmpty()) {
81       final QualifiedName qName = ((PyFromImportStatement)importStatementBase).getImportSourceQName();
82       if (qName != null) {
83         result.add(new GenerateBinaryStubsFix(importStatementBase, qName.toString()));
84       }
85     }
86     for (final String qualifiedName : names) {
87       result.add(new GenerateBinaryStubsFix(importStatementBase, qualifiedName));
88     }
89     return result;
90   }
91
92   /**
93    * @param importStatementBase statement to fix
94    * @param qualifiedName       name should be fixed (one of {@link com.jetbrains.python.psi.PyImportStatementBase#getFullyQualifiedObjectNames()})
95    */
96   private GenerateBinaryStubsFix(@NotNull final PyImportStatementBase importStatementBase, @NotNull final String qualifiedName) {
97     myQualifiedName = qualifiedName;
98     mySdk = getPythonSdk(importStatementBase);
99   }
100
101   @Override
102   @NotNull
103   public String getName() {
104     return PyBundle.message("sdk.gen.stubs.for.binary.modules", myQualifiedName);
105   }
106
107   @Override
108   @NotNull
109   public String getFamilyName() {
110     return "Generate binary stubs";
111   }
112
113   @Override
114   public boolean startInWriteAction() {
115     return false;
116   }
117
118   @Override
119   public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) {
120     final PsiFile file = descriptor.getPsiElement().getContainingFile();
121     final Backgroundable backgroundable = getFixTask(file);
122     ProgressManager.getInstance().runProcessWithProgressAsynchronously(backgroundable, new BackgroundableProcessIndicator(backgroundable));
123   }
124
125
126   /**
127    * Returns fix task that is used to generate stubs
128    *
129    * @param fileToRunTaskIn file where task should run
130    * @return task itself
131    */
132   @NotNull
133   public Backgroundable getFixTask(@NotNull final PsiFile fileToRunTaskIn) {
134     final Project project = fileToRunTaskIn.getProject();
135     final String folder = fileToRunTaskIn.getContainingDirectory().getVirtualFile().getCanonicalPath();
136     return new Task.Backgroundable(project, "Generating skeletons for binary module", false) {
137
138       @Override
139       public void run(@NotNull ProgressIndicator indicator) {
140         indicator.setIndeterminate(true);
141
142
143         final List<String> assemblyRefs = new ReadAction<List<String>>() {
144           @Override
145           protected void run(@NotNull Result<List<String>> result) throws Throwable {
146             result.setResult(collectAssemblyReferences(fileToRunTaskIn));
147           }
148         }.execute().getResultObject();
149
150
151         try {
152           final PySkeletonRefresher refresher = new PySkeletonRefresher(project, null, mySdk, null, null, folder);
153
154           if (needBinaryList(myQualifiedName)) {
155             if (!generateSkeletonsForList(refresher, indicator, folder)) return;
156           }
157           else {
158             //noinspection unchecked
159             refresher.generateSkeleton(myQualifiedName, "", assemblyRefs, Consumer.EMPTY_CONSUMER);
160           }
161           final VirtualFile skeletonDir;
162           skeletonDir = LocalFileSystem.getInstance().findFileByPath(refresher.getSkeletonsPath());
163           if (skeletonDir != null) {
164             skeletonDir.refresh(true, true);
165           }
166         }
167         catch (InvalidSdkException e) {
168           LOG.error(e);
169         }
170       }
171     };
172   }
173
174   private boolean generateSkeletonsForList(@NotNull final PySkeletonRefresher refresher,
175                                            ProgressIndicator indicator,
176                                            @Nullable final String currentBinaryFilesPath) throws InvalidSdkException {
177     final PySkeletonGenerator generator = new PySkeletonGenerator(refresher.getSkeletonsPath(), mySdk, currentBinaryFilesPath);
178     indicator.setIndeterminate(false);
179     final String homePath = mySdk.getHomePath();
180     if (homePath == null) return false;
181     GeneralCommandLine cmd = PythonHelper.EXTRA_SYSPATH.newCommandLine(homePath, Lists.newArrayList(myQualifiedName));
182     final ProcessOutput runResult = PySdkUtil.getProcessOutput(cmd,
183                                                                new File(homePath).getParent(),
184                                                                PythonSdkType.getVirtualEnvExtraEnv(homePath), 5000
185     );
186     if (runResult.getExitCode() == 0 && !runResult.isTimeout()) {
187       final String extraPath = runResult.getStdout();
188       final PySkeletonGenerator.ListBinariesResult binaries = generator.listBinaries(mySdk, extraPath);
189       final List<String> names = Lists.newArrayList(binaries.modules.keySet());
190       Collections.sort(names);
191       final int size = names.size();
192       for (int i = 0; i != size; ++i) {
193         final String name = names.get(i);
194         indicator.setFraction((double)i / size);
195         if (needBinaryList(name)) {
196           indicator.setText2(name);
197           final PySkeletonRefresher.PyBinaryItem item = binaries.modules.get(name);
198           final String modulePath = item != null ? item.getPath() : "";
199           //noinspection unchecked
200           refresher.generateSkeleton(name, modulePath, new ArrayList<>(), Consumer.EMPTY_CONSUMER);
201         }
202       }
203     }
204     return true;
205   }
206
207   private static boolean needBinaryList(@NotNull final String qualifiedName) {
208     return qualifiedName.startsWith("gi.repository");
209   }
210
211   private List<String> collectAssemblyReferences(PsiFile file) {
212     if (!(PythonSdkFlavor.getFlavor(mySdk) instanceof IronPythonSdkFlavor)) {
213       return Collections.emptyList();
214     }
215     final List<String> result = new ArrayList<>();
216     file.accept(new PyRecursiveElementVisitor() {
217       @Override
218       public void visitPyCallExpression(PyCallExpression node) {
219         super.visitPyCallExpression(node);
220         // TODO: What if user loads it not by literal? We need to ask user for list of DLLs
221         if (node.isCalleeText("AddReference", "AddReferenceByPartialName", "AddReferenceByName")) {
222           final PyExpression[] args = node.getArguments();
223           if (args.length == 1 && args[0] instanceof PyStringLiteralExpression) {
224             result.add(((PyStringLiteralExpression)args[0]).getStringValue());
225           }
226         }
227       }
228     });
229     return result;
230   }
231
232   /**
233    * Checks if this fix can help you to generate binary stubs
234    *
235    * @param importStatementBase statement to fix
236    * @return true if this fix could work
237    */
238   public static boolean isApplicable(@NotNull final PyImportStatementBase importStatementBase) {
239     if (importStatementBase.getFullyQualifiedObjectNames().isEmpty() &&
240         !(importStatementBase instanceof PyFromImportStatement && ((PyFromImportStatement)importStatementBase).isStarImport())) {
241       return false;
242     }
243     final Sdk sdk = getPythonSdk(importStatementBase);
244     if (sdk == null) {
245       return false;
246     }
247     final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk);
248     if (flavor instanceof IronPythonSdkFlavor) {
249       return true;
250     }
251     return isGtk(importStatementBase);
252   }
253
254   private static boolean isGtk(@NotNull final PyImportStatementBase importStatementBase) {
255     if (importStatementBase instanceof PyFromImportStatement) {
256       final QualifiedName qName = ((PyFromImportStatement)importStatementBase).getImportSourceQName();
257       if (qName != null && qName.matches("gi", "repository")) {
258         return true;
259       }
260     }
261     return false;
262   }
263
264   @Nullable
265   private static Sdk getPythonSdk(@NotNull final PsiElement element) {
266     final Module module = ModuleUtilCore.findModuleForPsiElement(element);
267     return (module == null) ? null : PythonSdkType.findPythonSdk(module);
268   }
269 }