/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.lang.parser;
import com.google.common.base.Objects;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.AnnotationSession;
import com.intellij.lang.annotation.Annotator;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.ElementManipulators;
import com.intellij.psi.LiteralTextEscaper;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.impl.source.tree.PsiCommentImpl;
import com.intellij.psi.util.PsiTreeUtil;
import gw.internal.gosu.parser.expressions.ArgumentListClause;
import gw.internal.gosu.parser.expressions.BeanMethodCallExpression;
import gw.internal.gosu.parser.expressions.CompoundTypeLiteral;
import gw.internal.gosu.parser.expressions.MethodCallExpression;
import gw.internal.gosu.parser.expressions.TypeAsExpression;
import gw.internal.gosu.parser.statements.FunctionStatement;
import gw.lang.parser.GosuParserFactory;
import gw.lang.parser.IExpression;
import gw.lang.parser.IHasType;
import gw.lang.parser.IParseIssue;
import gw.lang.parser.IParseTree;
import gw.lang.parser.IParsedElement;
import gw.lang.parser.TypelessScriptPartId;
import gw.lang.parser.exceptions.ICoercionIssue;
import gw.lang.parser.exceptions.ParseResultsException;
import gw.lang.parser.expressions.ITypeLiteralExpression;
import gw.lang.parser.resources.Res;
import gw.lang.parser.resources.ResourceKey;
import gw.lang.parser.statements.IArrayAssignmentStatement;
import gw.lang.parser.statements.IAssignmentStatement;
import gw.lang.parser.statements.IBeanMethodCallStatement;
import gw.lang.parser.statements.IClassFileStatement;
import gw.lang.parser.statements.IMapAssignmentStatement;
import gw.lang.parser.statements.IMemberAssignmentStatement;
import gw.lang.parser.statements.IMethodCallStatement;
import gw.lang.parser.statements.INoOpStatement;
import gw.lang.parser.statements.INotAStatement;
import gw.lang.parser.statements.IReturnStatement;
import gw.lang.parser.statements.IStatementList;
import gw.lang.reflect.IErrorType;
import gw.lang.reflect.IFunctionType;
import gw.lang.reflect.IType;
import gw.lang.reflect.TypeSystem;
import gw.lang.reflect.gs.IGosuClass;
import gw.plugin.ij.filetypes.GosuFileTypes;
import gw.plugin.ij.intentions.ChangeMethodTypeFix;
import gw.plugin.ij.intentions.CreateClassFix;
import gw.plugin.ij.intentions.CreateGosuClassFromNewFix;
import gw.plugin.ij.intentions.CreateJavaClassFromNewFix;
import gw.plugin.ij.intentions.CreateMethodFix;
import gw.plugin.ij.intentions.GosuAddMissingOverrideFix;
import gw.plugin.ij.intentions.GosuAddMissingUsesFix;
import gw.plugin.ij.intentions.GosuAddTypeCastFix;
import gw.plugin.ij.intentions.GosuChangeTypeCastFix;
import gw.plugin.ij.intentions.GosuCreateClassKind;
import gw.plugin.ij.intentions.GosuImplementMethodsFix;
import gw.plugin.ij.intentions.GosuImportReferenceAnalyzer;
import gw.plugin.ij.intentions.HandleInterfaceRedundantFix;
import gw.plugin.ij.intentions.HandleUnnecessaryCoercionFix;
import gw.plugin.ij.intentions.HandleVarArgFix;
import gw.plugin.ij.intentions.IQuickFixProvider;
import gw.plugin.ij.intentions.ObsoleteConstructorFix;
import gw.plugin.ij.intentions.OrganizeImports;
import gw.plugin.ij.intentions.QuickFixProviderExtensionBean;
import gw.plugin.ij.intentions.RemoveUnnecessaryImports;
import gw.plugin.ij.lang.psi.IGosuPsiElement;
import gw.plugin.ij.lang.psi.api.statements.IGosuUsesStatement;
import gw.plugin.ij.lang.psi.api.statements.IGosuUsesStatementList;
import gw.plugin.ij.lang.psi.api.statements.typedef.IGosuExtendsClause;
import gw.plugin.ij.lang.psi.api.statements.typedef.IGosuImplementsClause;
import gw.plugin.ij.lang.psi.api.types.IGosuCodeReferenceElement;
import gw.plugin.ij.lang.psi.impl.AbstractGosuClassFileImpl;
import gw.plugin.ij.lang.psi.impl.GosuBaseElementImpl;
import gw.plugin.ij.lang.psi.impl.expressions.GosuMethodCallExpressionImpl;
import gw.plugin.ij.lang.psi.impl.expressions.GosuNewExpressionImpl;
import gw.plugin.ij.lang.psi.impl.expressions.GosuParenthesizedExpressionImpl;
import gw.plugin.ij.lang.psi.impl.expressions.GosuTypeAsExpressionImpl;
import gw.plugin.ij.lang.psi.impl.expressions.GosuTypeLiteralImpl;
import gw.plugin.ij.lang.psi.impl.statements.typedef.GosuAnonymousClassDefinitionImpl;
import gw.plugin.ij.lang.psi.impl.statements.typedef.members.GosuMethodBaseImpl;
import gw.plugin.ij.lang.psi.impl.statements.typedef.members.GosuMethodImpl;
import gw.plugin.ij.util.InjectedElementEditor;
import gw.util.GosuStringUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import static com.intellij.codeInspection.ProblemHighlightType.LIKE_UNUSED_SYMBOL;
import static com.intellij.patterns.PlatformPatterns.psiElement;
import static com.intellij.psi.util.ClassUtil.extractPackageName;
import static com.intellij.psi.util.PsiTreeUtil.findChildOfType;
import static gw.plugin.ij.util.GosuBundle.message;
import static gw.plugin.ij.util.ParseTreeUtil.lookupAncestor;
import static java.util.Collections.singleton;
public class GosuParserAnnotator implements Annotator, Condition<VirtualFile> {
private AnnotationSession session;
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
ProgressManager.checkCanceled();
if (session != holder.getCurrentAnnotationSession()) {
session = holder.getCurrentAnnotationSession();
// Reparse the file at every session switch, but only if the file is not open in the current editor
// TODO-dp this may be a LARGE perf bottleneck.
final AbstractGosuClassFileImpl psiFile = ((AbstractGosuClassFileImpl) session.getFile());
if( psiFile.isValid() ) {
if( !psiFile.reparsePsiFromContent() ) {
annotateFile( psiFile, holder );
}
}
}
}
private void annotateFile(AbstractGosuClassFileImpl psiFile, AnnotationHolder holder) {
TypeSystem.pushModule( psiFile.getModule() );
try {
// annotateUnusedImports(psiFile, holder);
final IClassFileStatement classFileStatement = psiFile.getParseData().getClassFileStatement();
if (classFileStatement != null) {
final IGosuClass gsClass = classFileStatement.getGosuClass();
@SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
final ParseResultsException exception = gsClass.getParseResultsException();
if (exception != null) {
// Errors
for (IParseIssue error : exception.getParseExceptions()) {
final Annotation annotation = holder.createErrorAnnotation(getAnnotationRange(psiFile, error), error.getUIMessage());
maybeRegisterFix(error, annotation, psiFile);
}
// Warnings
for (IParseIssue warning : exception.getParseWarnings()) {
final Annotation annotation = holder.createWarningAnnotation(getAnnotationRange(psiFile, warning), warning.getUIMessage());
if (warning.getMessageKey() == Res.MSG_DEPRECATED_MEMBER) {
annotation.setHighlightType(ProblemHighlightType.LIKE_DEPRECATED);
}
maybeRegisterFix(warning, annotation, psiFile);
}
}
}
for (IQuickFixProvider qp : QuickFixProviderExtensionBean.getProviders()) {
qp.collectQuickFixes(psiFile, holder);
}
findUnusedImports(psiFile, holder);
}
finally {
TypeSystem.popModule( psiFile.getModule() );
}
}
private void findUnusedImports(PsiFile file, AnnotationHolder holder) {
final IGosuUsesStatementList usesList = findChildOfType(file, IGosuUsesStatementList.class);
if (usesList == null) {
return;
}
GosuImportReferenceAnalyzer analyzer = new GosuImportReferenceAnalyzer(file);
analyzer.analyze();
Set<String> requiredImports = analyzer.getRequiredImports();
Set<String> requiredPackages = new HashSet<>(requiredImports);
for (String className : requiredImports) {
String pckg = extractPackageName(className);
requiredPackages.add(pckg);
}
List<IGosuUsesStatement> unusedImports = new ArrayList<>();
for (IGosuUsesStatement stmt : usesList.getUsesStatements()) {
GosuTypeLiteralImpl typeLiteral = findChildOfType(stmt, GosuTypeLiteralImpl.class);
if (typeLiteral == null) {
continue;
}
String name = typeLiteral.getText();
boolean wildcardAccess = name.endsWith(".");
ITypeLiteralExpression pe = typeLiteral.getParsedElement();
if (!wildcardAccess && pe != null && pe.getType() != null && pe.getType().getType() instanceof IErrorType) {
continue;
}
if (wildcardAccess) {
if (!requiredPackages.contains(name.substring(0, name.length() - 1))) {
unusedImports.add(stmt);
}
} else {
if (!requiredImports.contains(name)) {
unusedImports.add(stmt);
}
}
}
if (!unusedImports.isEmpty()) {
RemoveUnnecessaryImports removeAll = new RemoveUnnecessaryImports(unusedImports);
for (IGosuUsesStatement stmt : unusedImports) {
Annotation ann = holder.createWarningAnnotation(stmt.getTextRange(), message("annotations.unused.import"));
ann.setHighlightType(LIKE_UNUSED_SYMBOL);
ann.registerFix(new RemoveUnnecessaryImports(singleton(stmt)));
ann.registerFix(removeAll);
ann.registerFix(new OrganizeImports());
}
}
}
private TextRange getAnnotationRange(PsiFile file, IParseIssue issue) {
if (changeReturnTypeFixCanBeApplied(issue)) {
// Make range for the whole return statement, like:
// return new FooClass()
// ^^^^^^^^^^^^^^^^^^^^^
TextRange returnExprRange = findReturnExprRange(file, issue);
if (returnExprRange != null) {
return returnExprRange;
}
}
int start = Objects.firstNonNull(issue.getTokenStart(), 0);
int end = Objects.firstNonNull(issue.getTokenEnd(), 0);
final PsiLanguageInjectionHost host = InjectedLanguageManager.getInstance(file.getProject()).getInjectionHost(file);
if (host != null) {
final LiteralTextEscaper<? extends PsiLanguageInjectionHost> escaper = host.createLiteralTextEscaper();
final TextRange range = ElementManipulators.getValueTextRange(host);
escaper.decode(range, new StringBuilder()); // It's requred to do decode for getOffsetInHost working correctly
final int startOffset = range.getStartOffset();
start = escaper.getOffsetInHost(start, range) - startOffset;
end = escaper.getOffsetInHost(end, range) - startOffset;
}
return new TextRange(start, Math.max(start, end));
}
private TextRange findReturnExprRange(PsiFile psiFile, IParseIssue issue) {
FunctionStatement func = lookupAncestor(issue.getSource(), FunctionStatement.class);
PsiElement target = psiFile.findElementAt(issue.getTokenStart());
GosuMethodImpl method = PsiTreeUtil.getParentOfType(target, GosuMethodImpl.class);
if (func != null && method != null && method.getBody() != null &&
method.getBody().getLastChild() != null) {
PsiElement last = method.getBody().getLastChild().getPrevSibling();
while (last != null && (last instanceof PsiWhiteSpace || last instanceof PsiCommentImpl)) {
last = last.getPrevSibling();
}
if (last != null) {
List<IReturnStatement> returnStmts = new ArrayList<>();
func.getContainedParsedElementsByType(IReturnStatement.class, returnStmts);
if (last != null && !returnStmts.isEmpty()) {
IParseTree location = returnStmts.get(0).getLocation();
if (location != null) {
return TextRange.create(location.getOffset(), last.getTextOffset() + last.getTextLength());
}
}
}
}
return null;
}
private void maybeRegisterFix(IParseIssue issue, Annotation annotation, PsiFile psiFile) {
final ResourceKey key = issue.getMessageKey();
final IParsedElement pe = issue.getSource();
PsiElement target = psiFile.findElementAt(issue.getTokenStart());
if (key == Res.MSG_LIKELY_JAVA_CAST) {
target = getParentOfType(psiFile, target, GosuTypeAsExpressionImpl.class);
if (target != null) {
annotation.registerFix(new GosuChangeTypeCastFix((GosuTypeAsExpressionImpl) target));
}
}
if (key == Res.MSG_OBSOLETE_CTOR_SYNTAX) {
target = getParentOfType(psiFile, target, GosuMethodImpl.class);
if (target != null) {
annotation.registerFix(new ObsoleteConstructorFix((GosuMethodImpl) target));
}
}
if (key == Res.MSG_INVALID_TYPE) {
target = getParentOfType(psiFile, target, GosuTypeLiteralImpl.class);
if (target != null) {
final GosuTypeLiteralImpl typeLiteral = (GosuTypeLiteralImpl) target;
if (typeLiteral.getParent() instanceof GosuNewExpressionImpl) {
annotation.registerFix(new CreateGosuClassFromNewFix((PsiNewExpression) typeLiteral.getParent()));
annotation.registerFix(new CreateJavaClassFromNewFix((PsiNewExpression) typeLiteral.getParent()));
} else {
for (GosuCreateClassKind kind : getClassKinds(typeLiteral)) {
annotation.registerFix(new CreateClassFix(typeLiteral, kind));
}
}
}
}
if (key == Res.MSG_MISSING_OVERRIDE_MODIFIER) {
target = getParentOfType(psiFile, target, GosuMethodImpl.class);
annotation.registerFix(new GosuAddMissingOverrideFix((GosuMethodBaseImpl) target));
}
if (issue instanceof ICoercionIssue) {
PsiElement psiElem = findExpressionAt(psiFile, pe.getLocation().getOffset(), pe.getLocation().getExtent() + 1);
if (psiElem instanceof IGosuPsiElement) {
PsiType type = GosuBaseElementImpl.createType(((ICoercionIssue) issue).getTypeToCoerceTo(), psiElem);
if (type != null) {
annotation.registerFix(new GosuAddTypeCastFix(type, (IGosuPsiElement) psiElem));
}
}
}
if (key == Res.MSG_TYPE_MISMATCH && pe instanceof IExpression) {
PsiElement psiElem = findExpressionAt(psiFile, pe.getLocation().getOffset(), pe.getLocation().getExtent() + 1);
if (psiElem != null && issue.getExpectedType() != null && ((IExpression) pe).getType().isAssignableFrom(issue.getExpectedType())) {
PsiType type = GosuBaseElementImpl.createType(issue.getExpectedType(), target);
if (type != null) {
annotation.registerFix(new GosuAddTypeCastFix(type, (IGosuPsiElement) psiElem));
}
}
}
// Anonymous class
if (key == Res.MSG_UNIMPLEMENTED_METHOD) {
if (target.getParent() instanceof GosuNewExpressionImpl) {
final GosuAnonymousClassDefinitionImpl anonymousClass = PsiTreeUtil.getNextSiblingOfType(target, GosuAnonymousClassDefinitionImpl.class);
if (anonymousClass != null) {
annotation.registerFix(new GosuImplementMethodsFix(anonymousClass));
}
}
}
if (changeReturnTypeFixCanBeApplied(issue)) {
Document document = PsiDocumentManager.getInstance(psiFile.getProject()).getDocument(psiFile);
IType type = inferReturnType(pe, document);
if (type != null && !(type instanceof IErrorType) && !"void".equals(type.getName())) {
annotation.registerFix(new ChangeMethodTypeFix(target, type.getName()));
}
}
if (key == Res.MSG_NO_SUCH_FUNCTION) {
GosuMethodCallExpressionImpl call = PsiTreeUtil.getParentOfType(target, GosuMethodCallExpressionImpl.class);
if (call != null) {
annotation.registerFix(new CreateMethodFix(call,
PsiTreeUtil.getParentOfType(call, PsiClass.class)));
}
}
if (key == Res.MSG_WRONG_NUMBER_OF_ARGS_TO_FUNCTION || key == Res.MSG_TYPE_MISMATCH) {
IFunctionType functionType = null;
IParsedElement p = pe;
if (p.getParent() instanceof ArgumentListClause) {
p = pe.findAncestorParsedElementByType(MethodCallExpression.class);
if (p == null) {
p = pe.findAncestorParsedElementByType(BeanMethodCallExpression.class);
}
}
if (p instanceof MethodCallExpression) {
functionType = ((MethodCallExpression) p).getFunctionType();
} else if (p instanceof BeanMethodCallExpression) {
functionType = ((BeanMethodCallExpression) p).getFunctionType();
}
if (functionType != null) {
IType[] parameterTypes = functionType.getParameterTypes();
int last = parameterTypes.length - 1;
if( last >= 0 && parameterTypes[last].isArray() ) {
annotation.registerFix(new HandleVarArgFix(target.getParent(), p));
}
}
}
if (key == Res.MSG_INTERFACE_REDUNDANT) {
if(pe.getParent() instanceof CompoundTypeLiteral) {
annotation.registerFix(new HandleInterfaceRedundantFix(target.getParent(), pe));
}
}
if (key == Res.MSG_UNNECESSARY_COERCION) {
if(pe.getParent() instanceof TypeAsExpression) {
annotation.registerFix(new HandleUnnecessaryCoercionFix(target.getParent()));
}
}
// Other cases
target = getParentOfType(psiFile, target, IGosuCodeReferenceElement.class);
if (target != null) {
final IGosuCodeReferenceElement referenceElement = (IGosuCodeReferenceElement) target;
if (hasBalancedCarets(target)) {
if (key == Res.MSG_INVALID_TYPE || key == Res.MSG_BAD_IDENTIFIER_NAME) {
Boolean singleLine = psiFile.getUserData(InjectedElementEditor.SINGLE_LINE_EDITOR);
if (singleLine == null || !singleLine) {
annotation.registerFix(new GosuAddMissingUsesFix(referenceElement));
}
} else if (key == Res.MSG_UNIMPLEMENTED_METHOD) {
annotation.registerFix(new GosuImplementMethodsFix(referenceElement));
}
}
}
}
private boolean changeReturnTypeFixCanBeApplied(IParseIssue issue) {
return issue.getMessageKey() == Res.MSG_RETURN_VAL_FROM_VOID_FUNCTION;
}
private IType inferReturnType(IParsedElement pe, Document document) {
if (pe instanceof INoOpStatement) {
IParseTree location = pe.getLocation();
if (location != null) {
String toParse = null;
IParsedElement stmtList = pe.findAncestorParsedElementByType(IStatementList.class);
if (stmtList != null) {
IParseTree stmtLocation = stmtList.getLocation();
if (stmtLocation != null) {
List<IParseTree> children = stmtLocation.getChildren();
ListIterator<IParseTree> it = children.listIterator(children.size());
IParsedElement lastStmt = null;
IParsedElement returnStmt = null;
while (it.hasPrevious()) {
IParseTree prev = it.previous();
IParsedElement child = prev.getParsedElement();
if (child != null) {
if (lastStmt == null) {
lastStmt = child;
} else {
if (child instanceof IReturnStatement) {
returnStmt = child;
break;
}
}
}
}
if (returnStmt != null && lastStmt != null) {
IParseTree returnLoc = returnStmt.getLocation();
IParseTree lastLoc = lastStmt.getLocation();
if (returnLoc != null && lastLoc != null) {
int from = returnLoc.getOffset() + returnLoc.getLength();
int to = lastLoc.getOffset() + lastLoc.getLength();
toParse = document.getText(TextRange.create(from, to));
}
}
}
}
if (toParse == null) {
toParse = location.getTextFromTokens();
}
toParse = toParse.trim();
// int lastSemiitoParse.lastIndexOf(';');
// if () {
//
// }
try {
IExpression exp = GosuParserFactory.createParser(toParse).parseExp(new TypelessScriptPartId("returned literal"));
return exp.getType();
} catch (ParseResultsException e) {
}
}
} else {
return extractFromStatement(pe);
}
return null;
}
private IType extractFromStatement(IParsedElement pe) {
if (pe instanceof IAssignmentStatement) {
return ((IAssignmentStatement) pe).getIdentifier().getType();
} else if (pe instanceof IArrayAssignmentStatement) {
return ((IArrayAssignmentStatement) pe).getArrayAccessExpression().getType();
} else if (pe instanceof IMapAssignmentStatement) {
return ((IMapAssignmentStatement) pe).getMapAccessExpression().getType();
} else if (pe instanceof IMemberAssignmentStatement) {
return ((IMemberAssignmentStatement) pe).getMemberAccess().getType();
} else if (pe instanceof INotAStatement) {
return ((INotAStatement) pe).getExpression().getType();
} else if (pe instanceof IBeanMethodCallStatement) {
return ((IBeanMethodCallStatement) pe).getBeanMethodCall().getType();
} else if (pe instanceof IMethodCallStatement) {
return ((IMethodCallStatement) pe).getMethodCall().getType();
} else if (pe instanceof IHasType) {
return ((IHasType) pe).getType();
}
return null;
}
private List<GosuCreateClassKind> getClassKinds(GosuTypeLiteralImpl typeLiteral) {
final PsiClass psiClass = (PsiClass) getParentOfType(typeLiteral.getContainingFile(), typeLiteral, PsiClass.class);
if (psiElement()
.withParent(IGosuExtendsClause.class)
.accepts(typeLiteral)) {
return psiClass.isInterface() ? GosuCreateClassKind.INTERFACES : GosuCreateClassKind.CLASSES;
}
if (psiElement()
.withParent(IGosuImplementsClause.class)
.accepts(typeLiteral)) {
return GosuCreateClassKind.INTERFACES;
}
return GosuCreateClassKind.ALL;
}
// TODO: use some existing utility method
private PsiElement getParentOfType(PsiFile psiFile, PsiElement target, Class<? extends PsiElement> parentType) {
while (target != null && !parentType.isAssignableFrom(target.getClass()) && target != psiFile) {
target = target.getParent();
}
if (target == null) {
return null;
}
return parentType.isAssignableFrom(target.getClass()) ? target : null;
}
// TODO: use some existing utility method
private PsiElement findExpressionAt(PsiFile psiFile, int offset, int end) {
PsiElement elem = PsiTreeUtil.findElementOfClassAtRange(psiFile, offset, end, PsiExpression.class);
if (elem != null) {
while (elem.getFirstChild() != null && elem.getFirstChild().getTextRange().equals(elem.getTextRange())) {
elem = elem.getFirstChild();
}
while (elem != null && !(elem instanceof PsiExpression)) {
elem = elem.getParent();
}
}
return elem;
}
private PsiElement maybeExpandReference(PsiElement referenceElement) {
final PsiElement parent = referenceElement.getParent();
if (parent instanceof GosuTypeAsExpressionImpl ||
parent instanceof GosuParenthesizedExpressionImpl) {
referenceElement = parent;
}
return referenceElement;
}
private boolean hasBalancedCarets(PsiElement element) {
final String text = element.getText();
return GosuStringUtil.countMatches(text, "<") == GosuStringUtil.countMatches(text, ">");
}
@Override
public boolean value(VirtualFile virtualFile) {
return GosuFileTypes.isGosuFile(virtualFile);
}
}