-
Notifications
You must be signed in to change notification settings - Fork 755
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42341 from nipunayf/fix-42314
Provide code actions to fix invalid non-private mutable field
- Loading branch information
Showing
47 changed files
with
1,294 additions
and
0 deletions.
There are no files selected for viewing
106 changes: 106 additions & 0 deletions
106
...java/org/ballerinalang/langserver/codeaction/providers/AddPrivateQualifierCodeAction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com) | ||
* | ||
* WSO2 LLC. licenses this file to you under the Apache License, | ||
* Version 2.0 (the "License"); you may not use this file except | ||
* in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.ballerinalang.langserver.codeaction.providers; | ||
|
||
import io.ballerina.compiler.syntax.tree.Node; | ||
import io.ballerina.compiler.syntax.tree.NodeList; | ||
import io.ballerina.compiler.syntax.tree.ObjectFieldNode; | ||
import io.ballerina.compiler.syntax.tree.SyntaxKind; | ||
import io.ballerina.compiler.syntax.tree.Token; | ||
import io.ballerina.tools.diagnostics.Diagnostic; | ||
import org.ballerinalang.annotation.JavaSPIService; | ||
import org.ballerinalang.langserver.codeaction.CodeActionNodeValidator; | ||
import org.ballerinalang.langserver.codeaction.CodeActionUtil; | ||
import org.ballerinalang.langserver.common.constants.CommandConstants; | ||
import org.ballerinalang.langserver.common.utils.PositionUtil; | ||
import org.ballerinalang.langserver.commons.CodeActionContext; | ||
import org.ballerinalang.langserver.commons.codeaction.spi.DiagBasedPositionDetails; | ||
import org.ballerinalang.langserver.commons.codeaction.spi.DiagnosticBasedCodeActionProvider; | ||
import org.eclipse.lsp4j.CodeAction; | ||
import org.eclipse.lsp4j.CodeActionKind; | ||
import org.eclipse.lsp4j.Position; | ||
import org.eclipse.lsp4j.Range; | ||
import org.eclipse.lsp4j.TextEdit; | ||
|
||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
/** | ||
* Code Action for adding the private visibility qualifier ƒor an object field. | ||
* | ||
* @since 2201.9.0 | ||
*/ | ||
@JavaSPIService("org.ballerinalang.langserver.commons.codeaction.spi.LSCodeActionProvider") | ||
public class AddPrivateQualifierCodeAction implements DiagnosticBasedCodeActionProvider { | ||
|
||
private static final String NAME = "Add private visibility qualifier"; | ||
private static final String DIAGNOSTIC_CODE = "BCE3956"; | ||
|
||
@Override | ||
public boolean validate(Diagnostic diagnostic, DiagBasedPositionDetails positionDetails, | ||
CodeActionContext context) { | ||
return DIAGNOSTIC_CODE.equals(diagnostic.diagnosticInfo().code()) | ||
&& CodeActionNodeValidator.validate(context.nodeAtRange()); | ||
} | ||
|
||
@Override | ||
public List<CodeAction> getCodeActions(Diagnostic diagnostic, DiagBasedPositionDetails positionDetails, | ||
CodeActionContext context) { | ||
Node cursorNode = positionDetails.matchedNode(); | ||
if (cursorNode.kind() != SyntaxKind.OBJECT_FIELD) { | ||
assert false : "This line is unreachable as the diagnostic is only generated for an object field."; | ||
return Collections.emptyList(); | ||
} | ||
|
||
return Collections.singletonList(CodeActionUtil.createCodeAction( | ||
CommandConstants.ADD_PRIVATE_QUALIFIER, | ||
List.of(getTextEdit((ObjectFieldNode) cursorNode)), | ||
context.fileUri(), | ||
CodeActionKind.QuickFix | ||
)); | ||
} | ||
|
||
private static TextEdit getTextEdit(ObjectFieldNode node) { | ||
String privateKeyword = SyntaxKind.PRIVATE_KEYWORD.stringValue(); | ||
|
||
// Get the line range of the existing visibility qualifier | ||
Optional<Token> visibilityQualifier = node.visibilityQualifier(); | ||
if (visibilityQualifier.isPresent()) { | ||
return new TextEdit(PositionUtil.toRange(visibilityQualifier.get().lineRange()), privateKeyword); | ||
} | ||
privateKeyword += " "; | ||
|
||
// Get the start position of the qualifier list | ||
NodeList<Token> qualifiers = node.qualifierList(); | ||
if (qualifiers.size() > 0) { | ||
Position position = PositionUtil.toPosition(qualifiers.get(0).lineRange().startLine()); | ||
return new TextEdit(new Range(position, position), privateKeyword); | ||
} | ||
|
||
// Get the start position of the type name | ||
Position position = PositionUtil.toPosition(node.typeName().lineRange().startLine()); | ||
return new TextEdit(new Range(position, position), privateKeyword); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return NAME; | ||
} | ||
} |
174 changes: 174 additions & 0 deletions
174
...va/org/ballerinalang/langserver/codeaction/providers/MakeVariableImmutableCodeAction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
/* | ||
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com) | ||
* | ||
* WSO2 LLC. licenses this file to you under the Apache License, | ||
* Version 2.0 (the "License"); you may not use this file except | ||
* in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.ballerinalang.langserver.codeaction.providers; | ||
|
||
import io.ballerina.compiler.api.SemanticModel; | ||
import io.ballerina.compiler.api.symbols.ClassFieldSymbol; | ||
import io.ballerina.compiler.api.symbols.Symbol; | ||
import io.ballerina.compiler.api.symbols.SymbolKind; | ||
import io.ballerina.compiler.api.symbols.TypeDescKind; | ||
import io.ballerina.compiler.api.symbols.TypeSymbol; | ||
import io.ballerina.compiler.syntax.tree.Node; | ||
import io.ballerina.compiler.syntax.tree.NonTerminalNode; | ||
import io.ballerina.compiler.syntax.tree.ObjectFieldNode; | ||
import io.ballerina.compiler.syntax.tree.SyntaxKind; | ||
import io.ballerina.tools.diagnostics.Diagnostic; | ||
import io.ballerina.tools.text.LinePosition; | ||
import org.ballerinalang.annotation.JavaSPIService; | ||
import org.ballerinalang.langserver.codeaction.CodeActionNodeValidator; | ||
import org.ballerinalang.langserver.codeaction.CodeActionUtil; | ||
import org.ballerinalang.langserver.common.constants.CommandConstants; | ||
import org.ballerinalang.langserver.common.utils.PositionUtil; | ||
import org.ballerinalang.langserver.commons.CodeActionContext; | ||
import org.ballerinalang.langserver.commons.codeaction.spi.DiagBasedPositionDetails; | ||
import org.ballerinalang.langserver.commons.codeaction.spi.DiagnosticBasedCodeActionProvider; | ||
import org.eclipse.lsp4j.CodeAction; | ||
import org.eclipse.lsp4j.CodeActionKind; | ||
import org.eclipse.lsp4j.Position; | ||
import org.eclipse.lsp4j.Range; | ||
import org.eclipse.lsp4j.TextEdit; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
/** | ||
* Code Action for making a variable immutable. This will ensure that the given variable is both final and readonly. | ||
* | ||
* @since 2201.9.0 | ||
*/ | ||
@JavaSPIService("org.ballerinalang.langserver.commons.codeaction.spi.LSCodeActionProvider") | ||
public class MakeVariableImmutableCodeAction implements DiagnosticBasedCodeActionProvider { | ||
|
||
private static final String NAME = "Make variable immutable"; | ||
private static final String DIAGNOSTIC_CODE = "BCE3956"; | ||
|
||
@Override | ||
public boolean validate(Diagnostic diagnostic, DiagBasedPositionDetails positionDetails, | ||
CodeActionContext context) { | ||
return DIAGNOSTIC_CODE.equals(diagnostic.diagnosticInfo().code()) | ||
&& CodeActionNodeValidator.validate(context.nodeAtRange()); | ||
} | ||
|
||
@Override | ||
public List<CodeAction> getCodeActions(Diagnostic diagnostic, DiagBasedPositionDetails positionDetails, | ||
CodeActionContext context) { | ||
NonTerminalNode cursorNode = positionDetails.matchedNode(); | ||
|
||
// The current implementation of the CA only supports object fields | ||
if (cursorNode.kind() != SyntaxKind.OBJECT_FIELD) { | ||
assert false : "This line is unreachable as the diagnostic is only generated for an object field."; | ||
return Collections.emptyList(); | ||
} | ||
|
||
ObjectFieldNode objectFieldNode = (ObjectFieldNode) cursorNode; | ||
Node typeNode = objectFieldNode.typeName(); | ||
List<TextEdit> textEdits = new ArrayList<>(); | ||
|
||
// Check if the type is final | ||
boolean isFinal = objectFieldNode.qualifierList().stream() | ||
.anyMatch(token -> token.kind().equals(SyntaxKind.FINAL_KEYWORD)); | ||
if (!isFinal) { | ||
textEdits.add(getFinalTextEdit(typeNode)); | ||
} | ||
|
||
// Check if the type is readonly | ||
TypeSymbol typeSymbol, readonlyType; | ||
try { | ||
SemanticModel semanticModel = context.currentSemanticModel().orElseThrow(); | ||
readonlyType = semanticModel.types().READONLY; | ||
Symbol symbol = semanticModel.symbol(cursorNode).orElseThrow(); | ||
typeSymbol = getTypeSymbol(symbol).orElseThrow(); | ||
} catch (RuntimeException e) { | ||
assert false : "This line is unreachable because the semantic model cannot be empty, and the type " + | ||
"symbol does not contain errors."; | ||
return Collections.emptyList(); | ||
} | ||
boolean isReadonly = typeSymbol.subtypeOf(readonlyType); | ||
if (!isReadonly) { | ||
textEdits.addAll(getReadonlyTextEdits(typeNode, typeSymbol.typeKind() == TypeDescKind.UNION)); | ||
} | ||
|
||
// Generate and return the code action | ||
return Collections.singletonList(CodeActionUtil.createCodeAction( | ||
String.format(CommandConstants.MAKE_VARIABLE_IMMUTABLE, getTitleText(isFinal, isReadonly)), | ||
textEdits, | ||
context.fileUri(), | ||
CodeActionKind.QuickFix)); | ||
} | ||
|
||
private static Optional<TypeSymbol> getTypeSymbol(Symbol symbol) { | ||
if (symbol.kind() == SymbolKind.CLASS_FIELD) { | ||
return Optional.of(((ClassFieldSymbol) symbol).typeDescriptor()); | ||
} | ||
assert false : "Unconsidered symbol type found: " + symbol.kind(); | ||
return Optional.empty(); | ||
} | ||
|
||
private static TextEdit getFinalTextEdit(Node typeNode) { | ||
LinePosition linePosition = typeNode.lineRange().startLine(); | ||
Position position = PositionUtil.toPosition(linePosition); | ||
String editText = SyntaxKind.FINAL_KEYWORD.stringValue() + " "; | ||
return new TextEdit(new Range(position, position), editText); | ||
} | ||
|
||
private static List<TextEdit> getReadonlyTextEdits(Node typeNode, boolean isUnion) { | ||
LinePosition startLinePosition = typeNode.lineRange().startLine(); | ||
LinePosition endLinePosition = typeNode.lineRange().endLine(); | ||
List<TextEdit> textEdits = new ArrayList<>(); | ||
|
||
if (isUnion) { | ||
Position startPosition = PositionUtil.toPosition(startLinePosition); | ||
TextEdit startTextEdit = new TextEdit(new Range(startPosition, startPosition), | ||
SyntaxKind.OPEN_PAREN_TOKEN.stringValue()); | ||
textEdits.add(startTextEdit); | ||
} | ||
|
||
Position endPosition = PositionUtil.toPosition(endLinePosition); | ||
String editText = (isUnion ? SyntaxKind.CLOSE_PAREN_TOKEN.stringValue() : "") + " & " + | ||
SyntaxKind.READONLY_KEYWORD.stringValue(); | ||
TextEdit endTextEdit = new TextEdit(new Range(endPosition, endPosition), editText); | ||
textEdits.add(endTextEdit); | ||
|
||
return textEdits; | ||
} | ||
|
||
private static String getTitleText(boolean isFinal, boolean isReadonly) { | ||
StringBuilder result = new StringBuilder(); | ||
|
||
if (!isFinal) { | ||
result.append("'").append(SyntaxKind.FINAL_KEYWORD.stringValue()).append("'"); | ||
} | ||
|
||
if (!isReadonly) { | ||
if (result.length() > 0) { | ||
result.append(" and "); | ||
} | ||
result.append("'").append(SyntaxKind.READONLY_KEYWORD.stringValue()).append("'"); | ||
} | ||
|
||
return result.toString(); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return NAME; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
.../test/java/org/ballerinalang/langserver/codeaction/AddPrivateQualifierCodeActionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com) | ||
* | ||
* WSO2 LLC. licenses this file to you under the Apache License, | ||
* Version 2.0 (the "License"); you may not use this file except | ||
* in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.ballerinalang.langserver.codeaction; | ||
|
||
import org.ballerinalang.langserver.commons.workspace.WorkspaceDocumentException; | ||
import org.testng.annotations.DataProvider; | ||
import org.testng.annotations.Test; | ||
|
||
import java.io.IOException; | ||
|
||
/** | ||
* Test class to test the change isolated field to private code action. | ||
* | ||
* @since 2201.9.0 | ||
*/ | ||
public class AddPrivateQualifierCodeActionTest extends AbstractCodeActionTest { | ||
|
||
@Test(dataProvider = "codeaction-data-provider") | ||
@Override | ||
public void test(String config) throws IOException, WorkspaceDocumentException { | ||
super.test(config); | ||
} | ||
|
||
@DataProvider(name = "codeaction-data-provider") | ||
@Override | ||
public Object[][] dataProvider() { | ||
return new Object[][]{ | ||
{"change_field_private1.json"}, | ||
{"change_field_private2.json"}, | ||
{"change_field_private3.json"}, | ||
{"change_field_private4.json"}, | ||
{"change_field_private5.json"}, | ||
{"change_field_private6.json"}, | ||
}; | ||
} | ||
|
||
@Override | ||
public String getResourceDir() { | ||
return "change-field-private"; | ||
} | ||
} |
Oops, something went wrong.