diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/RosettaIdeModule.xtend b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/RosettaIdeModule.xtend index c198ea4f0..c8759a0df 100644 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/RosettaIdeModule.xtend +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/RosettaIdeModule.xtend @@ -4,36 +4,40 @@ package com.regnosys.rosetta.ide import com.regnosys.rosetta.generator.RosettaOutputConfigurationProvider -import org.eclipse.xtext.generator.IContextualOutputConfigurationProvider -import org.eclipse.xtext.documentation.IEObjectDocumentationProvider +import com.regnosys.rosetta.ide.contentassist.RosettaContentProposalProvider +import com.regnosys.rosetta.ide.contentassist.cancellable.CancellableContentAssistService +import com.regnosys.rosetta.ide.contentassist.cancellable.CancellableRosettaParser +import com.regnosys.rosetta.ide.contentassist.cancellable.ICancellableContentAssistParser +import com.regnosys.rosetta.ide.contentassist.cancellable.RosettaOperationCanceledManager +import com.regnosys.rosetta.ide.formatting.RosettaFormattingService import com.regnosys.rosetta.ide.hover.RosettaDocumentationProvider +import com.regnosys.rosetta.ide.hover.RosettaHoverService import com.regnosys.rosetta.ide.inlayhints.IInlayHintsResolver -import com.regnosys.rosetta.ide.inlayhints.RosettaInlayHintsService import com.regnosys.rosetta.ide.inlayhints.IInlayHintsService -import com.regnosys.rosetta.ide.util.RangeUtils -import com.regnosys.rosetta.ide.semantictokens.ISemanticTokenTypesProvider +import com.regnosys.rosetta.ide.inlayhints.RosettaInlayHintsService +import com.regnosys.rosetta.ide.quickfix.ICodeActionProvider +import com.regnosys.rosetta.ide.quickfix.IResolveCodeActionService +import com.regnosys.rosetta.ide.quickfix.RosettaCodeActionProvider +import com.regnosys.rosetta.ide.quickfix.RosettaCodeActionService +import com.regnosys.rosetta.ide.quickfix.RosettaQuickFixProvider +import com.regnosys.rosetta.ide.quickfix.RosettaResolveCodeActionService import com.regnosys.rosetta.ide.semantictokens.ISemanticTokenModifiersProvider +import com.regnosys.rosetta.ide.semantictokens.ISemanticTokenTypesProvider import com.regnosys.rosetta.ide.semantictokens.ISemanticTokensService -import com.regnosys.rosetta.ide.semantictokens.RosettaSemanticTokensService +import com.regnosys.rosetta.ide.semantictokens.RosettaSemanticTokenModifiersProvider import com.regnosys.rosetta.ide.semantictokens.RosettaSemanticTokenTypesProvider +import com.regnosys.rosetta.ide.semantictokens.RosettaSemanticTokensService import com.regnosys.rosetta.ide.textmate.RosettaTextMateGrammarUtil -import org.eclipse.xtext.ide.server.formatting.FormattingService -import com.regnosys.rosetta.ide.formatting.RosettaFormattingService +import com.regnosys.rosetta.ide.util.RangeUtils +import org.eclipse.xtext.documentation.IEObjectDocumentationProvider +import org.eclipse.xtext.generator.IContextualOutputConfigurationProvider +import org.eclipse.xtext.ide.editor.contentassist.IdeContentProposalProvider import org.eclipse.xtext.ide.editor.quickfix.IQuickFixProvider -import com.regnosys.rosetta.ide.quickfix.RosettaQuickFixProvider import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2 -import com.regnosys.rosetta.ide.quickfix.RosettaQuickFixCodeActionService import org.eclipse.xtext.ide.server.contentassist.ContentAssistService -import org.eclipse.xtext.service.OperationCanceledManager -import com.regnosys.rosetta.ide.contentassist.cancellable.ICancellableContentAssistParser -import com.regnosys.rosetta.ide.contentassist.cancellable.CancellableRosettaParser -import com.regnosys.rosetta.ide.contentassist.cancellable.CancellableContentAssistService -import com.regnosys.rosetta.ide.contentassist.cancellable.RosettaOperationCanceledManager -import com.regnosys.rosetta.ide.semantictokens.RosettaSemanticTokenModifiersProvider +import org.eclipse.xtext.ide.server.formatting.FormattingService import org.eclipse.xtext.ide.server.hover.IHoverService -import com.regnosys.rosetta.ide.hover.RosettaHoverService -import org.eclipse.xtext.ide.editor.contentassist.IdeContentProposalProvider -import com.regnosys.rosetta.ide.contentassist.RosettaContentProposalProvider +import org.eclipse.xtext.service.OperationCanceledManager /** * Use this class to register ide components. @@ -85,7 +89,15 @@ class RosettaIdeModule extends AbstractRosettaIdeModule { } def Class bindICodeActionService2() { - RosettaQuickFixCodeActionService + RosettaCodeActionService + } + + def Class bindIResolveCodeActionService() { + RosettaResolveCodeActionService + } + + def Class bindICodeActionProvider() { + RosettaCodeActionProvider } def Class bindICancellableContentAssistParser() { diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/AbstractCodeActionProvider.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/AbstractCodeActionProvider.java new file mode 100644 index 000000000..56682ee45 --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/AbstractCodeActionProvider.java @@ -0,0 +1,57 @@ +package com.regnosys.rosetta.ide.quickfix; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.apache.log4j.Logger; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.xtext.ide.server.ILanguageServerAccess; +import org.eclipse.xtext.ide.server.TextEditAcceptor; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; + +import com.regnosys.rosetta.rosetta.RosettaModel; + +public abstract class AbstractCodeActionProvider implements ICodeActionProvider { + + private static final Logger LOG = Logger.getLogger(RosettaCodeActionProvider.class); + + @Override + public CodeAction resolve(CodeAction unresolved, Options options) { + String title = unresolved.getTitle(); + if (unresolved == null || title == null) { + return null; + } + + Method resolutionMethod = findResolutionMethod(getClass(), title); + + if (resolutionMethod != null) { + try { + RosettaModel model = (RosettaModel) options.getResource().getContents().get(0); + List edits = (List) resolutionMethod.invoke(this, model); + + ILanguageServerAccess languageServerAccess = options.getLanguageServerAccess(); + + WorkspaceEdit workspaceEdit = new WorkspaceEdit(); + TextEditAcceptor editAcceptor = new TextEditAcceptor(workspaceEdit, languageServerAccess); + String uri = options.getResource().getURI().toString(); + + editAcceptor.accept(uri, options.getDocument(), edits); + unresolved.setEdit(workspaceEdit); + return unresolved; + } catch (Exception e) { + LOG.error("Error resolving code action: " + title, e); + } + } + return unresolved; + } + + private Method findResolutionMethod(Class clazz, String title) { + return Arrays.stream(clazz.getMethods()).filter(method -> { + CodeActionResolution annotation = method.getAnnotation(CodeActionResolution.class); + return annotation != null && annotation.value().equals(title); + }).findFirst().orElse(null); + } +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/CodeActionResolution.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/CodeActionResolution.java new file mode 100644 index 000000000..be4e5e429 --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/CodeActionResolution.java @@ -0,0 +1,17 @@ +package com.regnosys.rosetta.ide.quickfix; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface CodeActionResolution { + /** + * Specifies the title of the code action for which the annotated method provides a resolution. + * + * @return the title of the associated code action. + */ + String value(); +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/ICodeActionProvider.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/ICodeActionProvider.java new file mode 100644 index 000000000..f0a995fcd --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/ICodeActionProvider.java @@ -0,0 +1,12 @@ +package com.regnosys.rosetta.ide.quickfix; + +import java.util.List; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; + +public interface ICodeActionProvider { + List getCodeActions(Options options); + + CodeAction resolve(CodeAction unresolved, Options options); +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/IResolveCodeActionService.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/IResolveCodeActionService.java new file mode 100644 index 000000000..c239168bc --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/IResolveCodeActionService.java @@ -0,0 +1,8 @@ +package com.regnosys.rosetta.ide.quickfix; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; + +public interface IResolveCodeActionService { + CodeAction getCodeActionResolution(CodeAction unresolved, Options baseOptions); +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaCodeActionProvider.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaCodeActionProvider.java new file mode 100644 index 000000000..9f97ea161 --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaCodeActionProvider.java @@ -0,0 +1,51 @@ +package com.regnosys.rosetta.ide.quickfix; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.eclipse.emf.common.util.EList; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; + +import com.regnosys.rosetta.ide.util.CodeActionUtils; +import com.regnosys.rosetta.rosetta.Import; +import com.regnosys.rosetta.rosetta.RosettaModel; +import com.regnosys.rosetta.utils.ImportManagementService; + +public class RosettaCodeActionProvider extends AbstractCodeActionProvider { + @Inject + private ImportManagementService importManagementService; + @Inject + private CodeActionUtils codeActionUtils; + + @Override + public List getCodeActions(Options options) { + List result = new ArrayList<>(); + + // Handle Sorting CodeAction + RosettaModel model = (RosettaModel) options.getResource().getContents().get(0); + if (!importManagementService.isSorted(model.getImports())) { + result.add(codeActionUtils.createUnresolvedCodeAction("Sort imports", options.getCodeActionParams(), + CodeActionKind.SourceOrganizeImports)); + } + + return result; + } + + @CodeActionResolution("Sort imports") + public List sortImports(RosettaModel model) { + EList imports = model.getImports(); + + Range importsRange = codeActionUtils.getImportsRange(imports); + + importManagementService.sortImports(imports); + String sortedImportsText = importManagementService.toString(imports); + + return List.of(new TextEdit(importsRange, sortedImportsText)); + } +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaCodeActionService.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaCodeActionService.java new file mode 100644 index 000000000..603cfd260 --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaCodeActionService.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 REGnosys + * + * Licensed 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 com.regnosys.rosetta.ide.quickfix; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.xtext.ide.editor.quickfix.DiagnosticResolution; +import org.eclipse.xtext.ide.editor.quickfix.IQuickFixProvider; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2; + +import com.regnosys.rosetta.ide.util.CodeActionUtils; + +public class RosettaCodeActionService implements ICodeActionService2 { + + @Inject + private IQuickFixProvider quickfixes; + @Inject + private CodeActionUtils codeActionUtils; + @Inject + private ICodeActionProvider codeActionProvider; + + @Override + public List> getCodeActions(Options options) { + List> result = new ArrayList<>(); + + //Handle Code Actions + List> codeActions = codeActionProvider.getCodeActions(options).stream() + .map(action -> Either.forRight(action)) + .collect(Collectors.toList()); + result.addAll(codeActions); + + boolean handleQuickfixes = options.getCodeActionParams().getContext().getOnly() == null + || options.getCodeActionParams().getContext().getOnly().isEmpty() + || options.getCodeActionParams().getContext().getOnly().contains(CodeActionKind.QuickFix); + + if (handleQuickfixes) { + List diagnostics = options.getCodeActionParams().getContext().getDiagnostics(); + + for (Diagnostic diagnostic : diagnostics) { + Options diagnosticOptions = codeActionUtils.createOptionsForSingleDiagnostic(options, diagnostic); + List resolutions = quickfixes.getResolutions(diagnosticOptions, diagnostic) + .stream().sorted(Comparator.nullsLast(Comparator.comparing(DiagnosticResolution::getLabel))) + .collect(Collectors.toList()); + for (DiagnosticResolution resolution : resolutions) { + result.add(Either + .forRight(codeActionUtils.createUnresolvedFix(resolution.getLabel(), options.getCodeActionParams(), diagnostic))); + } + } + } + + return result; + } + +} \ No newline at end of file diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixCodeActionService.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixCodeActionService.java deleted file mode 100644 index bdf113516..000000000 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixCodeActionService.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2024 REGnosys - * - * Licensed 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 com.regnosys.rosetta.ide.quickfix; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -import org.eclipse.lsp4j.CodeAction; -import org.eclipse.lsp4j.CodeActionContext; -import org.eclipse.lsp4j.CodeActionKind; -import org.eclipse.lsp4j.CodeActionParams; -import org.eclipse.lsp4j.Command; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.xtext.ide.editor.quickfix.DiagnosticResolution; -import org.eclipse.xtext.ide.editor.quickfix.IQuickFixProvider; -import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2; - -/* - * TODO: contribute to Xtext. - * This is a patch of org.eclipse.xtext.ide.server.codeActions.QuickFixCodeActionService. - */ -public class RosettaQuickFixCodeActionService implements ICodeActionService2 { - - @Inject - private IQuickFixProvider quickfixes; - - @Override - public List> getCodeActions(Options options) { - boolean handleQuickfixes = options.getCodeActionParams().getContext().getOnly() == null - || options.getCodeActionParams().getContext().getOnly().isEmpty() - || options.getCodeActionParams().getContext().getOnly().contains(CodeActionKind.QuickFix); - - if (!handleQuickfixes) { - return Collections.emptyList(); - } - - List> result = new ArrayList<>(); - for (Diagnostic diagnostic : options.getCodeActionParams().getContext().getDiagnostics()) { - Options diagnosticOptions = createOptionsForSingleDiagnostic(options, diagnostic); - List resolutions = quickfixes.getResolutions(diagnosticOptions, diagnostic).stream() - .sorted(Comparator.nullsLast(Comparator.comparing(DiagnosticResolution::getLabel))) - .collect(Collectors.toList()); - for (DiagnosticResolution resolution : resolutions) { - result.add(Either.forRight(createFix(resolution, diagnostic))); - } - } - return result; - } - - private CodeAction createFix(DiagnosticResolution resolution, Diagnostic diagnostic) { - CodeAction codeAction = new CodeAction(); - codeAction.setDiagnostics(Collections.singletonList(diagnostic)); - codeAction.setTitle(resolution.getLabel()); - codeAction.setEdit(resolution.apply()); - codeAction.setKind(CodeActionKind.QuickFix); - - return codeAction; - } - - private Options createOptionsForSingleDiagnostic(Options base, Diagnostic diagnostic) { - Options options = new Options(); - options.setCancelIndicator(base.getCancelIndicator()); - options.setDocument(base.getDocument()); - options.setLanguageServerAccess(base.getLanguageServerAccess()); - options.setResource(base.getResource()); - - CodeActionParams baseParams = base.getCodeActionParams(); - CodeActionContext baseContext = baseParams.getContext(); - CodeActionContext context = new CodeActionContext(List.of(diagnostic), baseContext.getOnly()); - context.setTriggerKind(baseContext.getTriggerKind()); - CodeActionParams params = new CodeActionParams(baseParams.getTextDocument(), diagnostic.getRange(), context); - - options.setCodeActionParams(params); - - return options; - } - -} \ No newline at end of file diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixProvider.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixProvider.java index 775d325f3..a57ea07b0 100644 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixProvider.java +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaQuickFixProvider.java @@ -16,6 +16,9 @@ package com.regnosys.rosetta.ide.quickfix; +import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.ROSETTA_OPERATION__OPERATOR; +import static org.eclipse.xtext.EcoreUtil2.getContainerOfType; + import java.util.List; import javax.inject.Inject; @@ -26,42 +29,54 @@ import org.eclipse.lsp4j.TextEdit; import org.eclipse.xtext.ide.editor.quickfix.AbstractDeclarativeIdeQuickfixProvider; import org.eclipse.xtext.ide.editor.quickfix.DiagnosticResolutionAcceptor; +import org.eclipse.xtext.ide.editor.quickfix.ISemanticModification; import org.eclipse.xtext.ide.editor.quickfix.QuickFix; import org.eclipse.xtext.ide.server.Document; +import com.regnosys.rosetta.ide.util.CodeActionUtils; import com.regnosys.rosetta.ide.util.RangeUtils; -import com.regnosys.rosetta.validation.RosettaIssueCodes; +import com.regnosys.rosetta.rosetta.Import; +import com.regnosys.rosetta.rosetta.RosettaModel; +import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression; import com.regnosys.rosetta.rosetta.expression.RosettaUnaryOperation; -import static com.regnosys.rosetta.rosetta.expression.ExpressionPackage.Literals.*; +import com.regnosys.rosetta.utils.ConstructorManagementService; +import com.regnosys.rosetta.utils.ImportManagementService; +import com.regnosys.rosetta.validation.RosettaIssueCodes; public class RosettaQuickFixProvider extends AbstractDeclarativeIdeQuickfixProvider { @Inject private RangeUtils rangeUtils; - + @Inject + private ImportManagementService importManagementService; + @Inject + private CodeActionUtils codeActionUtils; + @Inject + private ConstructorManagementService constructorManagementService; + @QuickFix(RosettaIssueCodes.REDUNDANT_SQUARE_BRACKETS) public void fixRedundantSquareBrackets(DiagnosticResolutionAcceptor acceptor) { - acceptor.accept("Remove square brackets.", (Diagnostic diagnostic, EObject object, Document document) -> { + acceptor.accept("Remove square brackets", (Diagnostic diagnostic, EObject object, Document document) -> { Range range = rangeUtils.getRange(object); String original = document.getSubstring(range); String edited = original.replaceAll("^\\[ +|\\s+\\]$", ""); return createTextEdit(diagnostic, edited); }); } - + @QuickFix(RosettaIssueCodes.MANDATORY_SQUARE_BRACKETS) public void fixMandatorySquareBrackets(DiagnosticResolutionAcceptor acceptor) { - acceptor.accept("Add square brackets.", (Diagnostic diagnostic, EObject object, Document document) -> { + acceptor.accept("Add square brackets", (Diagnostic diagnostic, EObject object, Document document) -> { Range range = rangeUtils.getRange(object); String original = document.getSubstring(range); String edited = "[ " + original + " ]"; return createTextEdit(diagnostic, edited); }); } - + @QuickFix(RosettaIssueCodes.MANDATORY_THEN) public void fixMandatoryThen(DiagnosticResolutionAcceptor acceptor) { - acceptor.accept("Add `then`.", (Diagnostic diagnostic, EObject object, Document document) -> { - RosettaUnaryOperation op = (RosettaUnaryOperation)object; + acceptor.accept("Add `then`", (Diagnostic diagnostic, EObject object, Document document) -> { + RosettaUnaryOperation op = (RosettaUnaryOperation) object; Range range = rangeUtils.getRange(op, ROSETTA_OPERATION__OPERATOR); String original = document.getSubstring(range); String edited = "then " + original; @@ -69,4 +84,44 @@ public void fixMandatoryThen(DiagnosticResolutionAcceptor acceptor) { return List.of(edit); }); } + + @QuickFix(RosettaIssueCodes.UNUSED_IMPORT) + @QuickFix(RosettaIssueCodes.DUPLICATE_IMPORT) + public void fixUnoptimizedImports(DiagnosticResolutionAcceptor acceptor) { + acceptor.accept("Optimize imports", (Diagnostic diagnostic, EObject object, Document document) -> { + Import importObj = (Import) object; + EObject container = importObj.eContainer(); + + if (container instanceof RosettaModel) { + RosettaModel model = (RosettaModel) container; + List imports = model.getImports(); + + Range importsRange = codeActionUtils.getImportsRange(imports); + + importManagementService.cleanupImports(model); + String sortedImportsText = importManagementService.toString(imports); + + return List.of(new TextEdit(importsRange, sortedImportsText)); + } + + // if not model, return empty list of edits + return List.of(); + + }); + } + + @QuickFix(RosettaIssueCodes.MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT) + public void missingAttributes(DiagnosticResolutionAcceptor acceptor) { + ISemanticModification semanticModification = (Diagnostic diagnostic, EObject object) -> + context -> constructorManagementService.modifyConstructorWithMandatoryAttributes(getContainerOfType(object, RosettaConstructorExpression.class)); + acceptor.accept("Add mandatory attributes", semanticModification); + } + + @QuickFix(RosettaIssueCodes.MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT) + public void addAllMissingAttributes(DiagnosticResolutionAcceptor acceptor) { + ISemanticModification semanticModification = (Diagnostic diagnostic, EObject object) -> + context -> constructorManagementService.modifyConstructorWithAllAttributes(getContainerOfType(object, RosettaConstructorExpression.class)); + acceptor.accept("Add all attributes", semanticModification); + } + } diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaResolveCodeActionService.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaResolveCodeActionService.java new file mode 100644 index 000000000..f0eba5a18 --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/quickfix/RosettaResolveCodeActionService.java @@ -0,0 +1,49 @@ +package com.regnosys.rosetta.ide.quickfix; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.xtext.ide.editor.quickfix.DiagnosticResolution; +import org.eclipse.xtext.ide.editor.quickfix.IQuickFixProvider; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; + +import com.google.common.collect.Iterables; +import com.regnosys.rosetta.ide.util.CodeActionUtils; + +public class RosettaResolveCodeActionService implements IResolveCodeActionService { + @Inject + CodeActionUtils codeActionUtils; + @Inject + IQuickFixProvider resolutionProvider; + @Inject + ICodeActionProvider codeActionProvider; + + @Override + public CodeAction getCodeActionResolution(CodeAction codeAction, Options baseOptions) { + switch (codeAction.getKind()) { + case CodeActionKind.QuickFix: // handling resolutions for quickFixes + Diagnostic diagnostic = Iterables.getOnlyElement(codeAction.getDiagnostics()); + ICodeActionService2.Options options = codeActionUtils.createOptionsForSingleDiagnostic(baseOptions, + diagnostic); + + List resolutions = resolutionProvider.getResolutions(options, diagnostic).stream() + .sorted(Comparator.nullsLast(Comparator.comparing(DiagnosticResolution::getLabel))) + .filter(r -> r.getLabel().equals(codeAction.getTitle())).collect(Collectors.toList()); + + // since a CodeAction has only one diagnostic, only one resolution should be found + codeAction.setEdit(resolutions.get(0).apply()); + + return codeAction; + default: // handling resolutions for all other types of codeActions + return codeActionProvider.resolve(codeAction, baseOptions); + } + } + +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java index aad31d7cf..0d3a0903a 100644 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java @@ -16,25 +16,47 @@ package com.regnosys.rosetta.ide.server; -import org.eclipse.xtext.ide.server.LanguageServerImpl; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import javax.inject.Inject; + import org.eclipse.emf.common.util.URI; -import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionOptions; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.FormattingOptions; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.InlayHintParams; +import org.eclipse.lsp4j.InlayHintRegistrationOptions; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDelta; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.eclipse.lsp4j.SemanticTokensRangeParams; +import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; +import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.xtext.ide.server.Document; +import org.eclipse.xtext.ide.server.ILanguageServerAccess; +import org.eclipse.xtext.ide.server.LanguageServerImpl; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; import org.eclipse.xtext.resource.IResourceServiceProvider; +import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.util.CancelIndicator; import com.regnosys.rosetta.formatting2.FormattingOptionsAdaptor; import com.regnosys.rosetta.ide.inlayhints.IInlayHintsResolver; import com.regnosys.rosetta.ide.inlayhints.IInlayHintsService; +import com.regnosys.rosetta.ide.quickfix.IResolveCodeActionService; import com.regnosys.rosetta.ide.semantictokens.ISemanticTokensService; import com.regnosys.rosetta.ide.semantictokens.SemanticToken; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import javax.inject.Inject; +import com.regnosys.rosetta.ide.util.CodeActionUtils; /** * TODO: contribute to Xtext. @@ -42,6 +64,7 @@ */ public class RosettaLanguageServerImpl extends LanguageServerImpl implements RosettaLanguageServer{ @Inject FormattingOptionsAdaptor formattingOptionsAdapter; + @Inject CodeActionUtils codeActionUtils; @Override protected ServerCapabilities createServerCapabilities(InitializeParams params) { @@ -54,6 +77,14 @@ protected ServerCapabilities createServerCapabilities(InitializeParams params) { serverCapabilities.setInlayHintProvider(inlayHintRegistrationOptions); } + if (resourceServiceProvider.get(ICodeActionService2.class) != null) { + CodeActionOptions codeActionProvider = new CodeActionOptions(); + codeActionProvider.setResolveProvider(true); + codeActionProvider.setCodeActionKinds(List.of(CodeActionKind.QuickFix, CodeActionKind.SourceOrganizeImports)); + codeActionProvider.setWorkDoneProgress(true); + serverCapabilities.setCodeActionProvider(codeActionProvider); + } + ISemanticTokensService semanticTokensService = resourceServiceProvider.get(ISemanticTokensService.class); if (semanticTokensService != null) { SemanticTokensWithRegistrationOptions semanticTokensOptions = new SemanticTokensWithRegistrationOptions(); @@ -185,4 +216,43 @@ public CompletableFuture getDefaultFormattingOptions() { return CompletableFuture.failedFuture(e); } } + + @Override + public CompletableFuture resolveCodeAction(CodeAction unresolved) { + return getRequestManager().runRead((cancelIndicator) -> this.resolveCodeAction(unresolved, cancelIndicator)); + } + + protected CodeAction resolveCodeAction(CodeAction codeAction, CancelIndicator cancelIndicator) { + CodeActionParams codeActionParams = codeActionUtils.getCodeActionParams(codeAction); + + if (codeActionParams.getTextDocument() == null) { + return null; + } + + URI uri = getURI(codeActionParams.getTextDocument()); + + return getWorkspaceManager().doRead(uri, (doc, resource) -> { + ICodeActionService2.Options baseOptions = createCodeActionBaseOptions(doc, + resource, getLanguageServerAccess(), codeActionParams, cancelIndicator); + + IResolveCodeActionService resolveCodeActionService = getService(uri, IResolveCodeActionService.class); + return resolveCodeActionService.getCodeActionResolution(codeAction, baseOptions); + }); + } + + + + private Options createCodeActionBaseOptions(Document doc, XtextResource resource, + ILanguageServerAccess languageServerAcces, CodeActionParams codeActionParams, + CancelIndicator cancelIndicator) { + Options baseOptions = new ICodeActionService2.Options(); + baseOptions.setDocument(doc); + baseOptions.setResource(resource); + baseOptions.setLanguageServerAccess(languageServerAcces); + baseOptions.setCodeActionParams(codeActionParams); + baseOptions.setCancelIndicator(cancelIndicator); + + return baseOptions; + } + } \ No newline at end of file diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaServerModule.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaServerModule.java index fd6c39760..42e64029f 100644 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaServerModule.java +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaServerModule.java @@ -26,8 +26,8 @@ import org.eclipse.xtext.ide.server.concurrent.RequestManager; import org.eclipse.xtext.service.AbstractGenericModule; -import com.google.inject.util.Modules; import com.google.inject.Module; +import com.google.inject.util.Modules; public class RosettaServerModule extends AbstractGenericModule { /** diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/util/CodeActionUtils.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/util/CodeActionUtils.java new file mode 100644 index 000000000..819cd92d5 --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/util/CodeActionUtils.java @@ -0,0 +1,87 @@ +package com.regnosys.rosetta.ide.util; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.xtext.ide.server.codeActions.ICodeActionService2.Options; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.regnosys.rosetta.rosetta.Import; + +public class CodeActionUtils { + @Inject + private RangeUtils rangeUtils; + + public Options createOptionsForSingleDiagnostic(Options base, Diagnostic diagnostic) { + Options options = new Options(); + options.setCancelIndicator(base.getCancelIndicator()); + options.setDocument(base.getDocument()); + options.setLanguageServerAccess(base.getLanguageServerAccess()); + options.setResource(base.getResource()); + + CodeActionParams baseParams = base.getCodeActionParams(); + CodeActionContext baseContext = baseParams.getContext(); + CodeActionContext context = new CodeActionContext(List.of(diagnostic), baseContext.getOnly()); + context.setTriggerKind(baseContext.getTriggerKind()); + CodeActionParams params = new CodeActionParams(baseParams.getTextDocument(), diagnostic.getRange(), context); + + options.setCodeActionParams(params); + + return options; + } + + public CodeActionParams getCodeActionParams(CodeAction codeAction) { + Object data = codeAction.getData(); + CodeActionParams codeActionParams = null; + if (data instanceof CodeActionParams) { + codeActionParams = (CodeActionParams) data; + } + if (data instanceof JsonObject) { + Gson gson = new MessageJsonHandler(Map.of()).getGson(); + codeActionParams = gson.fromJson(((JsonObject) data), CodeActionParams.class); + } + return codeActionParams; + } + + public CodeAction createUnresolvedFix(String resolutionLabel, CodeActionParams codeActionParams, + Diagnostic diagnostic) { + CodeAction codeAction = new CodeAction(); + if(diagnostic == null) { + codeAction.setDiagnostics(null); + } + else { + codeAction.setDiagnostics(Collections.singletonList(diagnostic)); + } + codeAction.setTitle(resolutionLabel); + codeAction.setData(codeActionParams); + codeAction.setKind(CodeActionKind.QuickFix); + + return codeAction; + } + + public CodeAction createUnresolvedCodeAction(String resolutionLabel, CodeActionParams codeActionParams, + String codeActionKind) { + CodeAction codeAction = createUnresolvedFix(resolutionLabel, codeActionParams, null); + codeAction.setKind(codeActionKind); + + return codeAction; + } + + public Range getImportsRange(List imports) { + Position importsStart = rangeUtils.getRange(imports.get(0)).getStart(); + Position importsEnd = rangeUtils.getRange(imports.get(imports.size() - 1)).getEnd(); + return new Range(importsStart, importsEnd); + } +} diff --git a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/quickfix/QuickFixTest.xtend b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/quickfix/QuickFixTest.xtend index 03bcc52b3..27bae3c9b 100644 --- a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/quickfix/QuickFixTest.xtend +++ b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/quickfix/QuickFixTest.xtend @@ -35,16 +35,53 @@ class QuickFixTest extends AbstractRosettaLanguageServerTest { val sorted = it.sortWith[a,b| ru.comparePositions(a.getRight.diagnostics.head.range.start, b.getRight.diagnostics.head.range.start)] sorted.get(0).getRight => [ - assertEquals("Add `then`.", title) + assertEquals("Add `then`", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + + sorted.get(1).getRight => [ + assertEquals("Remove square brackets", title) + assertEquals(edit, null) // make sure no edits are made at this point + + ] + ] + ] + } + + @Test + def testResolveRedundantSquareBrackets() { + val model = ''' + namespace foo.bar + + type Foo: + a int (1..1) + + func Bar: + inputs: foo Foo (1..1) + output: result int (1..1) + + set result: + foo + extract [ a ] + extract [ 42 ] + ''' + testResultCodeAction[ + it.model = model + it.assertCodeActionResolution = [ + assertEquals(2, size) + + val sorted = it.sortWith[a,b| ru.comparePositions(a.diagnostics.head.range.start, b.diagnostics.head.range.start)] + + sorted.get(0)=> [ + assertEquals("Add `then`", title) edit.changes.values.head.head => [ assertEquals("then extract", newText) assertEquals(new Position(12, 3), range.start) assertEquals(new Position(12, 10), range.end) ] ] - - sorted.get(1).getRight => [ - assertEquals("Remove square brackets.", title) + sorted.get(1)=> [ + assertEquals("Remove square brackets", title) edit.changes.values.head.head => [ assertEquals("42", newText) assertEquals(new Position(12, 11), range.start) @@ -54,4 +91,442 @@ class QuickFixTest extends AbstractRosettaLanguageServerTest { ] ] } + + @Test + def testQuickFixDuplicateImport() { + val model = ''' + namespace foo.bar + + import dsl.foo.* + import dsl.foo.* + + func Bar: + inputs: foo Foo (1..1) + output: result int (1..1) + + set result: foo -> a + ''' + testCodeAction[ + it.model = model + it.filesInScope = #{"foo.rosetta" -> ''' + namespace dsl.foo + + type Foo: + a int (1..1) + '''} + assertCodeActions = [ + assertEquals(1, size) // duplicate import + + val sorted = it.sortWith[a,b| ru.comparePositions(a.getRight.diagnostics.head.range.start, b.getRight.diagnostics.head.range.start)] + + sorted.get(0).getRight => [ + assertEquals("Optimize imports", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + ] + ] + } + + @Test + def testResolveDuplicateImport() { + val model = ''' + namespace foo.bar + + import dsl.foo.* + import dsl.foo.* + + func Bar: + inputs: foo Foo (1..1) + output: result int (1..1) + + set result: foo -> a + ''' + + testResultCodeAction[ + it.model = model + it.filesInScope = #{"foo.rosetta" -> ''' + namespace dsl.foo + + type Foo: + a int (1..1) + '''} + it.assertCodeActionResolution = [ + assertEquals(1, size) //duplicate import + + val sorted = it.sortWith[a,b| ru.comparePositions(a.diagnostics.head.range.start, b.diagnostics.head.range.start)] + + sorted.get(0) => [ + assertEquals("Optimize imports", title) + edit.changes.values.head.head => [ + assertEquals("import dsl.foo.*", newText) // second import is deleted + assertEquals(new Position(2, 0), range.start) + assertEquals(new Position(3, 16), range.end) + ] + ] + ] + ] + } + + @Test + def testQuickFixUnusedImport() { + val model = ''' + namespace foo.bar + + import dsl.foo.* + import dsl.bar.* + + func Bar: + inputs: foo Foo (1..1) + output: result int (1..1) + + set result: foo -> a + ''' + testCodeAction[ + it.model = model + it.filesInScope = #{"foo.rosetta" -> ''' + namespace dsl.foo + + type Foo: + a int (1..1) + '''} + assertCodeActions = [ + assertEquals(2, size) //one unused, one 'Sort Imports' codeAction + + val sorted = it.sortWith[a,b| a.getRight.title.compareTo(b.getRight.title)] + + sorted.get(0).getRight => [ + assertEquals("Optimize imports", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + sorted.get(1).getRight => [ + assertEquals("Sort imports", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + ] + ] + } + + @Test + def testResolveUnusedImport() { + val model = ''' + namespace foo.bar + + import dsl.foo.* + import dsl.bar.* + + func Bar: + inputs: foo Foo (1..1) + output: result int (1..1) + + set result: foo -> a + ''' + + testResultCodeAction[ + it.model = model + it.filesInScope = #{"foo.rosetta" -> ''' + namespace dsl.foo + + type Foo: + a int (1..1) + '''} + it.assertCodeActionResolution = [ + assertEquals(2, size) //one unused, one 'Sort Imports' codeAction + + val sorted = it.sortWith[a,b| a.title.compareTo(b.title)] + + sorted.get(0)=> [ + assertEquals("Optimize imports", title) + edit.changes.values.head.head => [ + assertEquals("import dsl.foo.*", newText) // second import is deleted + assertEquals(new Position(2, 0), range.start) + assertEquals(new Position(3, 16), range.end) + ] + ] + sorted.get(1)=> [ + assertEquals("Sort imports", title) + edit.changes.values.head.head => [ + assertEquals("import dsl.bar.*\nimport dsl.foo.*", newText) // imports are sorted + assertEquals(new Position(2, 0), range.start) + assertEquals(new Position(3, 16), range.end) + ] + ] + ] + ] + } + + @Test + def testQuickFixUnsortedImports() { + val model = ''' + namespace foo.bar + + import dsl.foo.* + import dsl.aaa.* + + func Bar: + inputs: + foo Foo (1..1) + aaa Aaa (1..1) + output: result int (1..1) + + set result: aaa -> a + ''' + testCodeAction[ + it.model = model + it.filesInScope = #{"foo.rosetta" -> ''' + namespace dsl.foo + + type Foo: + a int (1..1) + ''', "ach.rosetta" -> ''' + namespace dsl.aaa + + type Aaa: + a int (1..1) + '''} + assertCodeActions = [ + assertEquals(1, size) // one 'Sort Imports' codeAction + + val sorted = it.sortWith[a,b| ru.comparePositions(a.getRight.diagnostics.head.range.start, b.getRight.diagnostics.head.range.start)] + + sorted.get(0).getRight => [ + assertEquals("Sort imports", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + ] + ] + } + + @Test + def testResolveUnsortedImports() { + val model = ''' + namespace foo.bar + + import dsl.foo.* + import asl.aaa.* + + func Bar: + inputs: foo Foo (1..1) + aaa Aaa (1..1) + output: result int (1..1) + + set result: foo -> a + ''' + + testResultCodeAction[ + it.model = model + it.filesInScope = #{"foo.rosetta" -> ''' + namespace dsl.foo + + type Foo: + a int (1..1) + ''', "ach.rosetta" -> ''' + namespace asl.aaa + + type Aaa: + a int (1..1) + '''} + it.assertCodeActionResolution = [ + assertEquals(1, size) + + val sorted = it.sortWith[a,b| ru.comparePositions(a.diagnostics.head.range.start, b.diagnostics.head.range.start)] + + sorted.get(0) => [ + assertEquals("Sort imports", title) + edit.changes.values.head.head => [ + assertEquals("import asl.aaa.*\n\nimport dsl.foo.*", newText) // imports are sorted + assertEquals(new Position(2, 0), range.start) + assertEquals(new Position(3, 16), range.end) + ] + ] + ] + ] + } + + + @Test + def testQuickFixConstructorAttributes() { + val model = ''' + namespace test + + type T: + a string (1..1) + func F: + output: t T (1..1) + set t : T {} + ''' + + testCodeAction[ + it.model = model + assertCodeActions = [ + assertEquals(2, size) // mandatory attributes + all attributes quickfixes + + val sorted = it.sortWith[a,b| ru.comparePositions(a.getRight.diagnostics.head.range.start, b.getRight.diagnostics.head.range.start)] + + sorted.get(0).getRight => [ + assertEquals("Add all attributes", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + + sorted.get(1).getRight => [ + assertEquals("Add mandatory attributes", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + ] + ] + } + + + @Test + def testResolveConstructorAttributes() { + val model = ''' + namespace test + + type T: + a string (1..1) + b string (0..1) + func F: + output: t T (1..1) + set t : T {} + ''' + + testResultCodeAction[ + it.model = model + it.assertCodeActionResolution = [ + assertEquals(2, size) // mandatory attributes + all attributes quickfixes + + val sorted = it.sortWith[a,b| ru.comparePositions(a.diagnostics.head.range.start, b.diagnostics.head.range.start)] + + sorted.get(0) => [ + assertEquals("Add all attributes", title) + edit.changes.values.head.head => [ + val expectedResult = ''' + T { + a: empty, + b: empty + } + ''' + assertEquals(expectedResult, newText) // all attributes are added + assertEquals(new Position(7, 9), range.start) + assertEquals(new Position(8, 0), range.end) + ] + ] + + sorted.get(1) => [ + assertEquals("Add mandatory attributes", title) + edit.changes.values.head.head => [ + val expectedResult = ''' + T { + a: empty, + ... + } + ''' + assertEquals(expectedResult, newText) // mandatory attribute is added + assertEquals(new Position(7, 9), range.start) + assertEquals(new Position(8, 0), range.end) + ] + ] + ] + ] + } + + @Test + def testQuickFixChoiceAttributes() { + val model = ''' + namespace test + + type A: + n string (0..1) + + type B: + m string (0..1) + + choice C: + A + B + + func F: + output: c C (1..1) + set c : C {} + ''' + + testCodeAction[ + it.model = model + assertCodeActions = [ + assertEquals(2, size) // mandatory attributes + all attributes quickfixes + + val sorted = it.sortWith[a,b| ru.comparePositions(a.getRight.diagnostics.head.range.start, b.getRight.diagnostics.head.range.start)] + + sorted.get(0).getRight => [ + assertEquals("Add all attributes", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + + sorted.get(1).getRight => [ + assertEquals("Add mandatory attributes", title) + assertEquals(edit, null) // make sure no edits are made at this point + ] + ] + ] + } + + + @Test + def testResolveChoiceAttributes() { + val model = ''' + namespace test + + type A: + n string (0..1) + + type B: + m string (0..1) + + choice C: + A + B + + func F: + output: c C (1..1) + set c : C {} + ''' + + testResultCodeAction[ + it.model = model + it.assertCodeActionResolution = [ + assertEquals(2, size) // mandatory attributes + all attributes quickfixes + + val sorted = it.sortWith[a,b| ru.comparePositions(a.diagnostics.head.range.start, b.diagnostics.head.range.start)] + + sorted.get(0) => [ + assertEquals("Add all attributes", title) + edit.changes.values.head.head => [ + val expectedResult = ''' + C { + A: empty, + B: empty + } + ''' + assertEquals(expectedResult, newText) // all attributes are added + assertEquals(new Position(14, 9), range.start) + assertEquals(new Position(15, 0), range.end) + ] + ] + + sorted.get(1) => [ + assertEquals("Add mandatory attributes", title) + edit.changes.values.head.head => [ + val expectedResult = ''' + C { + A: empty, + B: empty + } + ''' + assertEquals(expectedResult, newText) // mandatory attribute is added + assertEquals(new Position(14, 9), range.start) + assertEquals(new Position(15, 0), range.end) + ] + ] + ] + ] + } } \ No newline at end of file diff --git a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend index 5a33929ae..de8ea93a6 100644 --- a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend +++ b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/tests/AbstractRosettaLanguageServerTest.xtend @@ -25,6 +25,12 @@ import org.eclipse.xtext.testing.FileInfo import org.eclipse.xtext.testing.TextDocumentConfiguration import org.eclipse.xtext.testing.TextDocumentPositionConfiguration import org.junit.jupiter.api.Assertions +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.Command +import org.eclipse.lsp4j.CodeAction +import org.eclipse.lsp4j.CodeActionParams +import org.eclipse.lsp4j.CodeActionContext +import java.util.concurrent.CompletableFuture /** * TODO: contribute to Xtext. @@ -140,4 +146,38 @@ abstract class AbstractRosettaLanguageServerTest extends AbstractLanguageServerT val filePath = super.initializeContext(configuration); return filePath } + + @Accessors static class TestCodeActionConfiguration extends TextDocumentPositionConfiguration { + (List)=>void assertCodeActionResolution= null + } + + protected def void testResultCodeAction((TestCodeActionConfiguration)=>void configurator) { + val extension configuration = new TestCodeActionConfiguration + configuration.filePath = 'MyModel.' + fileExtension + configurator.apply(configuration) + val filePath = initializeContext(configuration).uri + + val codeActions = languageServer.codeAction(new CodeActionParams=>[ + textDocument = new TextDocumentIdentifier(filePath) + range = new Range => [ + start = new Position(configuration.line, configuration.column) + end = start + ] + context = new CodeActionContext => [ + diagnostics = this.diagnostics.get(filePath) + ] + ]).get + + val resultCodeActionList = newArrayList + + // Add all resolved codeActions to result list + for (codeAction : codeActions) { + val resolveResult = languageServer.resolveCodeAction(codeAction.getRight) + resultCodeActionList.add(resolveResult.get) + } + + if (configuration.assertCodeActionResolution !== null) { + configuration.assertCodeActionResolution.apply(resultCodeActionList) + } + } } \ No newline at end of file diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java index a1af59fbf..add537182 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java @@ -11,11 +11,17 @@ import org.eclipse.xtext.formatting2.regionaccess.ITextRegionRewriter; import org.eclipse.xtext.formatting2.regionaccess.ITextReplacement; import org.eclipse.xtext.formatting2.regionaccess.TextRegionAccessBuilder; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.preferences.ITypedPreferenceValues; import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.util.ITextRegion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.regnosys.rosetta.rosetta.Import; +import com.regnosys.rosetta.rosetta.RosettaModel; +import com.regnosys.rosetta.utils.ImportManagementService; + import javax.inject.Inject; import javax.inject.Provider; @@ -30,6 +36,9 @@ public class XtextResourceFormatter implements ResourceFormatterService { @Inject private TextRegionAccessBuilder regionBuilder; + @Inject + private ImportManagementService importManagementService; + @Override public void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues, IFormattedResourceAcceptor acceptor) { @@ -70,6 +79,11 @@ public void formatXtextResource(XtextResource resource, ITypedPreferenceValues p replacements = new ArrayList<>(); } + // get text replacement for optimized imports + ITextReplacement importsReplacement = formattedImportsReplacement(resource, regionAccess); + if (importsReplacement != null) + replacements.add(importsReplacement); + // formatting using TextRegionRewriter ITextRegionRewriter regionRewriter = regionAccess.getRewriter(); String formattedString = regionRewriter.renderToString(regionAccess.regionForDocument(), replacements); @@ -78,4 +92,36 @@ public void formatXtextResource(XtextResource resource, ITypedPreferenceValues p acceptor.accept(resource, formattedString); } + public ITextReplacement formattedImportsReplacement(XtextResource resource, ITextRegionAccess regionAccess) { + RosettaModel model = (RosettaModel) resource.getContents().get(0); + ITextRegion importsRegion = getImportsTextRegion(model.getImports()); + + if (importsRegion == null) + return null; + + importManagementService.cleanupImports(model); + String sortedImportsText = importManagementService.toString(model.getImports()); + return regionAccess.getRewriter().createReplacement(importsRegion.getOffset(), importsRegion.getLength(), + sortedImportsText); + } + + /** + * Return a ITextRegion of all imports + * + * @param imports + * @return ITextRegion text region of imports + */ + private ITextRegion getImportsTextRegion(List imports) { + if (imports.isEmpty()) { + return null; + } + + Import firstImport = imports.get(0); + Import lastImport = imports.get(imports.size() - 1); + ITextRegion firstRegion = NodeModelUtils.getNode(firstImport).getTextRegion(); + ITextRegion lastRegion = NodeModelUtils.getNode(lastImport).getTextRegion(); + + return firstRegion.merge(lastRegion); + } + } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/ConstructorManagementService.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/ConstructorManagementService.java new file mode 100644 index 000000000..e96bd126f --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/ConstructorManagementService.java @@ -0,0 +1,110 @@ +package com.regnosys.rosetta.utils; + +import com.google.common.collect.Lists; +import com.regnosys.rosetta.RosettaEcoreUtil; +import com.regnosys.rosetta.rosetta.RosettaFeature; +import com.regnosys.rosetta.rosetta.expression.ConstructorKeyValuePair; +import com.regnosys.rosetta.rosetta.expression.ExpressionFactory; +import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression; +import com.regnosys.rosetta.rosetta.simple.Attribute; +import com.regnosys.rosetta.rosetta.simple.ChoiceOption; +import com.regnosys.rosetta.types.RMetaAnnotatedType; +import com.regnosys.rosetta.types.RosettaTypeProvider; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.function.Predicate.not; + +public class ConstructorManagementService { + @Inject + private RosettaTypeProvider types; + @Inject + private RosettaEcoreUtil extensions; + + public void modifyConstructorWithAllAttributes(RosettaConstructorExpression constructor) { + RosettaFeatureGroup rosettaFeatureGroup = groupConstructorFeatures(constructor); + List allAttributes = rosettaFeatureGroup.allAttributes(); + + allAttributes.forEach(attr -> { + ConstructorKeyValuePair constructorKeyValuePair = ExpressionFactory.eINSTANCE.createConstructorKeyValuePair(); + constructorKeyValuePair.setKey(attr); + constructorKeyValuePair.setValue(ExpressionFactory.eINSTANCE.createListLiteral()); + constructor.getValues().add(constructorKeyValuePair); + }); + } + + + public void modifyConstructorWithMandatoryAttributes(RosettaConstructorExpression constructor) { + RosettaFeatureGroup rosettaFeatureGroup = groupConstructorFeatures(constructor); + List requiredAbsentAttributes = rosettaFeatureGroup.requiredAbsentAttributes(); + List optionalAbsentAttributes = rosettaFeatureGroup.optionalAbsentAttributes(); + + requiredAbsentAttributes.forEach(attr -> { + ConstructorKeyValuePair constructorKeyValuePair = ExpressionFactory.eINSTANCE.createConstructorKeyValuePair(); + constructorKeyValuePair.setKey(attr); + constructorKeyValuePair.setValue(ExpressionFactory.eINSTANCE.createListLiteral()); + constructor.getValues().add(constructorKeyValuePair); + }); + if (!optionalAbsentAttributes.isEmpty()) { + constructor.setImplicitEmpty(true); + } + } + + public RosettaFeatureGroup groupConstructorFeatures(RosettaConstructorExpression constructor) { + if (constructor != null) { + RMetaAnnotatedType metaAnnotatedType = types.getRMetaAnnotatedType(constructor); + if (metaAnnotatedType != null && metaAnnotatedType.getRType() != null) { + List populatedFeatures = populatedFeaturesInConstructor(constructor); + List allFeatures = Lists.newArrayList(extensions.allFeatures(metaAnnotatedType.getRType(), constructor)); + return new RosettaFeatureGroup(populatedFeatures, allFeatures); + } + } + return new RosettaFeatureGroup(); + } + + private List populatedFeaturesInConstructor(RosettaConstructorExpression constructor) { + return constructor.getValues().stream() + .map(ConstructorKeyValuePair::getKey) + .collect(Collectors.toList()); + } + + public static class RosettaFeatureGroup { + private final List populated; + private final List all; + + private RosettaFeatureGroup() { + this.populated = Collections.emptyList(); + this.all = Collections.emptyList(); + } + + private RosettaFeatureGroup(List populated, List all) { + this.populated = populated; + this.all = all; + } + + private List allAttributes() { + return all.stream().filter(x -> !populated.contains(x)).collect(Collectors.toList()); + } + + public List requiredAbsentAttributes() { + return all.stream() + .filter(x -> !populated.contains(x)) + .filter(RosettaFeatureGroup::isRequired) + .collect(Collectors.toList()); + } + + public List optionalAbsentAttributes() { + return all.stream() + .filter(x -> !populated.contains(x)) + .filter(not(RosettaFeatureGroup::isRequired)) + .collect(Collectors.toList()); + } + + private static boolean isRequired(RosettaFeature it) { + return !(it instanceof Attribute) || ((Attribute) it).getCard().getInf() != 0 || (it instanceof ChoiceOption); + } + } +} \ No newline at end of file diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/ImportManagementService.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/ImportManagementService.java new file mode 100644 index 000000000..fcf45c6c5 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/utils/ImportManagementService.java @@ -0,0 +1,137 @@ +package com.regnosys.rosetta.utils; + +import com.google.common.collect.Comparators; +import com.regnosys.rosetta.RosettaEcoreUtil; +import com.regnosys.rosetta.rosetta.Import; +import com.regnosys.rosetta.rosetta.RosettaModel; +import com.regnosys.rosetta.rosetta.RosettaRootElement; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; + +import org.eclipse.emf.common.util.ECollections; +import org.eclipse.emf.common.util.EList; +import org.eclipse.xtext.naming.IQualifiedNameProvider; +import org.eclipse.xtext.naming.QualifiedName; + +public class ImportManagementService { + + @Inject + RosettaEcoreUtil rosettaEcoreUtil; + @Inject + IQualifiedNameProvider qualifiedNameProvider; + + private Comparator importComparator = Comparator.comparing(Import::getImportedNamespace, Comparator.nullsLast(String::compareTo)); + + public void cleanupImports(RosettaModel model) { + EList imports = model.getImports(); + + // remove all duplicate/unused imports + List unusedImports = findUnused(model); + imports.removeAll(unusedImports); + + List duplicateImports = findDuplicates(imports); + imports.removeAll(duplicateImports); + + sortImports(imports); + } + + public List findUnused(RosettaModel model) { + List usedNames = new ArrayList<>(); + + model.eAllContents().forEachRemaining(content -> { + content.eCrossReferences().stream().filter(ref -> ref instanceof RosettaRootElement) + .filter(ref -> rosettaEcoreUtil.isResolved((RosettaRootElement) ref)).forEach(ref -> { + // Extract fully qualified name and add it to the list + QualifiedName fullyQualifiedName = qualifiedNameProvider + .getFullyQualifiedName((RosettaRootElement) ref); + usedNames.add(fullyQualifiedName); + }); + }); + + List unusedImports = new ArrayList<>(); + for (Import ns : model.getImports()) { + if (ns.getImportedNamespace() != null) { + + String[] segments = ns.getImportedNamespace().split("\\."); + QualifiedName qn = QualifiedName.create(segments); + boolean isWildcard = "*".equals(qn.getLastSegment()); + + // Check if the import is used + boolean isUsed; + if (isWildcard) { + QualifiedName importNamespace = qn.skipLast(1); + isUsed = usedNames.stream().anyMatch(name -> { + if (name.getSegmentCount() < importNamespace.getSegmentCount()) { + return false; // Used name is too short to match + } + // compare first segments of the used name with the import namespace + return name.skipLast(name.getSegmentCount() - importNamespace.getSegmentCount()) + .equals(importNamespace); + }); + + } else { + isUsed = usedNames.contains(qn); + } + + if (!isUsed) { + unusedImports.add(ns); + } + } + } + + return unusedImports; + + } + + public List findDuplicates(List imports) { + Set seenNamespaces = new HashSet(); + List duplicates = new ArrayList(); + // check duplicates + for (Import imp : imports) { + if (!(seenNamespaces.add(imp.getImportedNamespace()))) { + duplicates.add(imp); + } + } + return duplicates; + } + + public String toString(List imports) { + StringBuilder sortedImportsText = new StringBuilder(); + + Import previousImport = null; + for (Import imp : imports) { + //if previous import comes from a different package, insert new line + if (previousImport != null) { + String previousFirstSegment = previousImport.getImportedNamespace().split("\\.")[0]; + String currentFirstSegment = imp.getImportedNamespace().split("\\.")[0]; + if(!previousFirstSegment.equals(currentFirstSegment)) { + sortedImportsText.append("\n"); + } + } + sortedImportsText.append("import ").append(imp.getImportedNamespace()); + if (imp.getNamespaceAlias() != null) { + sortedImportsText.append(" as ").append(imp.getNamespaceAlias()); + } + sortedImportsText.append("\n"); + + + previousImport = imp; + } + + return sortedImportsText.toString().strip(); + } + + public boolean isSorted(List imports) { + return Comparators.isInOrder(imports, importComparator); + } + + public void sortImports(EList imports) { + ECollections.sort(imports, importComparator); + } +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaIssueCodes.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaIssueCodes.java index f5b603e35..eb56ea3f9 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaIssueCodes.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaIssueCodes.java @@ -37,8 +37,10 @@ public interface RosettaIssueCodes { static final String CARDINALITY_ERROR=PREFIX +"cardinalityError"; static final String INVALID_ELEMENT_NAME=PREFIX +"invalidElementName"; static final String UNUSED_IMPORT = PREFIX + "unusedImport"; + static final String DUPLICATE_IMPORT = PREFIX + "duplicateImport"; static final String MANDATORY_SQUARE_BRACKETS = PREFIX + "mandatorySquareBrackets"; static final String REDUNDANT_SQUARE_BRACKETS = PREFIX + "redundantSquareBrackets"; static final String MANDATORY_THEN = PREFIX + "mandatoryThen"; + static final String MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT = PREFIX + "missingAttributes"; } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend index e1f76a543..c4379c4c6 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/validation/RosettaSimpleValidator.xtend @@ -108,6 +108,8 @@ import com.regnosys.rosetta.rosetta.expression.ComparingFunctionalOperation import com.regnosys.rosetta.rosetta.expression.AsKeyOperation import com.regnosys.rosetta.rosetta.expression.ConstructorKeyValuePair import com.regnosys.rosetta.rosetta.expression.CanHandleListOfLists +import com.regnosys.rosetta.utils.ImportManagementService +import com.regnosys.rosetta.utils.ConstructorManagementService // TODO: split expression validator // TODO: type check type call arguments @@ -129,6 +131,10 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { @Inject extension RosettaGrammarAccess @Inject extension RObjectFactory objectFactory + @Inject ImportManagementService importManagementService; + @Inject ConstructorManagementService constructorManagementService; + + @Check def void deprecatedWarning(EObject object) { val crossRefs = (object.eClass.EAllStructuralFeatures as EClassImpl.FeatureSubsetSupplier).crossReferences as List @@ -848,23 +854,23 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { CONSTRUCTOR_KEY_VALUE_PAIR__VALUE) } } - val absentAttributes = rType - .allFeatures(ele) - .filter[!seenFeatures.contains(it)] - val requiredAbsentAttributes = absentAttributes - .filter[!(it instanceof Attribute) || (it as Attribute).card.inf !== 0] + + val featureGroup = constructorManagementService.groupConstructorFeatures(ele) + val requiredAbsentAttributes = featureGroup.requiredAbsentAttributes + val optionalAbsentAttributes = featureGroup.optionalAbsentAttributes if (ele.implicitEmpty) { - if (!requiredAbsentAttributes.empty) { - error('''Missing attributes «FOR attr : requiredAbsentAttributes SEPARATOR ', '»`«attr.name»`«ENDFOR».''', ele.typeCall, null) - } - if (absentAttributes.size === requiredAbsentAttributes.size) { - error('''There are no optional attributes left.''', ele, ROSETTA_CONSTRUCTOR_EXPRESSION__IMPLICIT_EMPTY) - } - } else { - if (!absentAttributes.empty) { - error('''Missing attributes «FOR attr : absentAttributes SEPARATOR ', '»`«attr.name»`«ENDFOR».«IF requiredAbsentAttributes.empty» Perhaps you forgot a `...` at the end of the constructor?«ENDIF»''', ele.typeCall, null) - } - } + if (!requiredAbsentAttributes.isEmpty) { + error('''Missing attributes «FOR attr : requiredAbsentAttributes SEPARATOR ', '»`«attr.name»`«ENDFOR».''', ele, null, MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT) + } + if (optionalAbsentAttributes.isEmpty) { + error('''There are no optional attributes left.''', ele, ROSETTA_CONSTRUCTOR_EXPRESSION__IMPLICIT_EMPTY) + } + } else { + val allAbsentAttributes = requiredAbsentAttributes + optionalAbsentAttributes + if (!allAbsentAttributes.isEmpty) { + error('''Missing attributes «FOR attr : allAbsentAttributes SEPARATOR ', '»`«attr.name»`«ENDFOR».«IF requiredAbsentAttributes.isEmpty» Perhaps you forgot a `...` at the end of the constructor?«ENDIF»''', ele, null, MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT) + } + } } @Check @@ -1231,12 +1237,6 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { @Check def checkImport(RosettaModel model) { - var usedNames = model.eAllContents.flatMap[ - eCrossReferences.filter(RosettaRootElement).filter[isResolved].iterator - ].map[ - fullyQualifiedName - ].toList - for (ns : model.imports) { if (ns.importedNamespace !== null) { val qn = QualifiedName.create(ns.importedNamespace.split('\\.')) @@ -1244,18 +1244,18 @@ class RosettaSimpleValidator extends AbstractDeclarativeRosettaValidator { if (!isWildcard && ns.namespaceAlias !== null) { error('''"as" statement can only be used with wildcard imports''', ns, IMPORT__NAMESPACE_ALIAS); } - - - val isUsed = if (isWildcard) { - usedNames.stream.anyMatch[startsWith(qn.skipLast(1)) && segmentCount === qn.segmentCount] - } else { - usedNames.contains(qn) - } - if (!isUsed) { - warning('''Unused import «ns.importedNamespace»''', ns, IMPORT__IMPORTED_NAMESPACE, UNUSED_IMPORT) - } } } + + val unused = importManagementService.findUnused(model); + for (ns: unused) { + warning('''Unused import «ns.importedNamespace»''', ns, IMPORT__IMPORTED_NAMESPACE, UNUSED_IMPORT) + } + + val duplicates = importManagementService.findDuplicates(model.imports); + for (imp : duplicates) { + warning('''Duplicate import «imp.importedNamespace»''', imp, IMPORT__IMPORTED_NAMESPACE, DUPLICATE_IMPORT) + } } private def checkBodyExists(RosettaFunctionalOperation operation) { diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend index 1ff657605..76c958c0e 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/validation/RosettaValidatorTest.xtend @@ -689,7 +689,7 @@ class RosettaValidatorTest implements RosettaIssueCodes { } '''.parseRosetta - model.assertError(TYPE_CALL, null, + model.assertError(ROSETTA_CONSTRUCTOR_EXPRESSION, RosettaIssueCodes.MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT, "Missing attributes `b`, `c`. Perhaps you forgot a `...` at the end of the constructor?" ) } @@ -786,7 +786,7 @@ class RosettaValidatorTest implements RosettaIssueCodes { } '''.parseRosetta - model.assertError(TYPE_CALL, null, + model.assertError(ROSETTA_CONSTRUCTOR_EXPRESSION, RosettaIssueCodes.MISSING_MANDATORY_CONSTRUCTOR_ARGUMENT, "Missing attributes `month`, `year`." ) } @@ -3549,4 +3549,46 @@ class RosettaValidatorTest implements RosettaIssueCodes { models.forEach[assertNoIssues] } + + @Test + def void shouldNotWarnForUsedImports() { + val models = newArrayList(''' + namespace dsl.test + + import foo.bar.* + + type A: + a qux.MyType (1..1) + ''', + ''' + namespace foo.bar.qux + + + type MyType: + a int (0..1) + ''').parseRosetta + + models.forEach[assertNoIssues] + } + + @Test + def void shouldNotWarnForUsedImportsWithAlias() { + val models = newArrayList(''' + namespace dsl.test + + import foo.bar.* as bar + + type A: + a bar.qux.MyType (1..1) + ''', + ''' + namespace foo.bar.qux + + + type MyType: + a int (0..1) + ''').parseRosetta + + models.forEach[assertNoIssues] + } } \ No newline at end of file