mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Implement diagnostics for java source files
This commit is contained in:
@@ -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");
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user