Skip to content

Commit

Permalink
Merge pull request #42349 from nipunayf/fix-42323
Browse files Browse the repository at this point in the history
Increase the scope of the `Add isolated qualifier` code action
  • Loading branch information
nipunayf authored Apr 4, 2024
2 parents 223d87f + a4e9958 commit fe0590a
Show file tree
Hide file tree
Showing 16 changed files with 426 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package org.ballerinalang.langserver.codeaction.providers;

import io.ballerina.compiler.api.symbols.ClassSymbol;
import io.ballerina.compiler.api.symbols.Symbol;
import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol;
import io.ballerina.compiler.syntax.tree.ExplicitAnonymousFunctionExpressionNode;
import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode;
import io.ballerina.compiler.syntax.tree.NonTerminalNode;
import io.ballerina.compiler.syntax.tree.SyntaxKind;
import io.ballerina.compiler.syntax.tree.Token;
import io.ballerina.projects.Project;
import io.ballerina.tools.diagnostics.Diagnostic;
import org.ballerinalang.annotation.JavaSPIService;
Expand Down Expand Up @@ -51,8 +54,12 @@
*/
@JavaSPIService("org.ballerinalang.langserver.commons.codeaction.spi.LSCodeActionProvider")
public class AddIsolatedQualifierCodeAction implements DiagnosticBasedCodeActionProvider {

public static final String NAME = "Add Isolated Qualifier";
public static final Set<String> DIAGNOSTIC_CODES = Set.of("BCE3946", "BCE3947");
private static final String DIAGNOSTIC_CODE_3961 = "BCE3961";
private static final String ANONYMOUS_FUNCTION_EXPRESSION = "Anonymous function expression";
private static final Set<String> DIAGNOSTIC_CODES =
Set.of("BCE3946", "BCE3947", "BCE3950", DIAGNOSTIC_CODE_3961);

@Override
public boolean validate(Diagnostic diagnostic,
Expand All @@ -68,56 +75,88 @@ public boolean validate(Diagnostic diagnostic,
public List<CodeAction> getCodeActions(Diagnostic diagnostic,
DiagBasedPositionDetails positionDetails,
CodeActionContext context) {
Range diagnosticRange = PositionUtil.toRange(diagnostic.location().lineRange());
// isPresent() check is done in the validator
NonTerminalNode nonTerminalNode = CommonUtil.findNode(diagnosticRange, context.currentSyntaxTree().get());
NonTerminalNode nonTerminalNode = positionDetails.matchedNode();

// Check if the diagnostic is for an anonymous function expression
if (nonTerminalNode.kind() == SyntaxKind.EXPLICIT_ANONYMOUS_FUNCTION_EXPRESSION) {
return getExplicitAnonFuncExpressionCodeAction((ExplicitAnonymousFunctionExpressionNode) nonTerminalNode,
context);
ExplicitAnonymousFunctionExpressionNode functionExpressionNode =
(ExplicitAnonymousFunctionExpressionNode) nonTerminalNode;
return getCodeAction(functionExpressionNode.functionKeyword(), ANONYMOUS_FUNCTION_EXPRESSION,
context.fileUri());
}

// Check if there are multiple diagnostics of the considered category
List<Diagnostic> diagnostics = context.diagnostics(context.filePath());
if (diagnostics.size() > 1 && hasMultipleDiagnostics(nonTerminalNode, diagnostic, diagnostics)) {
return Collections.emptyList();
}
Optional<Symbol> symbol = context.currentSemanticModel().get().symbol(nonTerminalNode);

// Obtain the symbol of the referred function
Optional<Symbol> symbol = getReferredSymbol(context, nonTerminalNode);
if (symbol.isEmpty() || symbol.get().getModule().isEmpty()) {
return Collections.emptyList();
}

// Obtain the current project
Optional<Project> project = context.workspace().project(context.filePath());
if (project.isEmpty()) {
return Collections.emptyList();
}

// Obtain the file path of the referred symbol
Optional<Path> filePath = PathUtil.getFilePathForSymbol(symbol.get(), project.get(), context);
if (filePath.isEmpty() || context.workspace().syntaxTree(filePath.get()).isEmpty()) {
return Collections.emptyList();
}

// Obtain the node of the referred symbol
Optional<NonTerminalNode> node = CommonUtil.findNode(symbol.get(),
context.workspace().syntaxTree(filePath.get()).get());
if (node.isEmpty() || !node.get().kind().equals(SyntaxKind.FUNCTION_DEFINITION)) {
if (node.isEmpty() || isUnsupportedSyntaxKind(node.get().kind())) {
return Collections.emptyList();
}

FunctionDefinitionNode functionDefinitionNode = (FunctionDefinitionNode) node.get();
Position position = PositionUtil.toPosition(functionDefinitionNode.functionKeyword().lineRange().startLine());

Range range = new Range(position, position);
String editText = SyntaxKind.ISOLATED_KEYWORD.stringValue() + " ";
TextEdit textEdit = new TextEdit(range, editText);
List<TextEdit> editList = List.of(textEdit);
String commandTitle = String.format(CommandConstants.MAKE_FUNCTION_ISOLATE, symbol.get().getName().orElse(""));
return Collections.singletonList(CodeActionUtil
.createCodeAction(commandTitle, editList, filePath.get().toUri().toString(), CodeActionKind.QuickFix));
return getCodeAction(functionDefinitionNode.functionKeyword(), symbol.get().getName().orElse(""),
filePath.get().toUri().toString());
}

private List<CodeAction> getExplicitAnonFuncExpressionCodeAction(ExplicitAnonymousFunctionExpressionNode node,
CodeActionContext context) {
Position position = PositionUtil.toPosition(node.functionKeyword().lineRange().startLine());
Range range = new Range(position, position);
private static Optional<Symbol> getReferredSymbol(CodeActionContext context, NonTerminalNode node) {
SyntaxKind kind = node.kind();
if (kind == SyntaxKind.EXPLICIT_NEW_EXPRESSION || kind == SyntaxKind.IMPLICIT_NEW_EXPRESSION) {
try {
TypeReferenceTypeSymbol typeSymbol = (TypeReferenceTypeSymbol) context.currentSemanticModel()
.flatMap(semanticModel -> semanticModel.typeOf(node))
.orElseThrow();
ClassSymbol definition = (ClassSymbol) typeSymbol.definition();
return Optional.of(definition.initMethod().orElseThrow());
} catch (RuntimeException e) {
assert false : "This line is unreachable because the diagnostic is not produced otherwise.";
return Optional.empty();
}
}
return context.currentSemanticModel().flatMap(semanticModel -> semanticModel.symbol(node));
}

private static List<CodeAction> getCodeAction(Token functionKeyword, String expressionName, String filePath) {
Position position = PositionUtil.toPosition(functionKeyword.lineRange().startLine());
String editText = SyntaxKind.ISOLATED_KEYWORD.stringValue() + " ";
TextEdit textEdit = new TextEdit(range, editText);
List<TextEdit> editList = List.of(textEdit);
String commandTitle = String.format(CommandConstants.MAKE_FUNCTION_ISOLATE, "Anonymous function expression");
return Collections.singletonList(CodeActionUtil.createCodeAction(commandTitle, editList, context.fileUri(),
CodeActionKind.QuickFix));
TextEdit textEdit = new TextEdit(new Range(position, position), editText);
String commandTitle = String.format(CommandConstants.MAKE_FUNCTION_ISOLATE, expressionName);
return Collections.singletonList(
CodeActionUtil.createCodeAction(commandTitle, List.of(textEdit), filePath, CodeActionKind.QuickFix));
}

private static boolean isUnsupportedSyntaxKind(SyntaxKind kind) {
return kind != SyntaxKind.FUNCTION_DEFINITION && kind != SyntaxKind.OBJECT_METHOD_DEFINITION;
}

private static boolean hasMultipleDiagnostics(NonTerminalNode node, Diagnostic currentDiagnostic,
List<Diagnostic> diagnostics) {
return diagnostics.stream().anyMatch(diagnostic -> !currentDiagnostic.equals(diagnostic) &&
DIAGNOSTIC_CODES.contains(diagnostic.diagnosticInfo().code()) &&
PositionUtil.isWithinLineRange(diagnostic.location().lineRange(), node.lineRange())) &&
currentDiagnostic.diagnosticInfo().code().equals(DIAGNOSTIC_CODE_3961);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,16 @@ public Object[][] dataProvider() {
{"add_isolated_qualifier_config1.json"},
{"add_isolated_qualifier_config2.json"},
{"add_isolated_qualifier_config3.json"},
{"add_isolated_qualifier_config4.json"}
{"add_isolated_qualifier_config4.json"},
{"add_isolated_qualifier_config5.json"},
{"add_isolated_qualifier_config6.json"},
{"add_isolated_qualifier_config7.json"},
{"add_isolated_qualifier_config8.json"},
{"add_isolated_qualifier_config9.json"},
{"add_isolated_qualifier_config10.json"},
{"add_isolated_qualifier_config11.json"},
{"add_isolated_qualifier_config12.json"},
{"add_isolated_qualifier_config13.json"},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"position": {
"line": 35,
"character": 31
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source2.bal",
"expected": [
{
"title": "Add isolated qualifier to 'init'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 11,
"character": 4
},
"end": {
"line": 11,
"character": 4
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"position": {
"line": 38,
"character": 23
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source2.bal",
"expected": [
{
"title": "Add isolated qualifier to 'init'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 11,
"character": 4
},
"end": {
"line": 11,
"character": 4
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"position": {
"line": 40,
"character": 6
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source2.bal",
"expected": [
{
"title": "Add isolated qualifier to 'fn'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 5,
"character": 4
},
"end": {
"line": 5,
"character": 4
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"position": {
"line": 43,
"character": 15
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source2.bal",
"expected": [
{
"title": "Add isolated qualifier to 'fn'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 15,
"character": 4
},
"end": {
"line": 15,
"character": 4
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
{
"position": {
"line": 6,
"character": 24
"character": 42
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source4.bal",
"expected": []
"expected": [
{
"title": "Add isolated qualifier to 'Anonymous function expression'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 6,
"character": 36
},
"end": {
"line": 6,
"character": 36
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"position": {
"line": 9,
"character": 20
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source2.bal",
"expected": [
{
"title": "Add isolated qualifier to 'testFunctionWithReturn'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 4,
"character": 0
},
"end": {
"line": 4,
"character": 0
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"position": {
"line": 15,
"character": 15
},
"source": "isolatedFunctionCodeAction/modules/module2/add_isolated_qualifier_source2.bal",
"expected": [
{
"title": "Add isolated qualifier to 'testFunctionWithReturn'",
"kind": "quickfix",
"edits": [
{
"range": {
"start": {
"line": 4,
"character": 0
},
"end": {
"line": 4,
"character": 0
}
},
"newText": "isolated "
}
],
"resolvable": false
}
]
}
Loading

0 comments on commit fe0590a

Please sign in to comment.