2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.jetbrains.python.psi.resolve;
18 import com.intellij.openapi.extensions.Extensions;
19 import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher;
20 import com.intellij.openapi.fileTypes.FileNameMatcher;
21 import com.intellij.openapi.fileTypes.FileTypeManager;
22 import com.intellij.openapi.module.Module;
23 import com.intellij.openapi.module.ModuleUtilCore;
24 import com.intellij.openapi.projectRoots.Sdk;
25 import com.intellij.openapi.roots.FileIndexFacade;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.psi.PsiDirectory;
29 import com.intellij.psi.PsiElement;
30 import com.intellij.psi.PsiFile;
31 import com.intellij.psi.PsiInvalidElementAccessException;
32 import com.intellij.psi.util.PsiTreeUtil;
33 import com.intellij.psi.util.QualifiedName;
34 import com.intellij.util.containers.HashSet;
35 import com.jetbrains.python.PyNames;
36 import com.jetbrains.python.PythonFileType;
37 import com.jetbrains.python.psi.*;
38 import com.jetbrains.python.psi.impl.*;
39 import com.jetbrains.python.psi.types.PyModuleType;
40 import com.jetbrains.python.psi.types.PyType;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.List;
49 import static com.jetbrains.python.psi.FutureFeature.ABSOLUTE_IMPORT;
54 public class ResolveImportUtil {
55 private ResolveImportUtil() {
58 private static final ThreadLocal<Set<String>> ourBeingImported = new ThreadLocal<Set<String>>() {
60 protected Set<String> initialValue() {
61 return new HashSet<String>();
65 public static boolean isAbsoluteImportEnabledFor(PsiElement foothold) {
66 if (foothold != null) {
67 PsiFile file = foothold.getContainingFile();
68 if (file instanceof PyFile) {
69 final PyFile pyFile = (PyFile)file;
70 if (pyFile.getLanguageLevel().isPy3K()) {
73 return pyFile.hasImportFromFuture(ABSOLUTE_IMPORT);
76 // if the relevant import is below the foothold, it is either legal or we've detected the offending statement already
82 * Finds a directory that many levels above a given file, making sure that every level has an __init__.py.
84 * @param base file that works as a reference.
85 * @param depth must be positive, 1 means the dir that contains base, 2 is one dir above, etc.
86 * @return found directory, or null.
89 public static PsiDirectory stepBackFrom(PsiFile base, int depth) {
91 return base.getContainingDirectory();
95 base = base.getOriginalFile(); // just to make sure
96 result = base.getContainingDirectory();
98 while (result != null && PyUtil.isPackage(result, base)) {
99 if (count >= depth) return result;
100 result = result.getParentDirectory();
108 public static PsiElement resolveImportElement(PyImportElement importElement, @NotNull final QualifiedName qName) {
109 List<RatedResolveResult> targets;
110 final PyStatement importStatement = importElement.getContainingImportStatement();
111 if (importStatement instanceof PyFromImportStatement) {
112 targets = resolveNameInFromImport((PyFromImportStatement)importStatement, qName);
114 else { // "import foo"
115 targets = resolveNameInImportStatement(importElement, qName);
117 final List<RatedResolveResult> resultList = RatedResolveResult.sorted(targets);
118 return resultList.size() > 0 ? resultList.get(0).getElement() : null;
121 public static List<RatedResolveResult> resolveNameInImportStatement(PyImportElement importElement, @NotNull QualifiedName qName) {
122 final PsiFile file = importElement.getContainingFile().getOriginalFile();
123 boolean absoluteImportEnabled = isAbsoluteImportEnabledFor(importElement);
124 final List<PsiElement> modules = resolveModule(qName, file, absoluteImportEnabled, 0);
125 return rateResults(modules);
128 public static List<RatedResolveResult> resolveNameInFromImport(PyFromImportStatement importStatement, @NotNull QualifiedName qName) {
129 PsiFile file = importStatement.getContainingFile().getOriginalFile();
130 String name = qName.getComponents().get(0);
132 final List<PsiElement> candidates = importStatement.resolveImportSourceCandidates();
133 List<PsiElement> resultList = new ArrayList<PsiElement>();
134 for (PsiElement candidate : candidates) {
135 if (!candidate.isValid()) {
136 throw new PsiInvalidElementAccessException(candidate, "Got an invalid candidate from resolveImportSourceCandidates(): " + candidate.getClass());
138 if (candidate instanceof PsiDirectory) {
139 candidate = PyUtil.getPackageElement((PsiDirectory)candidate, importStatement);
141 PsiElement result = resolveChild(candidate, name, file, false, true);
142 if (result != null) {
143 if (!result.isValid()) {
144 throw new PsiInvalidElementAccessException(result, "Got an invalid candidate from resolveChild(): " + result.getClass());
146 resultList.add(result);
149 if (!resultList.isEmpty()) {
150 return rateResults(resultList);
152 return Collections.emptyList();
156 public static List<PsiElement> resolveFromImportStatementSource(@NotNull PyFromImportStatement fromImportStatement,
157 @Nullable QualifiedName qName) {
158 final boolean absoluteImportEnabled = isAbsoluteImportEnabledFor(fromImportStatement);
159 final PsiFile file = fromImportStatement.getContainingFile();
160 return resolveModule(qName, file, absoluteImportEnabled, fromImportStatement.getRelativeLevel());
164 * Resolves a module reference in a general case.
167 * @param qualifiedName qualified name of the module reference to resolve
168 * @param sourceFile where that reference resides; serves as PSI foothold to determine module, project, etc.
169 * @param importIsAbsolute if false, try old python 2.x's "relative first, absolute next" approach.
170 * @param relativeLevel if > 0, step back from sourceFile and resolve from there (even if importIsAbsolute is false!).
171 * @return list of possible candidates
174 public static List<PsiElement> resolveModule(@Nullable QualifiedName qualifiedName, @Nullable PsiFile sourceFile,
175 boolean importIsAbsolute, int relativeLevel) {
176 if (qualifiedName == null || sourceFile == null) {
177 return Collections.emptyList();
179 final String marker = qualifiedName + "#" + Integer.toString(relativeLevel);
180 final Set<String> beingImported = ourBeingImported.get();
181 if (beingImported.contains(marker)) {
182 return Collections.emptyList(); // break endless loop in import
185 beingImported.add(marker);
186 final QualifiedNameResolver visitor = new QualifiedNameResolverImpl(qualifiedName).fromElement(sourceFile);
187 if (relativeLevel > 0) {
188 // "from ...module import"
189 visitor.withRelative(relativeLevel).withoutRoots();
192 // "from module import"
193 if (!importIsAbsolute) {
194 visitor.withRelative(0);
197 List<PsiElement> results = visitor.resultsAsList();
198 if (results.isEmpty() && relativeLevel == 0 && !importIsAbsolute) {
199 results = resolveRelativeImportAsAbsolute(sourceFile, qualifiedName);
204 beingImported.remove(marker);
209 * Try to resolve relative import as absolute in roots, not in its parent directory.
211 * This may be useful for resolving to child skeleton modules located in other directories.
213 * @param foothold foothold file.
214 * @param qualifiedName relative import name.
215 * @return list of resolved elements.
218 private static List<PsiElement> resolveRelativeImportAsAbsolute(@NotNull PsiFile foothold,
219 @NotNull QualifiedName qualifiedName) {
220 final VirtualFile virtualFile = foothold.getVirtualFile();
221 if (virtualFile == null) return Collections.emptyList();
222 final boolean inSource = FileIndexFacade.getInstance(foothold.getProject()).isInContent(virtualFile);
223 if (inSource) return Collections.emptyList();
224 final PsiDirectory containingDirectory = foothold.getContainingDirectory();
225 if (containingDirectory != null) {
226 final QualifiedName containingPath = QualifiedNameFinder.findCanonicalImportPath(containingDirectory, null);
227 if (containingPath != null && containingPath.getComponentCount() > 0) {
228 final QualifiedName absolutePath = containingPath.append(qualifiedName.toString());
229 final QualifiedNameResolver absoluteVisitor = new QualifiedNameResolverImpl(absolutePath).fromElement(foothold);
230 return absoluteVisitor.resultsAsList();
233 return Collections.emptyList();
237 public static PsiElement resolveModuleInRoots(@NotNull QualifiedName moduleQualifiedName, @Nullable PsiElement foothold) {
238 if (foothold == null) return null;
239 QualifiedNameResolver visitor = new QualifiedNameResolverImpl(moduleQualifiedName).fromElement(foothold);
240 return visitor.firstResult();
244 static PythonPathCache getPathCache(PsiElement foothold) {
245 PythonPathCache cache = null;
246 final Module module = ModuleUtilCore.findModuleForPsiElement(foothold);
247 if (module != null) {
248 cache = PythonModulePathCache.getInstance(module);
251 final Sdk sdk = PyBuiltinCache.findSdkForFile(foothold.getContainingFile());
253 cache = PythonSdkPathCache.getInstance(foothold.getProject(), sdk);
260 * Tries to find referencedName under the parent element.
262 * @param parent element under which to look for referenced name; if null, null is returned.
263 * @param referencedName which name to look for.
264 * @param containingFile where we're in.
265 * @param fileOnly if true, considers only a PsiFile child as a valid result; non-file hits are ignored.
266 * @param checkForPackage if true, directories are returned only if they contain __init__.py
267 * @return the element the referencedName resolves to, or null.
270 public static PsiElement resolveChild(@Nullable final PsiElement parent, @NotNull final String referencedName,
271 @Nullable final PsiFile containingFile, boolean fileOnly, boolean checkForPackage) {
272 PsiDirectory dir = null;
273 PsiElement resultElement = null;
274 final PyResolveContext resolveContext = PyResolveContext.defaultContext();
275 if (parent instanceof PyFileImpl) {
276 PsiElement possibleResult = null;
277 if (PyNames.INIT_DOT_PY.equals(((PyFile)parent).getName())) {
278 dir = ((PyFile)parent).getContainingDirectory();
279 possibleResult = resolveInDirectory(referencedName, containingFile, dir, fileOnly, checkForPackage);
282 final PyModuleType moduleType = new PyModuleType((PyFile)parent);
283 final List<? extends RatedResolveResult> results = moduleType.resolveMember(referencedName, null, AccessDirection.READ,
285 final PsiElement moduleMember = results != null && !results.isEmpty() ? results.get(0).getElement() : null;
286 if (!fileOnly || PyUtil.instanceOf(moduleMember, PsiFile.class, PsiDirectory.class)) {
287 resultElement = moduleMember;
289 if (resultElement != null && !PyUtil.instanceOf(resultElement, PsiFile.class, PsiDirectory.class) &&
290 PsiTreeUtil.getStubOrPsiParentOfType(resultElement, PyExceptPart.class) == null && !isDunderAll(resultElement)) {
291 return resultElement;
294 if (possibleResult != null) return possibleResult;
296 if (resultElement != null) {
297 return resultElement;
300 else if (parent instanceof PsiDirectory) {
301 dir = (PsiDirectory)parent;
303 else if (parent != null) {
304 PyType refType = PyReferenceExpressionImpl.getReferenceTypeFromProviders(parent, resolveContext.getTypeEvalContext(), null);
305 if (refType != null) {
306 final List<? extends RatedResolveResult> result = refType.resolveMember(referencedName, null, AccessDirection.READ, resolveContext);
307 if (result != null && !result.isEmpty()) {
308 return result.get(0).getElement();
313 final PsiElement result = resolveInDirectory(referencedName, containingFile, dir, fileOnly, checkForPackage);
314 if (result != null) {
317 if (parent instanceof PsiFile) {
318 final PsiElement element = new QualifiedNameResolverImpl(referencedName).fromElement(parent).withoutRoots().firstResult();
319 if (element != null) {
327 private static boolean isDunderAll(@NotNull PsiElement element) {
328 return (element instanceof PyElement) && PyNames.ALL.equals(((PyElement)element).getName());
332 private static PsiElement resolveInDirectory(final String referencedName, @Nullable final PsiFile containingFile,
333 final PsiDirectory dir, boolean isFileOnly, boolean checkForPackage) {
334 if (referencedName == null) return null;
336 final PsiDirectory subdir = dir.findSubdirectory(referencedName);
337 if (subdir != null && (!checkForPackage || PyUtil.isPackage(subdir, containingFile))) {
341 final PsiFile module = findPyFileInDir(dir, referencedName);
342 if (module != null) return module;
345 // not a subdir, not a file; could be a name in parent/__init__.py
346 final PsiFile initPy = dir.findFile(PyNames.INIT_DOT_PY);
347 if (initPy == containingFile) return null; // don't dive into the file we're in
348 if (initPy instanceof PyFile) {
349 return ((PyFile)initPy).getElementNamed(referencedName);
356 private static PsiFile findPyFileInDir(PsiDirectory dir, String referencedName) {
357 PsiFile file = dir.findFile(referencedName + PyNames.DOT_PY);
359 final List<FileNameMatcher> associations = FileTypeManager.getInstance().getAssociations(PythonFileType.INSTANCE);
360 for (FileNameMatcher association : associations) {
361 if (association instanceof ExtensionFileNameMatcher) {
362 file = dir.findFile(referencedName + "." + ((ExtensionFileNameMatcher)association).getExtension());
363 if (file != null) break;
367 if (file != null && FileUtil.getNameWithoutExtension(file.getName()).equals(referencedName)) {
373 public static ResolveResultList rateResults(List<? extends PsiElement> targets) {
374 ResolveResultList ret = new ResolveResultList();
375 for (PsiElement target : targets) {
376 if (target instanceof PsiDirectory) {
377 target = PyUtil.getPackageElement((PsiDirectory)target, null);
379 if (target != null) { // Ignore non-package dirs, worthless
380 int rate = RatedResolveResult.RATE_HIGH;
381 if (target instanceof PyFile) {
382 VirtualFile vFile = ((PyFile)target).getVirtualFile();
383 if (vFile != null && vFile.getLength() > 0) {
386 for (PyResolveResultRater rater : Extensions.getExtensions(PyResolveResultRater.EP_NAME)) {
387 rate += rater.getImportElementRate(target);
390 ret.poke(target, rate);
397 * @param element what we test (identifier, reference, import element, etc)
398 * @return the how the element relates to an enclosing import statement, if any
402 public static PointInImport getPointInImport(@NotNull PsiElement element) {
403 final PsiElement parent = PsiTreeUtil.getNonStrictParentOfType(element, PyImportElement.class, PyFromImportStatement.class);
404 if (parent instanceof PyFromImportStatement) {
405 return PointInImport.AS_MODULE; // from foo ...
407 if (parent instanceof PyImportElement) {
408 final PsiElement statement = parent.getParent();
409 if (statement instanceof PyImportStatement) {
410 return PointInImport.AS_MODULE; // import foo,...
412 else if (statement instanceof PyFromImportStatement) {
413 return PointInImport.AS_NAME;
416 return PointInImport.NONE;