PY-44026 Always match the first component of a qualified name in suggestions

For instance, "numpy.foo" should not match "pkg.test_numpy.foo".

GitOrigin-RevId: b3f3dfb17b0705eb3f93368eb281ba662ea92e3c
This commit is contained in:
Mikhail Golubev
2021-02-17 15:56:31 +00:00
committed by intellij-monorepo-bot
parent e40791ec88
commit 9ad52f13a4
9 changed files with 91 additions and 43 deletions
@@ -104,7 +104,7 @@ class PyUnresolvedModuleAttributeCompletionContributor : CompletionContributor()
val suggestedQualifiedNames = HashSet<String>()
ProgressManager.checkCanceled()
val resultMatchingCompleteReference = result.withPrefixMatcher(QualifiedNameMatcher(qualifierString, attribute))
val resultMatchingCompleteReference = result.withPrefixMatcher(QualifiedNameMatcher(qualifier.append(attribute)))
PyModuleNameIndex.find(qualifier.lastComponent!!, project, true).asSequence()
.filter { QualifiedNameFinder.findShortestImportableQName(it) == qualifier }
.flatMap { it.iterateNames().asSequence() }
@@ -128,7 +128,7 @@ class PyUnresolvedModuleAttributeCompletionContributor : CompletionContributor()
}
val scope = PySearchUtilBase.excludeSdkTestsScope(project)
PyQualifiedNameCompletionMatcher.processMatchingExportedNames(
qualifierString, attribute, parameters.originalFile, scope,
qualifier, attribute, parameters.originalFile, scope,
Processor {
ProgressManager.checkCanceled()
if (suggestedQualifiedNames.add(it.qualifiedName.toString())) {
@@ -7,7 +7,6 @@ import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
@@ -37,18 +36,17 @@ public class PyQualifiedNameCompletionMatcher {
private PyQualifiedNameCompletionMatcher() {
}
public static void processMatchingExportedNames(@NotNull String qualifierPattern,
public static void processMatchingExportedNames(@NotNull QualifiedName qualifierPattern,
@NotNull String attributePattern,
@NotNull PsiFile currentFile,
@NotNull GlobalSearchScope scope,
@NotNull Processor<? super ExportedName> processor) {
if (attributePattern.isEmpty() || qualifierPattern.isEmpty()) return;
PrefixMatcher attributeMatcher = createNameMatcher(attributePattern);
PrefixMatcher qualifierMatcher = createQualifierMatcher(qualifierPattern);
if (attributePattern.isEmpty() || qualifierPattern.getComponentCount() == 0) return;
QualifiedNameMatcher matcher = new QualifiedNameMatcher(qualifierPattern.append(attributePattern));
StubIndex stubIndex = StubIndex.getInstance();
Project project = Objects.requireNonNull(scope.getProject());
GlobalSearchScope moduleMatchingScope = new ModuleQualifiedNameMatchingScope(scope, qualifierMatcher, project);
GlobalSearchScope moduleMatchingScope = new ModuleQualifiedNameMatchingScope(scope, matcher, project);
Set<QualifiedName> alreadySuggestedAttributes = new HashSet<>();
IndexLookupStats stats = new IndexLookupStats();
try {
@@ -56,7 +54,7 @@ public class PyQualifiedNameCompletionMatcher {
stubIndex.processAllKeys(PyExportedModuleAttributeIndex.KEY, attributeName -> {
ProgressManager.checkCanceled();
stats.scannedKeys++;
if (!attributeMatcher.isStartMatch(attributeName)) return true;
if (!matcher.attributeMatches(attributeName)) return true;
stats.matchingKeys++;
return stubIndex.processElements(PyExportedModuleAttributeIndex.KEY,
attributeName, project, moduleMatchingScope, idFilter, PyElement.class, element -> {
@@ -66,7 +64,7 @@ public class PyQualifiedNameCompletionMatcher {
assert moduleQualifiedName != null : vFile;
QualifiedName canonicalImportPath = findCanonicalImportPath(element, moduleQualifiedName, currentFile);
QualifiedName importPath;
if (canonicalImportPath != null && qualifierMatcher.prefixMatches(canonicalImportPath.toString())) {
if (canonicalImportPath != null && matcher.qualifierMatches(canonicalImportPath)) {
importPath = canonicalImportPath;
}
else {
@@ -108,16 +106,6 @@ public class PyQualifiedNameCompletionMatcher {
}
}
@NotNull
private static CamelHumpMatcher createQualifierMatcher(@NotNull String pattern) {
return new CamelHumpMatcher(pattern, false);
}
@NotNull
private static PrefixMatcher createNameMatcher(@NotNull String pattern) {
return new CamelHumpMatcher(pattern, false);
}
@Nullable
private static QualifiedName findCanonicalImportPath(@NotNull PyElement element,
@NotNull QualifiedName moduleQualifiedName,
@@ -153,43 +141,54 @@ public class PyQualifiedNameCompletionMatcher {
}
public static final class QualifiedNameMatcher extends PrefixMatcher {
final PrefixMatcher myLastNameMatcher;
final PrefixMatcher myQualifierMatcher;
private final PrefixMatcher myQualifierFirstComponentMatcher;
private final PrefixMatcher myQualifierRemainderMatcher;
private final PrefixMatcher myLastComponentMatcher;
public QualifiedNameMatcher(@NotNull String qualifierPattern, @NotNull String namePattern) {
super(qualifierPattern + "." + namePattern);
myLastNameMatcher = createNameMatcher(namePattern);
myQualifierMatcher = createQualifierMatcher(qualifierPattern);
public QualifiedNameMatcher(@NotNull QualifiedName qualifiedName) {
super(qualifiedName.toString());
if (qualifiedName.getComponentCount() < 2) {
throw new IllegalArgumentException("Qualified name should have at least two components, but was '" + qualifiedName + "'");
}
myLastComponentMatcher = new CamelHumpMatcher(qualifiedName.getLastComponent(), false);
QualifiedName qualifier = qualifiedName.removeLastComponent();
myQualifierFirstComponentMatcher = new CamelHumpMatcher(qualifier.getFirstComponent(), false);
myQualifierRemainderMatcher = new CamelHumpMatcher(qualifier.removeHead(1).toString(), false);
}
@Override
public boolean prefixMatches(@NotNull String qualifiedName) {
Couple<String> qualifierAndName = splitByLastDot(qualifiedName);
return myQualifierMatcher.prefixMatches(qualifierAndName.getFirst()) && myLastNameMatcher.prefixMatches(qualifierAndName.getSecond());
public boolean prefixMatches(@NotNull String name) {
QualifiedName qualifiedName = QualifiedName.fromDottedString(name);
if (qualifiedName.getComponentCount() == 0) return false;
if (!attributeMatches(qualifiedName.getLastComponent())) return false;
if (!qualifierMatches(qualifiedName.removeLastComponent())) return false;
return true;
}
private boolean attributeMatches(@Nullable String attribute) {
return myLastComponentMatcher.isStartMatch(attribute);
}
private boolean qualifierMatches(@NotNull QualifiedName qualifier) {
String firstComponent = Objects.requireNonNullElse(qualifier.getFirstComponent(), "");
if (!myQualifierFirstComponentMatcher.prefixMatches(firstComponent)) return false;
String remainder = qualifier.getComponentCount() == 0 ? "" : qualifier.removeHead(1).toString();
if (!myQualifierRemainderMatcher.prefixMatches(remainder)) return false;
return true;
}
@Override
public @NotNull PrefixMatcher cloneWithPrefix(@NotNull String prefix) {
Couple<String> qualifierAndName = splitByLastDot(prefix);
return new QualifiedNameMatcher(qualifierAndName.getFirst(), qualifierAndName.getSecond());
}
@NotNull
private static Couple<String> splitByLastDot(@NotNull String qualifiedName) {
int lastDotIndex = qualifiedName.lastIndexOf(".");
if (lastDotIndex < 0) {
return Couple.of("", qualifiedName);
}
return Couple.of(qualifiedName.substring(0, lastDotIndex), qualifiedName.substring(lastDotIndex + 1));
return new QualifiedNameMatcher(QualifiedName.fromDottedString(prefix));
}
}
private static class ModuleQualifiedNameMatchingScope extends DelegatingGlobalSearchScope {
private final PrefixMatcher myQualifiedNameMatcher;
private final QualifiedNameMatcher myQualifiedNameMatcher;
private final Project myProject;
ModuleQualifiedNameMatchingScope(@NotNull GlobalSearchScope baseScope,
@NotNull PrefixMatcher qualifiedNameMatcher,
@NotNull QualifiedNameMatcher qualifiedNameMatcher,
@NotNull Project project) {
super(baseScope);
myQualifiedNameMatcher = qualifiedNameMatcher;
@@ -201,7 +200,7 @@ public class PyQualifiedNameCompletionMatcher {
if (!super.contains(file)) return false;
QualifiedName qualifiedName = restoreModuleQualifiedName(file, myProject);
if (qualifiedName == null) return false;
return myQualifiedNameMatcher.prefixMatches(qualifiedName.toString());
return myQualifiedNameMatcher.qualifierMatches(qualifiedName);
}
@Nullable
@@ -0,0 +1,42 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python;
import com.intellij.psi.util.QualifiedName;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.stubs.PyQualifiedNameCompletionMatcher.QualifiedNameMatcher;
import java.util.List;
public class PyNotImportedQualifiedNameCompletionTest extends PyTestCase {
public void testQualifiedNamesAlwaysMatchedByFirstComponentOfQualifier() {
myFixture.copyDirectoryToProject(getTestName(false), "");
myFixture.configureByFile("main.py");
myFixture.completeBasic();
List<String> variants = myFixture.getLookupElementStrings();
assertNotNull(variants);
assertDoesntContain(variants, "foo.bar.func");
assertContainsElements(variants, "bar.func", "baz.quux.func");
}
public void testQualifiedNameMatcherTest() {
QualifiedNameMatcher matcher = new QualifiedNameMatcher(QualifiedName.fromDottedString("foo.bar.baz"));
assertTrue(matcher.prefixMatches("foo.bar.baz"));
assertTrue(matcher.prefixMatches("foo.bar.baz"));
assertTrue(matcher.prefixMatches("fooExtra.bar.baz"));
assertTrue(matcher.prefixMatches("foo.barExtra.baz"));
assertTrue(matcher.prefixMatches("foo.bar.bazExtra"));
assertFalse(matcher.prefixMatches(""));
assertFalse(matcher.prefixMatches("baz"));
assertFalse(matcher.prefixMatches("foo.bar"));
assertFalse(matcher.prefixMatches("bar.baz"));
assertTrue(matcher.prefixMatches("foo.xxx.bar.baz"));
assertTrue(matcher.prefixMatches("foo.bar.xxx.baz"));
assertFalse(matcher.prefixMatches("foo.bar.baz.xxx"));
assertFalse(matcher.prefixMatches("xxx.foo.bar.baz"));
}
@Override
protected String getTestDataPath() {
return super.getTestDataPath() + "/completion/notImportedQualifiedName/";
}
}