Skip to content

Commit

Permalink
Add code action to create DEFINE PROTOTYPE (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkusAmshove authored Nov 29, 2023
2 parents a8e7b1d + f4ab733 commit d655931
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.amshove.natlint.analyzers;

import org.amshove.natlint.api.AbstractAnalyzer;
import org.amshove.natlint.api.DiagnosticDescription;
import org.amshove.natlint.api.IAnalyzeContext;
import org.amshove.natlint.api.ILinterContext;
import org.amshove.natparse.DiagnosticSeverity;
import org.amshove.natparse.NodeUtil;
import org.amshove.natparse.ReadOnlyList;
import org.amshove.natparse.natural.IDefinePrototypeNode;
import org.amshove.natparse.natural.ISyntaxNode;

public class DefinePrototypeAnalyzer extends AbstractAnalyzer
{
public static final DiagnosticDescription PROTOTYPE_DEFINED_MORE_THAN_ONCE = DiagnosticDescription.create(
"NL022",
"Prototype for function %s is defined more than once",
DiagnosticSeverity.WARNING
);

@Override
public ReadOnlyList<DiagnosticDescription> getDiagnosticDescriptions()
{
return ReadOnlyList.of(PROTOTYPE_DEFINED_MORE_THAN_ONCE);
}

@Override
public void initialize(ILinterContext context)
{
context.registerNodeAnalyzer(IDefinePrototypeNode.class, this::analyzeDefinePrototype);
}

private void analyzeDefinePrototype(ISyntaxNode node, IAnalyzeContext context)
{
var prototype = ((IDefinePrototypeNode) node);
var prototypesInModule = NodeUtil.findNodesOfType(context.getModule().syntaxTree(), IDefinePrototypeNode.class);

for (var otherPrototype : prototypesInModule)
{
if (otherPrototype == prototype)
{
continue;
}

if (otherPrototype.nameToken().symbolName().equals(prototype.nameToken().symbolName()))
{
context.report(
PROTOTYPE_DEFINED_MORE_THAN_ONCE.createFormattedDiagnostic(
otherPrototype.nameToken(),
otherPrototype.nameToken().symbolName()
)
);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.amshove.natlint.analyzers;

import org.amshove.natlint.linter.AbstractAnalyzerTest;
import org.junit.jupiter.api.Test;

class DefinePrototypeAnalyzerShould extends AbstractAnalyzerTest
{

@Test
void beRaisedWhenMoreThanOnePrototypeForTheSameFunctionIsDefined()
{
testDiagnostics(
"""
DEFINE DATA LOCAL
END-DEFINE
DEFINE PROTOTYPE FUNC RETURNS (L)
END-PROTOTYPE
DEFINE PROTOTYPE FUNC RETURNS (L)
END-PROTOTYPE
END
""",
expectDiagnostic(3, DefinePrototypeAnalyzer.PROTOTYPE_DEFINED_MORE_THAN_ONCE),
expectDiagnostic(6, DefinePrototypeAnalyzer.PROTOTYPE_DEFINED_MORE_THAN_ONCE)
);
}

@Test
void notBeRaisedWhenUniquePrototypesAreDefined()
{
testDiagnostics(
"""
DEFINE DATA LOCAL
END-DEFINE
DEFINE PROTOTYPE FUNC RETURNS (L)
END-PROTOTYPE
DEFINE PROTOTYPE FUNC2 RETURNS (L)
END-PROTOTYPE
END
""",
expectNoDiagnosticOfType(DefinePrototypeAnalyzer.PROTOTYPE_DEFINED_MORE_THAN_ONCE)
);
}

@Test
void notBeRaisedWhenNoPrototypeIsDefined()
{
testDiagnostics(
"""
DEFINE DATA LOCAL
END-DEFINE
END
""",
expectNoDiagnosticOfType(DefinePrototypeAnalyzer.PROTOTYPE_DEFINED_MORE_THAN_ONCE)
);
}

protected DefinePrototypeAnalyzerShould()
{
super(new DefinePrototypeAnalyzer());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
import org.amshove.natls.languageserver.LspUtil;
import org.amshove.natls.project.LanguageServerFile;
import org.amshove.natparse.IPosition;
import org.amshove.natparse.natural.IHasDefineData;
import org.amshove.natparse.natural.ISyntaxNode;
import org.amshove.natparse.natural.ITokenNode;
import org.amshove.natparse.natural.VariableScope;
import org.amshove.natparse.natural.*;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
Expand Down Expand Up @@ -127,4 +124,12 @@ public WorkspaceEditBuilder changesRange(LanguageServerFile file, Range range, S
edits.add(new TextEdit(range, source));
return this;
}

public WorkspaceEditBuilder addPrototype(LanguageServerFile file, IFunction calledFunction)
{
var fileEdit = FileEdits.addPrototype(file, calledFunction);
var edits = textEdits.computeIfAbsent(fileEdit.fileUri(), u -> new ArrayList<>());
edits.add(fileEdit.textEdit());
return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.amshove.natls.codeactions;

import org.amshove.natls.languageserver.NaturalLanguageService;
import org.amshove.natls.project.LanguageServerFile;
import org.amshove.natparse.lexing.SyntaxToken;
import org.amshove.natparse.natural.INaturalModule;
Expand All @@ -14,6 +15,7 @@ public record RefactoringContext(
String fileUri,
INaturalModule module,
LanguageServerFile file,
NaturalLanguageService service, // TODO: Remove again?
SyntaxToken tokenUnderCursor,
Range originalRange,
ISyntaxNode nodeAtStartPosition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public CodeInsertion findInsertionPositionToInsertVariable(LanguageServerFile fi
.orElse(new CodeInsertion(file.getPath(), "%s%n".formatted(scope.toString()), findRangeForNewScope(file, scope), System.lineSeparator()));
}

public CodeInsertion findInsertionPositionForStatement(LanguageServerFile file)
public CodeInsertion findInsertionPositionForStatementAtEnd(LanguageServerFile file)
{
var withBody = (IModuleWithBody) file.module();

Expand Down Expand Up @@ -106,6 +106,14 @@ public CodeInsertion insertInNextLineAfter(ISyntaxNode node)
);
}

public CodeInsertion findInsertionPositionForStatementAtStart(LanguageServerFile file)
{
var withBody = (IModuleWithBody) file.module();

var firstStatement = withBody.body().statements().first();
return new CodeInsertion(file.getPath(), "", LspUtil.toRangeBefore(firstStatement.position()));
}

private static IPosition findLastNodeInFileThatCantHaveStatementsAfter(NaturalFileType type, IModuleWithBody withBody)
{
if (type == NaturalFileType.FUNCTION)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.amshove.natls.codemutation;

import org.amshove.natls.project.LanguageServerFile;
import org.amshove.natparse.natural.IGroupNode;
import org.amshove.natparse.natural.IHasDefineData;
import org.amshove.natparse.natural.VariableScope;
import org.amshove.natparse.natural.*;

import java.util.stream.Collectors;

public class FileEdits
{
Expand Down Expand Up @@ -61,10 +61,53 @@ public static FileEdit addSubroutine(LanguageServerFile file, String name, Strin
END-SUBROUTINE
""".formatted(name, source);

var insertion = rangeFinder.findInsertionPositionForStatement(file);
var insertion = rangeFinder.findInsertionPositionForStatementAtEnd(file);
return insertion.toFileEdit(subroutine);
}

public static FileEdit addPrototype(LanguageServerFile inFile, IFunction calledFunction)
{
var insertion = rangeFinder.findInsertionPositionForStatementAtStart(inFile);

var defineDataBlock = "";
var parameter = calledFunction.defineData().parameterInOrder();
if (!parameter.isEmpty())
{
defineDataBlock = """
%n DEFINE DATA
%s
END-DEFINE""".formatted(
calledFunction.defineData().parameterInOrder().stream().map(p ->
{
if (p instanceof IUsingNode using)
{
return "PARAMETER USING %s".formatted(using.target().symbolName());
}
var parameterVariable = (IVariableNode) p;
if (parameterVariable instanceof ITypedVariableNode typedParameter)
{
return "PARAMETER %d %s %s".formatted(typedParameter.level(), typedParameter.name(), typedParameter.formatTypeForDisplay());
}

return "PARAMETER %d %s".formatted(parameterVariable.level(), ((IVariableNode) p).name());
})
.map(p -> " " + p)
.collect(Collectors.joining(System.lineSeparator()))
);
}

return insertion.toFileEdit(
"""
DEFINE PROTOTYPE %s RETURNS %s%s
END-PROTOTYPE
""".formatted(
calledFunction.name(),
calledFunction.returnType().toShortString(),
defineDataBlock
)
);
}

private static FileEdit createUsingInsert(UsingToAdd using, LanguageServerFile file)
{
var insertion = rangeFinder.findInsertionPositionToInsertUsing(file, using.scope());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ public List<CodeAction> codeAction(CodeActionParams params)
var statementUnderCursor = nodeAtStart instanceof IStatementNode statement
? statement
: NodeUtil.findFirstParentOfType(nodeAtStart, IStatementNode.class);
var context = new RefactoringContext(params.getTextDocument().getUri(), file.module(), file, token, params.getRange(), nodeAtStart, nodeAtEnd, statementUnderCursor, diagnosticsAtPosition);
var context = new RefactoringContext(params.getTextDocument().getUri(), file.module(), file, this, token, params.getRange(), nodeAtStart, nodeAtEnd, statementUnderCursor, diagnosticsAtPosition);

return codeActionRegistry.createCodeActions(context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.amshove.natls.refactorings;

import org.amshove.natls.WorkspaceEditBuilder;
import org.amshove.natls.codeactions.ICodeActionProvider;
import org.amshove.natls.codeactions.RefactoringContext;
import org.amshove.natls.quickfixes.CodeActionBuilder;
import org.amshove.natparse.natural.IFunction;
import org.amshove.natparse.natural.IFunctionCallNode;
import org.amshove.natparse.natural.project.NaturalFileType;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;

import java.util.List;

public class DefinePrototypeAction implements ICodeActionProvider
{
@Override
public boolean isApplicable(RefactoringContext context)
{
return context.file().getType() == NaturalFileType.FUNCTION && context.nodeAtStartPosition() instanceof IFunctionCallNode;
}

@Override
public List<CodeAction> createCodeAction(RefactoringContext context)
{
var functionCall = (IFunctionCallNode) context.nodeAtStartPosition();
if (functionCall.reference() == null)
{
return List.of();
}

var function = (IFunction) context.service().findNaturalFile(functionCall.reference().file().getPath()).module();

return List.of(
new CodeActionBuilder("Define Prototype", CodeActionKind.Refactor)
.appliesWorkspaceEdit(
new WorkspaceEditBuilder()
.addPrototype(context.file(), function)
)
.build()
);
}
}
Loading

0 comments on commit d655931

Please sign in to comment.