/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.lang.psi.impl.expressions;
import com.google.common.collect.Lists;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiQualifiedNamedElement;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiMatcherImpl;
import com.intellij.psi.util.PsiMatchers;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import gw.internal.gosu.parser.expressions.Identifier;
import gw.lang.parser.IParseIssue;
import gw.lang.parser.IParsedElement;
import gw.lang.parser.ITypeUsesMap;
import gw.lang.parser.exceptions.ParseResultsException;
import gw.lang.parser.expressions.INotAWordExpression;
import gw.lang.parser.expressions.ITypeLiteralExpression;
import gw.lang.parser.statements.IClassFileStatement;
import gw.lang.reflect.IBlockType;
import gw.lang.reflect.IType;
import gw.lang.reflect.ITypeVariableType;
import gw.lang.reflect.TypeSystem;
import gw.lang.reflect.gs.IGosuClass;
import gw.lang.reflect.module.IModule;
import gw.plugin.ij.lang.parser.GosuCompositeElement;
import gw.plugin.ij.lang.parser.GosuElementTypes;
import gw.plugin.ij.lang.psi.IGosuFile;
import gw.plugin.ij.lang.psi.api.statements.IGosuUsesStatement;
import gw.plugin.ij.lang.psi.api.types.IGosuCodeReferenceElement;
import gw.plugin.ij.lang.psi.api.types.IGosuTypeElement;
import gw.plugin.ij.lang.psi.api.types.IGosuTypeParameterList;
import gw.plugin.ij.lang.psi.impl.AbstractGosuClassFileImpl;
import gw.plugin.ij.lang.psi.impl.GosuElementVisitor;
import gw.plugin.ij.lang.psi.impl.GosuFragmentFileImpl;
import gw.plugin.ij.lang.psi.impl.resolvers.PsiTypeResolver;
import gw.plugin.ij.lang.psi.impl.statements.GosuUsesStatementListImpl;
import gw.plugin.ij.lang.psi.impl.types.GosuTypeVariableImpl;
import gw.plugin.ij.lang.psi.util.ElementTypeMatcher;
import gw.plugin.ij.lang.psi.util.GosuPsiParseUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
public class GosuTypeLiteralImpl extends GosuReferenceExpressionImpl<ITypeLiteralExpression> implements IGosuCodeReferenceElement, IGosuTypeElement {
public GosuTypeLiteralImpl(GosuCompositeElement node) {
super(node);
}
@Nullable
public PsiElement getReferenceNameElement() {
return findLastChildByType(GosuElementTypes.TT_IDENTIFIER);
}
@Override
public IGosuCodeReferenceElement getQualifier() {
final PsiElement qualifier = getFirstChild();
return qualifier instanceof IGosuCodeReferenceElement ? (IGosuCodeReferenceElement) qualifier : null;
}
@Override
public void setQualifier(IGosuCodeReferenceElement newQualifier) {
throw new UnsupportedOperationException("Men at work");
}
@Nullable
protected IType getTypeReferenced() {
IParsedElement pe = getParsedElementImpl();
if (pe != null && !(pe instanceof ITypeLiteralExpression)) {
if( getContainingFile() instanceof AbstractGosuClassFileImpl ) {
// This can happen for example when a package is deleted. The annotator will try to resolve the reference which may
// have been parsed separate from the psi. In this case we must reparse here. Not the getParsedElementImpl() normally
// handles synchronization issues such as this by checking that the psi text is the same as the parsed text, but in
// this case the source is the same, only the semantics are different b/c of the package deletion.
((AbstractGosuClassFileImpl)getContainingFile()).reparseGosuFromPsi();
pe = getParsedElementImpl();
}
}
if (pe == null || pe instanceof INotAWordExpression) {
return null;
} else if( pe instanceof ITypeLiteralExpression ) {
return ((ITypeLiteralExpression) pe).getType().getType();
} else if (pe instanceof Identifier) {
return ((Identifier) pe).getType();
} else {
return null;
}
}
@Override
public PsiElement resolve() {
IType type = getTypeReferenced();
if( type != null ) {
if (type instanceof ITypeVariableType) {
return PsiTypeResolver.resolveTypeVariable( (ITypeVariableType)type, this );
}
else if( type instanceof IBlockType ) {
return null;
}
else {
return PsiTypeResolver.resolveType(type, this);
}
}
return null;
}
@NotNull
public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
final PsiElement elt = bindType(this, element, (AbstractGosuClassFileImpl) getContainingFile());
return elt != null ? elt : this;
}
public static PsiElement bindType(@NotNull PsiExpression originalElement, @NotNull PsiElement newElement, @NotNull AbstractGosuClassFileImpl gosuFile) {
String originalTypeName = originalElement.getText();
//TODO-dp sometimes this throws a class cast exception because the parsed element is an identifier rather than a type literal
try {
if (originalElement instanceof GosuTypeLiteralImpl) {
IType typeReferenced = ((GosuTypeLiteralImpl) originalElement).getTypeReferenced();
originalTypeName = typeReferenced != null ? typeReferenced.getName() : originalElement.getText();
}
} catch (ClassCastException e) {
// ignore
}
originalTypeName = removeParameterization(originalTypeName);
final String qname;
if (newElement instanceof GosuTypeVariableImpl) {
qname = ((GosuTypeVariableImpl) newElement).getName();
} else if (newElement instanceof PsiQualifiedNamedElement) {
qname = ((PsiQualifiedNamedElement) newElement).getQualifiedName();
} else if (newElement instanceof PsiClass) {
qname = ((PsiClass) newElement).getQualifiedName();
} else {
throw new IncorrectOperationException("Unsupported element in GosuTypeLiteralImpl.bindToElement");
}
PsiElement newTypeLiteral;
if (originalElement.getText().contains(".")) {
newTypeLiteral = GosuPsiParseUtil.parseExpression(qname, originalElement.getManager());
originalElement.replace(newTypeLiteral);
} else {
newTypeLiteral = GosuPsiParseUtil.parseRelativeTypeLiteral(qname, gosuFile);
PsiElement id1 = originalElement.getFirstChild();
PsiElement id2 = newTypeLiteral.getFirstChild();
if (id1 != null && id2 != null && !id1.getText().equals(id2.getText())) {
id1.replace(id2);
gosuFile.reparseGosuFromPsi();
}
}
if (shouldAddImport(gosuFile, originalElement, newElement, originalTypeName)) {
gosuFile.addImport(qname);
maybeRemoveStaleImport(originalTypeName, gosuFile);
gosuFile.reparseGosuFromPsi();
}
return newTypeLiteral;
}
@NotNull
private static String removeParameterization(@NotNull String originalTypeName) {
if (originalTypeName.contains("<")) {
originalTypeName = originalTypeName.substring(0, originalTypeName.indexOf("<")).trim();
}
return originalTypeName;
}
private static void maybeRemoveStaleImport(String originalTypeName, AbstractGosuClassFileImpl gosuFile) {
final PsiElement staleUsesStmt = new PsiMatcherImpl(gosuFile)
.descendant(new ElementTypeMatcher(GosuElementTypes.ELEM_TYPE_UsesStatementList))
.descendant(PsiMatchers.hasText(originalTypeName))
.dot(new ElementTypeMatcher(GosuElementTypes.ELEM_TYPE_TypeLiteral))
.ancestor(new ElementTypeMatcher(GosuElementTypes.ELEM_TYPE_UsesStatement))
.getElement();
if (staleUsesStmt != null) {
staleUsesStmt.delete();
}
}
private static boolean shouldAddImport(@NotNull AbstractGosuClassFileImpl gosuFile, @NotNull PsiElement originalElement, @NotNull PsiElement newElement, String qname) {
// no need to add imports for inner clasees
PsiFile psiFile = PsiTreeUtil.getParentOfType(newElement, PsiFile.class);
if (psiFile == gosuFile) {
return false;
}
if( gosuFile instanceof GosuFragmentFileImpl ) {
// Gosu framents (from the debugger expressio) add import implicitly
((GosuFragmentFileImpl)gosuFile).addImportsFromString( ((PsiClass)newElement).getQualifiedName() );
return false;
}
GosuUsesStatementListImpl usesStatementList = PsiTreeUtil.findChildOfType(gosuFile, GosuUsesStatementListImpl.class);
if (usesStatementList != null) {
for (IGosuUsesStatement usesStatement : usesStatementList.getUsesStatements()) {
if (usesStatement.getText().equals("uses " + qname)) {
return false;
}
}
}
gosuFile.reparseGosuFromPsi();
IClassFileStatement classFileStatement = gosuFile.getParseData().getClassFileStatement();
if (newElement instanceof PsiClass) {
String qualifiedName = ((PsiClass) newElement).getQualifiedName();
if (qualifiedName != null) {
int iLastDot = qualifiedName.lastIndexOf('.');
String pkgName = iLastDot > 0 ? qualifiedName.substring(0, iLastDot) : qualifiedName;
if (pkgName.equals(gosuFile.getPackageName())) {
return false;
} else if (((PsiClass) newElement).getName().equals(originalElement.getText())) {
String newName = originalElement.getText();
if (!newName.contains(".")) {
// For the case where the type name is a relative name and is a built-in type e.g., String
if (isBuiltInType(newName, classFileStatement)) {
return false;
}
// For the case where a class refs another class in the same package by relative name and the refed class is moved outside the package
if (classFileStatement != null) {
IGosuClass gsClass = classFileStatement.getGosuClass();
return !typeUsesMapHasNewTypeOrIsInPackage(gosuFile, gsClass, qualifiedName);
}
}
}
}
}
String rootType = originalElement.getText();
rootType = removeParameterization(rootType);
if (classFileStatement != null) {
IGosuClass gsClass = classFileStatement.getGosuClass();
ParseResultsException parseResultsException = gsClass.getParseResultsException();
if (parseResultsException != null) {
for (IParseIssue error : parseResultsException.getParseExceptions()) {
if (error.getSource().toString().equals(rootType)) {
return true;
}
}
}
}
return false;
}
private static boolean isBuiltInType(String relativeName, @NotNull IClassFileStatement classFileStatement) {
IModule module = classFileStatement.getGosuClass().getTypeLoader().getModule();
TypeSystem.pushModule(module);
try {
IType type = TypeSystem.getByFullNameIfValid(relativeName);
return type != null;
} finally {
TypeSystem.popModule(module);
}
}
private static boolean typeUsesMapHasNewTypeOrIsInPackage(IGosuFile gosuFile, @NotNull IGosuClass gsClass, @NotNull String qualifiedName) {
ITypeUsesMap usesMap = gsClass.getParser().getTypeUsesMap();
int iLastDot = qualifiedName.lastIndexOf('.');
String pkgName = iLastDot > 0 ? qualifiedName.substring(0, iLastDot) : qualifiedName;
for (String typeUses : (Set<String>) usesMap.getTypeUses()) {
if (typeUses.equals(qualifiedName) || typeUses.equals(pkgName + '.')) {
return true;
}
}
return false;
}
@Override
public PsiType[] getTypeArguments() {
final IGosuTypeParameterList typeParams = getTypeParameterList();
if (typeParams != null) {
final List<PsiType> types = Lists.newArrayList();
for (PsiElement typeParam : typeParams.getChildren()) {
if (typeParam instanceof GosuTypeLiteralImpl) {
types.add(((GosuTypeLiteralImpl) typeParam).getType());
}
}
return types.toArray(new PsiType[types.size()]);
}
return PsiType.EMPTY_ARRAY;
}
@Nullable
@Override
public IGosuTypeParameterList getTypeParameterList() {
return findChildByClass(IGosuTypeParameterList.class);
}
@Override
public void accept(@NotNull PsiElementVisitor visitor) {
if( visitor instanceof GosuElementVisitor) {
((GosuElementVisitor)visitor).visitTypeLiteral(this);
}
else {
visitor.visitElement( this );
}
}
}