Skip to content

Commit

Permalink
Merge pull request #42341 from nipunayf/fix-42314
Browse files Browse the repository at this point in the history
Provide code actions to fix invalid non-private mutable field
  • Loading branch information
LakshanWeerasinghe authored Apr 4, 2024
2 parents 582d5b3 + b57ea61 commit ef42f69
Show file tree
Hide file tree
Showing 47 changed files with 1,294 additions and 0 deletions.
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;
}
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ public class CommandConstants {

public static final String CHANGE_PARAM_TYPE_TITLE = "Change parameter '%s' type to '%s'";

public static final String ADD_PRIVATE_QUALIFIER = "Add private qualifier";

public static final String MAKE_VARIABLE_IMMUTABLE = "Add %s to the variable";

public static final String CREATE_VAR_TYPE_GUARD_TITLE = "Create variable and type guard";

public static final String TYPE_GUARD_TITLE = "Type guard variable '%s'";
Expand Down
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";
}
}
Loading

0 comments on commit ef42f69

Please sign in to comment.