KT-6050 Support completion for super qualifier

#KT-6050 Fixed
This commit is contained in:
Valentin Kipyatkov
2015-08-25 16:27:11 +03:00
parent 7458000f14
commit 66719923c2
17 changed files with 625 additions and 377 deletions
@@ -27,6 +27,7 @@ import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.ProcessingContext
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.ClassifierDescriptor
@@ -39,12 +40,10 @@ import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindExclude
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
class BasicCompletionSession(configuration: CompletionSessionConfiguration,
parameters: CompletionParameters,
@@ -79,7 +78,9 @@ class BasicCompletionSession(configuration: CompletionSessionConfiguration,
NAMED_ARGUMENTS_ONLY(descriptorKindFilter = null, classKindFilter = null),
PARAMETER_NAME(descriptorKindFilter = null, classKindFilter = null)
PARAMETER_NAME(descriptorKindFilter = null, classKindFilter = null),
SUPER_QUALIFIER(descriptorKindFilter = DescriptorKindFilter.NON_SINGLETON_CLASSIFIERS, classKindFilter = null)
}
private val completionKind = calcCompletionKind()
@@ -136,6 +137,10 @@ class BasicCompletionSession(configuration: CompletionSessionConfiguration,
// expression we are at first of it
val typeReference = position.getStrictParentOfType<JetTypeReference>()
if (typeReference != null) {
if (typeReference.parent is JetSuperExpression) {
return CompletionKind.SUPER_QUALIFIER
}
val firstPartReference = PsiTreeUtil.findChildOfType(typeReference, javaClass<JetSimpleNameExpression>())
if (firstPartReference == nameExpression) {
return CompletionKind.TYPES
@@ -182,6 +187,11 @@ class BasicCompletionSession(configuration: CompletionSessionConfiguration,
}
}
if (completionKind == CompletionKind.SUPER_QUALIFIER) {
completeSuperQualifier()
return
}
// if we are typing parameter name, restart completion each time we type an upper case letter because new suggestions will appear (previous words can be used as user prefix)
if (parameterNameAndTypeCompletion != null) {
val prefixPattern = StandardPatterns.string().with(object : PatternCondition<String>("Prefix ends with uppercase letter") {
@@ -306,6 +316,28 @@ class BasicCompletionSession(configuration: CompletionSessionConfiguration,
parameterNameAndTypeCompletion?.addFromAllClasses(parameters, indicesHelper)
}
private fun completeSuperQualifier() {
val classOrObject = position.parents.firstIsInstanceOrNull<JetClassOrObject>() ?: return
val classDescriptor = resolutionFacade.resolveToDescriptor(classOrObject) as ClassDescriptor
var superClasses = classDescriptor.defaultType.constructor.supertypes
.map { it.constructor.declarationDescriptor as? ClassDescriptor }
.filterNotNull()
//TODO: IMO it's not good that Any is to be added manually
if (superClasses.all { it.kind == ClassKind.INTERFACE }) {
superClasses += KotlinBuiltIns.getInstance().any
}
if (!isNoQualifierContext()) {
val referenceVariantsSet = referenceVariants.toSet()
superClasses = superClasses.filter { it in referenceVariantsSet }
}
superClasses
.map { lookupElementFactory.createLookupElement(it, boldImmediateMembers = false, qualifyNestedClasses = true, includeClassTypeArguments = false) }
.forEach { collector.addElement(it) }
}
override fun createSorter(): CompletionSorter {
var sorter = super.createSorter()
@@ -74,6 +74,8 @@ public class KotlinCompletionContributor : CompletionContributor() {
isInClassHeader(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER // do not add '$' to not interrupt class declaration parsing
isInUnclosedSuperQualifier(tokenBefore) -> CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + ">"
else -> specialLambdaSignatureDummyIdentifier(tokenBefore)
?: specialExtensionReceiverDummyIdentifier(tokenBefore)
?: specialInTypeArgsDummyIdentifier(tokenBefore)
@@ -415,4 +417,14 @@ public class KotlinCompletionContributor : CompletionContributor() {
}
return balance
}
private fun isInUnclosedSuperQualifier(tokenBefore: PsiElement?): Boolean {
if (tokenBefore == null) return false
val tokensToSkip = TokenSet.orSet(TokenSet.create(JetTokens.IDENTIFIER, JetTokens.DOT ), JetTokens.WHITE_SPACE_OR_COMMENT_BIT_SET)
val tokens = sequence(tokenBefore) { it.prevLeaf() }
val ltToken = tokens.firstOrNull { it.node.elementType !in tokensToSkip } ?: return false
if (ltToken.node.elementType != JetTokens.LT) return false
val superToken = ltToken.prevLeaf { it !is PsiWhiteSpace && it !is PsiComment }
return superToken?.node?.elementType == JetTokens.SUPER_KEYWORD
}
}
@@ -146,7 +146,9 @@ public class LookupElementFactory(
if (nestLevel > 0) {
var itemText = psiClass.getName()
for (i in 1..nestLevel) {
itemText = containerName.substringAfterLast('.') + "." + itemText
val outerClassName = containerName.substringAfterLast('.')
element = element.withLookupString(outerClassName)
itemText = outerClassName + "." + itemText
containerName = containerName.substringBeforeLast('.', FqName.ROOT.toString())
}
element = element.withPresentableText(itemText!!)
@@ -246,6 +248,7 @@ public class LookupElementFactory(
element = element.withPresentableText(DescriptorRenderer.SHORT_NAMES_IN_TYPES.renderClassifierName(descriptor))
while (container is ClassDescriptor) {
element = element.withLookupString(container.name.asString())
container = container.getContainingDeclaration()
}
}
@@ -2,7 +2,7 @@ package Test
import <caret>
// EXIST: java,
// EXIST: java
// EXIST_JAVA_ONLY: javax
// EXIST_JS_ONLY: jquery, html5
// ABSENT: Array, Integer
@@ -0,0 +1,15 @@
open class Base
open class A : Base
interface I
class B : A(), I {
fun foo() {
super<<caret>
}
}
// EXIST: A
// EXIST: I
// NOTHING_ELSE
@@ -0,0 +1,13 @@
open class A
interface I
class B : A(), I {
fun foo() {
super<<caret>>
}
}
// EXIST: A
// EXIST: I
// NOTHING_ELSE
@@ -0,0 +1,13 @@
open class AA
interface AI
class B : AA(), AI {
fun foo() {
super<A<caret>
}
}
// EXIST: AA
// EXIST: AI
// NOTHING_ELSE
@@ -0,0 +1,17 @@
package p
open class A
interface I {
interface Nested
}
class B : A(), I.Nested {
fun foo() {
super<<caret>
}
}
// EXIST: A
// EXIST: { lookupString: "Nested", allLookupStrings: "I, Nested", itemText: "I.Nested", tailText: " (p)" }
// NOTHING_ELSE
@@ -0,0 +1,16 @@
package p
open class A
interface I {
interface Nested
}
class B : A(), I.Nested {
fun foo() {
super<I<caret>
}
}
// EXIST: { lookupString: "Nested", allLookupStrings: "I, Nested", itemText: "I.Nested", tailText: " (p)" }
// NOTHING_ELSE
@@ -0,0 +1,17 @@
package p
open class A
interface I {
interface Nested1
interface Nested2
}
class B : A(), I.Nested1 {
fun foo() {
super<I.<caret>
}
}
// EXIST: Nested1
// NOTHING_ELSE
@@ -0,0 +1,11 @@
interface I
class B : I {
fun foo() {
super<<caret>
}
}
// EXIST: Any
// EXIST: I
// NOTHING_ELSE
@@ -0,0 +1,11 @@
interface X
interface I : X {
fun foo() {
super<<caret>
}
}
// EXIST: Any
// EXIST: X
// NOTHING_ELSE
@@ -0,0 +1,11 @@
open class A<T>
interface I
class B : A<String>(), I {
fun foo() {
super<<caret>
}
}
// EXIST: { itemText: "A", tailText: " (<root>)" }
@@ -1,370 +0,0 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.completion.test;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.codeInsight.lookup.impl.LookupCellRenderer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.JBColor;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.test.AstAccessControl;
import org.jetbrains.kotlin.js.resolve.JsPlatform;
import org.jetbrains.kotlin.resolve.TargetPlatform;
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform;
import org.jetbrains.kotlin.test.InTextDirectivesUtils;
import org.junit.Assert;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* Extract a number of statements about completion from the given text. Those statements
* should be asserted during test execution.
*/
public class ExpectedCompletionUtils {
private ExpectedCompletionUtils() {
}
public static class CompletionProposal {
public static final String LOOKUP_STRING = "lookupString";
public static final String PRESENTATION_ITEM_TEXT = "itemText";
public static final String PRESENTATION_TYPE_TEXT = "typeText";
public static final String PRESENTATION_TAIL_TEXT = "tailText";
public static final String PRESENTATION_TEXT_ATTRIBUTES = "attributes";
public static final Set<String> validKeys = new HashSet<String>(
Arrays.asList(LOOKUP_STRING, PRESENTATION_ITEM_TEXT, PRESENTATION_TYPE_TEXT, PRESENTATION_TAIL_TEXT,
PRESENTATION_TEXT_ATTRIBUTES)
);
private final Map<String, String> map;
public CompletionProposal(@NotNull String lookupString) {
map = new HashMap<String, String>();
map.put(LOOKUP_STRING, lookupString);
}
public CompletionProposal(@NotNull Map<String, String> map) {
this.map = map;
for (String key : map.keySet()) {
if (!validKeys.contains(key)){
throw new RuntimeException("Invalid key '" + key + "'");
}
}
}
public CompletionProposal(@NotNull JsonObject json) {
map = new HashMap<String, String>();
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
String key = entry.getKey();
if (!validKeys.contains(key)) {
throw new RuntimeException("Invalid json property '" + key + "'");
}
JsonElement value = entry.getValue();
if (!(value instanceof JsonNull)) {
map.put(key, value.getAsString());
}
}
}
public boolean matches(CompletionProposal expectedProposal) {
for (Map.Entry<String, String> entry : expectedProposal.map.entrySet()) {
if (!entry.getValue().equals(map.get(entry.getKey()))) {
return false;
}
}
return true;
}
@Override
public String toString() {
JsonObject jsonObject = new JsonObject();
for (Map.Entry<String, String> entry : map.entrySet()) {
jsonObject.addProperty(entry.getKey(), entry.getValue());
}
return jsonObject.toString();
}
}
private static final String UNSUPPORTED_PLATFORM_MESSAGE = String.format("Only %s and %s platforms are supported", JvmPlatform.INSTANCE$, JsPlatform.INSTANCE$);
private static final String EXIST_LINE_PREFIX = "EXIST:";
private static final String ABSENT_LINE_PREFIX = "ABSENT:";
private static final String ABSENT_JS_LINE_PREFIX = "ABSENT_JS:";
private static final String ABSENT_JAVA_LINE_PREFIX = "ABSENT_JAVA:";
private static final String EXIST_JAVA_ONLY_LINE_PREFIX = "EXIST_JAVA_ONLY:";
private static final String EXIST_JS_ONLY_LINE_PREFIX = "EXIST_JS_ONLY:";
private static final String NUMBER_LINE_PREFIX = "NUMBER:";
private static final String NUMBER_JS_LINE_PREFIX = "NUMBER_JS:";
private static final String NUMBER_JAVA_LINE_PREFIX = "NUMBER_JAVA:";
private static final String NOTHING_ELSE_PREFIX = "NOTHING_ELSE";
private static final String INVOCATION_COUNT_PREFIX = "INVOCATION_COUNT:";
private static final String WITH_ORDER_PREFIX = "WITH_ORDER";
private static final String AUTOCOMPLETE_SETTING_PREFIX = "AUTOCOMPLETE_SETTING:";
public static final String RUNTIME_TYPE = "RUNTIME_TYPE:";
public static final List<String> KNOWN_PREFIXES = ImmutableList.of(
EXIST_LINE_PREFIX,
ABSENT_LINE_PREFIX,
ABSENT_JS_LINE_PREFIX,
ABSENT_JAVA_LINE_PREFIX,
EXIST_JAVA_ONLY_LINE_PREFIX,
EXIST_JS_ONLY_LINE_PREFIX,
NUMBER_LINE_PREFIX,
NUMBER_JS_LINE_PREFIX,
NUMBER_JAVA_LINE_PREFIX,
INVOCATION_COUNT_PREFIX,
WITH_ORDER_PREFIX,
AUTOCOMPLETE_SETTING_PREFIX,
NOTHING_ELSE_PREFIX,
RUNTIME_TYPE,
AstAccessControl.INSTANCE$.getALLOW_AST_ACCESS_DIRECTIVE());
@NotNull
public static CompletionProposal[] itemsShouldExist(String fileText, @Nullable TargetPlatform platform) {
if (platform == null) {
return processProposalAssertions(fileText, EXIST_LINE_PREFIX);
}
else if (platform == JvmPlatform.INSTANCE$) {
return processProposalAssertions(fileText, EXIST_LINE_PREFIX, EXIST_JAVA_ONLY_LINE_PREFIX);
}
else if (platform == JsPlatform.INSTANCE$) {
return processProposalAssertions(fileText, EXIST_LINE_PREFIX, EXIST_JS_ONLY_LINE_PREFIX);
}
else {
throw new IllegalArgumentException(UNSUPPORTED_PLATFORM_MESSAGE);
}
}
@NotNull
public static CompletionProposal[] itemsShouldAbsent(String fileText, @Nullable TargetPlatform platform) {
if (platform == null) {
return processProposalAssertions(fileText, ABSENT_LINE_PREFIX);
}
else if (platform == JvmPlatform.INSTANCE$) {
return processProposalAssertions(fileText, ABSENT_LINE_PREFIX, ABSENT_JAVA_LINE_PREFIX, EXIST_JS_ONLY_LINE_PREFIX);
}
else if (platform == JsPlatform.INSTANCE$) {
return processProposalAssertions(fileText, ABSENT_LINE_PREFIX, ABSENT_JS_LINE_PREFIX, EXIST_JAVA_ONLY_LINE_PREFIX);
}
else {
throw new IllegalArgumentException(UNSUPPORTED_PLATFORM_MESSAGE);
}
}
public static CompletionProposal[] processProposalAssertions(String fileText, String... prefixes) {
Collection<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
for (String proposalStr : InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, prefixes)) {
if (proposalStr.startsWith("{")){
JsonParser parser = new JsonParser();
JsonElement json = parser.parse(proposalStr);
proposals.add(new CompletionProposal((JsonObject) json));
}
else if (proposalStr.startsWith("\"") && proposalStr.endsWith("\"")) {
proposals.add(new CompletionProposal(proposalStr.substring(1, proposalStr.length() - 1)));
}
else{
for(String item : proposalStr.split(",")){
proposals.add(new CompletionProposal(item.trim()));
}
}
}
return ArrayUtil.toObjectArray(proposals, CompletionProposal.class);
}
@Nullable
public static Integer getExpectedNumber(String fileText, @Nullable TargetPlatform platform) {
if (platform == null) {
return InTextDirectivesUtils.getPrefixedInt(fileText, NUMBER_LINE_PREFIX);
}
else if (platform == JvmPlatform.INSTANCE$) {
return getPlatformExpectedNumber(fileText, NUMBER_JAVA_LINE_PREFIX);
}
else if (platform == JsPlatform.INSTANCE$) {
return getPlatformExpectedNumber(fileText, NUMBER_JS_LINE_PREFIX);
}
else {
throw new IllegalArgumentException(UNSUPPORTED_PLATFORM_MESSAGE);
}
}
public static boolean isNothingElseExpected(String fileText) {
return !InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, NOTHING_ELSE_PREFIX).isEmpty();
}
@Nullable
public static Integer getInvocationCount(String fileText) {
return InTextDirectivesUtils.getPrefixedInt(fileText, INVOCATION_COUNT_PREFIX);
}
@Nullable
public static Boolean getAutocompleteSetting(String fileText) {
return InTextDirectivesUtils.getPrefixedBoolean(fileText, AUTOCOMPLETE_SETTING_PREFIX);
}
public static boolean isWithOrder(String fileText) {
return !InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, WITH_ORDER_PREFIX).isEmpty();
}
public static void assertDirectivesValid(String fileText) {
InTextDirectivesUtils.assertHasUnknownPrefixes(fileText, KNOWN_PREFIXES);
}
public static void assertContainsRenderedItems(CompletionProposal[] expected, LookupElement[] items, boolean checkOrder, boolean nothingElse) {
List<CompletionProposal> itemsInformation = getItemsInformation(items);
String allItemsString = listToString(itemsInformation);
Set<CompletionProposal> leftItems = nothingElse ? new LinkedHashSet<CompletionProposal>(itemsInformation) : null;
int indexOfPrevious = Integer.MIN_VALUE;
for (CompletionProposal expectedProposal : expected) {
boolean isFound = false;
for (int index = 0; index < itemsInformation.size(); index++) {
CompletionProposal proposal = itemsInformation.get(index);
if (proposal.matches(expectedProposal)) {
isFound = true;
Assert.assertTrue("Invalid order of existent elements in " + allItemsString,
!checkOrder || index > indexOfPrevious);
indexOfPrevious = index;
if (leftItems != null) {
leftItems.remove(proposal);
}
break;
}
}
if (!isFound) {
if (allItemsString.isEmpty()) {
Assert.fail("Completion is empty but " + expectedProposal + " is expected");
}
else {
Assert.fail("Expected " + expectedProposal + " not found in:\n" + allItemsString);
}
}
}
if (leftItems != null && !leftItems.isEmpty()) {
Assert.fail("No items not mentioned in EXIST directives expected but some found:\n" + listToString(leftItems));
}
}
private static Integer getPlatformExpectedNumber(String fileText, String platformNumberPrefix) {
Integer prefixedInt = InTextDirectivesUtils.getPrefixedInt(fileText, platformNumberPrefix);
if (prefixedInt != null) {
Assert.assertNull(String.format("There shouldn't be %s and %s prefixes set in same time", NUMBER_LINE_PREFIX,
platformNumberPrefix),
InTextDirectivesUtils.getPrefixedInt(fileText, NUMBER_LINE_PREFIX));
return prefixedInt;
}
return InTextDirectivesUtils.getPrefixedInt(fileText, NUMBER_LINE_PREFIX);
}
public static void assertNotContainsRenderedItems(CompletionProposal[] unexpected, LookupElement[] items) {
List<CompletionProposal> itemsInformation = getItemsInformation(items);
String allItemsString = listToString(itemsInformation);
for (CompletionProposal unexpectedProposal : unexpected) {
for (CompletionProposal proposal : itemsInformation) {
Assert.assertFalse("Unexpected '" + unexpectedProposal + "' presented in " + allItemsString,
proposal.matches(unexpectedProposal));
}
}
}
public static List<CompletionProposal> getItemsInformation(LookupElement[] items) {
LookupElementPresentation presentation = new LookupElementPresentation();
List<CompletionProposal> result = new ArrayList<CompletionProposal>();
if (items != null) {
for (LookupElement item : items) {
item.renderElement(presentation);
Map<String, String> map = new HashMap<String, String>();
map.put(CompletionProposal.LOOKUP_STRING, item.getLookupString());
if (presentation.getItemText() != null){
map.put(CompletionProposal.PRESENTATION_ITEM_TEXT, presentation.getItemText());
map.put(CompletionProposal.PRESENTATION_TEXT_ATTRIBUTES, textAttributes(presentation));
}
if (presentation.getTypeText() != null){
map.put(CompletionProposal.PRESENTATION_TYPE_TEXT, presentation.getTypeText());
}
if (presentation.getTailText() != null){
map.put(CompletionProposal.PRESENTATION_TAIL_TEXT, presentation.getTailText());
}
result.add(new ExpectedCompletionUtils.CompletionProposal(map));
}
}
return result;
}
private static String textAttributes(LookupElementPresentation presentation) {
StringBuilder builder = new StringBuilder();
if (presentation.isItemTextBold()) {
builder.append("bold");
}
if (presentation.isItemTextUnderlined()) {
if (builder.length() > 0) builder.append(" ");
builder.append("underlined");
}
Color foreground = presentation.getItemTextForeground();
if (!foreground.equals(JBColor.foreground())) {
assert foreground.equals(LookupCellRenderer.getGrayedForeground(false));
if (builder.length() > 0) builder.append(" ");
builder.append("grayed");
}
if (presentation.isStrikeout()) {
if (builder.length() > 0) builder.append(" ");
builder.append("strikeout");
}
return builder.toString();
}
public static String listToString(Collection<CompletionProposal> items) {
return StringUtil.join(
Collections2.transform(items, new Function<CompletionProposal, String>() {
@Override
public String apply(@Nullable CompletionProposal proposal) {
assert proposal != null;
return proposal.toString();
}
}), "\n");
}
}
@@ -0,0 +1,321 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.completion.test
import com.google.common.collect.ImmutableList
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.lookup.impl.LookupCellRenderer
import com.intellij.ui.JBColor
import com.intellij.util.ArrayUtil
import org.jetbrains.kotlin.idea.test.AstAccessControl
import org.jetbrains.kotlin.js.resolve.JsPlatform
import org.jetbrains.kotlin.resolve.TargetPlatform
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.junit.Assert
import java.util.*
/**
* Extract a number of statements about completion from the given text. Those statements
* should be asserted during test execution.
*/
public object ExpectedCompletionUtils {
public class CompletionProposal {
private val map: Map<String, String?>
public constructor(lookupString: String) {
map = HashMap<String, String?>()
map.put(LOOKUP_STRING, lookupString)
}
public constructor(map: MutableMap<String, String?>) {
this.map = map
for (key in map.keySet()) {
if (key !in validKeys) {
throw RuntimeException("Invalid key '$key'")
}
}
}
public constructor(json: JsonObject) {
map = HashMap<String, String?>()
for (entry in json.entrySet()) {
val key = entry.getKey()
if (key !in validKeys) {
throw RuntimeException("Invalid json property '$key'")
}
val value = entry.value
if (value !is JsonNull) {
map.put(key, value.asString)
}
}
}
public fun matches(expectedProposal: CompletionProposal): Boolean
= expectedProposal.map.entrySet().none { it.value != map[it.key] }
override fun toString(): String {
val jsonObject = JsonObject()
for ((key, value) in map) {
jsonObject.addProperty(key, value)
}
return jsonObject.toString()
}
companion object {
public val LOOKUP_STRING: String = "lookupString"
public val ALL_LOOKUP_STRINGS: String = "allLookupStrings"
public val PRESENTATION_ITEM_TEXT: String = "itemText"
public val PRESENTATION_TYPE_TEXT: String = "typeText"
public val PRESENTATION_TAIL_TEXT: String = "tailText"
public val PRESENTATION_TEXT_ATTRIBUTES: String = "attributes"
public val validKeys: Set<String> = setOf(LOOKUP_STRING, ALL_LOOKUP_STRINGS, PRESENTATION_ITEM_TEXT, PRESENTATION_TYPE_TEXT, PRESENTATION_TAIL_TEXT, PRESENTATION_TEXT_ATTRIBUTES)
}
}
private val UNSUPPORTED_PLATFORM_MESSAGE = "Only $JvmPlatform and $JsPlatform platforms are supported"
private val EXIST_LINE_PREFIX = "EXIST:"
private val ABSENT_LINE_PREFIX = "ABSENT:"
private val ABSENT_JS_LINE_PREFIX = "ABSENT_JS:"
private val ABSENT_JAVA_LINE_PREFIX = "ABSENT_JAVA:"
private val EXIST_JAVA_ONLY_LINE_PREFIX = "EXIST_JAVA_ONLY:"
private val EXIST_JS_ONLY_LINE_PREFIX = "EXIST_JS_ONLY:"
private val NUMBER_LINE_PREFIX = "NUMBER:"
private val NUMBER_JS_LINE_PREFIX = "NUMBER_JS:"
private val NUMBER_JAVA_LINE_PREFIX = "NUMBER_JAVA:"
private val NOTHING_ELSE_PREFIX = "NOTHING_ELSE"
private val INVOCATION_COUNT_PREFIX = "INVOCATION_COUNT:"
private val WITH_ORDER_PREFIX = "WITH_ORDER"
private val AUTOCOMPLETE_SETTING_PREFIX = "AUTOCOMPLETE_SETTING:"
public val RUNTIME_TYPE: String = "RUNTIME_TYPE:"
public val KNOWN_PREFIXES: List<String> = ImmutableList.of(
EXIST_LINE_PREFIX,
ABSENT_LINE_PREFIX,
ABSENT_JS_LINE_PREFIX,
ABSENT_JAVA_LINE_PREFIX,
EXIST_JAVA_ONLY_LINE_PREFIX,
EXIST_JS_ONLY_LINE_PREFIX,
NUMBER_LINE_PREFIX,
NUMBER_JS_LINE_PREFIX,
NUMBER_JAVA_LINE_PREFIX,
INVOCATION_COUNT_PREFIX,
WITH_ORDER_PREFIX,
AUTOCOMPLETE_SETTING_PREFIX,
NOTHING_ELSE_PREFIX,
RUNTIME_TYPE,
AstAccessControl.ALLOW_AST_ACCESS_DIRECTIVE)
public fun itemsShouldExist(fileText: String, platform: TargetPlatform?): Array<CompletionProposal> {
return when (platform) {
JvmPlatform -> processProposalAssertions(fileText, EXIST_LINE_PREFIX, EXIST_JAVA_ONLY_LINE_PREFIX)
JsPlatform -> processProposalAssertions(fileText, EXIST_LINE_PREFIX, EXIST_JS_ONLY_LINE_PREFIX)
null -> processProposalAssertions(fileText, EXIST_LINE_PREFIX)
else -> throw IllegalArgumentException(UNSUPPORTED_PLATFORM_MESSAGE)
}
}
public fun itemsShouldAbsent(fileText: String, platform: TargetPlatform?): Array<CompletionProposal> {
return when (platform) {
JvmPlatform -> processProposalAssertions(fileText, ABSENT_LINE_PREFIX, ABSENT_JAVA_LINE_PREFIX, EXIST_JS_ONLY_LINE_PREFIX)
JsPlatform -> processProposalAssertions(fileText, ABSENT_LINE_PREFIX, ABSENT_JS_LINE_PREFIX, EXIST_JAVA_ONLY_LINE_PREFIX)
null -> processProposalAssertions(fileText, ABSENT_LINE_PREFIX)
else -> throw IllegalArgumentException(UNSUPPORTED_PLATFORM_MESSAGE)
}
}
public fun processProposalAssertions(fileText: String, vararg prefixes: String): Array<CompletionProposal> {
val proposals = ArrayList<CompletionProposal>()
for (proposalStr in InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, *prefixes)) {
if (proposalStr.startsWith("{")) {
val parser = JsonParser()
val json = parser.parse(proposalStr)
proposals.add(CompletionProposal(json as JsonObject))
}
else if (proposalStr.startsWith("\"") && proposalStr.endsWith("\"")) {
proposals.add(CompletionProposal(proposalStr.substring(1, proposalStr.length() - 1)))
}
else {
for (item in proposalStr.split(",")) {
proposals.add(CompletionProposal(item.trim()))
}
}
}
return ArrayUtil.toObjectArray(proposals, javaClass<CompletionProposal>())
}
public fun getExpectedNumber(fileText: String, platform: TargetPlatform?): Int? {
return when (platform) {
null -> InTextDirectivesUtils.getPrefixedInt(fileText, NUMBER_LINE_PREFIX)
JvmPlatform -> getPlatformExpectedNumber(fileText, NUMBER_JAVA_LINE_PREFIX)
JsPlatform -> getPlatformExpectedNumber(fileText, NUMBER_JS_LINE_PREFIX)
else -> throw IllegalArgumentException(UNSUPPORTED_PLATFORM_MESSAGE)
}
}
public fun isNothingElseExpected(fileText: String): Boolean {
return !InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, NOTHING_ELSE_PREFIX).isEmpty()
}
public fun getInvocationCount(fileText: String): Int? {
return InTextDirectivesUtils.getPrefixedInt(fileText, INVOCATION_COUNT_PREFIX)
}
public fun getAutocompleteSetting(fileText: String): Boolean? {
return InTextDirectivesUtils.getPrefixedBoolean(fileText, AUTOCOMPLETE_SETTING_PREFIX)
}
public fun isWithOrder(fileText: String): Boolean {
return !InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, WITH_ORDER_PREFIX).isEmpty()
}
public fun assertDirectivesValid(fileText: String) {
InTextDirectivesUtils.assertHasUnknownPrefixes(fileText, KNOWN_PREFIXES)
}
public fun assertContainsRenderedItems(expected: Array<CompletionProposal>, items: Array<LookupElement>, checkOrder: Boolean, nothingElse: Boolean) {
val itemsInformation = getItemsInformation(items)
val allItemsString = listToString(itemsInformation)
val leftItems = if (nothingElse) LinkedHashSet(itemsInformation) else null
var indexOfPrevious = Integer.MIN_VALUE
for (expectedProposal in expected) {
var isFound = false
for (index in itemsInformation.indices) {
val proposal = itemsInformation.get(index)
if (proposal.matches(expectedProposal)) {
isFound = true
Assert.assertTrue("Invalid order of existent elements in $allItemsString",
!checkOrder || index > indexOfPrevious)
indexOfPrevious = index
leftItems?.remove(proposal)
break
}
}
if (!isFound) {
if (allItemsString.isEmpty()) {
Assert.fail("Completion is empty but $expectedProposal is expected")
}
else {
Assert.fail("Expected $expectedProposal not found in:\n$allItemsString")
}
}
}
if (leftItems != null && !leftItems.isEmpty()) {
Assert.fail("No items not mentioned in EXIST directives expected but some found:\n" + listToString(leftItems))
}
}
private fun getPlatformExpectedNumber(fileText: String, platformNumberPrefix: String): Int? {
val prefixedInt = InTextDirectivesUtils.getPrefixedInt(fileText, platformNumberPrefix)
if (prefixedInt != null) {
Assert.assertNull("There shouldn't be $NUMBER_LINE_PREFIX and $platformNumberPrefix prefixes set in same time",
InTextDirectivesUtils.getPrefixedInt(fileText, NUMBER_LINE_PREFIX))
return prefixedInt
}
return InTextDirectivesUtils.getPrefixedInt(fileText, NUMBER_LINE_PREFIX)
}
public fun assertNotContainsRenderedItems(unexpected: Array<CompletionProposal>, items: Array<LookupElement>) {
val itemsInformation = getItemsInformation(items)
val allItemsString = listToString(itemsInformation)
for (unexpectedProposal in unexpected) {
for (proposal in itemsInformation) {
Assert.assertFalse("Unexpected '$unexpectedProposal' presented in $allItemsString",
proposal.matches(unexpectedProposal))
}
}
}
public fun getItemsInformation(items: Array<LookupElement>): List<CompletionProposal> {
val presentation = LookupElementPresentation()
val result = ArrayList<CompletionProposal>(items.size())
for (item in items) {
item.renderElement(presentation)
val map = HashMap<String, String?>()
map.put(CompletionProposal.LOOKUP_STRING, item.lookupString)
map.put(CompletionProposal.ALL_LOOKUP_STRINGS, item.allLookupStrings.sort().joinToString())
if (presentation.itemText != null) {
map.put(CompletionProposal.PRESENTATION_ITEM_TEXT, presentation.itemText)
map.put(CompletionProposal.PRESENTATION_TEXT_ATTRIBUTES, textAttributes(presentation))
}
if (presentation.typeText != null) {
map.put(CompletionProposal.PRESENTATION_TYPE_TEXT, presentation.typeText)
}
if (presentation.tailText != null) {
map.put(CompletionProposal.PRESENTATION_TAIL_TEXT, presentation.tailText)
}
result.add(ExpectedCompletionUtils.CompletionProposal(map))
}
return result
}
private fun textAttributes(presentation: LookupElementPresentation): String {
return StringBuilder {
if (presentation.isItemTextBold) {
append("bold")
}
if (presentation.isItemTextUnderlined) {
if (length() > 0) append(" ")
append("underlined")
}
val foreground = presentation.itemTextForeground
if (foreground != JBColor.foreground()) {
assert(foreground == LookupCellRenderer.getGrayedForeground(false))
if (length() > 0) append(" ")
append("grayed")
}
if (presentation.isStrikeout) {
if (length() > 0) append(" ")
append("strikeout")
}
}.toString()
}
public fun listToString(items: Collection<CompletionProposal>): String = items.joinToString("\n")
}
@@ -1918,6 +1918,69 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/super")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Super extends AbstractJSBasicCompletionTest {
public void testAllFilesPresentInSuper() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/basic/common/super"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("QualifierType1.kt")
public void testQualifierType1() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType1.kt");
doTest(fileName);
}
@TestMetadata("QualifierType2.kt")
public void testQualifierType2() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType2.kt");
doTest(fileName);
}
@TestMetadata("QualifierType3.kt")
public void testQualifierType3() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType3.kt");
doTest(fileName);
}
@TestMetadata("QualifierType4.kt")
public void testQualifierType4() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType4.kt");
doTest(fileName);
}
@TestMetadata("QualifierType5.kt")
public void testQualifierType5() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType5.kt");
doTest(fileName);
}
@TestMetadata("QualifierType6.kt")
public void testQualifierType6() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType6.kt");
doTest(fileName);
}
@TestMetadata("QualifierTypeAny.kt")
public void testQualifierTypeAny() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierTypeAny.kt");
doTest(fileName);
}
@TestMetadata("QualifierTypeAnyInInterface.kt")
public void testQualifierTypeAnyInInterface() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierTypeAnyInInterface.kt");
doTest(fileName);
}
@TestMetadata("QualifierTypeGeneric.kt")
public void testQualifierTypeGeneric() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierTypeGeneric.kt");
doTest(fileName);
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/typeArgsOrNot")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -1918,6 +1918,69 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/super")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Super extends AbstractJvmBasicCompletionTest {
public void testAllFilesPresentInSuper() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/basic/common/super"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("QualifierType1.kt")
public void testQualifierType1() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType1.kt");
doTest(fileName);
}
@TestMetadata("QualifierType2.kt")
public void testQualifierType2() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType2.kt");
doTest(fileName);
}
@TestMetadata("QualifierType3.kt")
public void testQualifierType3() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType3.kt");
doTest(fileName);
}
@TestMetadata("QualifierType4.kt")
public void testQualifierType4() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType4.kt");
doTest(fileName);
}
@TestMetadata("QualifierType5.kt")
public void testQualifierType5() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType5.kt");
doTest(fileName);
}
@TestMetadata("QualifierType6.kt")
public void testQualifierType6() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierType6.kt");
doTest(fileName);
}
@TestMetadata("QualifierTypeAny.kt")
public void testQualifierTypeAny() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierTypeAny.kt");
doTest(fileName);
}
@TestMetadata("QualifierTypeAnyInInterface.kt")
public void testQualifierTypeAnyInInterface() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierTypeAnyInInterface.kt");
doTest(fileName);
}
@TestMetadata("QualifierTypeGeneric.kt")
public void testQualifierTypeGeneric() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/super/QualifierTypeGeneric.kt");
doTest(fileName);
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/typeArgsOrNot")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)