From 42b195cd7cf62cd37cfa3459b0562da04078ec8f Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 18 Jan 2022 18:27:39 +0530 Subject: [PATCH] Implement diagnostics for java source files --- .../language/groovy/GroovyAnalyzer.java | 26 +- .../language/groovy/GroovyLanguage.java | 2 +- .../language/java/JavaAnalyzer.java | 437 ++---------------- .../language/logs/LogLanguageImpl.java | 8 - .../androidide/language/xml/XMLAnalyzer.java | 8 - .../com/itsaky/androidide/utils/LSPUtils.java | 10 +- .../androidide/views/CodeEditorView.java | 27 -- .../editor/interfaces/CodeAnalyzer.java | 27 -- .../editor/langs/AbstractCodeAnalyzer.java | 18 - .../rosemoe/editor/langs/EmptyLanguage.java | 8 - .../rosemoe/editor/text/TextAnalyzer.java | 6 +- .../rosemoe/editor/widget/CodeEditor.java | 32 +- .../java/com/itsaky/lsp/models/CodeActions.kt | 4 +- .../com/itsaky/lsp/models/CodeAnalysis.kt | 4 +- .../java/com/itsaky/lsp/models/Completions.kt | 3 +- .../java/com/itsaky/lsp/models/Diagnostics.kt | 5 +- .../com/itsaky/lsp/models/Initialization.kt | 18 +- .../java/com/itsaky/lsp/models/Locations.kt | 70 ++- .../itsaky/lsp/java/JavaLanguageServer.java | 6 +- .../lsp/java/compiler/ReusableCompiler.java | 2 +- .../java/providers/CodeActionProvider.java | 1 + .../java/providers/DiagnosticsProvider.java | 263 +++++++++++ .../lsp/java/providers/JavaCodeAnalyzer.java | 33 +- .../lsp/java/visitors/DiagnosticVisitor.java | 314 +++++++++++++ lsp/java/src/main/res/values/strings.xml | 21 + 25 files changed, 792 insertions(+), 561 deletions(-) create mode 100644 lsp/java/src/main/java/com/itsaky/lsp/java/providers/DiagnosticsProvider.java create mode 100644 lsp/java/src/main/java/com/itsaky/lsp/java/visitors/DiagnosticVisitor.java create mode 100644 lsp/java/src/main/res/values/strings.xml diff --git a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAnalyzer.java b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAnalyzer.java index a6fae25a5..b70ee6ef7 100755 --- a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAnalyzer.java +++ b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyAnalyzer.java @@ -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 . * -**************************************************************************************/ + */ 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> diagnostics) { - } } diff --git a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java index ec6b882d6..e70a1201d 100755 --- a/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java +++ b/app/src/main/java/com/itsaky/androidide/language/groovy/GroovyLanguage.java @@ -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 (); } diff --git a/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java b/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java index 62ff95840..71fc8c499 100644 --- a/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java +++ b/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java @@ -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> diagnostics = new HashMap<> (); - private final Map> stringMap = new HashMap<> (); + private List diagnostics = new ArrayList<> (); + private CompletableFuture 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 (); + 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 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> 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 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 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 findDiagnosticsContainingLine (int line) { - final Map 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 getDiagnosticsAtLine (int line) { - return diagnostics.get (line); - } + private static final Logger LOG = Logger.instance ("JavaAnalyzer"); } diff --git a/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java b/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java index 938d6c62c..6c9d9845d 100755 --- a/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java +++ b/app/src/main/java/com/itsaky/androidide/language/logs/LogLanguageImpl.java @@ -107,14 +107,6 @@ public class LogLanguageImpl extends BaseLanguage { } colors.determine(lastLine); } - - @Override - public void setSemanticHighlights(SemanticHighlight highlights) { - } - - @Override - public void updateDiagnostics(Map> diagnostics) { - } } private static class LogCompletor implements AutoCompleteProvider { diff --git a/app/src/main/java/com/itsaky/androidide/language/xml/XMLAnalyzer.java b/app/src/main/java/com/itsaky/androidide/language/xml/XMLAnalyzer.java index 57f9bdb2e..24854da14 100755 --- a/app/src/main/java/com/itsaky/androidide/language/xml/XMLAnalyzer.java +++ b/app/src/main/java/com/itsaky/androidide/language/xml/XMLAnalyzer.java @@ -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> diagnostics) { - } } diff --git a/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java b/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java index da520dd75..3048ffd00 100755 --- a/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java +++ b/app/src/main/java/com/itsaky/androidide/utils/LSPUtils.java @@ -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); } } }; diff --git a/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java b/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java index a8cf94595..c9a39a7fb 100644 --- a/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java +++ b/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java @@ -233,33 +233,6 @@ public class CodeEditorView extends FrameLayout { binding.editor.beginSearchMode (); } - public void setDiagnostics (List diagnostics) { - if (diagnostics == null) { - LOG.info ("Clearing diagnostics of code editor", "file:", file); - binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (new HashMap<> ()); - return; - } - - Map> 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 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 ()); diff --git a/app/src/main/java/io/github/rosemoe/editor/interfaces/CodeAnalyzer.java b/app/src/main/java/io/github/rosemoe/editor/interfaces/CodeAnalyzer.java index 8fdece314..8229dc17e 100755 --- a/app/src/main/java/io/github/rosemoe/editor/interfaces/CodeAnalyzer.java +++ b/app/src/main/java/io/github/rosemoe/editor/interfaces/CodeAnalyzer.java @@ -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> diagnostics); - /** * Find the diagnostic containing the specific position * @@ -66,18 +51,6 @@ public interface CodeAnalyzer { */ List findDiagnosticsContainingLine(int line); - /** - * Get diagnostics at the specified line. - * - * @return A map containing the diagnostics mapped by their columns - */ - Map getDiagnosticsAtLine(int line); - - /** - * Get any hex color string is present in provided line - */ - Map getHexColorsInLine (int line); - /** * Analyze spans for the given input * diff --git a/app/src/main/java/io/github/rosemoe/editor/langs/AbstractCodeAnalyzer.java b/app/src/main/java/io/github/rosemoe/editor/langs/AbstractCodeAnalyzer.java index 6e355c75a..602d5c0e5 100755 --- a/app/src/main/java/io/github/rosemoe/editor/langs/AbstractCodeAnalyzer.java +++ b/app/src/main/java/io/github/rosemoe/editor/langs/AbstractCodeAnalyzer.java @@ -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> diagnostics) { - } @Override public DiagnosticItem findDiagnosticContaining(int line, int column) { @@ -38,16 +30,6 @@ public abstract class AbstractCodeAnalyzer implements CodeAnalyzer { public List findDiagnosticsContainingLine(int line) { return new ArrayList<>(); } - - @Override - public Map getDiagnosticsAtLine(int line) { - return new HashMap<>(); - } - - @Override - public Map getHexColorsInLine(int line) { - return new HashMap<> (); - } protected void checkAndAddHexString (@NonNull Token token, final int defaultColorId, TextAnalyzeResult result) { final var line = token.getLine () - 1; diff --git a/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java b/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java index 8d27b6ecf..47582088a 100755 --- a/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java +++ b/app/src/main/java/io/github/rosemoe/editor/langs/EmptyLanguage.java @@ -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> diagnostics) { - } } } diff --git a/app/src/main/java/io/github/rosemoe/editor/text/TextAnalyzer.java b/app/src/main/java/io/github/rosemoe/editor/text/TextAnalyzer.java index 17b0ce804..e23620c60 100755 --- a/app/src/main/java/io/github/rosemoe/editor/text/TextAnalyzer.java +++ b/app/src/main/java/io/github/rosemoe/editor/text/TextAnalyzer.java @@ -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; diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java b/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java index 0447a46a9..b732a2586 100755 --- a/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java @@ -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 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 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"); - } diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt index ea7ca1fe6..c2181d16e 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/CodeActions.kt @@ -32,7 +32,7 @@ data class CodeActionItem ( var changes: List, 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) { - constructor() : this (null, Collections.emptyList()) + constructor() : this (null, ArrayList()) } data class TextEdit (var range: Range, var newText: String) { diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/CodeAnalysis.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/CodeAnalysis.kt index 3c060c485..e4e645caf 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/CodeAnalysis.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/CodeAnalysis.kt @@ -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, var semanticHighlights: List) \ No newline at end of file +data class AnalyzeResult (var diagnostics: List, var semanticHighlights: List) { + constructor() : this (ArrayList(), ArrayList()) +} \ No newline at end of file diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt index 413970a93..be98ff47b 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt @@ -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 ) diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/Diagnostics.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/Diagnostics.kt index a82018dc6..a638fd193 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/Diagnostics.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/Diagnostics.kt @@ -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) + +data class DiagnosticResult(var file: Path, var diagnostics: List) { + +} enum class DiagnosticSeverity { ERROR, WARNING, INFO, HINT } \ No newline at end of file diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/Initialization.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/Initialization.kt index f5a6997a5..c87c1ac4e 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/Initialization.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/Initialization.kt @@ -20,7 +20,19 @@ package com.itsaky.lsp.models import java.nio.file.Path data class InitializeParams (var workspaceRoots: Set) -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) } \ No newline at end of file diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt index cb875f862..56952b205 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/Locations.kt @@ -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 { /** * 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 { 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; + } } \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java b/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java index 8f394c5b1..a1babc6ae 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/JavaLanguageServer.java @@ -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 diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/compiler/ReusableCompiler.java b/lsp/java/src/main/java/com/itsaky/lsp/java/compiler/ReusableCompiler.java index 36d6e82bb..ae31d1a99 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/compiler/ReusableCompiler.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/compiler/ReusableCompiler.java @@ -107,7 +107,7 @@ public class ReusableCompiler { Iterable compilationUnits) { if (checkedOut) { - throw new RuntimeException ("Compiler us already in use."); + throw new RuntimeException ("Compiler is already in-use!"); } checkedOut = true; diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java index de57dfc67..3117ca982 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CodeActionProvider.java @@ -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); } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/DiagnosticsProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/DiagnosticsProvider.java new file mode 100644 index 000000000..f5222872b --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/DiagnosticsProvider.java @@ -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 . + */ + +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 + 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 findDiagnostics (final CompileTask task, final Path file) { + final List 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 result) { + Map 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 result) { + for (Diagnostic 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("")) { + 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 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 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; + } + } +} diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCodeAnalyzer.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCodeAnalyzer.java index 024e382c8..acf0e5dfb 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCodeAnalyzer.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/JavaCodeAnalyzer.java @@ -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; } } diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/DiagnosticVisitor.java b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/DiagnosticVisitor.java new file mode 100644 index 000000000..3055b3a3d --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/visitors/DiagnosticVisitor.java @@ -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 . + */ + +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> { + + // Copied from TreePathScanner + // We need to be able to call scan(path, _) recursively + private TreePath path; + private Map declaredExceptions = new HashMap<> (); + private Set observedExceptions = new HashSet<> (); + + private final Trees trees; + private final Map privateDeclarations = new HashMap<>(), localVariables = new HashMap<>(); + private final Set 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 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 notUsed() { + Set 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 field was injected while forming the AST + unused.removeIf(i -> i.toString().equals("")); + 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 declared(MethodTree t) { + Map 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 notThrown) { + return super.visitCompilationUnit(t, notThrown); + } + + @Override + public Void visitVariable(VariableTree t, Map 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 notThrown) { + // Create a new method scope + Map pushDeclared = declaredExceptions; + Set 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 notThrown) { + if (isReachable(path)) { + super.visitClass(t, notThrown); + } else { + foundPrivateDeclaration(); + } + return null; + } + + @Override + public Void visitIdentifier(IdentifierTree t, Map notThrown) { + foundReference(); + return super.visitIdentifier(t, notThrown); + } + + @Override + public Void visitMemberSelect(MemberSelectTree t, Map notThrown) { + foundReference(); + return super.visitMemberSelect(t, notThrown); + } + + @Override + public Void visitMemberReference(MemberReferenceTree t, Map notThrown) { + foundReference(); + return super.visitMemberReference(t, notThrown); + } + + @Override + public Void visitNewClass(NewClassTree t, Map notThrown) { + foundReference(); + return super.visitNewClass(t, notThrown); + } + + @Override + public Void visitThrow(ThrowTree t, Map 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 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"); +} \ No newline at end of file diff --git a/lsp/java/src/main/res/values/strings.xml b/lsp/java/src/main/res/values/strings.xml new file mode 100644 index 000000000..fda0480b5 --- /dev/null +++ b/lsp/java/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + \'%s\' is not thrown in the body of the method + \'%s\' is not used + \ No newline at end of file