diff --git a/python/build/pycharm_build.gant b/python/build/pycharm_build.gant index 39b645d6f5d2..cb8da051f73d 100644 --- a/python/build/pycharm_build.gant +++ b/python/build/pycharm_build.gant @@ -90,10 +90,10 @@ target('default': "Build artifacts") { loadProject() - project.stage("Cleaning up sandbox folder") + projectBuilder.stage("Cleaning up sandbox folder") - project.targetFolder = "${paths.sandbox}/classes" - project.dryRun = dryRun + projectBuilder.targetFolder = "${paths.sandbox}/classes" + projectBuilder.dryRun = dryRun if (!dryRun) { forceDelete(paths.sandbox) @@ -131,12 +131,12 @@ target('default': "Build artifacts") { def forbiddenJars = ["client-api", "ideaLicenseDecoder", "eawtstub.jar", "ui.jar", "exe4jlib.jar", "cglib", "jcip-annotations", "/y.jar", "ysvg.jar"] - project.clean() + projectBuilder.cleanOutput() modules.each { - Module module = project[it] as Module + Module module = findModule(it) if (module != null) { - project.info("Building $module.name") - module.runtimeClasspath().each { + projectBuilder.info("Building $module.name") + projectBuilder.moduleRuntimeClasspath(module, false).each { File file = new File(it) if (file.exists()) { String path = file.canonicalPath.replace('\\', '/') @@ -155,15 +155,15 @@ target('default': "Build artifacts") { module.make() } else { - project.warning("$it is not a module") + projectBuilder.warning("$it is not a module") } } usedJars.add("$home/community/lib/jgoodies-common-1.2.1.jar") usedJars.add("$home/community/lib/jgoodies-looks-2.4.2.jar") - buildSearchableOptions("${project["platform-resources"].output}/search", ["$home/build/idea.license", "$home/build/pycharm.license"], { - project["python-ide"].runtimeClasspath().each { + buildSearchableOptions("${findModule("platform-resources").output}/search", ["$home/build/idea.license", "$home/build/pycharm.license"], { + projectBuilder.moduleRuntimeClasspath(findModule("python-ide"), false).each { pathelement(location: it) } }, "-Didea.platform.prefix=Python -Didea.no.jre.check=true") diff --git a/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java b/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java index 95462203829a..3d9b561b1769 100644 --- a/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java +++ b/python/openapi/src/com/jetbrains/python/packaging/PyRequirement.java @@ -161,6 +161,9 @@ public class PyRequirement { @NotNull private List parse(@Nullable String s) { // Version parsing from pkg_resources ensures that all the "pre", "alpha", "rc", etc. are sorted correctly + if (s == null) { + return Collections.emptyList(); + } final Pattern COMPONENT_RE = Pattern.compile("\\d+|[a-z]+|\\.|-|.+"); final List results = new ArrayList(); final Matcher matcher = COMPONENT_RE.matcher(s); diff --git a/python/psi-api/src/com/jetbrains/python/codeInsight/PyDynamicMember.java b/python/psi-api/src/com/jetbrains/python/codeInsight/PyDynamicMember.java index 866b83a3a210..a7a87892fe77 100644 --- a/python/psi-api/src/com/jetbrains/python/codeInsight/PyDynamicMember.java +++ b/python/psi-api/src/com/jetbrains/python/codeInsight/PyDynamicMember.java @@ -181,7 +181,10 @@ public class PyDynamicMember { if (myTypeCallback != null) { return myTypeCallback.fun(myContext); } - return PyPsiFacade.getInstance(getProject()).createClassType(myClass, !myResolveToInstance); + else if (myClass != null) { + return PyPsiFacade.getInstance(getProject()).createClassType(myClass, !myResolveToInstance); + } + return null; } } } diff --git a/python/src/com/jetbrains/python/actions/AddIgnoredIdentifierFix.java b/python/src/com/jetbrains/python/actions/AddIgnoredIdentifierQuickFix.java similarity index 56% rename from python/src/com/jetbrains/python/actions/AddIgnoredIdentifierFix.java rename to python/src/com/jetbrains/python/actions/AddIgnoredIdentifierQuickFix.java index 53cec5ae626e..132cdd740667 100644 --- a/python/src/com/jetbrains/python/actions/AddIgnoredIdentifierFix.java +++ b/python/src/com/jetbrains/python/actions/AddIgnoredIdentifierQuickFix.java @@ -8,35 +8,49 @@ import com.intellij.openapi.project.Project; import com.intellij.profile.codeInspection.InspectionProfileManager; import com.intellij.profile.codeInspection.InspectionProjectProfileManager; import com.jetbrains.python.inspections.PyUnresolvedReferencesInspection; +import com.jetbrains.python.psi.impl.PyQualifiedName; import org.jetbrains.annotations.NotNull; /** * @author yole */ -public class AddIgnoredIdentifierFix implements LocalQuickFix, LowPriorityAction { - private final String myIdentifier; +public class AddIgnoredIdentifierQuickFix implements LocalQuickFix, LowPriorityAction { + public static final String END_WILDCARD = ".*"; - public AddIgnoredIdentifierFix(String identifier) { + @NotNull private final PyQualifiedName myIdentifier; + private final boolean myIgnoreAllAttributes; + + public AddIgnoredIdentifierQuickFix(@NotNull PyQualifiedName identifier, boolean ignoreAllAttributes) { myIdentifier = identifier; + myIgnoreAllAttributes = ignoreAllAttributes; } @NotNull @Override public String getName() { - return "Ignore unresolved identifier " + myIdentifier; + if (myIgnoreAllAttributes) { + return "Mark all unresolved attributes of '" + myIdentifier + "' as ignored"; + } + else { + return "Ignore unresolved reference '" + myIdentifier + "'"; + } } @NotNull @Override public String getFamilyName() { - return "Ignore unresolved identifier"; + return "Ignore unresolved reference"; } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { final PyUnresolvedReferencesInspection instance = PyUnresolvedReferencesInspection.getInstance(descriptor.getPsiElement()); - if (!instance.ignoredIdentifiers.contains(myIdentifier)) { - instance.ignoredIdentifiers.add(myIdentifier); + String name = myIdentifier.toString(); + if (myIgnoreAllAttributes) { + name = name + END_WILDCARD; + } + if (!instance.ignoredIdentifiers.contains(name)) { + instance.ignoredIdentifiers.add(name); final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile(); InspectionProfileManager.getInstance().fireProfileChanged(profile); } diff --git a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java index ffd52de5a3a7..f893757dcc8e 100644 --- a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java +++ b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java @@ -470,9 +470,12 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i @Nullable private static PyClass inferClassOfParameter(PsiElement context) { - final PyType type = ((PyNamedParameter)context).getType(TypeEvalContext.fast()); - if (type instanceof PyClassType) - return ((PyClassType)type).getPyClass(); + if (context instanceof PyNamedParameter) { + final PyType type = ((PyNamedParameter)context).getType(TypeEvalContext.fast()); + if (type instanceof PyClassType) { + return ((PyClassType)type).getPyClass(); + } + } return null; } diff --git a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java index 2653221016fb..bf3ebab42a0f 100644 --- a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java @@ -49,7 +49,10 @@ import com.jetbrains.python.psi.impl.PyImportedModule; import com.jetbrains.python.psi.impl.PyQualifiedName; import com.jetbrains.python.psi.impl.references.PyImportReference; import com.jetbrains.python.psi.impl.references.PyOperatorReference; -import com.jetbrains.python.psi.resolve.*; +import com.jetbrains.python.psi.resolve.ImportedResolveResult; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.QualifiedNameFinder; +import com.jetbrains.python.psi.resolve.RatedResolveResult; import com.jetbrains.python.psi.types.*; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.Nls; @@ -59,6 +62,8 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; +import static com.jetbrains.python.actions.AddIgnoredIdentifierQuickFix.END_WILDCARD; + /** * Marks references that fail to resolve. Also tracks unused imports and provides "optimize imports" support. * User: dcheryasov @@ -108,7 +113,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection { @Override public JComponent createOptionsPanel() { - ListEditForm form = new ListEditForm("Ignore identifiers", ignoredIdentifiers); + ListEditForm form = new ListEditForm("Ignore references", ignoredIdentifiers); return form.getContentPanel(); } @@ -366,7 +371,8 @@ public class PyUnresolvedReferencesInspection extends PyInspection { } } - private void registerUnresolvedReferenceProblem(final PyElement node, final PsiReference reference, HighlightSeverity severity) { + private void registerUnresolvedReferenceProblem(@NotNull PyElement node, @NotNull PsiReference reference, + @NotNull HighlightSeverity severity) { String description = null; final String text = reference.getElement().getText(); TextRange rangeInElement = reference.getRangeInElement(); @@ -378,9 +384,30 @@ public class PyUnresolvedReferencesInspection extends PyInspection { final List actions = new ArrayList(2); if (ref_text.length() <= 0) return; // empty text, nothing to highlight final String refname = (element instanceof PyQualifiedExpression) ? ((PyQualifiedExpression)element).getReferencedName() : ref_text; + + final PyQualifiedName canonicalQName = getCanonicalName(reference, myTypeEvalContext); + final String canonicalName = canonicalQName != null ? canonicalQName.toString() : null; + if (canonicalName != null) { + for (String ignored : myIgnoredIdentifiers) { + if (ignored.endsWith(END_WILDCARD)) { + final String prefix = ignored.substring(0, ignored.length() - END_WILDCARD.length()); + if (canonicalName.startsWith(prefix)) { + return; + } + } + else if (canonicalName.equals(ignored)) { + return; + } + } + } + // Legacy non-qualified ignore patterns + if (myIgnoredIdentifiers.contains(refname)) { + return; + } + if (element instanceof PyReferenceExpression) { PyReferenceExpression refex = (PyReferenceExpression)element; - if (myIgnoredIdentifiers.contains(refname) || PyNames.COMPARISON_OPERATORS.contains(refname)) { + if (PyNames.COMPARISON_OPERATORS.contains(refname)) { return; } if (CythonLanguageDialect.isInsideCythonFile(element) && CythonNames.BUILTINS.contains(text)) { @@ -419,11 +446,6 @@ public class PyUnresolvedReferencesInspection extends PyInspection { // TODO: mark the node so that future references pointing to it won't result in a error, but in a warning } } - if (reference instanceof DocStringParameterReference) { - if (myIgnoredIdentifiers.contains(reference.getCanonicalText())) { - return; - } - } if (reference instanceof PsiReferenceEx && description == null) { description = ((PsiReferenceEx)reference).getUnresolvedDescription(); } @@ -431,9 +453,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection { boolean marked_qualified = false; if (element instanceof PyQualifiedExpression) { final PyQualifiedExpression qexpr = (PyQualifiedExpression)element; - if (myIgnoredIdentifiers.contains(ref_text) || - PyNames.COMPARISON_OPERATORS.contains(qexpr.getReferencedName()) || - refname == null) { + if (PyNames.COMPARISON_OPERATORS.contains(qexpr.getReferencedName()) || refname == null) { return; } final PyExpression qualifier = qexpr.getQualifier(); @@ -487,7 +507,12 @@ public class PyUnresolvedReferencesInspection extends PyInspection { if (GenerateBinaryStubsFix.isApplicable(reference)) { actions.add(new GenerateBinaryStubsFix(reference)); } - actions.add(new AddIgnoredIdentifierFix(ref_text)); + if (canonicalQName != null) { + actions.add(new AddIgnoredIdentifierQuickFix(canonicalQName, false)); + if (canonicalQName.getComponentCount() > 1) { + actions.add(new AddIgnoredIdentifierQuickFix(canonicalQName.removeLastComponent(), true)); + } + } addPluginQuickFixes(reference, actions); PsiElement point = node.getLastChild(); // usually the identifier at the end of qual ref @@ -515,6 +540,77 @@ public class PyUnresolvedReferencesInspection extends PyInspection { registerProblem(point, description, hl_type, null, range, actions.toArray(new LocalQuickFix[actions.size()])); } + /** + * Return the canonical qualified name for a reference (even for an unresolved one). + */ + @Nullable + private static PyQualifiedName getCanonicalName(@NotNull PsiReference reference, @NotNull TypeEvalContext context) { + final PsiElement element = reference.getElement(); + if (reference instanceof PyOperatorReference && element instanceof PyQualifiedExpression) { + final PyExpression receiver = ((PyOperatorReference)reference).getReceiver(); + if (receiver != null) { + final PyType type = receiver.getType(context); + if (type instanceof PyClassType) { + final String name = ((PyClassType)type).getClassQName(); + if (name != null) { + return PyQualifiedName.fromDottedString(name).append(((PyQualifiedExpression)element).getReferencedName()); + } + } + } + } + else if (element instanceof PyReferenceExpression) { + final PyReferenceExpression expr = (PyReferenceExpression)element; + final PyExpression qualifier = expr.getQualifier(); + final String exprName = expr.getName(); + if (exprName != null) { + if (qualifier != null) { + final PyType type = qualifier.getType(context); + if (type instanceof PyClassType) { + final String name = ((PyClassType)type).getClassQName(); + if (name != null) { + return PyQualifiedName.fromDottedString(name).append(exprName); + } + } + else if (type instanceof PyModuleType) { + final PyFile file = ((PyModuleType)type).getModule(); + final PyQualifiedName name = QualifiedNameFinder.findCanonicalImportPath(file, element); + if (name != null) { + return name.append(exprName); + } + } + } + else { + final PsiElement parent = element.getParent(); + if (parent instanceof PyImportElement) { + final PyImportStatementBase importStmt = PsiTreeUtil.getParentOfType(parent, PyImportStatementBase.class); + if (importStmt instanceof PyImportStatement) { + return PyQualifiedName.fromComponents(exprName); + } + else if (importStmt instanceof PyFromImportStatement) { + final PsiElement resolved = ((PyFromImportStatement)importStmt).resolveImportSource(); + if (resolved != null) { + final PyQualifiedName path = QualifiedNameFinder.findCanonicalImportPath(resolved, element); + if (path != null) { + return path.append(exprName); + } + } + } + } + else { + final PyQualifiedName path = QualifiedNameFinder.findCanonicalImportPath(element, element); + if (path != null) { + return path.append(exprName); + } + } + } + } + } + else if (reference instanceof DocStringParameterReference) { + return PyQualifiedName.fromDottedString(reference.getCanonicalText()); + } + return null; + } + private static boolean ignoreUnresolvedMemberForType(@NotNull PyType qtype, PsiReference reference, String refText) { if (qtype instanceof PyNoneType || PyTypeChecker.isUnknown(qtype)) { // this almost always means that we don't know the type, so don't show an error in this case diff --git a/python/src/com/jetbrains/python/psi/impl/references/PyOperatorReference.java b/python/src/com/jetbrains/python/psi/impl/references/PyOperatorReference.java index 01f11424891f..48614982b79c 100644 --- a/python/src/com/jetbrains/python/psi/impl/references/PyOperatorReference.java +++ b/python/src/com/jetbrains/python/psi/impl/references/PyOperatorReference.java @@ -76,6 +76,20 @@ public class PyOperatorReference extends PyReferenceImpl { } } + @Nullable + public PyExpression getReceiver() { + if (myElement instanceof PyBinaryExpression) { + return ((PyBinaryExpression)myElement).getLeftExpression(); + } + else if (myElement instanceof PySubscriptionExpression) { + return ((PySubscriptionExpression)myElement).getOperand(); + } + else if (myElement instanceof PyPrefixExpression) { + return ((PyPrefixExpression)myElement).getOperand(); + } + return null; + } + private static String leftToRightOperatorName(String name) { return name.replaceFirst("__([a-z]+)__", "__r$1__"); } diff --git a/python/testSrc/com/jetbrains/python/PySuppressInspectionsTest.java b/python/testSrc/com/jetbrains/python/PySuppressInspectionsTest.java index 4daa7ed52d05..c2cd759252df 100644 --- a/python/testSrc/com/jetbrains/python/PySuppressInspectionsTest.java +++ b/python/testSrc/com/jetbrains/python/PySuppressInspectionsTest.java @@ -6,6 +6,8 @@ import com.jetbrains.python.inspections.PyInspection; import com.jetbrains.python.inspections.PyUnresolvedReferencesInspection; import com.jetbrains.python.inspections.PyUnusedLocalInspection; +import java.util.List; + /** * @author yole */ @@ -39,8 +41,9 @@ public class PySuppressInspectionsTest extends PyTestCase { public void testSuppressForStatement() { myFixture.configureByFile("inspections/suppress/suppressForStatement.py"); myFixture.enableInspections(PyUnresolvedReferencesInspection.class); - final IntentionAction suppressAction = myFixture.findSingleIntention("Suppress for statement"); - assertNotNull(suppressAction); + final List intentions = myFixture.filterAvailableIntentions("Suppress for statement"); + assertEquals(2, intentions.size()); + final IntentionAction suppressAction = intentions.get(0); myFixture.launchAction(suppressAction); myFixture.checkResultByFile("inspections/suppress/suppressForStatement.after.py"); }