Implement diagnostics for java source files

This commit is contained in:
Akash Yadav
2022-01-18 18:27:39 +05:30
parent 8aa86707fb
commit 42b195cd7c
25 changed files with 792 additions and 561 deletions
@@ -1,8 +1,6 @@
/************************************************************************************
/*
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -16,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
*/
package com.itsaky.androidide.language.groovy;
@@ -24,8 +22,6 @@ import androidx.annotation.NonNull;
import com.itsaky.androidide.lexers.groovy.GroovyLexer;
import com.itsaky.lsp.api.ILanguageServer;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.SemanticHighlight;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
@@ -33,10 +29,8 @@ import org.antlr.v4.runtime.Token;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.Map;
import java.util.Stack;
import io.github.rosemoe.editor.interfaces.EditorLanguage;
import io.github.rosemoe.editor.struct.BlockLine;
import io.github.rosemoe.editor.text.Content;
import io.github.rosemoe.editor.text.TextAnalyzeResult;
@@ -44,13 +38,7 @@ import io.github.rosemoe.editor.text.TextAnalyzer;
import io.github.rosemoe.editor.widget.EditorColorScheme;
public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnalyzer {
private final EditorLanguage language;
public GroovyAnalyzer (EditorLanguage language) {
this.language = language;
}
@Override
public void analyze(ILanguageServer server, File file, @NonNull Content content, TextAnalyzeResult colors, @NonNull TextAnalyzer.AnalyzeThread.Delegate delegate) throws IOException {
final var stream = CharStreams.fromReader (new StringReader (content.toString ()));
@@ -254,12 +242,4 @@ public class GroovyAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeA
colors.determine(lastLine);
colors.setSuppressSwitch(maxSwitch + 10);
}
@Override
public void setSemanticHighlights(SemanticHighlight highlights) {
}
@Override
public void updateDiagnostics(Map<Integer, Map<Integer, DiagnosticItem>> diagnostics) {
}
}
@@ -42,7 +42,7 @@ public class GroovyLanguage extends BaseLanguage {
private static final Logger LOG = Logger.instance ("GroovyLanguage");
public GroovyLanguage () {
analyzer = new GroovyAnalyzer (this);
analyzer = new GroovyAnalyzer ();
completer = new GroovyAutoComplete ();
}
@@ -17,37 +17,28 @@
package com.itsaky.androidide.language.java;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.lexers.java.JavaLexer;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.lsp.api.ILanguageServer;
import com.itsaky.lsp.models.AnalyzeParams;
import com.itsaky.lsp.models.AnalyzeResult;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.Position;
import com.itsaky.lsp.models.Range;
import com.itsaky.lsp.models.SemanticHighlight;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import java.io.File;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import io.github.rosemoe.editor.langs.AbstractCodeAnalyzer;
import io.github.rosemoe.editor.struct.BlockLine;
import io.github.rosemoe.editor.text.CharPosition;
import io.github.rosemoe.editor.text.Content;
import io.github.rosemoe.editor.text.TextAnalyzeResult;
import io.github.rosemoe.editor.text.TextAnalyzer;
import io.github.rosemoe.editor.widget.EditorColorScheme;
// TODO request language server for semantic highlights instead of waiting for it to send
@@ -58,405 +49,65 @@ import io.github.rosemoe.editor.widget.EditorColorScheme;
*/
public class JavaAnalyzer extends AbstractCodeAnalyzer {
private HighlightRangeHelper helper;
private Map<Integer, Map<Integer, DiagnosticItem>> diagnostics = new HashMap<> ();
private final Map<Integer, List<Range>> stringMap = new HashMap<> ();
private List<DiagnosticItem> diagnostics = new ArrayList<> ();
private CompletableFuture<AnalyzeResult> future;
@Override
public void analyze (ILanguageServer languageServer, File file, @NonNull Content content, TextAnalyzeResult colors, @NonNull TextAnalyzer.AnalyzeThread.Delegate delegate) throws Exception {
final var stream = CharStreams.fromReader (new StringReader (content.toString ()));
final var lexer = new JavaLexer (stream);
final var stack = new Stack<BlockLine> ();
public void analyze (@Nullable ILanguageServer languageServer,
@Nullable File file,
@NonNull Content content,
@NonNull TextAnalyzeResult colors,
@NonNull TextAnalyzer.AnalyzeThread.Delegate delegate
) throws Exception {
colors.addNormalIfNull ();
Token token;
int line, column, lastLine = 1, type, currSwitch = 0, maxSwitch = 0;
boolean isFirst = true;
this.stringMap.clear ();
while (delegate.shouldAnalyze ()) {
token = lexer.nextToken ();
if (token == null) {
break;
}
type = token.getType ();
line = token.getLine () - 1;
column = token.getCharPositionInLine ();
if (type == JavaLexer.EOF) {
lastLine = line;
break;
}
switch (type) {
case JavaLexer.WS:
if (isFirst) {
colors.addNormalIfNull ();
}
break;
case JavaLexer.ABSTRACT:
case JavaLexer.ASSERT:
case JavaLexer.BREAK:
case JavaLexer.CASE:
case JavaLexer.CATCH:
case JavaLexer.CLASS:
case JavaLexer.CONST:
case JavaLexer.CONTINUE:
case JavaLexer.DEFAULT:
case JavaLexer.DO:
case JavaLexer.ELSE:
case JavaLexer.EXTENDS:
case JavaLexer.FINAL:
case JavaLexer.FINALLY:
case JavaLexer.FOR:
case JavaLexer.IF:
case JavaLexer.GOTO:
case JavaLexer.IMPLEMENTS:
case JavaLexer.IMPORT:
case JavaLexer.INSTANCEOF:
case JavaLexer.INTERFACE:
case JavaLexer.NATIVE:
case JavaLexer.NEW:
case JavaLexer.PACKAGE:
case JavaLexer.PRIVATE:
case JavaLexer.PROTECTED:
case JavaLexer.PUBLIC:
case JavaLexer.RETURN:
case JavaLexer.STATIC:
case JavaLexer.STRICTFP:
case JavaLexer.SUPER:
case JavaLexer.SWITCH:
case JavaLexer.SYNCHRONIZED:
case JavaLexer.THIS:
case JavaLexer.THROW:
case JavaLexer.THROWS:
case JavaLexer.TRANSIENT:
case JavaLexer.TRY:
case JavaLexer.VOID:
case JavaLexer.VOLATILE:
case JavaLexer.WHILE:
case JavaLexer.VAR:
colors.addIfNeeded (line, column, EditorColorScheme.KEYWORD);
break;
case JavaLexer.DECIMAL_LITERAL:
case JavaLexer.HEX_LITERAL:
case JavaLexer.OCT_LITERAL:
case JavaLexer.BINARY_LITERAL:
case JavaLexer.FLOAT_LITERAL:
case JavaLexer.HEX_FLOAT_LITERAL:
case JavaLexer.BOOL_LITERAL:
case JavaLexer.CHAR_LITERAL:
case JavaLexer.NULL_LITERAL:
colors.addIfNeeded (line, column, EditorColorScheme.LITERAL);
break;
case JavaLexer.STRING_LITERAL:
checkAndAddHexString (token, EditorColorScheme.LITERAL, colors);
Position start = new Position (line, column);
Position end = new Position (line, column + token.getText ().length ());
colors.addStringRange (line, new Range (start, end));
{
List<Range> ranges = stringMap.get (line);
if (ranges == null) {
ranges = new ArrayList<> ();
}
ranges.add (new Range (start, end));
stringMap.put (line, ranges);
}
break;
case JavaLexer.LPAREN:
case JavaLexer.RPAREN:
case JavaLexer.LBRACK:
case JavaLexer.RBRACK:
case JavaLexer.SEMI:
case JavaLexer.COMMA:
case JavaLexer.ASSIGN:
case JavaLexer.GT:
case JavaLexer.LT:
case JavaLexer.BANG:
case JavaLexer.TILDE:
case JavaLexer.QUESTION:
case JavaLexer.COLON:
case JavaLexer.EQUAL:
case JavaLexer.GE:
case JavaLexer.LE:
case JavaLexer.NOTEQUAL:
case JavaLexer.AND:
case JavaLexer.OR:
case JavaLexer.INC:
case JavaLexer.DEC:
case JavaLexer.ADD:
case JavaLexer.SUB:
case JavaLexer.MUL:
case JavaLexer.DIV:
case JavaLexer.BITAND:
case JavaLexer.BITOR:
case JavaLexer.CARET:
case JavaLexer.MOD:
case JavaLexer.ADD_ASSIGN:
case JavaLexer.SUB_ASSIGN:
case JavaLexer.MUL_ASSIGN:
case JavaLexer.DIV_ASSIGN:
case JavaLexer.AND_ASSIGN:
case JavaLexer.OR_ASSIGN:
case JavaLexer.XOR_ASSIGN:
case JavaLexer.MOD_ASSIGN:
case JavaLexer.LSHIFT_ASSIGN:
case JavaLexer.RSHIFT_ASSIGN:
case JavaLexer.URSHIFT_ASSIGN:
case JavaLexer.ARROW:
case JavaLexer.COLONCOLON:
case JavaLexer.ELLIPSIS:
case JavaLexer.DOT:
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
break;
case JavaLexer.BOOLEAN:
case JavaLexer.BYTE:
case JavaLexer.CHAR:
case JavaLexer.DOUBLE:
case JavaLexer.ENUM:
case JavaLexer.FLOAT:
case JavaLexer.INT:
case JavaLexer.LONG:
case JavaLexer.SHORT:
colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME);
break;
case JavaLexer.BLOCK_COMMENT:
case JavaLexer.LINE_COMMENT:
colors.addIfNeeded (line, column, EditorColorScheme.COMMENT);
final CharPosition a = content.getIndexer ().getCharPosition (token.getStopIndex ());
final Position s = new Position (line, column);
final Position e = new Position (a.line, a.column);
colors.addCommentRange (line, new Range (s, e));
String lineComment = token.getText ();
if (lineComment != null) {
if (lineComment.startsWith ("//")) {
lineComment = lineComment.substring (2);
}
// Trim leading whitespaces
int startsFrom = 0;
for (int i = 0; i < lineComment.length (); i++) {
if (Character.isWhitespace (lineComment.charAt (i))) {
startsFrom++;
} else {
break;
}
}
lineComment = lineComment.substring (startsFrom);
final String text = new String (lineComment.toCharArray ());
lineComment = lineComment.toLowerCase (Locale.getDefault ());
if (lineComment.startsWith ("todo ")) {
diagnostics.put (line, Collections.singletonMap (column, LSPUtils.newInfoDiagnostic (line,
column,
token.getText ().length (),
text,
token.getText ())));
} else if (lineComment.startsWith (ConstantsBridge.CUSTOM_COMMENT_WARNING_TOKEN)) {
diagnostics.put (line, Collections.singletonMap (column, LSPUtils.newWarningDiagnostic (line,
column,
token.getText ().length (),
text,
token.getText ())));
}
}
break;
case JavaLexer.AT:
colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION);
break;
case JavaLexer.IDENTIFIER:
if (helper != null && helper.isPackageName (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.PACKAGE_NAME);
break;
}
if (helper != null && helper.isEnumType (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.ENUM_TYPE);
break;
}
if (helper != null && helper.isClassName (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME);
break;
}
if (helper != null && helper.isAnnotationType (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION);
break;
}
if (helper != null && helper.isInterface (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.INTERFACE);
break;
}
if (helper != null && helper.isEnum (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.ENUM);
break;
}
if (helper != null && helper.isStaticField (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.STATIC_FIELD);
break;
}
if (helper != null && helper.isField (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.FIELD);
break;
}
if (helper != null && helper.isParameter (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.PARAMETER);
break;
}
if (helper != null && helper.isLocal (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.LOCAL_VARIABLE);
break;
}
if (helper != null && helper.isExceptionParam (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.EXCEPTION_PARAM);
break;
}
if (helper != null && helper.isMethodDeclaration (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.METHOD_DECLARATION);
break;
}
if (helper != null && helper.isMethodInvocation (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.METHOD_INVOCATION);
break;
}
if (helper != null && helper.isConstructor (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.CONSTRUCTOR);
break;
}
if (helper != null && helper.isStaticInit (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.STATIC_INIT);
break;
}
if (helper != null && helper.isInstanceInit (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.INSTANCE_INIT);
break;
}
if (helper != null && helper.isTypeParam (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.TYPE_PARAM);
break;
}
if (helper != null && helper.isResourceVariable (line, column)) {
colors.addIfNeeded (line, column, EditorColorScheme.RESOURCE_VARIABLE);
break;
}
colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL);
break;
case JavaLexer.LBRACE:
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
if (stack.isEmpty ()) {
if (currSwitch > maxSwitch) {
maxSwitch = currSwitch;
}
currSwitch = 0;
}
currSwitch++;
BlockLine block = colors.obtainNewBlock ();
block.startLine = line;
block.startColumn = column;
stack.push (block);
break;
case JavaLexer.RBRACE:
colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR);
if (!stack.isEmpty ()) {
BlockLine block2 = stack.pop ();
block2.endLine = line;
block2.endColumn = column;
if (block2.startLine != block2.endLine) {
colors.addBlockLine (block2);
}
}
break;
default:
colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL);
break;
}
isFirst = false;
if (languageServer == null || file == null) {
return;
}
if (stack.isEmpty () && currSwitch > maxSwitch) {
maxSwitch = currSwitch;
if (future != null && !future.isDone ()) {
future.cancel (true);
}
colors.determine (lastLine);
colors.setSuppressSwitch (maxSwitch + 10);
}
@Override
public void setSemanticHighlights (SemanticHighlight highlights) {
this.helper = new HighlightRangeHelper (highlights);
this.helper.sort ();
}
@Override
public void updateDiagnostics (Map<Integer, Map<Integer, DiagnosticItem>> diagnostics) {
if (diagnostics == null) {
diagnostics = new HashMap<> ();
}
this.diagnostics = diagnostics;
future = CompletableFuture.supplyAsync (() -> languageServer
.getCodeAnalyzer ()
.analyze (new AnalyzeParams (file.toPath ()))
);
final var result = future.get ();
this.diagnostics = result.getDiagnostics ();
}
@Override
public DiagnosticItem findDiagnosticContaining (int line, int column) {
final Map<Integer, DiagnosticItem> mappedByColumn = diagnostics.get (line);
if (mappedByColumn != null) {
DiagnosticItem diag = mappedByColumn.get (column);
if (diag != null) {
return diag;
final var position = new Position (line, column);
// binary search the diagnostic item at the given position
// compare by search position
int left = 0; int right = diagnostics.size () - 1; int mid;
while (left < right) {
mid = (left + right) / 2;
final var diagnostic = diagnostics.get (mid);
final var range = diagnostic.getRange ();
final var res = range.containsForBinarySearch (position);
if (res < 0) {
left = mid + 1;
} else if (res > 0) {
right = mid - 1;
} else {
for (Map.Entry<Integer, DiagnosticItem> entry : mappedByColumn.entrySet ()) {
diag = entry.getValue ();
final int start = diag.getRange ().getStart ().getColumn ();
final int end = diag.getRange ().getEnd ().getColumn ();
if (column >= start && column <= end) {
return diag;
}
}
return diagnostic;
}
}
return super.findDiagnosticContaining (line, column);
}
@Override
public List<DiagnosticItem> findDiagnosticsContainingLine (int line) {
final Map<Integer, DiagnosticItem> diags = diagnostics.get (line);
if (diags == null) {
return super.findDiagnosticsContainingLine (line);
}
return diags.entrySet ().stream ()
.filter (d -> d != null && d.getValue () != null)
.map (Map.Entry::getValue)
return diagnostics.stream ()
.filter (diagnostic -> diagnostic.getRange ().containsLine (line))
.collect (Collectors.toList ());
}
@Override
public Map<Integer, DiagnosticItem> getDiagnosticsAtLine (int line) {
return diagnostics.get (line);
}
private static final Logger LOG = Logger.instance ("JavaAnalyzer");
}
@@ -107,14 +107,6 @@ public class LogLanguageImpl extends BaseLanguage {
}
colors.determine(lastLine);
}
@Override
public void setSemanticHighlights(SemanticHighlight highlights) {
}
@Override
public void updateDiagnostics(Map<Integer, Map<Integer, DiagnosticItem>> diagnostics) {
}
}
private static class LogCompletor implements AutoCompleteProvider {
@@ -109,12 +109,4 @@ public class XMLAnalyzer extends io.github.rosemoe.editor.langs.AbstractCodeAnal
}
colors.determine (lastLine);
}
@Override
public void setSemanticHighlights (SemanticHighlight highlights) {
}
@Override
public void updateDiagnostics (Map<Integer, Map<Integer, DiagnosticItem>> diagnostics) {
}
}
@@ -148,14 +148,8 @@ public class LSPUtils {
// Lines are same
// Compare by columns
if(r1StartCol < r2StartCol) {
return -1;
} else if (r1StartCol > r2StartCol) {
return 1;
} else {
// Both ranges have same start position
return 0;
}
// Both ranges have same start position
return Integer.compare (r1StartCol, r2StartCol);
}
}
};
@@ -233,33 +233,6 @@ public class CodeEditorView extends FrameLayout {
binding.editor.beginSearchMode ();
}
public void setDiagnostics (List<DiagnosticItem> diagnostics) {
if (diagnostics == null) {
LOG.info ("Clearing diagnostics of code editor", "file:", file);
binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (new HashMap<> ());
return;
}
Map<Integer, Map<Integer, DiagnosticItem>> map = new HashMap<> ();
for (int i = 0; i < diagnostics.size (); i++) {
final var d = diagnostics.get (i);
if (d == null) {
continue;
}
final var range = d.getRange ();
final int line = range.getStart ().getLine ();
final int column = range.getStart ().getColumn ();
Map<Integer, DiagnosticItem> mappedByColumn = map.get (line);
if (mappedByColumn == null) {
mappedByColumn = new HashMap<> ();
}
mappedByColumn.put (column, d);
map.put (line, mappedByColumn);
}
binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (map);
}
protected void postRead () {
if (file.isFile () && file.getName ().endsWith (".java")) {
binding.editor.setEditorLanguage (new JavaLanguage (), StudioApp.getInstance ().getJavaLanguageServer ());
@@ -34,21 +34,6 @@ import java.util.Map;
* @author Rose
*/
public interface CodeAnalyzer {
/**
* Tells the analyzer to store the semantic highlight ranges provided by LanguageServer
*/
void setSemanticHighlights(SemanticHighlight highlights);
/**
* Tells the analyzer to update the diagnostics locally.
* They will be then used to provide spans
*
* @param diagnostics The diagnostics to update to.
They are first mapped by their start line and then by their start column.
*/
void updateDiagnostics(Map<Integer, Map<Integer, DiagnosticItem>> diagnostics);
/**
* Find the diagnostic containing the specific position
*
@@ -66,18 +51,6 @@ public interface CodeAnalyzer {
*/
List<DiagnosticItem> findDiagnosticsContainingLine(int line);
/**
* Get diagnostics at the specified line.
*
* @return A map containing the diagnostics mapped by their columns
*/
Map<Integer, DiagnosticItem> getDiagnosticsAtLine(int line);
/**
* Get any hex color string is present in provided line
*/
Map<Integer, Integer> getHexColorsInLine (int line);
/**
* Analyze spans for the given input
*
@@ -20,14 +20,6 @@ import io.github.rosemoe.editor.struct.Span;
import io.github.rosemoe.editor.text.TextAnalyzeResult;
public abstract class AbstractCodeAnalyzer implements CodeAnalyzer {
@Override
public void setSemanticHighlights(SemanticHighlight highlights) {
}
@Override
public void updateDiagnostics(Map<Integer, Map<Integer, DiagnosticItem>> diagnostics) {
}
@Override
public DiagnosticItem findDiagnosticContaining(int line, int column) {
@@ -38,16 +30,6 @@ public abstract class AbstractCodeAnalyzer implements CodeAnalyzer {
public List<DiagnosticItem> findDiagnosticsContainingLine(int line) {
return new ArrayList<>();
}
@Override
public Map<Integer, DiagnosticItem> getDiagnosticsAtLine(int line) {
return new HashMap<>();
}
@Override
public Map<Integer, Integer> getHexColorsInLine(int line) {
return new HashMap<> ();
}
protected void checkAndAddHexString (@NonNull Token token, final int defaultColorId, TextAnalyzeResult result) {
final var line = token.getLine () - 1;
@@ -97,14 +97,6 @@ public class EmptyLanguage implements EditorLanguage {
public void analyze(ILanguageServer server, File file, Content content, TextAnalyzeResult colors, TextAnalyzer.AnalyzeThread.Delegate delegate) {
colors.addNormalIfNull();
}
@Override
public void setSemanticHighlights(SemanticHighlight highlights) {
}
@Override
public void updateDiagnostics(Map<Integer, Map<Integer, DiagnosticItem>> diagnostics) {
}
}
}
@@ -44,7 +44,7 @@ public class TextAnalyzer {
private AnalyzeThread mThread;
private final EditorLanguage mLanguage;
private final ILanguageServer mLanguageServer;
private final File file;
private File file;
private static final Logger LOG = Logger.instance ("TextAnalyzer");
@@ -65,6 +65,10 @@ public class TextAnalyzer {
mLanguage = language;
}
public void setFile (File file) {
this.file = file;
}
private synchronized static int nextThreadId () {
sThreadId++;
return sThreadId;
@@ -213,7 +213,6 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
private float mInsertSelWidth;
private float mBlockLineWidth;
private float mLineInfoTextSize;
private long mLastEdited;
private boolean mWait;
private boolean mDrag;
private boolean mScalable;
@@ -281,14 +280,12 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
private Paint.FontMetricsInt mGraphMetrics;
private CursorBlink mCursorBlink;
private SymbolPairMatch mOverrideSymbolPairs;
private LongArrayList mPostDrawLineNumbers = new LongArrayList ();
private final LongArrayList mPostDrawLineNumbers = new LongArrayList ();
private CharPosition mLockedSelection;
EditorInputConnection mConnection;
KeyMetaStates mKeyMetaStates = new KeyMetaStates (this);
private List<String> mSignatureHelpTriggerChars = new ArrayList<> ();
private File currentFile;
public CodeEditor (Context context) {
@@ -329,7 +326,11 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
*/
public void setFile (File file) {
this.currentFile = file;
if (mSpanner != null) {
mSpanner.setFile (file);
}
if (mLanguageServer != null) {
final var text = getText ().toString ();
final var event = new DocumentOpenEvent (file.toPath (), text, mFileVersion = 0);
@@ -501,7 +502,6 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
mPaintOther.setTypeface (Typeface.MONOSPACE);
mBuffer = new char[256];
mBuffer2 = new char[16];
mLastEdited = 0;
mStartedActionMode = ACTION_MODE_NONE;
setTextSize (DEFAULT_TEXT_SIZE);
setLineInfoTextSize (mPaint.getTextSize ());
@@ -603,12 +603,6 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
}
}
public void notifySpansChanged () {
if (mSpanner != null) {
mSpanner.analyze (getText ());
}
}
/**
* Define symbol pairs for any language,
* Override language settings.
@@ -761,12 +755,6 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
invalidate ();
}
public void setSemanticHighlights (SemanticHighlight highlights) {
mLanguage.getAnalyzer ().setSemanticHighlights (highlights);
notifySpansChanged ();
}
public EditorLanguage getEditorLanguage () {
return mLanguage;
}
@@ -4575,12 +4563,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
}
public void signatureHelp (String insertedContent) {
if (mLanguageServer != null
&& getFile () != null
&& mSignatureHelpTriggerChars != null
&& mSignatureHelpTriggerChars.size () > 0
&& mSignatureHelpTriggerChars.contains (insertedContent)) {
if (mLanguageServer != null && getFile () != null) {
final CompletableFuture<SignatureHelp> future = CompletableFuture.supplyAsync (() -> {
final var provider = mLanguageServer.getSignatureHelpProvider ();
return provider.provideSignatures (new SignatureHelpParams (getFile ().toPath (), new com.itsaky.lsp.models.Position (
@@ -5031,5 +5014,4 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca
}
private static final Logger LOG = Logger.instance ("CodeEditor");
}
@@ -32,7 +32,7 @@ data class CodeActionItem (
var changes: List<DocumentChange>,
var kind: CodeActionKind
) {
constructor() : this ("", Collections.emptyList(), CodeActionKind.None)
constructor() : this ("", ArrayList(), CodeActionKind.None)
}
enum class CodeActionKind {
@@ -42,7 +42,7 @@ enum class CodeActionKind {
}
data class DocumentChange (var file: Path?, var edits: List<TextEdit>) {
constructor() : this (null, Collections.emptyList())
constructor() : this (null, ArrayList())
}
data class TextEdit (var range: Range, var newText: String) {
@@ -20,4 +20,6 @@ package com.itsaky.lsp.models
import java.nio.file.Path
data class AnalyzeParams(var file: Path)
data class AnalyzeResult (var diagnostics: List<DiagnosticItem>, var semanticHighlights: List<Int>)
data class AnalyzeResult (var diagnostics: List<DiagnosticItem>, var semanticHighlights: List<Int>) {
constructor() : this (ArrayList(), ArrayList())
}
@@ -20,6 +20,7 @@ package com.itsaky.lsp.models
import android.text.TextUtils
import java.nio.file.Path
import java.util.*
import kotlin.collections.ArrayList
data class CompletionParams (var position: Position, var file: Path)
@@ -48,7 +49,7 @@ data class CompletionItem(var label: String,
null,
null,
CompletionItemKind.NONE,
null,
ArrayList(),
null
)
@@ -22,7 +22,10 @@ import java.nio.file.Path
data class DiagnosticItem(var message: String, var code: String, var range: Range, var source: String, var severity: DiagnosticSeverity) {
constructor() : this ("", "", Range.NONE, "", DiagnosticSeverity.HINT)
}
data class DiagnosticResult(var file: Path, var diagnostics: List<DiagnosticItem>)
data class DiagnosticResult(var file: Path, var diagnostics: List<DiagnosticItem>) {
}
enum class DiagnosticSeverity {
ERROR, WARNING, INFO, HINT
}
@@ -20,7 +20,19 @@ package com.itsaky.lsp.models
import java.nio.file.Path
data class InitializeParams (var workspaceRoots: Set<Path>)
data class InitializeResult (var completionsAvailable: Boolean,
var codeActionsAvailable: Boolean) {
constructor() : this (false, false)
data class InitializeResult
(
var completionsAvailable: Boolean,
var codeActionsAvailable: Boolean,
var definitionsAvailable: Boolean,
var referencesAvailable: Boolean,
var signatureHelpAvailable: Boolean,
var codeAnalysisAvailable: Boolean
) {
constructor() : this(false,
false,
false,
false,
false,
false)
}
@@ -21,7 +21,7 @@ import java.nio.file.Path
data class Location (var file: Path, var range: Range)
data class Position(var line: Int, var column: Int) {
data class Position(var line: Int, var column: Int) : Comparable<Position> {
/**
* Validate the line and column positions if necessary.
@@ -41,9 +41,28 @@ data class Position(var line: Int, var column: Int) {
companion object {
@JvmField val NONE = Position (-1, -1)
}
override fun compareTo(other: Position): Int {
val byLine = when {
line < other.line -> -1
line > other.line -> 1
else -> 0
}
if (byLine != 0) {
return byLine
}
return when {
column < other.column -> -1
column > other.column -> 1
else -> 0
}
}
}
open class Range(var start: Position, var end: Position) {
open class Range(var start: Position, var end: Position) : Comparable<Range> {
constructor() : this (Position(0, 0), Position(0, 0));
@@ -75,4 +94,51 @@ open class Range(var start: Position, var end: Position) {
result = 31 * result + end.hashCode()
return result
}
override fun compareTo(other: Range): Int = start.compareTo(other.start)
fun compareByEnd (other: Range) : Int = end.compareTo(other.end)
fun contains (position: Position) : Boolean {
return !(position.line < start.line || position.line > end.line)
}
/**
* Check if this range contains the given position or not.
* Return an integer result indicating where the index
* of the range containing this position might be.
*
* The return value will be used in a binary search.
*/
fun containsForBinarySearch (position: Position) : Int {
// The position might appear before this range
if (position.line < start.line) {
return -1;
}
// The position might appear after this range
if (position.line > end.line) {
return 1;
}
// If start and end lines are same, compare by column indexes
if (start.line == end.line) {
if (position.column < start.column) {
return -1;
}
if (position.column > end.column) {
return 1;
}
}
// This range definitely contains the position.
return 0;
}
fun containsLine (line: Int): Boolean {
return start.line <= line && end.line >= line;
}
}
@@ -114,6 +114,10 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
final InitializeResult result = new InitializeResult ();
result.setCompletionsAvailable (true);
result.setCodeActionsAvailable (true);
result.setDefinitionsAvailable (true);
result.setReferencesAvailable (true);
result.setSignatureHelpAvailable (true);
result.setCodeAnalysisAvailable (true);
initialized = true;
@@ -221,7 +225,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
return new NoCodeAnalyzer ();
}
return new JavaCodeAnalyzer ();
return new JavaCodeAnalyzer (getCompiler ());
}
@Override
@@ -107,7 +107,7 @@ public class ReusableCompiler {
Iterable<? extends JavaFileObject> compilationUnits) {
if (checkedOut) {
throw new RuntimeException ("Compiler us already in use.");
throw new RuntimeException ("Compiler is already in-use!");
}
checkedOut = true;
@@ -413,6 +413,7 @@ public class CodeActionProvider implements ICodeActionProvider {
continue;
}
final DocumentChange change = new DocumentChange ();
change.setFile (file);
change.setEdits (Arrays.asList (textEdits));
action.getChanges ().add (change);
}
@@ -0,0 +1,263 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.java.providers;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.lsp.java.FileStore;
import com.itsaky.lsp.java.R;
import com.itsaky.lsp.java.compiler.CompileTask;
import com.itsaky.lsp.java.visitors.DiagnosticVisitor;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.DiagnosticSeverity;
import com.itsaky.lsp.models.Position;
import com.itsaky.lsp.models.Range;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.Element;
import javax.lang.model.element.Name;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
* Finds errors and warnings from a compilation task.
*
* @author Akash Yadav
*/
public class DiagnosticsProvider {
private static final Comparator<? super DiagnosticItem>
DIAGNOSTIC_START_COMPARATOR = Comparator.comparing (DiagnosticItem::getRange);
/**
* Finds diagnostics from the given task (only the diagnostics for the given file). The task should be
* a valid task.
*
* As the file might be too long, the diagnostics list must be sorted so we can quickly
* binary search the list when needed.
*
* @param task The compilation task to get diagnostics from.
* @param file The file of which the diagnostics must be extracted.
* @return The list of diagnostics retrieved from the task. Never null.
*/
public static List<DiagnosticItem> findDiagnostics (final CompileTask task, final Path file) {
final List<DiagnosticItem> result = new ArrayList<> ();
CompilationUnitTree root = null;
for (CompilationUnitTree tree : task.roots) {
final Path path = Paths.get (tree.getSourceFile ().toUri ());
if (path.equals (file)) {
root = tree;
break;
}
}
if (root == null) {
// CompilationUnitTree for the file was not found
// Can't do anything...
return result;
}
addCompilerErrors (task, root, result);
addDiagnosticsByVisiting (task, root, result);
result.sort (DIAGNOSTIC_START_COMPARATOR);
return result;
}
private static void addDiagnosticsByVisiting (CompileTask task, CompilationUnitTree root, List<DiagnosticItem> result) {
Map<TreePath, String> notThrown = new HashMap<> ();
DiagnosticVisitor warnUnused = new DiagnosticVisitor(task.task);
warnUnused.scan(root, notThrown);
for (Element unusedEl : warnUnused.notUsed()) {
result.add(warnUnused(task, unusedEl));
}
for (TreePath location : notThrown.keySet()) {
result.add(warnNotThrown(task, notThrown.get(location), location));
}
}
private static void addCompilerErrors (final CompileTask task, final CompilationUnitTree root, final List<DiagnosticItem> result) {
for (Diagnostic<? extends JavaFileObject> diagnostic : task.diagnostics) {
if (diagnostic.getSource () == null
|| !diagnostic.getSource ().toUri ().equals (root.getSourceFile ().toUri ())) {
continue;
}
if (diagnostic.getStartPosition () == -1
|| diagnostic.getEndPosition () == - 1) {
continue;
}
result.add (asDiagnosticItem (diagnostic, root.getLineMap ()));
}
}
@NonNull
private static DiagnosticItem warnNotThrown (CompileTask task, String name, TreePath path) {
Trees trees = Trees.instance(task.task);
SourcePositions pos = trees.getSourcePositions();
CompilationUnitTree root = path.getCompilationUnit();
LineMap lines = root.getLineMap ();
long start = pos.getStartPosition(root, path.getLeaf());
long end = pos.getEndPosition(root, path.getLeaf());
final DiagnosticItem d = new DiagnosticItem();
d.setMessage(BaseApplication.getBaseInstance ().getString(R.string.msg_not_thrown, name));
d.setRange(new Range (getPosition (start, lines), getPosition (end, lines)));
d.setCode("unused_throws");
d.setSeverity(DiagnosticSeverity.INFO);
return d;
}
@NonNull
private static DiagnosticItem warnUnused (CompileTask task, Element unusedEl) {
Trees trees = Trees.instance(task.task);
TreePath path = trees.getPath(unusedEl);
if (path == null) {
throw new RuntimeException(unusedEl + " has no path");
}
CompilationUnitTree root = path.getCompilationUnit();
Tree leaf = path.getLeaf();
SourcePositions pos = trees.getSourcePositions();
int start = (int) pos.getStartPosition(root, leaf);
int end = (int) pos.getEndPosition(root, leaf);
if (leaf instanceof VariableTree) {
VariableTree v = (VariableTree) leaf;
int offset = (int) pos.getEndPosition(root, v.getType());
if (offset != -1) {
start = offset;
}
}
Path file = Paths.get(root.getSourceFile().toUri());
CharSequence contents = FileStore.contents(file);
Name name = unusedEl.getSimpleName();
if (name.contentEquals("<init>")) {
name = unusedEl.getEnclosingElement().getSimpleName();
}
CharSequence region = contents.subSequence(start, end);
Matcher matcher = Pattern.compile("\\b" + name + "\\b").matcher(region);
if (matcher.find()) {
start += matcher.start();
end = start + name.length();
}
String message = BaseApplication.getBaseInstance ().getString(R.string.msg_not_used, name);
String code;
DiagnosticSeverity severity ;
if (leaf instanceof VariableTree) {
Tree parent = path.getParentPath().getLeaf();
if (parent instanceof MethodTree) {
code = "unused_param";
severity = DiagnosticSeverity.HINT;
} else if (parent instanceof BlockTree) {
code = "unused_local";
severity = DiagnosticSeverity.INFO;
} else if (parent instanceof ClassTree) {
code = "unused_field";
severity = DiagnosticSeverity.INFO;
} else {
code = "unused_other";
severity = DiagnosticSeverity.HINT;
}
} else if (leaf instanceof MethodTree) {
code = "unused_method";
severity = DiagnosticSeverity.INFO;
} else if (leaf instanceof ClassTree) {
code = "unused_class";
severity = DiagnosticSeverity.INFO;
} else {
code = "unused_other";
severity = DiagnosticSeverity.INFO;
}
return asDiagnosticItem (severity, code, message, start, end, root);
}
@NonNull
private static DiagnosticItem asDiagnosticItem (DiagnosticSeverity severity, String code, String message, long start, long end, @NonNull CompilationUnitTree root) {
final DiagnosticItem item = new DiagnosticItem ();
item.setMessage (message);
item.setCode (code);
item.setSeverity (severity);
item.setRange (new Range (getPosition (start, root.getLineMap ()), getPosition (end, root.getLineMap ())));
return item;
}
@NonNull
private static DiagnosticItem asDiagnosticItem (final Diagnostic<? extends JavaFileObject> diagnostic, final LineMap lines) {
final DiagnosticItem result = new DiagnosticItem ();
result.setRange (getDiagnosticRange (diagnostic, lines));
result.setSeverity (severityFor (diagnostic.getKind ()));
result.setCode (diagnostic.getCode ());
result.setMessage (diagnostic.getMessage (Locale.getDefault ()));
return result;
}
@NonNull
private static Range getDiagnosticRange (@NonNull Diagnostic<? extends JavaFileObject> diagnostic, LineMap lines) {
final Position start = getPosition (diagnostic.getStartPosition (), lines);
final Position end = getPosition (diagnostic.getEndPosition (), lines);
return new Range (start, end);
}
@NonNull
private static Position getPosition (long position, @NonNull LineMap lines) {
// decrement the numbers
// to convert 1-based indexes to 0-based
final int line = (int) (lines.getLineNumber (position) - 1);
final int column = (int) (lines.getColumnNumber (position) - 1);
return new Position (line, column);
}
private static DiagnosticSeverity severityFor (javax.tools.Diagnostic.Kind kind) {
switch (kind) {
case ERROR:
return DiagnosticSeverity.ERROR;
case WARNING:
case MANDATORY_WARNING:
return DiagnosticSeverity.WARNING;
case NOTE:
return DiagnosticSeverity.INFO;
case OTHER:
default:
return DiagnosticSeverity.HINT;
}
}
}
@@ -20,9 +20,14 @@ package com.itsaky.lsp.java.providers;
import androidx.annotation.NonNull;
import com.itsaky.lsp.api.ICodeAnalyzer;
import com.itsaky.lsp.java.compiler.CompileTask;
import com.itsaky.lsp.java.compiler.CompilerProvider;
import com.itsaky.lsp.java.compiler.SynchronizedTask;
import com.itsaky.lsp.models.AnalyzeParams;
import com.itsaky.lsp.models.AnalyzeResult;
import java.nio.file.Path;
/**
* Code analyzer for java source code.
*
@@ -30,9 +35,33 @@ import com.itsaky.lsp.models.AnalyzeResult;
*/
public class JavaCodeAnalyzer implements ICodeAnalyzer {
private final CompilerProvider compiler;
public JavaCodeAnalyzer (CompilerProvider compiler) {
this.compiler = compiler;
}
@NonNull
@Override
public AnalyzeResult analyze (AnalyzeParams params) {
return null;
public AnalyzeResult analyze (@NonNull AnalyzeParams params) {
final Path file = params.getFile ();
try (final SynchronizedTask synchronizedTask = compiler.compile (file)) {
return synchronizedTask.getWithTask (task -> {
final AnalyzeResult result = new AnalyzeResult ();
if (!isTaskValid (task)) {
return result;
}
result.setDiagnostics (DiagnosticsProvider.findDiagnostics (task, file));
return result;
});
}
}
private static boolean isTaskValid (CompileTask task) {
return task != null
&& task.task != null
&& task.roots != null
&& task.roots.size () > 0;
}
}
@@ -0,0 +1,314 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.lsp.java.visitors;
import com.itsaky.androidide.utils.Logger;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.source.util.Trees;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
public class DiagnosticVisitor extends TreeScanner<Void, Map<TreePath, String>> {
// Copied from TreePathScanner
// We need to be able to call scan(path, _) recursively
private TreePath path;
private Map<String, TreePath> declaredExceptions = new HashMap<> ();
private Set<String> observedExceptions = new HashSet<> ();
private final Trees trees;
private final Map<Element, TreePath> privateDeclarations = new HashMap<>(), localVariables = new HashMap<>();
private final Set<Element> used = new HashSet<>();
public DiagnosticVisitor(JavacTask task) {
this.trees = Trees.instance(task);
}
private void scanPath(TreePath path) {
TreePath prev = this.path;
this.path = path;
try {
path.getLeaf().accept(this, null);
} finally {
this.path = prev; // So we can call scan(path, _) recursively
}
}
@Override
public Void scan(Tree tree, Map<TreePath, String> p) {
if (tree == null) return null;
TreePath prev = path;
path = new TreePath(path, tree);
try {
return tree.accept(this, p);
} finally {
path = prev;
}
}
public Set<Element> notUsed() {
Set<Element> unused = new HashSet<>();
unused.addAll(privateDeclarations.keySet());
unused.addAll(localVariables.keySet());
unused.removeAll(used);
// Remove if there are any null elements somehow ended up being added
// during async work which calls `lint`
unused.removeIf(Objects::isNull);
// Remove if <error > field was injected while forming the AST
unused.removeIf(i -> i.toString().equals("<error>"));
return unused;
}
private void foundPrivateDeclaration() {
privateDeclarations.put(trees.getElement(path), path);
}
private void foundLocalVariable() {
localVariables.put(trees.getElement(path), path);
}
private void foundReference() {
Element toEl = trees.getElement(path);
if (toEl == null) {
return;
}
if (toEl.asType().getKind() == TypeKind.ERROR) {
foundPseudoReference(toEl);
return;
}
sweep(toEl);
}
private void foundPseudoReference(Element toEl) {
Element parent = toEl.getEnclosingElement();
if (!(parent instanceof TypeElement)) {
return;
}
Name memberName = toEl.getSimpleName();
TypeElement type = (TypeElement) parent;
for (Element member : type.getEnclosedElements()) {
if (member.getSimpleName().contentEquals(memberName)) {
sweep(member);
}
}
}
private void sweep(Element toEl) {
boolean firstUse = used.add(toEl);
boolean notScanned = firstUse && privateDeclarations.containsKey(toEl);
if (notScanned) {
scanPath(privateDeclarations.get(toEl));
}
}
private boolean isReachable(TreePath path) {
// Check if t is reachable because it's public
Tree t = path.getLeaf();
if (t instanceof VariableTree) {
VariableTree v = (VariableTree) t;
boolean isPrivate = v.getModifiers().getFlags().contains(Modifier.PRIVATE);
if (!isPrivate || isLocalVariable(path)) {
return true;
}
}
if (t instanceof MethodTree) {
MethodTree m = (MethodTree) t;
boolean isPrivate = m.getModifiers().getFlags().contains(Modifier.PRIVATE);
boolean isEmptyConstructor = m.getParameters().isEmpty() && m.getReturnType() == null;
if (!isPrivate || isEmptyConstructor) {
return true;
}
}
if (t instanceof ClassTree) {
ClassTree c = (ClassTree) t;
boolean isPrivate = c.getModifiers().getFlags().contains(Modifier.PRIVATE);
if (!isPrivate) {
return true;
}
}
// Check if t has been referenced by a reachable element
Element el = trees.getElement(path);
return used.contains(el);
}
private boolean isLocalVariable(TreePath path) {
Tree.Kind kind = path.getLeaf().getKind();
if (kind != Tree.Kind.VARIABLE) {
return false;
}
Tree.Kind parent = path.getParentPath().getLeaf().getKind();
if (parent == Tree.Kind.CLASS || parent == Tree.Kind.INTERFACE) {
return false;
}
if (parent == Tree.Kind.METHOD) {
MethodTree method = (MethodTree) path.getParentPath().getLeaf();
return method.getBody () != null;
}
return true;
}
private Map<String, TreePath> declared(MethodTree t) {
Map<String, TreePath> names = new HashMap<> ();
for (ExpressionTree e : t.getThrows()) {
TreePath path = new TreePath(this.path, e);
Element to = trees.getElement(path);
if (!(to instanceof TypeElement)) continue;
TypeElement type = (TypeElement) to;
String name = type.getQualifiedName().toString();
names.put(name, path);
}
return names;
}
@Override
public Void visitCompilationUnit(CompilationUnitTree t, Map<TreePath, String> notThrown) {
return super.visitCompilationUnit(t, notThrown);
}
@Override
public Void visitVariable(VariableTree t, Map<TreePath, String> notThrown) {
if (isLocalVariable(path)) {
foundLocalVariable();
super.visitVariable(t, notThrown);
} else if (isReachable(path)) {
super.visitVariable(t, notThrown);
} else {
foundPrivateDeclaration();
}
return null;
}
@Override
public Void visitMethod(MethodTree t, Map<TreePath, String> notThrown) {
// Create a new method scope
Map<String, TreePath> pushDeclared = declaredExceptions;
Set<String> pushObserved = observedExceptions;
declaredExceptions = declared(t);
observedExceptions = new HashSet<>();
// Recursively scan for 'throw' and method calls
super.visitMethod(t, notThrown);
// Check for exceptions that were never thrown
for (String exception : declaredExceptions.keySet()) {
if (!observedExceptions.contains(exception)) {
notThrown.put(declaredExceptions.get(exception), exception);
}
}
declaredExceptions = pushDeclared;
observedExceptions = pushObserved;
if (!isReachable(path)) {
foundPrivateDeclaration();
}
return null;
}
@Override
public Void visitClass(ClassTree t, Map<TreePath, String> notThrown) {
if (isReachable(path)) {
super.visitClass(t, notThrown);
} else {
foundPrivateDeclaration();
}
return null;
}
@Override
public Void visitIdentifier(IdentifierTree t, Map<TreePath, String> notThrown) {
foundReference();
return super.visitIdentifier(t, notThrown);
}
@Override
public Void visitMemberSelect(MemberSelectTree t, Map<TreePath, String> notThrown) {
foundReference();
return super.visitMemberSelect(t, notThrown);
}
@Override
public Void visitMemberReference(MemberReferenceTree t, Map<TreePath, String> notThrown) {
foundReference();
return super.visitMemberReference(t, notThrown);
}
@Override
public Void visitNewClass(NewClassTree t, Map<TreePath, String> notThrown) {
foundReference();
return super.visitNewClass(t, notThrown);
}
@Override
public Void visitThrow(ThrowTree t, Map<TreePath, String> notThrown) {
TreePath path = new TreePath(this.path, t.getExpression());
TypeMirror type = trees.getTypeMirror(path);
addThrown(type);
return super.visitThrow(t, notThrown);
}
@Override
public Void visitMethodInvocation(MethodInvocationTree t, Map<TreePath, String> notThrown) {
Element target = trees.getElement(this.path);
if (target instanceof ExecutableElement) {
ExecutableElement method = (ExecutableElement) target;
for (TypeMirror type : method.getThrownTypes()) {
addThrown(type);
}
}
return super.visitMethodInvocation(t, notThrown);
}
private void addThrown(TypeMirror type) {
if (type instanceof DeclaredType) {
DeclaredType declared = (DeclaredType) type;
TypeElement el = (TypeElement) declared.asElement();
String name = el.getQualifiedName().toString();
observedExceptions.add(name);
}
}
private static final Logger LOG = Logger.instance ("main");
}
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="msg_not_thrown">\'%s\' is not thrown in the body of the method</string>
<string name="msg_not_used">\'%s\' is not used</string>
</resources>