/* * 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.resolve; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiQualifiedNamedElement; import com.intellij.psi.util.PsiTreeUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.builtins.DefaultBuiltIns; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; import org.jetbrains.kotlin.descriptors.*; import org.jetbrains.kotlin.diagnostics.Diagnostic; import org.jetbrains.kotlin.diagnostics.DiagnosticUtils; import org.jetbrains.kotlin.diagnostics.Errors; import org.jetbrains.kotlin.name.FqName; import org.jetbrains.kotlin.name.Name; import org.jetbrains.kotlin.psi.*; import org.jetbrains.kotlin.renderer.DescriptorRenderer; import org.jetbrains.kotlin.resolve.lazy.JvmResolveUtil; import org.jetbrains.kotlin.types.ErrorUtils; import org.jetbrains.kotlin.types.KotlinType; import org.jetbrains.kotlin.types.TypeConstructor; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.jetbrains.kotlin.resolve.BindingContext.AMBIGUOUS_REFERENCE_TARGET; import static org.jetbrains.kotlin.resolve.BindingContext.REFERENCE_TARGET; import static org.junit.Assert.*; public abstract class ExpectedResolveData { protected static final String STANDARD_PREFIX = "kotlin::"; private static class Position { private final PsiElement element; private Position(KtFile file, int offset) { this.element = file.findElementAt(offset); } public PsiElement getElement() { return element; } @Override public String toString() { return DiagnosticUtils.atLocation(element); } } private final Map<String, Position> declarationToPosition = Maps.newHashMap(); private final Map<Position, String> positionToReference = Maps.newHashMap(); private final Map<Position, String> positionToType = Maps.newHashMap(); private final Map<String, DeclarationDescriptor> nameToDescriptor; private final Map<String, PsiElement> nameToPsiElement; public ExpectedResolveData(Map<String, DeclarationDescriptor> nameToDescriptor, Map<String, PsiElement> nameToPsiElement) { this.nameToDescriptor = nameToDescriptor; this.nameToPsiElement = nameToPsiElement; } public final KtFile createFileFromMarkedUpText(String fileName, String text) { Map<String, Integer> declarationToIntPosition = Maps.newHashMap(); Map<Integer, String> intPositionToReference = Maps.newHashMap(); Map<Integer, String> intPositionToType = Maps.newHashMap(); Pattern pattern = Pattern.compile("(~[^~]+~)|(`[^`]+`)"); while (true) { Matcher matcher = pattern.matcher(text); if (!matcher.find()) break; String group = matcher.group(); String name = group.substring(1, group.length() - 1); int start = matcher.start(); if (group.startsWith("~")) { if (declarationToIntPosition.put(name, start) != null) { throw new IllegalArgumentException("Redeclaration: " + name); } } else if (group.startsWith("`")) { if (name.startsWith(":")) { intPositionToType.put(start - 1, name.substring(1)); } else { intPositionToReference.put(start, name); } } else { throw new IllegalStateException(); } text = text.substring(0, start) + text.substring(matcher.end()); } KtFile ktFile = createKtFile(fileName, text); for (Map.Entry<Integer, String> entry : intPositionToType.entrySet()) { positionToType.put(new Position(ktFile, entry.getKey()), entry.getValue()); } for (Map.Entry<String, Integer> entry : declarationToIntPosition.entrySet()) { declarationToPosition.put(entry.getKey(), new Position(ktFile, entry.getValue())); } for (Map.Entry<Integer, String> entry : intPositionToReference.entrySet()) { positionToReference.put(new Position(ktFile, entry.getKey()), entry.getValue()); } return ktFile; } protected abstract KtFile createKtFile(String fileName, String text); protected static BindingContext analyze(List<KtFile> files, KotlinCoreEnvironment environment) { if (files.isEmpty()) { System.err.println("Suspicious: no files"); return BindingContext.EMPTY; } return JvmResolveUtil.analyze(files, environment).getBindingContext(); } public final void checkResult(BindingContext bindingContext) { Set<PsiElement> unresolvedReferences = Sets.newHashSet(); for (Diagnostic diagnostic : bindingContext.getDiagnostics()) { if (Errors.UNRESOLVED_REFERENCE_DIAGNOSTICS.contains(diagnostic.getFactory())) { unresolvedReferences.add(diagnostic.getPsiElement()); } } Map<String, PsiElement> nameToDeclaration = Maps.newHashMap(); Map<PsiElement, String> declarationToName = Maps.newHashMap(); for (Map.Entry<String, Position> entry : declarationToPosition.entrySet()) { String name = entry.getKey(); Position position = entry.getValue(); PsiElement element = position.getElement(); PsiElement ancestorOfType; if (name.equals("file")) { ancestorOfType = element.getContainingFile(); } else { ancestorOfType = getAncestorOfType(KtDeclaration.class, element); if (ancestorOfType == null) { KtPackageDirective directive = getAncestorOfType(KtPackageDirective.class, element); assert directive != null : "Not a declaration: " + name; ancestorOfType = element; } } nameToDeclaration.put(name, ancestorOfType); declarationToName.put(ancestorOfType, name); } for (Map.Entry<Position, String> entry : positionToReference.entrySet()) { Position position = entry.getKey(); String name = entry.getValue(); PsiElement element = position.getElement(); KtReferenceExpression referenceExpression = PsiTreeUtil.getParentOfType(element, KtReferenceExpression.class); DeclarationDescriptor referenceTarget = bindingContext.get(REFERENCE_TARGET, referenceExpression); if ("!".equals(name)) { assertTrue( "Must have been unresolved: " + renderReferenceInContext(referenceExpression) + " but was resolved to " + renderNullableDescriptor(referenceTarget), unresolvedReferences.contains(referenceExpression)); assertTrue( String.format("Reference =%s= has a reference target =%s= but expected to be unresolved", renderReferenceInContext(referenceExpression), renderNullableDescriptor(referenceTarget)), referenceTarget == null); continue; } if ("!!".equals(name)) { assertTrue( "Must have been resolved to multiple descriptors: " + renderReferenceInContext(referenceExpression) + " but was resolved to " + renderNullableDescriptor(referenceTarget), bindingContext.get(AMBIGUOUS_REFERENCE_TARGET, referenceExpression) != null); continue; } else if ("!null".equals(name)) { assertTrue( "Must have been resolved to null: " + renderReferenceInContext(referenceExpression) + " but was resolved to " + renderNullableDescriptor(referenceTarget), referenceTarget == null ); continue; } else if ("!error".equals(name)) { assertTrue( "Must have been resolved to error: " + renderReferenceInContext(referenceExpression) + " but was resolved to " + renderNullableDescriptor(referenceTarget), ErrorUtils.isError(referenceTarget) ); continue; } PsiElement expected = nameToDeclaration.get(name); if (expected == null) { expected = nameToPsiElement.get(name); } KtReferenceExpression reference = getAncestorOfType(KtReferenceExpression.class, element); if (expected == null && name.startsWith(STANDARD_PREFIX)) { DeclarationDescriptor expectedDescriptor = nameToDescriptor.get(name); KtTypeReference typeReference = getAncestorOfType(KtTypeReference.class, element); if (expectedDescriptor != null) { DeclarationDescriptor actual = bindingContext.get(REFERENCE_TARGET, reference); assertDescriptorsEqual("Expected: " + name, expectedDescriptor.getOriginal(), actual == null ? null : actual.getOriginal()); continue; } KotlinType actualType = bindingContext.get(BindingContext.TYPE, typeReference); assertNotNull("Type " + name + " not resolved for reference " + name, actualType); ClassifierDescriptor expectedClass = getBuiltinClass(name.substring(STANDARD_PREFIX.length())); assertTypeConstructorEquals("Type resolution mismatch: ", expectedClass.getTypeConstructor(), actualType.getConstructor()); continue; } assert expected != null : "No declaration for " + name; if (referenceTarget instanceof PackageViewDescriptor) { KtPackageDirective expectedDirective = PsiTreeUtil.getParentOfType(expected, KtPackageDirective.class); FqName expectedFqName; if (expectedDirective != null) { expectedFqName = expectedDirective.getFqName(); } else if (expected instanceof PsiQualifiedNamedElement) { String qualifiedName = ((PsiQualifiedNamedElement) expected).getQualifiedName(); assert qualifiedName != null : "No qualified name for " + name; expectedFqName = new FqName(qualifiedName); } else { throw new IllegalStateException(expected.getClass().getName() + " name=" + name); } assertEquals(expectedFqName, ((PackageViewDescriptor) referenceTarget).getFqName()); continue; } PsiElement actual = referenceTarget == null ? bindingContext.get(BindingContext.LABEL_TARGET, referenceExpression) : DescriptorToSourceUtils.descriptorToDeclaration(referenceTarget); if (actual instanceof KtSimpleNameExpression) { actual = ((KtSimpleNameExpression)actual).getIdentifier(); } String actualName = null; if (actual != null) { actualName = declarationToName.get(actual); if (actualName == null) { actualName = actual.toString(); } } assertNotNull(element.getText(), reference); assertEquals( "Reference `" + name + "`" + renderReferenceInContext(reference) + " is resolved into " + actualName + ".", expected, actual); } for (Map.Entry<Position, String> entry : positionToType.entrySet()) { Position position = entry.getKey(); String typeName = entry.getValue(); PsiElement element = position.getElement(); KtExpression expression = getAncestorOfType(KtExpression.class, element); KotlinType expressionType = bindingContext.getType(expression); TypeConstructor expectedTypeConstructor; if (typeName.startsWith(STANDARD_PREFIX)) { String name = typeName.substring(STANDARD_PREFIX.length()); ClassifierDescriptor expectedClass = getBuiltinClass(name); expectedTypeConstructor = expectedClass.getTypeConstructor(); } else { Position declarationPosition = declarationToPosition.get(typeName); assertNotNull("Undeclared: " + typeName, declarationPosition); PsiElement declElement = declarationPosition.getElement(); assertNotNull(declarationPosition); KtDeclaration declaration = getAncestorOfType(KtDeclaration.class, declElement); assertNotNull(declaration); if (declaration instanceof KtClass) { ClassDescriptor classDescriptor = bindingContext.get(BindingContext.CLASS, declaration); expectedTypeConstructor = classDescriptor.getTypeConstructor(); } else if (declaration instanceof KtTypeParameter) { TypeParameterDescriptor typeParameterDescriptor = bindingContext.get(BindingContext.TYPE_PARAMETER, (KtTypeParameter) declaration); expectedTypeConstructor = typeParameterDescriptor.getTypeConstructor(); } else { fail("Unsupported declaration: " + declaration); return; } } assertNotNull(expression.getText() + " type is null", expressionType); assertTypeConstructorEquals("At " + position + ": ", expectedTypeConstructor, expressionType.getConstructor()); } } private static void assertTypeConstructorEquals(String message, TypeConstructor expected, TypeConstructor actual) { assertDescriptorsEqual(message, expected.getDeclarationDescriptor(), actual.getDeclarationDescriptor()); } private static void assertDescriptorsEqual(String message, DeclarationDescriptor expected, DeclarationDescriptor actual) { if (DescriptorEquivalenceForOverrides.INSTANCE.areEquivalent(expected, actual)) { return; } String formatted = ""; if (message != null) { formatted = message + " "; } fail(formatted + "expected same:<" + expected + "> was not:<" + actual + ">"); } @NotNull public static ClassifierDescriptor getBuiltinClass(String nameOrFqName) { ClassifierDescriptor expectedClass; if (nameOrFqName.indexOf('.') >= 0) { expectedClass = DefaultBuiltIns.getInstance().getBuiltInClassByFqNameNullable(FqName.fromSegments(Arrays.asList(nameOrFqName.split("\\.")))); } else { expectedClass = DefaultBuiltIns.getInstance().getBuiltInClassByNameNullable(Name.identifier(nameOrFqName)); } assertNotNull("Expected class not found: " + nameOrFqName, expectedClass); return expectedClass; } private static String renderReferenceInContext(KtReferenceExpression referenceExpression) { KtExpression statement = referenceExpression; while (true) { PsiElement parent = statement.getParent(); if (!(parent instanceof KtExpression)) break; if (parent instanceof KtBlockExpression) break; statement = (KtExpression) parent; } KtDeclaration declaration = PsiTreeUtil.getParentOfType(referenceExpression, KtDeclaration.class); return referenceExpression.getText() + " at " + DiagnosticUtils.atLocation(referenceExpression) + " in " + statement.getText() + (declaration == null ? "" : " in " + declaration.getText()); } private static <T> T getAncestorOfType(Class<T> type, PsiElement element) { while (element != null && !type.isInstance(element)) { element = element.getParent(); } @SuppressWarnings({"unchecked", "UnnecessaryLocalVariable"}) T result = (T) element; return result; } @NotNull private static String renderNullableDescriptor(@Nullable DeclarationDescriptor d) { return d == null ? "<null>" : DescriptorRenderer.FQ_NAMES_IN_TYPES.render(d); } }